diff options
Diffstat (limited to 'util/nvmutil/lib/io.c')
| -rw-r--r-- | util/nvmutil/lib/io.c | 746 |
1 files changed, 746 insertions, 0 deletions
diff --git a/util/nvmutil/lib/io.c b/util/nvmutil/lib/io.c new file mode 100644 index 00000000..99c9f04d --- /dev/null +++ b/util/nvmutil/lib/io.c @@ -0,0 +1,746 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * I/O functions specific to nvmutil. + * + * Related: file.c + */ + +#ifdef __OpenBSD__ +#include <sys/param.h> +#endif +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "../include/common.h" + +void +open_gbe_file(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd; + struct xfile *f; + + struct stat _st; + int _flags; + + cmd = &x->cmd[x->i]; + f = &x->f; + + xopen(&f->gbe_fd, f->fname, + cmd->flags | O_BINARY | + O_NOFOLLOW | O_CLOEXEC, &_st); + + /* inode will be checked later on write */ + f->gbe_dev = _st.st_dev; + f->gbe_ino = _st.st_ino; + + if (_st.st_nlink > 1) + err(EINVAL, + "%s: warning: file has multiple (%lu) hard links\n", + f->fname, (unsigned long)_st.st_nlink); + + if (_st.st_nlink == 0) + err(EIO, "%s: file unlinked while open", f->fname); + + _flags = fcntl(f->gbe_fd, F_GETFL); + if (_flags == -1) + err(errno, "%s: fcntl(F_GETFL)", f->fname); + + /* + * O_APPEND must not be used, because this + * allows POSIX write() to ignore the + * current write offset and write at EOF, + * which would therefore break pread/pwrite + */ + if (_flags & O_APPEND) + err(EIO, "%s: O_APPEND flag", f->fname); + + f->gbe_file_size = _st.st_size; + + switch (f->gbe_file_size) { + case SIZE_8KB: + case SIZE_16KB: + case SIZE_128KB: + break; + default: + err(EINVAL, "File size must be 8KB, 16KB or 128KB"); + } + + if (lock_file(f->gbe_fd, cmd->flags) == -1) + err(errno, "%s: can't lock", f->fname); +} + +/* + * We copy the entire gbe file + * to the tmpfile, and then we + * work on that. We copy back + * afterward. this is the copy. + * + * we copy to tmpfile even on + * read-only commands, for the + * double-read verification, + * which also benefits cmd_cat. + */ +void +copy_gbe(void) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + f = &x->f; + + read_file(); + + /* + regular operations post-read operate only on the first + 8KB, because each GbE part is the first 4KB of each + half of the file. + + we no longer care about anything past 8KB, until we get + to writing, at which point we will flush the buffer + again + */ + + if (f->gbe_file_size == SIZE_8KB) + return; + + x_v_memcpy(f->buf + (unsigned long)GBE_PART_SIZE, + f->buf + (unsigned long)(f->gbe_file_size >> 1), + (unsigned long)GBE_PART_SIZE); +} + +void +read_file(void) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + struct stat _st; + long _r; + + f = &x->f; + + /* read main file */ + _r = rw_file_exact(f->gbe_fd, f->buf, f->gbe_file_size, + 0, IO_PREAD, NO_LOOP_EAGAIN, LOOP_EINTR, + MAX_ZERO_RW_RETRY, OFF_ERR); + + if (_r < 0) + err(errno, "%s: read failed", f->fname); + + /* copy to tmpfile */ + + _r = rw_file_exact(f->tmp_fd, f->buf, f->gbe_file_size, + 0, IO_PWRITE, NO_LOOP_EAGAIN, LOOP_EINTR, + MAX_ZERO_RW_RETRY, OFF_ERR); + + if (_r < 0) + err(errno, "%s: %s: copy failed", + f->fname, f->tname); + + /* + * file size comparison + */ + + if (fstat(f->tmp_fd, &_st) == -1) + err(errno, "%s: stat", f->tname); + + f->gbe_tmp_size = _st.st_size; + + if (f->gbe_tmp_size != f->gbe_file_size) + err(EIO, "%s: %s: not the same size", + f->fname, f->tname); + + /* + * fsync tmp gbe file, because we will compare + * its contents to what was read (for safety) + */ + if (x_i_fsync(f->tmp_fd) == -1) + err(errno, "%s: fsync (tmpfile copy)", f->tname); + + _r = rw_file_exact(f->tmp_fd, f->bufcmp, f->gbe_file_size, + 0, IO_PREAD, NO_LOOP_EAGAIN, LOOP_EINTR, + MAX_ZERO_RW_RETRY, OFF_ERR); + + if (_r < 0) + err(errno, "%s: read failed (cmp)", f->tname); + + if (x_i_memcmp(f->buf, f->bufcmp, f->gbe_file_size) != 0) + err(errno, "%s: %s: read contents differ (pre-test)", + f->fname, f->tname); +} +void +write_gbe_file(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd; + struct xfile *f; + + struct stat _gbe_st; + struct stat _tmp_st; + + unsigned long p; + unsigned char update_checksum; + + cmd = &x->cmd[x->i]; + f = &x->f; + + if ((cmd->flags & O_ACCMODE) == O_RDONLY) + return; + + if (fstat(f->gbe_fd, &_gbe_st) == -1) + err(errno, "%s: re-check", f->fname); + if (_gbe_st.st_dev != f->gbe_dev || _gbe_st.st_ino != f->gbe_ino) + err(EIO, "%s: file replaced while open", f->fname); + if (_gbe_st.st_size != f->gbe_file_size) + err(errno, "%s: file size changed before write", f->fname); + if (!S_ISREG(_gbe_st.st_mode)) + err(errno, "%s: file type changed before write", f->fname); + + if (fstat(f->tmp_fd, &_tmp_st) == -1) + err(errno, "%s: re-check", f->tname); + if (_tmp_st.st_dev != f->tmp_dev || _tmp_st.st_ino != f->tmp_ino) + err(EIO, "%s: file replaced while open", f->tname); + if (_tmp_st.st_size != f->gbe_file_size) + err(errno, "%s: file size changed before write", f->tname); + if (!S_ISREG(_tmp_st.st_mode)) + err(errno, "%s: file type changed before write", f->tname); + + update_checksum = cmd->chksum_write; + + for (p = 0; p < 2; p++) { + if (!f->part_modified[p]) + continue; + + if (update_checksum) + set_checksum(p); + + rw_gbe_file_part(p, IO_PWRITE, "pwrite"); + } +} + +void +rw_gbe_file_part(unsigned long p, int rw_type, + const char *rw_type_str) +{ + struct xstate *x = xstatus(); + struct commands *cmd; + struct xfile *f; + + long rval; + + off_t file_offset; + + unsigned long gbe_rw_size; + unsigned char *mem_offset; + + cmd = &x->cmd[x->i]; + f = &x->f; + + gbe_rw_size = cmd->rw_size; + + if (rw_type < IO_PREAD || rw_type > IO_PWRITE) + err(errno, "%s: %s: part %lu: invalid rw_type, %d", + f->fname, rw_type_str, (unsigned long)p, rw_type); + + mem_offset = gbe_mem_offset(p, rw_type_str); + file_offset = (off_t)gbe_file_offset(p, rw_type_str); + + rval = rw_gbe_file_exact(f->tmp_fd, mem_offset, + gbe_rw_size, file_offset, rw_type); + + if (rval == -1) + err(errno, "%s: %s: part %lu", + f->fname, rw_type_str, (unsigned long)p); + + if ((unsigned long)rval != gbe_rw_size) + err(EIO, "%s: partial %s: part %lu", + f->fname, rw_type_str, (unsigned long)p); +} + +void +write_to_gbe_bin(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd; + struct xfile *f; + + int saved_errno; + int mv; + + cmd = &x->cmd[x->i]; + f = &x->f; + + if ((cmd->flags & O_ACCMODE) != O_RDWR) + return; + + write_gbe_file(); + + /* + * We may otherwise read from + * cache, so we must sync. + */ + if (x_i_fsync(f->tmp_fd) == -1) + err(errno, "%s: fsync (pre-verification)", + f->tname); + + check_written_part(0); + check_written_part(1); + + report_io_err_rw(); + + if (f->io_err_gbe) + err(EIO, "%s: bad write", f->fname); + + /* + * success! + * now just rename the tmpfile + */ + + saved_errno = errno; + + if (x_i_close(f->tmp_fd) == -1) { + fprintf(stderr, "FAIL: %s: close\n", f->tname); + f->io_err_gbe_bin = 1; + } + + if (x_i_close(f->gbe_fd) == -1) { + fprintf(stderr, "FAIL: %s: close\n", f->fname); + f->io_err_gbe_bin = 1; + } + + errno = saved_errno; + + f->tmp_fd = -1; + f->gbe_fd = -1; + + if (!f->io_err_gbe_bin) { + + mv = gbe_mv(); + + if (mv < 0) { + + f->io_err_gbe_bin = 1; + + fprintf(stderr, "%s: %s\n", + f->fname, strerror(errno)); + } else { + /* + * tmpfile removed + * by the rename + */ + + if (f->tname != NULL) + free(f->tname); + + f->tname = NULL; + } + } + + /* + * finally: + * must sync to disk! + * very nearly done + */ + + if (!f->io_err_gbe_bin) + return; + + fprintf(stderr, "FAIL (rename): %s: skipping fsync\n", + f->fname); + if (errno) + fprintf(stderr, + "errno %d: %s\n", errno, strerror(errno)); +} + +void +check_written_part(unsigned long p) +{ + struct xstate *x = xstatus(); + struct commands *cmd; + struct xfile *f; + + long rval; + + unsigned long gbe_rw_size; + + off_t file_offset; + unsigned char *mem_offset; + + struct stat st; + unsigned char *buf_restore; + + cmd = &x->cmd[x->i]; + f = &x->f; + + if (!f->part_modified[p]) + return; + + gbe_rw_size = cmd->rw_size; + + mem_offset = gbe_mem_offset(p, "pwrite"); + file_offset = (off_t)gbe_file_offset(p, "pwrite"); + + memset(f->pad, 0xff, sizeof(f->pad)); + + if (fstat(f->gbe_fd, &st) == -1) + err(errno, "%s: fstat (post-write)", f->fname); + if (st.st_dev != f->gbe_dev || st.st_ino != f->gbe_ino) + err(EIO, "%s: file changed during write", f->fname); + + if (fstat(f->tmp_fd, &st) == -1) + err(errno, "%s: fstat (post-write)", f->tname); + if (st.st_dev != f->tmp_dev || st.st_ino != f->tmp_ino) + err(EIO, "%s: file changed during write", f->tname); + + rval = rw_gbe_file_exact(f->tmp_fd, f->pad, + gbe_rw_size, file_offset, IO_PREAD); + + if (rval == -1) + f->rw_check_err_read[p] = f->io_err_gbe = 1; + else if ((unsigned long)rval != gbe_rw_size) + f->rw_check_partial_read[p] = f->io_err_gbe = 1; + else if (x_i_memcmp(mem_offset, f->pad, gbe_rw_size) != 0) + f->rw_check_bad_part[p] = f->io_err_gbe = 1; + + if (f->rw_check_err_read[p] || + f->rw_check_partial_read[p]) + return; + + /* + * We only load one part on-file, into memory but + * always at offset zero, for post-write checks. + * That's why we hardcode good_checksum(0). + */ + buf_restore = f->buf; + + /* + * good_checksum works on f->buf + * so let's change f->buf for now + */ + f->buf = f->pad; + + if (good_checksum(0)) + f->post_rw_checksum[p] = 1; + + f->buf = buf_restore; +} + +void +report_io_err_rw(void) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + unsigned long p; + + f = &x->f; + + if (!f->io_err_gbe) + return; + + for (p = 0; p < 2; p++) { + if (!f->part_modified[p]) + continue; + + if (f->rw_check_err_read[p]) + fprintf(stderr, + "%s: pread: p%lu (post-verification)\n", + f->fname, (unsigned long)p); + if (f->rw_check_partial_read[p]) + fprintf(stderr, + "%s: partial pread: p%lu (post-verification)\n", + f->fname, (unsigned long)p); + if (f->rw_check_bad_part[p]) + fprintf(stderr, + "%s: pwrite: corrupt write on p%lu\n", + f->fname, (unsigned long)p); + + if (f->rw_check_err_read[p] || + f->rw_check_partial_read[p]) { + fprintf(stderr, + "%s: p%lu: skipped checksum verification " + "(because read failed)\n", + f->fname, (unsigned long)p); + + continue; + } + + fprintf(stderr, "%s: ", f->fname); + + if (f->post_rw_checksum[p]) + fprintf(stderr, "GOOD"); + else + fprintf(stderr, "BAD"); + + fprintf(stderr, " checksum in p%lu on-disk.\n", + (unsigned long)p); + + if (f->post_rw_checksum[p]) { + fprintf(stderr, + " This does NOT mean it's safe. it may be\n" + " salvageable if you use the cat feature.\n"); + } + } +} + +int +gbe_mv(void) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + int rval; + + int saved_errno; + int tmp_gbe_bin_exists; + + char *dest_tmp; + int dest_fd; + + f = &x->f; + + /* will be set 0 if it doesn't */ + tmp_gbe_bin_exists = 1; + + dest_tmp = NULL; + dest_fd = -1; + + saved_errno = errno; + + rval = rename(f->tname, f->fname); + + if (rval > -1) { + /* + * same filesystem + */ + + tmp_gbe_bin_exists = 0; + + if (fsync_dir(f->fname) < 0) { + f->io_err_gbe_bin = 1; + rval = -1; + } + + goto ret_gbe_mv; + } + + if (errno != EXDEV) + goto ret_gbe_mv; + + /* cross-filesystem rename */ + + if ((rval = f->tmp_fd = open(f->tname, + O_RDONLY | O_BINARY)) == -1) + goto ret_gbe_mv; + + /* create replacement temp in target directory */ + dest_tmp = new_tmpfile(&dest_fd, 1, f->fname); + if (dest_tmp == NULL) + goto ret_gbe_mv; + + /* copy data */ + + rval = rw_file_exact(f->tmp_fd, f->bufcmp, + f->gbe_file_size, 0, IO_PREAD, + NO_LOOP_EAGAIN, LOOP_EINTR, + MAX_ZERO_RW_RETRY, OFF_ERR); + + if (rval < 0) + goto ret_gbe_mv; + + rval = rw_file_exact(dest_fd, f->bufcmp, + f->gbe_file_size, 0, IO_PWRITE, + NO_LOOP_EAGAIN, LOOP_EINTR, + MAX_ZERO_RW_RETRY, OFF_ERR); + + if (rval < 0) + goto ret_gbe_mv; + + if (x_i_fsync(dest_fd) == -1) + goto ret_gbe_mv; + + if (x_i_close(dest_fd) == -1) + goto ret_gbe_mv; + + if (rename(dest_tmp, f->fname) == -1) + goto ret_gbe_mv; + + if (fsync_dir(f->fname) < 0) { + f->io_err_gbe_bin = 1; + goto ret_gbe_mv; + } + + free(dest_tmp); + dest_tmp = NULL; + +ret_gbe_mv: + + if (f->gbe_fd > -1) { + if (x_i_close(f->gbe_fd) < 0) + rval = -1; + if (fsync_dir(f->fname) < 0) { + f->io_err_gbe_bin = 1; + rval = -1; + } + f->gbe_fd = -1; + } + + if (f->tmp_fd > -1) { + if (x_i_close(f->tmp_fd) < 0) + rval = -1; + + f->tmp_fd = -1; + } + + /* + * before this function is called, + * tmp_fd may have been moved + */ + if (tmp_gbe_bin_exists) { + if (unlink(f->tname) < 0) + rval = -1; + else + tmp_gbe_bin_exists = 0; + } + + if (rval < 0) { + /* + * if nothing set errno, + * we assume EIO, or we + * use what was set + */ + if (errno == saved_errno) + errno = EIO; + } else { + errno = saved_errno; + } + + return rval; +} + +/* + * This one is similar to gbe_file_offset, + * but used to check Gbe bounds in memory, + * and it is *also* used during file I/O. + */ +unsigned char * +gbe_mem_offset(unsigned long p, const char *f_op) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + off_t gbe_off; + + f = &x->f; + + gbe_off = gbe_x_offset(p, f_op, "mem", + GBE_PART_SIZE, GBE_WORK_SIZE); + + return (unsigned char *) + (f->buf + (unsigned long)gbe_off); +} + +/* + * I/O operations filtered here. These operations must + * only write from the 0th position or the half position + * within the GbE file, and write 4KB of data. + * + * This check is called, to ensure just that. + */ +off_t +gbe_file_offset(unsigned long p, const char *f_op) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + off_t gbe_file_half_size; + + f = &x->f; + + gbe_file_half_size = f->gbe_file_size >> 1; + + return gbe_x_offset(p, f_op, "file", + gbe_file_half_size, f->gbe_file_size); +} + +off_t +gbe_x_offset(unsigned long p, const char *f_op, const char *d_type, + off_t nsize, off_t ncmp) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + off_t off; + + check_bin(p, "part number"); + + f = &x->f; + + off = ((off_t)p) * (off_t)nsize; + + if (off > ncmp - GBE_PART_SIZE) + err(ECANCELED, "%s: GbE %s %s out of bounds", + f->fname, d_type, f_op); + + if (off != 0 && off != ncmp >> 1) + err(ECANCELED, "%s: GbE %s %s at bad offset", + f->fname, d_type, f_op); + + return off; +} + +long +rw_gbe_file_exact(int fd, unsigned char *mem, unsigned long nrw, + off_t off, int rw_type) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + long r; + + f = &x->f; + + if (io_args(fd, mem, nrw, off, rw_type) == -1) + return -1; + + if (mem != (void *)f->pad) { + if (mem < f->buf) + goto err_rw_gbe_file_exact; + + if ((unsigned long)(mem - f->buf) >= GBE_WORK_SIZE) + goto err_rw_gbe_file_exact; + } + + if (off < 0 || off >= f->gbe_file_size) + goto err_rw_gbe_file_exact; + + if (nrw > (unsigned long)(f->gbe_file_size - off)) + goto err_rw_gbe_file_exact; + + if (nrw > (unsigned long)GBE_PART_SIZE) + goto err_rw_gbe_file_exact; + + r = rw_file_exact(fd, mem, nrw, off, rw_type, + NO_LOOP_EAGAIN, LOOP_EINTR, MAX_ZERO_RW_RETRY, + OFF_ERR); + + return rw_over_nrw(r, nrw); + +err_rw_gbe_file_exact: + errno = EIO; + return -1; +} |
