summaryrefslogtreecommitdiff
path: root/util/libreboot-utils/lib
diff options
context:
space:
mode:
Diffstat (limited to 'util/libreboot-utils/lib')
-rw-r--r--util/libreboot-utils/lib/checksum.c108
-rw-r--r--util/libreboot-utils/lib/command.c563
-rw-r--r--util/libreboot-utils/lib/file.c1136
-rw-r--r--util/libreboot-utils/lib/io.c673
-rw-r--r--util/libreboot-utils/lib/mkhtemp.c1133
-rw-r--r--util/libreboot-utils/lib/num.c444
-rw-r--r--util/libreboot-utils/lib/state.c252
-rw-r--r--util/libreboot-utils/lib/string.c146
-rw-r--r--util/libreboot-utils/lib/usage.c30
-rw-r--r--util/libreboot-utils/lib/word.c68
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);
+}