diff options
Diffstat (limited to 'util/libreboot-utils/lib')
| -rw-r--r-- | util/libreboot-utils/lib/checksum.c | 108 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/command.c | 563 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/file.c | 1136 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/io.c | 673 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/mkhtemp.c | 1133 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/num.c | 444 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/state.c | 252 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/string.c | 146 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/usage.c | 30 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/word.c | 68 |
10 files changed, 4553 insertions, 0 deletions
diff --git a/util/libreboot-utils/lib/checksum.c b/util/libreboot-utils/lib/checksum.c new file mode 100644 index 00000000..b417dc7e --- /dev/null +++ b/util/libreboot-utils/lib/checksum.c @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org> + * + * Functions related to GbE NVM checksums. + */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <limits.h> +#include <stddef.h> +#include <stdlib.h> + +#include "../include/common.h" + +void +read_checksums(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[x->i]; + struct xfile *f = &x->f; + + size_t _p; + size_t _skip_part; + + unsigned char _num_invalid; + unsigned char _max_invalid; + + f->part_valid[0] = 0; + f->part_valid[1] = 0; + + if (!cmd->chksum_read) + return; + + _num_invalid = 0; + _max_invalid = 2; + + if (cmd->arg_part) + _max_invalid = 1; + + /* Skip verification on this part, + * but only when arg_part is set. + */ + _skip_part = f->part ^ 1; + + for (_p = 0; _p < 2; _p++) { + + /* Only verify a part if it was *read* + */ + if (cmd->arg_part && (_p == _skip_part)) + continue; + + f->part_valid[_p] = good_checksum(_p); + if (!f->part_valid[_p]) + ++_num_invalid; + } + + if (_num_invalid >= _max_invalid) { + + if (_max_invalid == 1) + err(ECANCELED, "%s: part %lu has a bad checksum", + f->fname, (size_t)f->part); + + err(ECANCELED, "%s: No valid checksum found in file", + f->fname); + } +} + +int +good_checksum(size_t partnum) +{ + unsigned short expected_checksum; + unsigned short actual_checksum; + + expected_checksum = + calculated_checksum(partnum); + + actual_checksum = + nvm_word(NVM_CHECKSUM_WORD, partnum); + + if (expected_checksum == actual_checksum) { + return 1; + } else { + return 0; + } +} + +void +set_checksum(size_t p) +{ + check_bin(p, "part number"); + set_nvm_word(NVM_CHECKSUM_WORD, p, calculated_checksum(p)); +} + +unsigned short +calculated_checksum(size_t p) +{ + size_t c; + unsigned int val16; + + val16 = 0; + + for (c = 0; c < NVM_CHECKSUM_WORD; c++) + val16 += (unsigned int)nvm_word(c, p); + + return (unsigned short)((NVM_CHECKSUM - val16) & 0xffff); +} diff --git a/util/libreboot-utils/lib/command.c b/util/libreboot-utils/lib/command.c new file mode 100644 index 00000000..3a863d23 --- /dev/null +++ b/util/libreboot-utils/lib/command.c @@ -0,0 +1,563 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org> + */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdio.h> +#include <stddef.h> +#include <string.h> +#include <unistd.h> + +#include "../include/common.h" + +/* Guard against regressions by maintainers (command table) + */ + +void +sanitize_command_list(void) +{ + struct xstate *x = xstatus(); + + size_t c; + size_t num_commands; + + num_commands = items(x->cmd); + + for (c = 0; c < num_commands; c++) + sanitize_command_index(c); +} + +void +sanitize_command_index(size_t c) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[c]; + + int _flag; + size_t gbe_rw_size; + + size_t rval; + + check_command_num(c); + + if (cmd->argc < 3) + err(EINVAL, "cmd index %lu: argc below 3, %d", + (size_t)c, cmd->argc); + + if (cmd->str == NULL) + err(EINVAL, "cmd index %lu: NULL str", + (size_t)c); + + if (*cmd->str == '\0') + err(EINVAL, "cmd index %lu: empty str", + (size_t)c); + + if (slen(cmd->str, MAX_CMD_LEN +1, &rval) < 0) + err(errno, "Could not get command length"); + + if (rval > MAX_CMD_LEN) { + err(EINVAL, "cmd index %lu: str too long: %s", + (size_t)c, cmd->str); + } + + if (cmd->run == NULL) + err(EINVAL, "cmd index %lu: cmd ptr null", + (size_t)c); + + check_bin(cmd->arg_part, "cmd.arg_part"); + check_bin(cmd->chksum_read, "cmd.chksum_read"); + check_bin(cmd->chksum_write, "cmd.chksum_write"); + + gbe_rw_size = cmd->rw_size; + + switch (gbe_rw_size) { + case GBE_PART_SIZE: + case NVM_SIZE: + break; + default: + err(EINVAL, "Unsupported rw_size: %lu", + (size_t)gbe_rw_size); + } + + if (gbe_rw_size > GBE_PART_SIZE) + err(EINVAL, "rw_size larger than GbE part: %lu", + (size_t)gbe_rw_size); + + _flag = (cmd->flags & O_ACCMODE); + + if (_flag != O_RDONLY && + _flag != O_RDWR) + err(EINVAL, "invalid cmd.flags setting"); +} + +void +set_cmd(int argc, char *argv[]) +{ + struct xstate *x = xstatus(); + const char *cmd; + + int rval; + + size_t c; + + for (c = 0; c < items(x->cmd); c++) { + + cmd = x->cmd[c].str; + + if (scmp(argv[2], cmd, MAX_CMD_LEN, &rval) < 0) + err_no_cleanup(EINVAL, + "could not compare command strings"); + if (rval != 0) + continue; /* not the right command */ + + /* valid command found */ + if (argc >= x->cmd[c].argc) { + x->no_cmd = 0; + x->i = c; /* set command */ + + return; + } + + err_no_cleanup(EINVAL, + "Too few args on command '%s'", cmd); + } + + x->no_cmd = 1; +} + +void +set_cmd_args(int argc, char *argv[]) +{ + struct xstate *x = xstatus(); + size_t i = x->i; + struct commands *cmd = &x->cmd[i]; + struct xfile *f = &x->f; + + if (!valid_command(i) || argc < 3) + usage(); + + if (x->no_cmd) + usage(); + + /* Maintainer bug + */ + if (cmd->arg_part && argc < 4) + err(EINVAL, + "arg_part set for command that needs argc4"); + + if (cmd->arg_part && i == CMD_SETMAC) + err(EINVAL, + "arg_part set on CMD_SETMAC"); + + if (i == CMD_SETMAC) { + + if (argc >= 4) + x->mac.str = argv[3]; + else + x->mac.str = x->mac.rmac; + + } else if (cmd->arg_part) { + + f->part = conv_argv_part_num(argv[3]); + } +} + +size_t +conv_argv_part_num(const char *part_str) +{ + unsigned char ch; + + if (part_str[0] == '\0' || part_str[1] != '\0') + err(EINVAL, "Partnum string '%s' wrong length", part_str); + + /* char signedness is implementation-defined + */ + ch = (unsigned char)part_str[0]; + if (ch < '0' || ch > '1') + err(EINVAL, "Bad part number (%c)", ch); + + return (size_t)(ch - '0'); +} + +void +check_command_num(size_t c) +{ + if (!valid_command(c)) + err(EINVAL, "Invalid run_cmd arg: %lu", + (size_t)c); +} + +unsigned char +valid_command(size_t c) +{ + struct xstate *x = xstatus(); + struct commands *cmd; + + if (c >= items(x->cmd)) + return 0; + + cmd = &x->cmd[c]; + + if (c != cmd->chk) + err(EINVAL, + "Invalid cmd chk value (%lu) vs arg: %lu", + cmd->chk, c); + + return 1; +} + +void +cmd_helper_setmac(void) +{ + struct xstate *x = xstatus(); + struct macaddr *mac = &x->mac; + + size_t partnum; + + check_cmd(cmd_helper_setmac, "setmac"); + + printf("MAC address to be written: %s\n", mac->str); + parse_mac_string(); + + for (partnum = 0; partnum < 2; partnum++) + write_mac_part(partnum); +} + +void +parse_mac_string(void) +{ + struct xstate *x = xstatus(); + struct macaddr *mac = &x->mac; + + size_t mac_byte; + + size_t rval; + + if (slen(x->mac.str, 18, &rval) < 0) + err(EINVAL, "Could not determine MAC length"); + + if (rval != 17) + err(EINVAL, "MAC address is the wrong length"); + + memset(mac->mac_buf, 0, sizeof(mac->mac_buf)); + + for (mac_byte = 0; mac_byte < 6; mac_byte++) + set_mac_byte(mac_byte); + + if ((mac->mac_buf[0] | mac->mac_buf[1] | mac->mac_buf[2]) == 0) + err(EINVAL, "Must not specify all-zeroes MAC address"); + + if (mac->mac_buf[0] & 1) + err(EINVAL, "Must not specify multicast MAC address"); +} + +void +set_mac_byte(size_t mac_byte_pos) +{ + struct xstate *x = xstatus(); + struct macaddr *mac = &x->mac; + + char separator; + + size_t mac_str_pos; + size_t mac_nib_pos; + + mac_str_pos = mac_byte_pos * 3; + + if (mac_str_pos < 15) { + if ((separator = mac->str[mac_str_pos + 2]) != ':') + err(EINVAL, "Invalid MAC address separator '%c'", + separator); + } + + for (mac_nib_pos = 0; mac_nib_pos < 2; mac_nib_pos++) + set_mac_nib(mac_str_pos, mac_byte_pos, mac_nib_pos); +} + +void +set_mac_nib(size_t mac_str_pos, + size_t mac_byte_pos, size_t mac_nib_pos) +{ + struct xstate *x = xstatus(); + struct macaddr *mac = &x->mac; + + char mac_ch; + unsigned short hex_num; + + mac_ch = mac->str[mac_str_pos + mac_nib_pos]; + + if ((hex_num = hextonum(mac_ch)) > 15) { + if (hex_num >= 17) + err(EIO, "Randomisation failure"); + else + err(EINVAL, "Invalid character '%c'", + mac->str[mac_str_pos + mac_nib_pos]); + } + + /* If random, ensure that local/unicast bits are set. + */ + if ((mac_byte_pos == 0) && (mac_nib_pos == 1) && + ((mac_ch | 0x20) == 'x' || + (mac_ch == '?'))) + hex_num = (hex_num & 0xE) | 2; /* local, unicast */ + + /* MAC words stored big endian in-file, little-endian + * logically, so we reverse the order. + */ + mac->mac_buf[mac_byte_pos >> 1] |= hex_num << + (((mac_byte_pos & 1) << 3) /* left or right byte? */ + | ((mac_nib_pos ^ 1) << 2)); /* left or right nib? */ +} + +void +write_mac_part(size_t partnum) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + struct macaddr *mac = &x->mac; + + size_t w; + + check_bin(partnum, "part number"); + if (!f->part_valid[partnum]) + return; + + for (w = 0; w < 3; w++) + set_nvm_word(w, partnum, mac->mac_buf[w]); + + printf("Wrote MAC address to part %lu: ", + (size_t)partnum); + print_mac_from_nvm(partnum); +} + +void +cmd_helper_dump(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t p; + + check_cmd(cmd_helper_dump, "dump"); + + f->part_valid[0] = good_checksum(0); + f->part_valid[1] = good_checksum(1); + + for (p = 0; p < 2; p++) { + + if (!f->part_valid[p]) { + + fprintf(stderr, + "BAD checksum %04x in part %lu (expected %04x)\n", + nvm_word(NVM_CHECKSUM_WORD, p), + (size_t)p, + calculated_checksum(p)); + } + + printf("MAC (part %lu): ", + (size_t)p); + + print_mac_from_nvm(p); + + hexdump(p); + } +} + +void +print_mac_from_nvm(size_t partnum) +{ + size_t c; + unsigned short val16; + + for (c = 0; c < 3; c++) { + + val16 = nvm_word(c, partnum); + + printf("%02x:%02x", + (unsigned int)(val16 & 0xff), + (unsigned int)(val16 >> 8)); + + if (c == 2) + printf("\n"); + else + printf(":"); + } +} + +void +hexdump(size_t partnum) +{ + size_t c; + size_t row; + unsigned short val16; + + for (row = 0; row < 8; row++) { + + printf("%08lx ", + (size_t)((size_t)row << 4)); + + for (c = 0; c < 8; c++) { + + val16 = nvm_word((row << 3) + c, partnum); + + if (c == 4) + printf(" "); + + printf(" %02x %02x", + (unsigned int)(val16 & 0xff), + (unsigned int)(val16 >> 8)); + + } + + printf("\n"); + } +} + +void +cmd_helper_swap(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + check_cmd(cmd_helper_swap, "swap"); + + memcpy( + f->buf + (size_t)GBE_WORK_SIZE, + f->buf, + GBE_PART_SIZE); + + memcpy( + f->buf, + f->buf + (size_t)GBE_PART_SIZE, + GBE_PART_SIZE); + + memcpy( + f->buf + (size_t)GBE_PART_SIZE, + f->buf + (size_t)GBE_WORK_SIZE, + GBE_PART_SIZE); + + set_part_modified(0); + set_part_modified(1); +} + +void +cmd_helper_copy(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + check_cmd(cmd_helper_copy, "copy"); + + memcpy( + f->buf + (size_t)((f->part ^ 1) * GBE_PART_SIZE), + f->buf + (size_t)(f->part * GBE_PART_SIZE), + GBE_PART_SIZE); + + set_part_modified(f->part ^ 1); +} + +void +cmd_helper_cat(void) +{ + struct xstate *x = xstatus(); + + check_cmd(cmd_helper_cat, "cat"); + + x->cat = 0; + cat(0); +} + +void +cmd_helper_cat16(void) +{ + struct xstate *x = xstatus(); + + check_cmd(cmd_helper_cat16, "cat16"); + + x->cat = 1; + cat(1); +} + +void +cmd_helper_cat128(void) +{ + struct xstate *x = xstatus(); + + check_cmd(cmd_helper_cat128, "cat128"); + + x->cat = 15; + cat(15); +} + +void +cat(size_t nff) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t p; + size_t ff; + + p = 0; + ff = 0; + + if ((size_t)x->cat != nff) { + + err(ECANCELED, "erroneous call to cat"); + } + + fflush(NULL); + + memset(f->pad, 0xff, GBE_PART_SIZE); + + for (p = 0; p < 2; p++) { + + cat_buf(f->bufcmp + + (size_t)(p * (f->gbe_file_size >> 1))); + + for (ff = 0; ff < nff; ff++) { + + cat_buf(f->pad); + } + } +} + +void +cat_buf(unsigned char *b) +{ + if (b == NULL) + err(errno, "null pointer in cat command"); + + if (rw_file_exact(STDOUT_FILENO, b, + GBE_PART_SIZE, 0, IO_WRITE, LOOP_EAGAIN, LOOP_EINTR, + MAX_ZERO_RW_RETRY, OFF_ERR) < 0) + err(errno, "stdout: cat"); +} +void +check_cmd(void (*fn)(void), + const char *name) +{ + struct xstate *x = xstatus(); + size_t i = x->i; + + if (x->cmd[i].run != fn) + err(ECANCELED, "Running %s, but cmd %s is set", + name, x->cmd[i].str); + + /* prevent second command + */ + for (i = 0; i < items(x->cmd); i++) + x->cmd[i].run = cmd_helper_err; +} + +void +cmd_helper_err(void) +{ + err(ECANCELED, + "Erroneously running command twice"); +} diff --git a/util/libreboot-utils/lib/file.c b/util/libreboot-utils/lib/file.c new file mode 100644 index 00000000..ea2bcd0b --- /dev/null +++ b/util/libreboot-utils/lib/file.c @@ -0,0 +1,1136 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * Pathless i/o, and some stuff you probably never saw. + * Be nice to the demon. + */ + + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/* for openat2: */ +#ifdef __linux__ +#include <linux/openat2.h> +#include <sys/syscall.h> +#endif + +#include "../include/common.h" + +/* check that a file changed + */ + +int +same_file(int fd, struct stat *st_old, + int check_size) +{ + struct stat st; + int saved_errno = errno; + + /* TODO: null/-1 checks + * like this can be + * generalised + */ + if (st_old == NULL) { + errno = EFAULT; + goto err_same_file; + } + if (fd < 0) { + errno = EBADF; + goto err_same_file; + } + + if (fstat(fd, &st) == -1) + goto err_same_file; + + if (fd_verify_regular(fd, st_old, &st) < 0) + goto err_same_file; + + if (check_size && + st.st_size != st_old->st_size) + goto err_same_file; + + errno = saved_errno; + return 0; + +err_same_file: + + if (errno == saved_errno) + errno = ESTALE; + + return -1; +} + +/* open() but with abort traps + */ +/* TODO: also support other things here than files. + and then use, throughout the program. + in particular, use of openat might help + (split the path) + (see: link attack mitigations throughout nvmutil) + + make it return, and handle the return value/errno + + (this could return e.g. EINTR) + + TODO: this function is not used by mkhtemp, nor will + it probably be, it's currently used by nvmutil, + for opening intel gbe nvm config files. i can + probably remove it though and unify witth some + of the verification code now used for mkhtemp + +TODO: and don't abort. return -1. and handle in the caller. + +minor obstacle: the mkhtemp code always requires absolute +paths, whereas the gbe editor takes relative paths. + */ +void +xopen(int *fd_ptr, const char *path, int flags, struct stat *st) +{ + if ((*fd_ptr = open(path, flags)) < 0) + err(errno, "%s", path); + + if (fstat(*fd_ptr, st) < 0) + err(errno, "%s: stat", path); + + if (!S_ISREG(st->st_mode)) + err(errno, "%s: not a regular file", path); + + if (lseek_on_eintr(*fd_ptr, 0, SEEK_CUR, 1, 1) == (off_t)-1) + err(errno, "%s: file not seekable", path); +} + +/* fsync() the directory of a file, + * useful for atomic writes + */ + +int +fsync_dir(const char *path) +{ + int saved_errno = errno; + + size_t pathlen = 0; + size_t maxlen = 0; + + char *dirbuf = NULL; + int dirfd = -1; + + char *slash = NULL; + struct stat st = {0}; + + int close_errno; + +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + maxlen = PATH_LEN; +#else + maxlen = 4096; +#endif + + if (path == NULL) { + errno = EFAULT; + goto err_fsync_dir; + } + + if (slen(path, maxlen, &pathlen) < 0) + goto err_fsync_dir; + + if (pathlen >= maxlen || pathlen < 0) { + errno = EMSGSIZE; + goto err_fsync_dir; + } + + if (pathlen == 0) + { + errno = EINVAL; + goto err_fsync_dir; + } + + dirbuf = malloc(pathlen + 1); + if (dirbuf == NULL) { + + errno = ENOMEM; + goto err_fsync_dir; + } + + memcpy(dirbuf, path, pathlen + 1); + slash = strrchr(dirbuf, '/'); + + if (slash != NULL) { + *slash = '\0'; + if (*dirbuf == '\0') { + dirbuf[0] = '/'; + dirbuf[1] = '\0'; + } + } else { + dirbuf[0] = '.'; + dirbuf[1] = '\0'; + } + + dirfd = fs_open(dirbuf, + O_RDONLY | O_CLOEXEC | O_NOCTTY +#ifdef O_DIRECTORY + | O_DIRECTORY +#endif +#ifdef O_NOFOLLOW + | O_NOFOLLOW +#endif +); + if (dirfd < 0) + goto err_fsync_dir; + + if (fstat(dirfd, &st) < 0) + goto err_fsync_dir; + + if (!S_ISDIR(st.st_mode)) { + + errno = ENOTDIR; + goto err_fsync_dir; + } + + /* sync file on disk */ + if (fsync_on_eintr(dirfd) == -1) + goto err_fsync_dir; + + if (close_on_eintr(dirfd) == -1) { + + dirfd = -1; + goto err_fsync_dir; + } + + if (dirbuf != NULL) { + + free(dirbuf); + dirbuf = NULL; + } + + dirbuf = NULL; + + errno = saved_errno; + return 0; + +err_fsync_dir: + + if (errno == saved_errno) + errno = EIO; + + if (dirbuf != NULL) { + + free(dirbuf); + dirbuf = NULL; + } + + if (dirfd >= 0) { + + close_errno = errno; + (void) close_on_eintr(dirfd); + errno = close_errno; + dirfd = -1; + } + + return -1; +} + +/* + * Safe I/O functions wrapping around + * read(), write() and providing a portable + * analog of both pread() and pwrite(). + * These functions are designed for maximum + * robustness, checking NULL inputs, overflowed + * outputs, and all kinds of errors that the + * standard libc functions don't. + * + * Looping on EINTR and EAGAIN is supported. + * EINTR/EAGAIN looping is done indefinitely. + */ + +/* rw_file_exact() - Read perfectly or die + * + * Read/write, and absolutely insist on an + * absolute read; e.g. if 100 bytes are + * requested, this MUST return 100. + * + * This function will never return zero. + * It will only return below (error), + * or above (success). On error, -1 is + * returned and errno is set accordingly. + * + * Zero-byte returns are not allowed. + * It will re-spin a finite number of + * times upon zero-return, to recover, + * otherwise it will return an error. + */ + +ssize_t +rw_file_exact(int fd, unsigned char *mem, size_t nrw, + off_t off, int rw_type, int loop_eagain, + int loop_eintr, size_t max_retries, + int off_reset) +{ + ssize_t rval; + ssize_t rc; + + size_t nrw_cur; + + off_t off_cur; + void *mem_cur; + + size_t retries_on_zero; + + int saved_errno = errno; + + rval = 0; + + rc = 0; + retries_on_zero = 0; + + if (io_args(fd, mem, nrw, off, rw_type) == -1) + goto err_rw_file_exact; + + while (1) { + + /* Prevent theoretical overflow */ + if (rval >= 0 && (size_t)rval > (nrw - rc)) { + errno = EOVERFLOW; + goto err_rw_file_exact; + } + + rc += rval; + if ((size_t)rc >= nrw) + break; + + mem_cur = (void *)(mem + (size_t)rc); + nrw_cur = (size_t)(nrw - (size_t)rc); + + if (off < 0) { + errno = EOVERFLOW; + goto err_rw_file_exact; + } + + off_cur = off + (off_t)rc; + + rval = prw(fd, mem_cur, nrw_cur, off_cur, + rw_type, loop_eagain, loop_eintr, + off_reset); + + if (rval < 0) + goto err_rw_file_exact; + + if (rval == 0) { + if (retries_on_zero++ < max_retries) + continue; + + errno = EIO; + goto err_rw_file_exact; + } + + retries_on_zero = 0; + } + + if ((size_t)rc != nrw) { + + errno = EIO; + goto err_rw_file_exact; + } + + rval = rw_over_nrw(rc, nrw); + if (rval < 0) + goto err_rw_file_exact; + + errno = saved_errno; + + return rval; + +err_rw_file_exact: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +/* prw() - portable read-write with more + * safety checks than barebones libc + * + * portable pwrite/pread on request, or real + * pwrite/pread libc functions can be used. + * the portable (non-libc) pread/pwrite is not + * thread-safe, because it does not prevent or + * mitigate race conditions on file descriptors + * + * If you need real pwrite/pread, just compile + * with flag: REAL_POS_IO=1 + * + * A fallback is provided for regular read/write. + * rw_type can be IO_READ (read), IO_WRITE (write), + * IO_PREAD (pread) or IO_PWRITE + * + * loop_eagain does a retry loop on EAGAIN if set + * loop_eintr does a retry loop on EINTR if set + * + * race conditions on non-libc pread/pwrite: + * if a file offset changes, abort, to mitage. + * + * off_reset 1: reset the file offset *once* if + * a change was detected, assuming + * nothing else is touching it now + * off_reset 0: never reset if changed + */ + +ssize_t +prw(int fd, void *mem, size_t nrw, + off_t off, int rw_type, + int loop_eagain, int loop_eintr, + int off_reset) +{ + ssize_t rval; + ssize_t r; + int positional_rw; + struct stat st; +#if !defined(REAL_POS_IO) || \ + REAL_POS_IO < 1 + off_t verified; + off_t off_orig; + off_t off_last; +#endif + int saved_errno = errno; + + if (io_args(fd, mem, nrw, off, rw_type) + == -1) + goto err_prw; + + r = -1; + + /* do not use loop_eagain on + * normal files + */ + + if (!loop_eagain) { + /* check whether the file + * changed + */ + + if (check_file(fd, &st) == -1) + goto err_prw; + } + + if (rw_type >= IO_PREAD) + positional_rw = 1; /* pread/pwrite */ + else + positional_rw = 0; /* read/write */ + +try_rw_again: + + if (!positional_rw) { +#if defined(REAL_POS_IO) && \ + REAL_POS_IO > 0 +real_pread_pwrite: +#endif + if (rw_type == IO_WRITE) + r = write(fd, mem, nrw); + else if (rw_type == IO_READ) + r = read(fd, mem, nrw); +#if defined(REAL_POS_IO) && \ + REAL_POS_IO > 0 + else if (rw_type == IO_PWRITE) + r = pwrite(fd, mem, nrw, off); + else if (rw_type == IO_PREAD) + r = pread(fd, mem, nrw, off); +#endif + + if (r == -1 && (errno == try_err(loop_eintr, EINTR) + || errno == try_err(loop_eagain, EAGAIN))) + goto try_rw_again; + + rval = rw_over_nrw(r, nrw); + if (rval < 0) + goto err_prw; + + errno = saved_errno; + + return rval; + } + +#if defined(REAL_POS_IO) && \ + REAL_POS_IO > 0 + goto real_pread_pwrite; +#else + if ((off_orig = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, + loop_eagain, loop_eintr)) == (off_t)-1) { + r = -1; + } else if (lseek_on_eintr(fd, off, SEEK_SET, + loop_eagain, loop_eintr) == (off_t)-1) { + r = -1; + } else { + verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, + loop_eagain, loop_eintr); + + /* abort if the offset changed, + * indicating race condition. if + * off_reset enabled, reset *ONCE* + */ + + if (off_reset && off != verified) + lseek_on_eintr(fd, off, SEEK_SET, + loop_eagain, loop_eintr); + + do { + /* check offset again, repeatedly. + * even if off_reset is set, this + * aborts if offsets change again + */ + + verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, + loop_eagain, loop_eintr); + + if (off != verified) { + + errno = EBUSY; + goto err_prw; + } + + if (rw_type == IO_PREAD) + r = read(fd, mem, nrw); + else if (rw_type == IO_PWRITE) + r = write(fd, mem, nrw); + + if (rw_over_nrw(r, nrw) == -1) + break; + + } while (r == -1 && + (errno == try_err(loop_eintr, EINTR) || + errno == try_err(loop_eagain, EAGAIN))); + } + + saved_errno = errno; + + off_last = lseek_on_eintr(fd, off_orig, SEEK_SET, + loop_eagain, loop_eintr); + + if (off_last != off_orig) { + errno = saved_errno; + goto err_prw; + } + + errno = saved_errno; + + rval = rw_over_nrw(r, nrw); + if (rval < 0) + goto err_prw; + + errno = saved_errno; + + return rval; + +#endif + +err_prw: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +int +io_args(int fd, void *mem, size_t nrw, + off_t off, int rw_type) +{ + int saved_errno = errno; + + /* obviously */ + if (mem == NULL) { + + errno = EFAULT; + goto err_io_args; + } + + /* uninitialised fd */ + if (fd < 0) { + + errno = EBADF; + goto err_io_args; + } + + /* negative offset */ + if (off < 0) { + + errno = ERANGE; + goto err_io_args; + } + + /* prevent zero-byte rw */ + if (!nrw) + goto err_io_args; + + /* prevent overflow */ + if (nrw > (size_t)SSIZE_MAX) { + + errno = ERANGE; + goto err_io_args; + } + + /* prevent overflow */ + if (((size_t)off + nrw) < (size_t)off) { + + errno = ERANGE; + goto err_io_args; + } + + if (rw_type > IO_PWRITE) { + + errno = EINVAL; + goto err_io_args; + } + + errno = saved_errno; + + return 0; + +err_io_args: + + if (errno == saved_errno) + errno = EINVAL; + + return -1; +} + +int +check_file(int fd, struct stat *st) +{ + int saved_errno = errno; + + if (fd < 0) { + errno = EBADF; + goto err_is_file; + } + + if (st == NULL) { + errno = EFAULT; + goto err_is_file; + } + + if (fstat(fd, st) == -1) + goto err_is_file; + + if (!S_ISREG(st->st_mode)) { + + errno = EBADF; + goto err_is_file; + } + + errno = saved_errno; + + return 0; + +err_is_file: + + if (errno == saved_errno) + errno = EINVAL; + + return -1; +} + +/* POSIX can say whatever it wants. + * specification != implementation + */ + +ssize_t +rw_over_nrw(ssize_t r, size_t nrw) +{ + int saved_errno = errno; + + /* not a libc bug, but we + * don't like the number zero + */ + if (!nrw) + goto err_rw_over_nrw; + + if (r == -1) + return r; + + if ((size_t) + r > SSIZE_MAX) { + + /* Theoretical buggy libc + * check. Extremely academic. + * + * Specifications never + * allow this return value + * to exceed SSIZE_T, but + * spec != implementation + * + * Check this after using + * [p]read() or [p]write() + * + * NOTE: here, we assume + * ssize_t integers are the + * same size as SSIZE_T + */ + + errno = ERANGE; + goto err_rw_over_nrw; + } + + /* Theoretical buggy libc: + * Should never return a number of + * bytes above the requested length. + */ + if ((size_t)r > nrw) { + + errno = ERANGE; + goto err_rw_over_nrw; + } + + errno = saved_errno; + + return r; + +err_rw_over_nrw: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +#if !defined(REAL_POS_IO) || \ + REAL_POS_IO < 1 +off_t +lseek_on_eintr(int fd, off_t off, int whence, + int loop_eagain, int loop_eintr) +{ + off_t old; + + old = -1; + + do { + old = lseek(fd, off, whence); + } while (old == (off_t)-1 && ( + errno == try_err(loop_eintr, EINTR) || + errno == try_err(loop_eintr, ETXTBSY) || + errno == try_err(loop_eagain, EAGAIN) || + errno == try_err(loop_eagain, EWOULDBLOCK))); + + return old; +} +#endif + +int +try_err(int loop_err, int errval) +{ + if (loop_err) + return errval; + + return -1; +} + +int +close_on_eintr(int fd) +{ + int r; + int saved_errno = errno; + + do { + r = close(fd); + } while (r == -1 && ( + errno == EINTR || errno == EAGAIN || + errno == EWOULDBLOCK || errno == ETXTBSY)); + + if (r >= 0) + errno = saved_errno; + + return r; +} + +int +fsync_on_eintr(int fd) +{ + int r; + int saved_errno = errno; + + do { + r = fsync(fd); + } while (r == -1 && (errno == EINTR || errno == EAGAIN || + errno == ETXTBSY || errno == EWOULDBLOCK)); + + if (r >= 0) + errno = saved_errno; + + return r; +} + +int +fs_rename_at(int olddirfd, const char *old, + int newdirfd, const char *new) +{ + if (new == NULL || old == NULL) { + + errno = EFAULT; + return -1; + } + + if (olddirfd < 0 || newdirfd < 0) { + + errno = EBADF; + return -1; + } + + return renameat(olddirfd, old, newdirfd, new); +} + +/* secure open, based on + * relative path to root + * + * always a fixed fd for / + * see: rootfs() + */ +int +fs_open(const char *path, int flags) +{ + struct filesystem *fs; + const char *rel; + + if (path == NULL) { + errno = EFAULT; + return -1; + } + + if (path[0] != '/') { + errno = EINVAL; + return -1; + } + + fs = rootfs(); + if (fs == NULL) + return -1; + + rel = path + 1; + + return fs_resolve_at(fs->rootfd, rel, flags); +} + +/* singleton function + * that returns a fixed + * descriptor of / + * + * used throughout, for + * repeated integrity checks + */ +struct filesystem * +rootfs(void) +{ + static struct filesystem global_fs; + static int fs_initialised = 0; + + if (!fs_initialised) { + + global_fs.rootfd = + open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + + if (global_fs.rootfd < 0) + return NULL; + + fs_initialised = 1; + } + + return &global_fs; +} + +/* filesystem sandboxing. + * (in userspace) + */ +int +fs_resolve_at(int dirfd, const char *path, int flags) +{ + int nextfd = -1; + int curfd; + const char *p; + char name[256]; + int saved_errno = errno; + int r; + int is_last; + + if (dirfd < 0 || path == NULL || *path == '\0') { + errno = EINVAL; + return -1; + } + + p = path; + curfd = dirfd; /* start here */ + + for (;;) { + r = fs_next_component(&p, name, sizeof(name)); + if (r < 0) + goto err; + if (r == 0) + break; + + is_last = (*p == '\0'); + + nextfd = fs_open_component(curfd, name, flags, is_last); + if (nextfd < 0) + goto err; + + /* close previous fd IF it is not the original input */ + if (curfd != dirfd) { + (void) close_on_eintr(curfd); + } + + curfd = nextfd; + nextfd = -1; + } + + errno = saved_errno; + return curfd; + +err: + saved_errno = errno; + + if (nextfd >= 0) + (void) close_on_eintr(nextfd); + + /* close curfd only if it's not the original */ + if (curfd != dirfd && curfd >= 0) + (void) close_on_eintr(curfd); + + errno = saved_errno; + return -1; +} + +int +fs_next_component(const char **p, + char *name, size_t namesz) +{ + const char *s = *p; + size_t len = 0; +#if defined(PATH_LEN) && \ +(PATH_LEN) >= 256 + size_t maxlen = PATH_LEN; +#else + size_t maxlen = 4096; +#endif + + while (*s == '/') + s++; + + if (*s == '\0') { + *p = s; + return 0; + } + + while (s[len] != '/' && s[len] != '\0') + len++; + + if (len == 0 || len >= namesz || + len >= maxlen) { + errno = ENAMETOOLONG; + return -1; + } + + memcpy(name, s, len); + name[len] = '\0'; + + /* reject . and .. */ + if ((name[0] == '.' && name[1] == '\0') || + (name[0] == '.' && name[1] == '.' && name[2] == '\0')) { + errno = EPERM; + return -1; + } + + *p = s + len; + return 1; +} + +int +fs_open_component(int dirfd, const char *name, + int flags, int is_last) +{ + int fd; + struct stat st; + + fd = openat2p(dirfd, name, + (is_last ? flags : (O_RDONLY | O_DIRECTORY)) | + O_NOFOLLOW | O_CLOEXEC, (flags & O_CREAT) ? 0600 : 0); + + /* the patient always lies + */ + if (!is_last) { + + if (fd < 0) { + errno = EBADF; + return -1; + } + + if (fstat(fd, &st) < 0) + return -1; + + if (!S_ISDIR(st.st_mode)) { + + (void) close_on_eintr(fd); + errno = ENOTDIR; + return -1; + } + } + + return fd; +} + +int +fs_dirname_basename(const char *path, + char **dir, char **base, + int allow_relative) +{ + char *buf; + char *slash; + size_t len; + int rval; +#if defined(PATH_LEN) && \ +(PATH_LEN) >= 256 + size_t maxlen = PATH_LEN; +#else + size_t maxlen = 4096; +#endif + + if (path == NULL || dir == NULL || base == NULL) + return -1; + + if (slen(path, maxlen, &len) < 0) + return -1; + + buf = malloc(len + 1); + if (buf == NULL) + return -1; + + memcpy(buf, path, len + 1); + + /* strip trailing slashes */ + while (len > 1 && buf[len - 1] == '/') + buf[--len] = '\0'; + + slash = strrchr(buf, '/'); + + if (slash) { + + *slash = '\0'; + *dir = buf; + *base = slash + 1; + + if (**dir == '\0') { + (*dir)[0] = '/'; + (*dir)[1] = '\0'; + } + } else if (allow_relative) { + + *dir = strdup("."); + *base = buf; + } else { + errno = EINVAL; + free(buf); + return -1; + } + + return 0; +} + +/* portable wrapper for use of openat2 on linux, + * with fallback for others e.g. openbsd + * + * BONUS: arg checks + * TODO: consider EINTR/EAGAIN retry loop + */ +int +openat2p(int dirfd, const char *path, + int flags, mode_t mode) +{ +#ifdef __linux__ + struct open_how how = { + .flags = flags, + .mode = mode, + .resolve = + RESOLVE_BENEATH | + RESOLVE_NO_SYMLINKS | + RESOLVE_NO_MAGICLINKS + }; + int saved_errno = errno; + int rval; +#endif + + if (dirfd < 0) { + errno = EBADF; + return -1; + } + + if (path == NULL) { + errno = EFAULT; + return -1; + } + +retry: + errno = 0; + +#ifdef __linux__ + /* more secure than regular openat, + * but linux-only at the time of writing + */ + rval = syscall(SYS_openat2, dirfd, path, &how, sizeof(how)); +#else + /* less secure, but e.g. openbsd + * doesn't have openat2 yet + */ + rval = openat(dirfd, path, flags, mode); +#endif + if (rval == -1 && ( + errno == EINTR || + errno == EAGAIN || + errno == EWOULDBLOCK || + errno == ETXTBSY)) + goto retry; + + if (rval >= 0) + errno = saved_errno; + + return rval; +} + +int +mkdirat_on_eintr( /* <-- say that 10 times to please the demon */ + int dirfd, + const char *path, mode_t mode) +{ + int saved_errno = errno; + int rval; + + if (dirfd < 0) { + errno = EBADF; + return -1; + } + + if (path == NULL) { + errno = EFAULT; + return -1; + } + +retry: + errno = 0; + rval = mkdirat(dirfd, path, mode); + + if (rval == -1 && ( + errno == EINTR || + errno == EAGAIN || + errno == EWOULDBLOCK || + errno == ETXTBSY)) + goto retry; + + if (rval >= 0) + errno = saved_errno; + + return rval; +} diff --git a/util/libreboot-utils/lib/io.c b/util/libreboot-utils/lib/io.c new file mode 100644 index 00000000..94bde87e --- /dev/null +++ b/util/libreboot-utils/lib/io.c @@ -0,0 +1,673 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * I/O functions specific to nvmutil. + */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "../include/common.h" + +void +open_gbe_file(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[x->i]; + struct xfile *f = &x->f; + + int _flags; + + xopen(&f->gbe_fd, f->fname, + cmd->flags | O_BINARY | + O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, &f->gbe_st); + + if (f->gbe_st.st_nlink > 1) + err(EINVAL, + "%s: warning: file has multiple (%lu) hard links\n", + f->fname, (size_t)f->gbe_st.st_nlink); + + if (f->gbe_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 allows POSIX write() to ignore + * the current write offset and write at EOF, + * which would break positional read/write + */ + + if (_flags & O_APPEND) + err(EIO, "%s: O_APPEND flag", f->fname); + + f->gbe_file_size = f->gbe_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); +} + +void +copy_gbe(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + read_file(); + + if (f->gbe_file_size == SIZE_8KB) + return; + + memcpy(f->buf + (size_t)GBE_PART_SIZE, + f->buf + (size_t)(f->gbe_file_size >> 1), + (size_t)GBE_PART_SIZE); +} + +void +read_file(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + struct stat _st; + ssize_t _r; + + /* 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); + + /* needs sync, for verification + */ + if (fsync_on_eintr(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 (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 = &x->cmd[x->i]; + struct xfile *f = &x->f; + + size_t p; + unsigned char update_checksum; + + if ((cmd->flags & O_ACCMODE) == O_RDONLY) + return; + + if (same_file(f->tmp_fd, &f->tmp_st, 0) < 0) + err(errno, "%s: file inode/device changed", f->tname); + + if (same_file(f->gbe_fd, &f->gbe_st, 1) < 0) + err(errno, "%s: file has changed", f->fname); + + 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(size_t p, int rw_type, + const char *rw_type_str) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[x->i]; + struct xfile *f = &x->f; + + ssize_t rval; + + off_t file_offset; + + size_t gbe_rw_size; + unsigned char *mem_offset; + + 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, (size_t)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, (size_t)p); + + if ((size_t)rval != gbe_rw_size) + err(EIO, "%s: partial %s: part %lu", + f->fname, rw_type_str, (size_t)p); +} + +void +write_to_gbe_bin(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[x->i]; + struct xfile *f = &x->f; + + int saved_errno; + int mv; + + if ((cmd->flags & O_ACCMODE) != O_RDWR) + return; + + write_gbe_file(); + + /* We may otherwise read from + * cache, so we must sync. + */ + + if (fsync_on_eintr(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); + + saved_errno = errno; + + if (close_on_eintr(f->tmp_fd) == -1) { + f->tmp_fd = -1; + + fprintf(stderr, "FAIL: %s: close\n", f->tname); + f->io_err_gbe_bin = 1; + } + f->tmp_fd = -1; + + if (close_on_eintr(f->gbe_fd) == -1) { + f->gbe_fd = -1; + + fprintf(stderr, "FAIL: %s: close\n", f->fname); + f->io_err_gbe_bin = 1; + } + f->gbe_fd = -1; + + errno = saved_errno; + + /* tmpfile written, now we + * rename it back to the main file + * (we do atomic writes) + */ + + 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 { + + /* removed by rename + */ + + if (f->tname != NULL) { + free(f->tname); + f->tname = NULL; + } + + f->tname = NULL; + } + } + + 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(size_t p) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[x->i]; + struct xfile *f = &x->f; + + ssize_t rval; + + size_t gbe_rw_size; + + off_t file_offset; + unsigned char *mem_offset; + + unsigned char *buf_restore; + + 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 (same_file(f->tmp_fd, &f->tmp_st, 0) < 0) + err(errno, "%s: file inode/device changed", f->tname); + + if (same_file(f->gbe_fd, &f->gbe_st, 1) < 0) + err(errno, "%s: file changed during write", f->fname); + + 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 ((size_t)rval != gbe_rw_size) + f->rw_check_partial_read[p] = f->io_err_gbe = 1; + else if (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 = &x->f; + + size_t p; + + 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, (size_t)p); + if (f->rw_check_partial_read[p]) + fprintf(stderr, + "%s: partial pread: p%lu (post-verification)\n", + f->fname, (size_t)p); + if (f->rw_check_bad_part[p]) + fprintf(stderr, + "%s: pwrite: corrupt write on p%lu\n", + f->fname, (size_t)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, (size_t)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", + (size_t)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 = &x->f; + + int rval; + + int saved_errno; + int tmp_gbe_bin_exists; + + char *dest_tmp; + int dest_fd; + + /* 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) { + + /* rename on 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; + + /* + * OR, 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 + */ + if (new_tmpfile(&dest_fd, &f->fname) < 1) + goto ret_gbe_mv; + 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 (fsync_on_eintr(dest_fd) == -1) + goto ret_gbe_mv; + + if (close_on_eintr(dest_fd) == -1) { + dest_fd = -1; + goto ret_gbe_mv; + } + dest_fd = -1; + + 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; + } + + if (dest_tmp != NULL) { + free(dest_tmp); + dest_tmp = NULL; + } + + dest_tmp = NULL; + +ret_gbe_mv: + + /* TODO: this whole section is bloat. + it can be generalised + */ + + if (f->gbe_fd > -1) { + if (close_on_eintr(f->gbe_fd) < 0) { + f->gbe_fd = -1; + rval = -1; + } + f->gbe_fd = -1; + + if (fsync_dir(f->fname) < 0) { + f->io_err_gbe_bin = 1; + rval = -1; + } + } + + if (f->tmp_fd > -1) { + if (close_on_eintr(f->tmp_fd) < 0) { + f->tmp_fd = -1; + 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(size_t p, const char *f_op) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + off_t gbe_off; + + gbe_off = gbe_x_offset(p, f_op, "mem", + GBE_PART_SIZE, GBE_WORK_SIZE); + + return (unsigned char *) + (f->buf + (size_t)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. + */ +off_t +gbe_file_offset(size_t p, const char *f_op) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + off_t gbe_file_half_size; + + 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(size_t p, const char *f_op, const char *d_type, + off_t nsize, off_t ncmp) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + off_t off; + + check_bin(p, "part number"); + + 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; +} + +ssize_t +rw_gbe_file_exact(int fd, unsigned char *mem, size_t nrw, + off_t off, int rw_type) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + ssize_t r; + + 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 ((size_t)(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 > (size_t)(f->gbe_file_size - off)) + goto err_rw_gbe_file_exact; + + if (nrw > (size_t)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; +} diff --git a/util/libreboot-utils/lib/mkhtemp.c b/util/libreboot-utils/lib/mkhtemp.c new file mode 100644 index 00000000..7c2f1fde --- /dev/null +++ b/util/libreboot-utils/lib/mkhtemp.c @@ -0,0 +1,1133 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * Hardened mktemp (be nice to the demon). + */ + +#if defined(__linux__) && !defined(_GNU_SOURCE) +/* for openat2 syscall on linux */ +#define _GNU_SOURCE 1 +#endif + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/* for openat2: */ +#ifdef __linux__ +#include <linux/openat2.h> +#include <sys/syscall.h> +#endif + +#include "../include/common.h" + +int +new_tmpfile(int *fd, char **path) +{ + return new_tmp_common(fd, path, MKHTEMP_FILE); +} + +int +new_tmpdir(int *fd, char **path) +{ + return new_tmp_common(fd, path, MKHTEMP_DIR); +} + +int +new_tmp_common(int *fd, char **path, int type) +{ +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + size_t maxlen = PATH_LEN; +#else + size_t maxlen = 4096; +#endif + struct stat st; + + char suffix[] = + "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + char *tmpdir = NULL; + + int close_errno; + size_t dirlen; + size_t destlen; + char *dest = NULL; /* final path (will be written into "path") */ + int saved_errno = errno; + int dirfd = -1; + const char *fname = NULL; + + struct stat st_dir_initial; + + if (path == NULL || fd == NULL) { + errno = EFAULT; + goto err; + } + + /* don't mess with someone elses file */ + if (*fd >= 0) { + errno = EEXIST; + goto err; + } + + /* regarding **path: + * the pointer (to the pointer) + * must nott be null, but we don't + * care about the pointer it points + * to. you should expect it to be + * replaced upon successful return + * + * (on error, it will not be touched) + */ + + + *fd = -1; + +#if defined(PERMIT_NON_STICKY_ALWAYS) && \ + ((PERMIT_NON_STICKY_ALWAYS) > 0) + tmpdir = env_tmpdir(PERMIT_NON_STICKY_ALWAYS); +#else + tmpdir = env_tmpdir(0); +#endif + if (tmpdir == NULL) + goto err; + + if (slen(tmpdir, maxlen, &dirlen) < 0) + goto err; + if (*tmpdir == '\0') + goto err; + if (*tmpdir != '/') + goto err; + + /* sizeof adds an extra byte, useful + * because we also want '.' or '/' + */ + destlen = dirlen + sizeof(suffix); + if (destlen > maxlen - 1) { + errno = EOVERFLOW; + goto err; + } + + dest = malloc(destlen + 1); + if (dest == NULL) { + errno = ENOMEM; + goto err; + } + + memcpy(dest, tmpdir, dirlen); + *(dest + dirlen) = '/'; + memcpy(dest + dirlen + 1, suffix, sizeof(suffix) - 1); + *(dest + destlen) = '\0'; + + fname = dest + dirlen + 1; + + dirfd = fs_open(tmpdir, + O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + goto err; + + if (fstat(dirfd, &st_dir_initial) < 0) + goto err; + + *fd = mkhtemp(fd, &st, dest, dirfd, + fname, &st_dir_initial, type); + if (*fd < 0) + goto err; + + if (dirfd >= 0) { + close_errno = errno; + (void) close_on_eintr(dirfd); + errno = close_errno; + dirfd = -1; + } + + errno = saved_errno; + *path = dest; + + return 0; + +err: + + if (errno != saved_errno) + saved_errno = errno; + else + saved_errno = errno = EIO; + + if (dest != NULL) { + free(dest); + dest = NULL; + } + + if (dirfd >= 0) { + close_errno = errno; + (void) close_on_eintr(dirfd); + errno = close_errno; + dirfd = -1; + } + + if (*fd >= 0) { + close_errno = errno; + (void) close_on_eintr(*fd); + errno = close_errno; + *fd = -1; + } + + errno = saved_errno; + return -1; +} + + +/* hardened TMPDIR parsing + */ + +char * +env_tmpdir(int bypass_all_sticky_checks) +{ + char *t; + int allow_noworld_unsticky; + int saved_errno = errno; + + t = getenv("TMPDIR"); + + if (t != NULL && *t != '\0') { + + if (tmpdir_policy(t, + &allow_noworld_unsticky) < 0) { + errno = EPERM; + return NULL; /* errno already set */ + } + + if (!world_writeable_and_sticky(t, + allow_noworld_unsticky, + bypass_all_sticky_checks)) { + errno = EPERM; + return NULL; + } + + errno = saved_errno; + return t; + } + + allow_noworld_unsticky = 0; + + if (world_writeable_and_sticky("/tmp", + allow_noworld_unsticky, + bypass_all_sticky_checks)) { + + errno = saved_errno; + return "/tmp"; + } + + if (world_writeable_and_sticky("/var/tmp", + allow_noworld_unsticky, + bypass_all_sticky_checks)) { + + errno = saved_errno; + return "/var/tmp"; + } + + if (errno == saved_errno) + errno = EPERM; + + return NULL; +} + +int +tmpdir_policy(const char *path, + int *allow_noworld_unsticky) +{ + int saved_errno = errno; + int r; + + if (path == NULL || + allow_noworld_unsticky == NULL) { + + errno = EFAULT; + return -1; + } + + *allow_noworld_unsticky = 1; + + r = same_dir(path, "/tmp"); + if (r < 0) + goto err_tmpdir_policy; + if (r > 0) + *allow_noworld_unsticky = 0; + + r = same_dir(path, "/var/tmp"); + if (r < 0) + goto err_tmpdir_policy; + if (r > 0) + *allow_noworld_unsticky = 0; + + errno = saved_errno; + return 0; + +err_tmpdir_policy: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +int +same_dir(const char *a, const char *b) +{ + int fd_a = -1; + int fd_b = -1; + + struct stat st_a; + struct stat st_b; + + int saved_errno = errno; + int rval_scmp; + +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + size_t maxlen = (PATH_LEN); +#else + size_t maxlen = 4096; +#endif + + /* optimisation: if both dirs + are the same, we don't need + to check anything. sehr schnell: + */ + if (scmp(a, b, maxlen, &rval_scmp) < 0) + goto err_same_dir; + /* bonus: scmp checks null for us */ + if (rval_scmp == 0) + goto success_same_dir; + + fd_a = fs_open(a, O_RDONLY | O_DIRECTORY); + if (fd_a < 0) + goto err_same_dir; + + fd_b = fs_open(b, O_RDONLY | O_DIRECTORY); + if (fd_b < 0) + goto err_same_dir; + + if (fstat(fd_a, &st_a) < 0) + goto err_same_dir; + + if (fstat(fd_b, &st_b) < 0) + goto err_same_dir; + + if (st_a.st_dev == st_b.st_dev && + st_a.st_ino == st_b.st_ino) { + + (void) close_on_eintr(fd_a); + (void) close_on_eintr(fd_b); + +success_same_dir: + + /* SUCCESS + */ + + errno = saved_errno; + return 1; + } + + (void) close_on_eintr(fd_a); + (void) close_on_eintr(fd_b); + + /* FAILURE (logical) + */ + + errno = saved_errno; + return 0; + +err_same_dir: + + /* FAILURE (probably syscall) + */ + + if (fd_a >= 0) + (void) close_on_eintr(fd_a); + if (fd_b >= 0) + (void) close_on_eintr(fd_b); + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +/* bypass_all_sticky_checks: if set, + disable stickiness checks (libc behaviour) + (if not set: leah behaviour) + + allow_noworld_unsticky: + allow non-sticky files if not world-writeable + (still block non-sticky in standard TMPDIR) +*/ +int +world_writeable_and_sticky( + const char *s, + int allow_noworld_unsticky, + int bypass_all_sticky_checks) +{ + struct stat st; + int dirfd = -1; + + int saved_errno = errno; + + if (s == NULL || *s == '\0') { + errno = EINVAL; + goto sticky_hell; + } + + /* mitigate symlink attacks* + */ + dirfd = fs_open(s, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + goto sticky_hell; + + if (fstat(dirfd, &st) < 0) + goto sticky_hell; + + if (!S_ISDIR(st.st_mode)) { + errno = ENOTDIR; + goto sticky_hell; + } + + /* must be fully executable + * by everyone, or openat2 + * becomes unreliable** + */ + if (!(st.st_mode & S_IXUSR) || + !(st.st_mode & S_IXGRP) || + !(st.st_mode & S_IXOTH)) { + + errno = EACCES; + goto sticky_hell; + } + + /* *normal-**ish mode (libc): + */ + + if (bypass_all_sticky_checks) + goto sticky_heaven; /* normal == no security */ + + /* unhinged leah mode: + */ + + if (st.st_mode & S_IWOTH) { /* world writeable */ + + /* if world-writeable, only + * allow sticky files + */ + if (st.st_mode & S_ISVTX) + goto sticky_heaven; /* sticky */ + + errno = EPERM; + goto sticky_hell; /* not sticky */ + } + + /* non-world-writeable, so + * stickiness is do-not-care + */ + if (allow_noworld_unsticky) + goto sticky_heaven; /* sticky! */ + + goto sticky_hell; /* heaven visa denied */ + +sticky_heaven: +/* i like the one in hamburg better */ + + if (dirfd >= 0) + (void) close_on_eintr(dirfd); + + errno = saved_errno; + + return 1; + +sticky_hell: + + if (errno == saved_errno) + errno = EPERM; + + saved_errno = errno; + + if (dirfd >= 0) + (void) close_on_eintr(dirfd); + + errno = saved_errno; + + return 0; +} + +/* mk(h)temp - hardened mktemp. + * like mkstemp, but (MUCH) harder. + * + * designed to resist TOCTOU attacks + * e.g. directory race / symlink attack + * + * extremely strict and even implements + * some limited userspace-level sandboxing, + * similar in spirit to openbsd unveil, + * though unveil is from kernel space. + * + * supports both files and directories. + * file: type = MKHTEMP_FILE (0) + * dir: type = MKHTEMP_DIR (1) + * + * DESIGN NOTES: + * + * caller is expected to handle + * cleanup e.g. free(), on *st, + * *template, *fname (all of the + * pointers). ditto fd cleanup. + * + * some limited cleanup is + * performed here, e.g. directory/file + * cleanup on error in mkhtemp_try_create + * + * we only check if these are not NULL, + * and the caller is expected to take + * care; without too many conditions, + * these functions are more flexible, + * but some precauttions are taken: + * + * when used via the function new_tmpfile + * or new_tmpdir, thtis is extremely strict, + * much stricter than previous mktemp + * variants. for example, it is much + * stricter about stickiness on world + * writeable directories, and it enforces + * file ownership under hardened mode + * (only lets you touch your own files/dirs) + */ +int +mkhtemp(int *fd, + struct stat *st, + char *template, + int dirfd, + const char *fname, + struct stat *st_dir_initial, + int type) +{ + size_t len = 0; + size_t xc = 0; + size_t fname_len = 0; + + char *fname_copy = NULL; + char *p; + + size_t retries; + + int close_errno; + int saved_errno = errno; + +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + size_t max_len = PATH_LEN; +#else + size_t max_len = 4096; +#endif + int r; + char *end; + + if (fd == NULL || + template == NULL || + fname == NULL || + st_dir_initial == NULL) { + + errno = EFAULT; + return -1; + } + + /* we do not mess with an + open descriptor. + */ + if (*fd >= 0) { + errno = EEXIST; /* leave their file alone */ + return -1; + } + + if (dirfd < 0) { + errno = EBADF; + return -1; + } + + if (slen(template, max_len, &len) < 0) + return -1; + + if (len >= max_len) { + errno = EMSGSIZE; + return -1; + } + + if (slen(fname, max_len, &fname_len) < 0) + return -1; + + if (fname_len == 0) { + errno = EINVAL; + return -1; + } + + if (strrchr(fname, '/') != NULL) { + errno = EINVAL; + return -1; + } + + /* count trailing X */ + end = template + len; + while (end > template && *--end == 'X') + xc++; + + if (xc < 12 || xc > len) { + errno = EINVAL; + return -1; + } + + if (fname_len > len) { + errno = EOVERFLOW; + return -1; + } + + if (memcmp(fname, + template + len - fname_len, + fname_len) != 0) { + errno = EINVAL; + return -1; + } + + fname_copy = malloc(fname_len + 1); + if (fname_copy == NULL) { + errno = ENOMEM; + goto err; + } + + /* fname_copy = suffix region only; p points to trailing XXXXXX */ + memcpy(fname_copy, + template + len - fname_len, + fname_len + 1); + p = fname_copy + fname_len - xc; + + for (retries = 0; retries < MKHTEMP_RETRY_MAX; retries++) { + + r = mkhtemp_try_create( + dirfd, + st_dir_initial, + fname_copy, + p, + xc, + fd, + st, + type); + + if (r == 0) { + if (retries >= MKHTEMP_SPIN_THRESHOLD) { + /* usleep can return EINTR */ + close_errno = errno; + usleep((useconds_t)rlong() & 0x3FF); + errno = close_errno; + } + continue; + } + if (r < 0) + goto err; + + /* success: copy final name back */ + memcpy(template + len - fname_len, + fname_copy, fname_len); + + errno = saved_errno; + goto success; + } + + errno = EEXIST; + +err: + if (*fd >= 0) { + close_errno = errno; + (void)close_on_eintr(*fd); + errno = close_errno; + *fd = -1; + } + +success: + + if (fname_copy != NULL) + free(fname_copy); + + return (*fd >= 0) ? *fd : -1; +} + +int +mkhtemp_try_create(int dirfd, + struct stat *st_dir_initial, + char *fname_copy, + char *p, + size_t xc, + int *fd, + struct stat *st, + int type) +{ + struct stat st_open; + int saved_errno = errno; + int close_errno; + int rval = -1; + + int file_created = 0; + int dir_created = 0; + + if (fd == NULL || st == NULL || p == NULL || fname_copy == NULL || + st_dir_initial == NULL) { + errno = EFAULT; + goto err; + } else if (*fd >= 0) { /* do not mess with someone else's file */ + errno = EEXIST; + goto err; + } + + if (mkhtemp_fill_random(p, xc) < 0) + goto err; + + if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0) + goto err; + + if (type == MKHTEMP_FILE) { + *fd = openat2p(dirfd, fname_copy, + O_RDWR | O_CREAT | O_EXCL | + O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, + 0600); + + /* O_CREAT and O_EXCL guarantees + * creation upon success + */ + if (*fd >= 0) + file_created = 1; + + } else { /* dir: MKHTEMP_DIR */ + + if (mkdirat_on_eintr(dirfd, fname_copy, 0700) < 0) + goto err; + + /* ^ NOTE: opening the directory here + will never set errno=EEXIST, + since we're not creating it */ + + dir_created = 1; + + /* do it again (mitigate directory race) */ + if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0) + goto err; + + *fd = openat2p(dirfd, fname_copy, + O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0); + if (*fd < 0) + goto err; + + if (fstat(*fd, &st_open) < 0) + goto err; + + if (!S_ISDIR(st_open.st_mode)) { + errno = ENOTDIR; + goto err; + } + + /* NOTE: could check nlink count here, + * but it's not very reliable here. skipped. + */ + + if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0) + goto err; + + } + + /* NOTE: openat2p and mkdirat_on_eintr + * already handled EINTR/EAGAIN looping + */ + + if (*fd < 0) { + if (errno == EEXIST) { + + rval = 0; + goto out; + } + goto err; + } + + if (fstat(*fd, &st_open) < 0) + goto err; + + if (type == MKHTEMP_FILE) { + + if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0) + goto err; + + if (secure_file(fd, st, &st_open, + O_APPEND, 1, 1, 0600) < 0) /* WARNING: only once */ + goto err; + + } else { /* dir: MKHTEMP_DIR */ + + if (fd_verify_identity(*fd, &st_open, st_dir_initial) < 0) + goto err; + + if (!S_ISDIR(st_open.st_mode)) { + errno = ENOTDIR; + goto err; + } + + if (is_owner(&st_open) < 0) + goto err; + + /* group/world writeable */ + if (st_open.st_mode & (S_IWGRP | S_IWOTH)) { + errno = EPERM; + goto err; + } + } + + errno = saved_errno; + rval = 1; + goto out; + +err: + close_errno = errno; + + if (fd != NULL && *fd >= 0) { + (void) close_on_eintr(*fd); + *fd = -1; + } + + if (file_created) + (void) unlinkat(dirfd, fname_copy, 0); + + if (dir_created) + (void) unlinkat(dirfd, fname_copy, AT_REMOVEDIR); + + errno = close_errno; + rval = -1; +out: + return rval; +} + +int +mkhtemp_fill_random(char *p, size_t xc) +{ + size_t chx = 0; + int rand_failures = 0; + + size_t r; + + int saved_rand_error = 0; + static char ch[] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + /* clamp rand to prevent modulo bias + * (reduced risk of entropy leak) + */ + size_t limit = ((size_t)-1) - (((size_t)-1) % (sizeof(ch) - 1)); + + int saved_errno = errno; + + if (p == NULL) { + errno = EFAULT; + goto err_mkhtemp_fill_random; + } + + for (chx = 0; chx < xc; chx++) { + + do { + saved_rand_error = errno; + rand_failures = 0; +retry_rand: + errno = 0; + + /* on bsd: uses arc4random + on linux: uses getrandom + on OLD linux: /dev/urandom + on old/other unix: /dev/urandom + */ + r = rlong(); + + if (errno > 0) { + if (++rand_failures <= 8) + goto retry_rand; + + goto err_mkhtemp_fill_random; + } + + rand_failures = 0; + errno = saved_rand_error; + + } while (r >= limit); + + p[chx] = ch[r % (sizeof(ch) - 1)]; + } + + errno = saved_errno; + return 0; + +err_mkhtemp_fill_random: + + if (errno == saved_errno) + errno = ECANCELED; + + return -1; +} + +/* WARNING: **ONCE** per file. + * + * !!! DO NOT RUN TWICE PER FILE. BEWARE OF THE DEMON !!! + * watch out for spikes! + */ +int secure_file(int *fd, + struct stat *st, + struct stat *expected, + int bad_flags, + int check_seek, + int do_lock, + mode_t mode) +{ + int flags; + struct stat st_now; + int saved_errno = errno; + /* you have been warned */ + if (fd == NULL) { + errno = EFAULT; + goto err_demons; + } + if (*fd < 0) { + errno = EBADF; + goto err_demons; + } + + if (st == NULL) { + errno = EFAULT; + goto err_demons; + } + + flags = fcntl(*fd, F_GETFL); + + if (flags == -1) + goto err_demons; + + if (bad_flags > 0) { + + /* e.g. O_APPEND breaks pwrite/pread + * by allowing offsets to be ignored */ + if (flags & bad_flags) { + errno = EPERM; + goto err_demons; + } + } + + if (expected != NULL) { + if (fd_verify_regular(*fd, expected, st) < 0) + goto err_demons; + } else { + if (fstat(*fd, &st_now) == -1) + goto err_demons; + + if (!S_ISREG(st_now.st_mode)) { + errno = EBADF; + goto err_demons; + } + + *st = st_now; + } + + if (check_seek) { + + /* check if it's seekable */ + if (lseek(*fd, 0, SEEK_CUR) == (off_t)-1) + goto err_demons; + } + + /* don't release the demon + */ + if (st->st_nlink != 1) { /***********/ + /* ( >:3 ) */ + errno = ELOOP; /* /| |\ */ /* don't let him out */ + goto err_demons; /* / \ */ + /***********/ + } + + if (st->st_uid != geteuid() && /* someone else's file */ + geteuid() != 0) { /* override for root */ + + errno = EPERM; + goto err_demons; + } + if (is_owner(st) < 0) + goto err_demons; + + /* world-writeable or group-ownership. + * if these are set, then others could + * modify the file (not secure) + */ + if (st->st_mode & (S_IWGRP | S_IWOTH)) { + errno = EPERM; + goto err_demons; + } + + if (do_lock) { + if (lock_file(*fd, flags) == -1) + goto err_demons; + + if (expected != NULL) { + if (fd_verify_identity(*fd, expected, &st_now) < 0) + goto err_demons; + } + } + + if (fchmod(*fd, mode) == -1) + goto err_demons; + + errno = saved_errno; + return 0; + +err_demons: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +int +fd_verify_regular(int fd, + const struct stat *expected, + struct stat *out) +{ + if (fd_verify_identity(fd, expected, out) < 0) + return -1; + + if (!S_ISREG(out->st_mode)) { + errno = EBADF; + return -1; + } + + return 0; /* regular file */ +} + +int +fd_verify_identity(int fd, + const struct stat *expected, + struct stat *out) +{ + struct stat st_now; + int saved_errno = errno; + + if (fd < 0 || expected == NULL) { + errno = EFAULT; + return -1; + } + + if (fstat(fd, &st_now) < 0) + return -1; + + if (st_now.st_dev != expected->st_dev || + st_now.st_ino != expected->st_ino) { + errno = ESTALE; + return -1; + } + + if (out != NULL) + *out = st_now; + + errno = saved_errno; + return 0; +} + +int +fd_verify_dir_identity(int fd, + const struct stat *expected) +{ + struct stat st_now; + int saved_errno = errno; + + if (fd < 0 || expected == NULL) { + errno = EFAULT; + return -1; + } + + if (fstat(fd, &st_now) < 0) + return -1; + + if (st_now.st_dev != expected->st_dev || + st_now.st_ino != expected->st_ino) { + errno = ESTALE; + return -1; + } + + if (!S_ISDIR(st_now.st_mode)) { + errno = ENOTDIR; + return -1; + } + + errno = saved_errno; + return 0; +} + +int +is_owner(struct stat *st) +{ + if (st == NULL) { + + errno = EFAULT; + return -1; + } + +#if ALLOW_ROOT_OVERRIDE + if (st->st_uid != geteuid() && /* someone else's file */ + geteuid() != 0) { /* override for root */ +#else + if (st->st_uid != geteuid()) { /* someone else's file */ +#endif /* and no root override */ + errno = EPERM; + return -1; + } + + return 0; +} + +int +lock_file(int fd, int flags) +{ + struct flock fl; + int saved_errno = errno; + + if (fd < 0) { + errno = EBADF; + goto err_lock_file; + } + + if (flags < 0) { + errno = EINVAL; + goto err_lock_file; + } + + memset(&fl, 0, sizeof(fl)); + + if ((flags & O_ACCMODE) == O_RDONLY) + fl.l_type = F_RDLCK; + else + fl.l_type = F_WRLCK; + + fl.l_whence = SEEK_SET; + + if (fcntl(fd, F_SETLK, &fl) == -1) + goto err_lock_file; + + saved_errno = errno; + return 0; + +err_lock_file: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} diff --git a/util/libreboot-utils/lib/num.c b/util/libreboot-utils/lib/num.c new file mode 100644 index 00000000..343350b0 --- /dev/null +++ b/util/libreboot-utils/lib/num.c @@ -0,0 +1,444 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * Numerical functions. + */ + +/* +TODO: properly handle errno in this file + */ + +#ifdef __OpenBSD__ +#include <sys/param.h> +#endif +#include <sys/types.h> +#if defined(FALLBACK_RAND_1989) && \ + (FALLBACK_RAND_1989) > 0 +#include <sys/time.h> +#endif + +#include <errno.h> +#if !((defined(__OpenBSD__) && (OpenBSD) >= 201) || \ + defined(__FreeBSD__) || \ + defined(__NetBSD__) || defined(__APPLE__)) +#include <fcntl.h> /* if not arc4random: /dev/urandom */ +#endif +#include <limits.h> +#include <stddef.h> +#include <string.h> +#if defined(FALLBACK_RAND_1989) && \ + (FALLBACK_RAND_1989) > 0 +#include <time.h> +#endif +#include <unistd.h> + +#include "../include/common.h" + +/* TODO: + * make this and errno handling more + * flexible + + in particular: + hextonum could be modified to + write into a buffer instead, + with the converted numbers, + of an arbitrary length + */ +unsigned short +hextonum(char ch_s) +{ + int saved_errno = errno; + + /* rlong() can return error, + but preserves errno if no + error. we need to detect + this because it handles + /dev/urandom sometimes + + therefore, if it's zero + at start, we know if there + was an err at the end, by + return value zero, if errno + was set; this is technically + valid, since zero is also + a valid random number! + + it's an edge case that i had + to fix. i'll rewrite the code + better later. for now, it + should be ok. + */ + errno = 0; + + unsigned char ch; + size_t rval; + + ch = (unsigned char)ch_s; + + if ((unsigned int)(ch - '0') <= 9) { + + rval = ch - '0'; + goto hextonum_success; + } + + ch |= 0x20; + + if ((unsigned int)(ch - 'a') <= 5) { + + rval = ch - 'a' + 10; + goto hextonum_success; + } + + if (ch == '?' || ch == 'x') { + + rval = rlong(); + if (errno > 0) + goto err_hextonum; + + goto hextonum_success; + } + + goto err_hextonum; + +hextonum_success: + + errno = saved_errno; + return (unsigned short)rval & 0xf; + +err_hextonum: + + if (errno == saved_errno) + errno = EINVAL; + else + return 17; /* 17 indicates getrandom/urandom fail */ + + return 16; /* invalid character */ + + /* caller just checks >15. */ +} + +/* Random numbers + */ + +/* when calling this: save errno + * first, then set errno to zero. + * on error, this function will + * set errno and possibly return + * + * rlong also preserves errno + * and leaves it unchanged on + * success, so if you do it + * right, you can detect error. + * this is because it uses + * /dev/urandom which can err. + * ditto getrandom (EINTR), + * theoretically. + */ + +size_t +rlong(void) +{ +#if !(defined(FALLBACK_RAND_1989) && \ + ((FALLBACK_RAND_1989) > 0)) +#if (defined(__OpenBSD__) && (OpenBSD) >= 201) || \ + defined(__FreeBSD__) || \ + defined(__NetBSD__) || defined(__APPLE__) + + int saved_errno = errno; + size_t rval; + + arc4random_buf(&rval, sizeof(size_t)); + + errno = saved_errno; + return rval; +#else + static int fd = -1; + static ssize_t nr = -1; + static size_t off = 0; +#if defined (BUFSIZ) + static char rbuf[BUFSIZ]; +#else +#ifndef PORTABLE + static char rbuf[4096]; +#elif ((PORTABLE) > 0) + static char rbuf[256]; /* scarce memory on old systems */ +#else + static char rbuf[4096]; /* typical 32-bit BUFSIZ */ +#endif +#endif + size_t rval; + ssize_t new_nr; + + int retries = 0; + int max_retries = 100; + + int saved_errno = errno; + +#if defined(__linux__) +#if defined(HAVE_GETRANDOM) || \ + defined(HAVE_GETRANDOM_SYSCALL) + + /* linux getrandom() + * + * we *can* use arc4random on + * modern linux, but not on + * every libc. better use the + * official linux function + */ + + if (fallback_rand_getrandom(&rval, sizeof(rval)) == 0) { + errno = saved_errno; + return rval; + } + + /* + * now fall back to urandom if getrandom failed: + */ +#endif +#endif + + /* reading from urandom is inherently + * unreliable on old systems, even if + * newer systems make it more reliable + * + * modern linux/bsd make it safe, but + * we have to assume that someone is + * compiling this on linux from 1999 + * + * this logic therefore applies various + * tricks to mitigate possible os bugs + */ + +retry_urandom_read: + + if (++retries > max_retries) + goto err_rlong; + + if (nr < 0 || nr < (ssize_t)sizeof(size_t)) { + + if (fd < 0) { + + fd = open("/dev/urandom", + O_RDONLY | O_BINARY | O_NOFOLLOW | + O_CLOEXEC | O_NOCTTY); + +#ifdef USE_OLD_DEV_RANDOM +#if (USE_OLD_DEV_RANDOM) > 0 + /* WARNING: + * /dev/random may block + * forever and does **NOT** + * guarantee better entropy + * on old systems + * + * only use it if needed + */ + + if (fd < 0) + fd = open("/dev/random", + O_RDONLY | O_BINARY | O_NOFOLLOW | + O_CLOEXEC | O_NOCTTY); +#endif +#endif + + if (fd < 0) + goto retry_urandom_read; + + retries = 0; + } + + new_nr = rw_file_exact(fd, (unsigned char *)rbuf, + sizeof(rbuf), 0, IO_READ, LOOP_EAGAIN, + LOOP_EINTR, MAX_ZERO_RW_RETRY, OFF_ERR); + + if (new_nr < 0 || new_nr < (ssize_t)sizeof(rbuf)) + goto retry_urandom_read; + + /* only reset buffer after successful refill */ + nr = new_nr; + off = 0; + + /* to mitigate file descriptor + * injection, we do not re-use + * the same descriptor each time + */ + (void) close_on_eintr(fd); + fd = -1; + } + + fd = -1; + retries = 0; + + memcpy(&rval, rbuf + off, sizeof(size_t)); + + nr -= (ssize_t)sizeof(size_t); + off += sizeof(size_t); + + errno = saved_errno; + + return rval; + +err_rlong: + + if (errno == saved_errno) + errno = EIO; + + return 0; + +#endif +#else /* FALLBACK_RAND_1989 */ + /* your computer is from a museum + */ + size_t mix = 0; + int nr = 0; + int saved_errno = errno; + + /* 100 times, for entropy + */ + for (nr = 0; nr < 100; nr++) + mix ^= fallback_rand_1989(); + + errno = saved_errno; + return mix; +#endif +} + +#if !(defined(FALLBACK_RAND_1989) && \ + ((FALLBACK_RAND_1989) > 0)) +#if defined(__linux__) +#if defined(HAVE_GETRANDOM) || \ + defined(HAVE_GETRANDOM_SYSCALL) +int /* yes, linux is a fallback */ +fallback_rand_getrandom(void *buf, size_t len) +{ + size_t off = 0; + ssize_t rval = -1; + int saved_errno = errno; + + if (!len) { + errno = EINVAL; + return -1; + } + + if (buf == NULL) { + errno = EFAULT; + return -1; + } + +#if defined(HAVE_GETRANDOM) || \ + defined(HAVE_GETRANDOM_SYSCALL) + + while (off < len) { + +#if defined(HAVE_GETRANDOM) + rval = (ssize_t)getrandom((char *)buf + off, len - off, 0); +#elif defined(HAVE_GETRANDOM_SYSCALL) + rval = (ssize_t)syscall(SYS_getrandom, + (char *)buf + off, len - off, 0); +#endif + + if (rval < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + + errno = EIO; + return -1; /* unsupported by kernel */ + } + + if (rval == 0) { + errno = EIO; + return -1; + } + + off += (size_t)rval; + } + + errno = saved_errno; + return 0; + +#else + (void)buf; + (void)len; + + errno = EIO; + return -1; +#endif +} +#endif +#endif +#else +size_t +fallback_rand_1989(void) +{ + static size_t mix = 0; + static size_t counter = 0; + + struct timeval tv; + + /* nobody should use this + * (not crypto-safe) + */ + + gettimeofday(&tv, NULL); + + mix ^= (size_t)tv.tv_sec + ^ (size_t)tv.tv_usec + ^ (size_t)getpid() + ^ (size_t)&mix + ^ counter++ + ^ entropy_jitter(); + + /* + * Stack addresses can vary between + * calls, thus increasing entropy. + */ + mix ^= (size_t)&mix; + mix ^= (size_t)&tv; + mix ^= (size_t)&counter; + + return mix; +} + +size_t +entropy_jitter(void) +{ + size_t mix; + + struct timeval a, b; + ssize_t mix_diff; + + int c; + + mix = 0; + + gettimeofday(&a, NULL); + + for (c = 0; c < 32; c++) { + + getpid(); + gettimeofday(&b, NULL); + + /* + * prevent negative numbers to prevent overflow, + * which would bias rand to large numbers + */ + mix_diff = (ssize_t)(b.tv_usec - a.tv_usec); + if (mix_diff < 0) + mix_diff = -mix_diff; + + mix ^= (size_t)(mix_diff); + + mix ^= (size_t)&mix; + + } + + return mix; +} +#endif + +void +check_bin(size_t a, const char *a_name) +{ + if (a > 1) + err(EINVAL, "%s must be 0 or 1, but is %lu", + a_name, (size_t)a); +} diff --git a/util/libreboot-utils/lib/state.c b/util/libreboot-utils/lib/state.c new file mode 100644 index 00000000..d06a8869 --- /dev/null +++ b/util/libreboot-utils/lib/state.c @@ -0,0 +1,252 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org> + * + * State machine (singleton) for nvmutil data. + */ + +#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 <unistd.h> + +#include "../include/common.h" + +struct xstate * +xstart(int argc, char *argv[]) +{ + static int first_run = 1; + static int pre_init = 0; + + static struct xstate us = { + /* DO NOT MESS THIS UP, OR THERE WILL BE DEMONS */ + { + { + CMD_DUMP, "dump", cmd_helper_dump, ARGC_3, + ARG_NOPART, + SKIP_CHECKSUM_READ, SKIP_CHECKSUM_WRITE, + NVM_SIZE, O_RDONLY + }, { + CMD_SETMAC, "setmac", cmd_helper_setmac, ARGC_3, + ARG_NOPART, + CHECKSUM_READ, CHECKSUM_WRITE, + NVM_SIZE, O_RDWR + }, { + CMD_SWAP, "swap", cmd_helper_swap, ARGC_3, + ARG_NOPART, + CHECKSUM_READ, SKIP_CHECKSUM_WRITE, + GBE_PART_SIZE, O_RDWR + }, { + CMD_COPY, "copy", cmd_helper_copy, ARGC_4, + ARG_PART, + CHECKSUM_READ, SKIP_CHECKSUM_WRITE, + GBE_PART_SIZE, O_RDWR + }, { + CMD_CAT, "cat", cmd_helper_cat, ARGC_3, + ARG_NOPART, + CHECKSUM_READ, SKIP_CHECKSUM_WRITE, + GBE_PART_SIZE, O_RDONLY + }, { + CMD_CAT16, "cat16", cmd_helper_cat16, ARGC_3, + ARG_NOPART, + CHECKSUM_READ, SKIP_CHECKSUM_WRITE, + GBE_PART_SIZE, O_RDONLY + }, { + CMD_CAT128, "cat128", cmd_helper_cat128, ARGC_3, + ARG_NOPART, + CHECKSUM_READ, SKIP_CHECKSUM_WRITE, + GBE_PART_SIZE, O_RDONLY + } + }, + + /* ->mac */ + {NULL, "xx:xx:xx:xx:xx:xx", {0, 0, 0}}, /* .str, .rmac, .mac_buf */ + + /* .f */ + {0}, + + /* .argv0 (for our getprogname implementation) */ + NULL, + + /* ->i (index to cmd[]) */ + 0, + + /* .no_cmd (set 0 when a command is found) */ + 1, + + /* .cat (cat helpers set this) */ + -1 + + }; + + if (!first_run) { + if (pre_init) + err_no_cleanup(ECANCELED, + "Outside access to state during init"); + + first_run = 0; + + return &us; + } + + if (argc < 3) + err_no_cleanup(EINVAL, "xstart: Too few arguments"); + if (argv == NULL) + err_no_cleanup(EINVAL, "xstart: NULL argv"); + + first_run = 0; + pre_init = 1; + + us.f.buf = us.f.real_buf; + + us.argv0 = argv[0]; + us.f.fname = argv[1]; + + if (new_tmpfile(&us.f.tmp_fd, &us.f.tname) < 0) + err_no_cleanup(errno, "xstart: cannot create tmpfile"); + + /* parse user command */ +/* TODO: CHECK ACCESSES VIA xstatus() */ + set_cmd(argc, argv); + set_cmd_args(argc, argv); + + if (us.f.tname == NULL) + err_no_cleanup(errno, "x->f.tname null"); + if (*us.f.tname == '\0') + err_no_cleanup(errno, "x->f.tname empty"); + + if (fstat(us.f.tmp_fd, &us.f.tmp_st) < 0) + err_no_cleanup(errno, "%s: stat", us.f.tname); + + memset(us.f.real_buf, 0, sizeof(us.f.real_buf)); + memset(us.f.bufcmp, 0, sizeof(us.f.bufcmp)); + + /* for good measure */ + memset(us.f.pad, 0, sizeof(us.f.pad)); + + pre_init = 0; + + return &us; +} + +struct xstate * +xstatus(void) +{ + struct xstate *x = xstart(0, NULL); + + if (x == NULL) + err_no_cleanup(EACCES, "NULL pointer to xstate"); + + sanitize_command_list(); + + return x; +} + +void +err(int nvm_errval, const char *msg, ...) +{ + struct xstate *x = xstatus(); + + va_list args; + + if (errno == 0) + errno = nvm_errval; + if (!errno) + errno = ECANCELED; + + (void)exit_cleanup(); + + if (x != NULL) + fprintf(stderr, "%s: ", getnvmprogname()); + + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + + fprintf(stderr, ": %s\n", strerror(errno)); + + exit(EXIT_FAILURE); +} + +const char * +getnvmprogname(void) +{ + struct xstate *x = xstatus(); + + const char *p; + static char fallback[] = "nvmutil"; + + char *rval = fallback; + + if (x != NULL) { + if (x->argv0 == NULL || *x->argv0 == '\0') + return ""; + + rval = x->argv0; + } + + p = strrchr(rval, '/'); + + if (p) + return p + 1; + else + return rval; +} + +int +exit_cleanup(void) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + int close_err; + int saved_errno; + + close_err = 0; + saved_errno = errno; + + if (x != NULL) { + f = &x->f; + + if (f->gbe_fd > -1) { + if (close_on_eintr(f->gbe_fd) == -1) { + f->gbe_fd = -1; + close_err = 1; + } + f->gbe_fd = -1; + } + + if (f->tmp_fd > -1) { + if (close_on_eintr(f->tmp_fd) == -1) { + f->tmp_fd = -1; + close_err = 1; + } + f->tmp_fd = -1; + } + + if (f->tname != NULL) { + if (unlink(f->tname) == -1) + close_err = 1; + } + + f->tmp_fd = -1; + } + + if (saved_errno) + errno = saved_errno; + + if (close_err) + return -1; + + return 0; +} diff --git a/util/libreboot-utils/lib/string.c b/util/libreboot-utils/lib/string.c new file mode 100644 index 00000000..cb37c1ba --- /dev/null +++ b/util/libreboot-utils/lib/string.c @@ -0,0 +1,146 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * String functions + */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#include "../include/common.h" + +/* scmp() - strict string comparison + * + * strict string comparison + * similar to strncmp, but null and + * unterminated inputs do not produce + * a return value; on error, errno is + * set and -1 is returned. + * + * the real return value is stored in + * the 4th argument by pointer. + * + * the value at rval pointer is set, + * only upon success. callers should + * check the return value accordingly. + */ + +int +scmp(const char *a, + const char *b, + size_t maxlen, + int *rval) +{ + size_t ch; + unsigned char ac; + unsigned char bc; + + if (a == NULL || + b == NULL || + rval == NULL) { + + errno = EFAULT; + return -1; + } + + for (ch = 0; ch < maxlen; ch++) { + + ac = (unsigned char)a[ch]; + bc = (unsigned char)b[ch]; + + if (ac != bc) { + *rval = ac - bc; + return 0; + } + + if (ac == '\0') { + *rval = 0; + return 0; + } + } + + /* block unterminated strings */ + errno = EFAULT; + return -1; +} + +/* slen() - strict strict length + * + * strict string length calculation + * similar to strnlen, but null and + * unterminated inputs do not produce + * a return value; on error, errno is + * set and -1 is returned. + * + * the real return value is stored in + * the 3rd argument by pointer. + * + * the value at rval pointer is set, + * only upon success. callers should + * check the return value accordingly. + */ + +int +slen(const char *s, + size_t maxlen, + size_t *rval) +{ + size_t ch; + + if (s == NULL || + rval == NULL) { + + errno = EFAULT; + return -1; + } + + for (ch = 0; + ch < maxlen && s[ch] != '\0'; + ch++); + + if (ch == maxlen) { + /* unterminated */ + errno = EFAULT; + return -1; + } + + *rval = ch; + return 0; +} + +/* the one for nvmutil state is in state.c */ +/* this one just exits */ +void +err_no_cleanup(int nvm_errval, const char *msg, ...) +{ + va_list args; + +#if defined(__OpenBSD__) && defined(OpenBSD) +#if (OpenBSD) >= 509 + if (pledge("stdio", NULL) == -1) + fprintf(stderr, "pledge failure during exit"); +#endif +#endif + + if (!errno) + errno = ECANCELED; + + fprintf(stderr, "nvmutil: "); + + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + + fprintf(stderr, ": %s\n", strerror(errno)); + + exit(EXIT_FAILURE); +} + diff --git a/util/libreboot-utils/lib/usage.c b/util/libreboot-utils/lib/usage.c new file mode 100644 index 00000000..3b0614e8 --- /dev/null +++ b/util/libreboot-utils/lib/usage.c @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com> + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + */ + +#include <errno.h> +#include <stdio.h> + +#include "../include/common.h" + +void +usage(void) +{ + const char *util = getnvmprogname(); + + fprintf(stderr, + "Modify Intel GbE NVM images e.g. set MAC\n" + "USAGE:\n" + "\t%s FILE dump\n" + "\t%s FILE setmac [MAC]\n" + "\t%s FILE swap\n" + "\t%s FILE copy 0|1\n" + "\t%s FILE cat\n" + "\t%s FILE cat16\n" + "\t%s FILE cat128\n", + util, util, util, util, + util, util, util); + + err(EINVAL, "Too few arguments"); +} diff --git a/util/libreboot-utils/lib/word.c b/util/libreboot-utils/lib/word.c new file mode 100644 index 00000000..f84dae6a --- /dev/null +++ b/util/libreboot-utils/lib/word.c @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org> + * + * Manipulate Intel GbE NVM words, which are 16-bit little + * endian in the files (MAC address words are big endian). + */ + +#include <sys/types.h> + +#include <errno.h> +#include <stddef.h> + +#include "../include/common.h" + +unsigned short +nvm_word(size_t pos16, size_t p) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t pos; + + check_nvm_bound(pos16, p); + pos = (pos16 << 1) + (p * GBE_PART_SIZE); + + return (unsigned short)f->buf[pos] | + ((unsigned short)f->buf[pos + 1] << 8); +} + +void +set_nvm_word(size_t pos16, size_t p, unsigned short val16) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t pos; + + check_nvm_bound(pos16, p); + pos = (pos16 << 1) + (p * GBE_PART_SIZE); + + f->buf[pos] = (unsigned char)(val16 & 0xff); + f->buf[pos + 1] = (unsigned char)(val16 >> 8); + + set_part_modified(p); +} + +void +set_part_modified(size_t p) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + check_bin(p, "part number"); + f->part_modified[p] = 1; +} + +void +check_nvm_bound(size_t c, size_t p) +{ + /* Block out of bound NVM access + */ + + check_bin(p, "part number"); + + if (c >= NVM_WORDS) + err(ECANCELED, "check_nvm_bound: out of bounds %lu", + (size_t)c); +} |
