summaryrefslogtreecommitdiff
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/libreboot-utils/.gitignore7
-rw-r--r--util/libreboot-utils/AUTHORS (renamed from util/nvmutil/AUTHORS)0
-rw-r--r--util/libreboot-utils/COPYING (renamed from util/nvmutil/COPYING)0
-rw-r--r--util/libreboot-utils/Makefile161
-rw-r--r--util/libreboot-utils/README.md254
-rw-r--r--util/libreboot-utils/include/common.h640
-rw-r--r--util/libreboot-utils/lib/checksum.c108
-rw-r--r--util/libreboot-utils/lib/command.c525
-rw-r--r--util/libreboot-utils/lib/file.c1070
-rw-r--r--util/libreboot-utils/lib/io.c581
-rw-r--r--util/libreboot-utils/lib/mkhtemp.c999
-rw-r--r--util/libreboot-utils/lib/num.c119
-rw-r--r--util/libreboot-utils/lib/rand.c193
-rw-r--r--util/libreboot-utils/lib/state.c166
-rw-r--r--util/libreboot-utils/lib/string.c636
-rw-r--r--util/libreboot-utils/lib/usage.c30
-rw-r--r--util/libreboot-utils/lib/word.c68
-rw-r--r--util/libreboot-utils/lottery.c67
-rw-r--r--util/libreboot-utils/mkhtemp.c152
-rw-r--r--util/libreboot-utils/nvmutil.c116
-rw-r--r--util/nvmutil/.gitignore3
-rw-r--r--util/nvmutil/Makefile30
-rw-r--r--util/nvmutil/nvmutil.c1617
-rw-r--r--util/spkmodem_decode/.gitignore2
-rw-r--r--util/spkmodem_decode/Makefile (renamed from util/spkmodem_recv/Makefile)6
-rw-r--r--util/spkmodem_decode/spkmodem-decode.c725
-rw-r--r--util/spkmodem_recv/.gitignore1
-rw-r--r--util/spkmodem_recv/spkmodem-recv.c313
28 files changed, 6622 insertions, 1967 deletions
diff --git a/util/libreboot-utils/.gitignore b/util/libreboot-utils/.gitignore
new file mode 100644
index 00000000..fbfbd130
--- /dev/null
+++ b/util/libreboot-utils/.gitignore
@@ -0,0 +1,7 @@
+/nvm
+/nvmutil
+/mkhtemp
+/lottery
+*.bin
+*.o
+*.d
diff --git a/util/nvmutil/AUTHORS b/util/libreboot-utils/AUTHORS
index f38ea210..f38ea210 100644
--- a/util/nvmutil/AUTHORS
+++ b/util/libreboot-utils/AUTHORS
diff --git a/util/nvmutil/COPYING b/util/libreboot-utils/COPYING
index 47c35a86..47c35a86 100644
--- a/util/nvmutil/COPYING
+++ b/util/libreboot-utils/COPYING
diff --git a/util/libreboot-utils/Makefile b/util/libreboot-utils/Makefile
new file mode 100644
index 00000000..07b3a727
--- /dev/null
+++ b/util/libreboot-utils/Makefile
@@ -0,0 +1,161 @@
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2022,2026 Leah Rowe <leah@libreboot.org>
+# Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com>
+
+# Makefile for nvmutil, which is an application
+# that modifies Intel GbE NVM configurations.
+
+CC = cc
+HELLCC = clang
+
+CFLAGS =
+LDFLAGS =
+DESTDIR =
+PREFIX = /usr/local
+INSTALL = install
+
+.SUFFIXES: .c .o
+
+LDIR =
+
+PORTABLE = $(LDIR) $(CFLAGS)
+WARN = $(PORTABLE) -Wall -Wextra
+STRICT = $(WARN) -std=c99 -pedantic -Werror
+HELLFLAGS = $(STRICT) -Weverything
+
+PROG = nvmutil
+PROGMKH = mkhtemp
+PROGLOT = lottery
+
+OBJS_NVMUTIL = \
+ obj/nvmutil.o \
+ obj/lib/state.o \
+ obj/lib/file.o \
+ obj/lib/string.o \
+ obj/lib/usage.o \
+ obj/lib/command.o \
+ obj/lib/num.o \
+ obj/lib/io.o \
+ obj/lib/checksum.o \
+ obj/lib/word.o \
+ obj/lib/mkhtemp.o \
+ obj/lib/rand.o
+
+OBJS_MKHTEMP = \
+ obj/mkhtemp.o \
+ obj/lib/file.o \
+ obj/lib/string.o \
+ obj/lib/num.o \
+ obj/lib/mkhtemp.o \
+ obj/lib/rand.o
+
+OBJS_LOTTERY = \
+ obj/lottery.o \
+ obj/lib/file.o \
+ obj/lib/string.o \
+ obj/lib/num.o \
+ obj/lib/mkhtemp.o \
+ obj/lib/rand.o
+
+# default mode
+CFLAGS_MODE = $(PORTABLE)
+CC_MODE = $(CC)
+
+all: $(PROG) $(PROGMKH) $(PROGLOT)
+
+$(PROG): $(OBJS_NVMUTIL)
+ $(CC_MODE) $(OBJS_NVMUTIL) -o $(PROG) $(LDFLAGS)
+
+$(PROGMKH): $(OBJS_MKHTEMP)
+ $(CC_MODE) $(OBJS_MKHTEMP) -o $(PROGMKH) $(LDFLAGS)
+
+$(PROGLOT): $(OBJS_LOTTERY)
+ $(CC_MODE) $(OBJS_LOTTERY) -o $(PROGLOT) $(LDFLAGS)
+
+# ensure obj directory exists
+$(OBJS_NVMUTIL): obj
+$(OBJS_MKHTEMP): obj
+$(OBJS_LOTTERY): obj
+
+obj:
+ mkdir obj || true
+ mkdir obj/lib || true
+
+# main program object
+
+obj/nvmutil.o: nvmutil.c
+ $(CC_MODE) $(CFLAGS_MODE) -c nvmutil.c -o obj/nvmutil.o
+
+obj/mkhtemp.o: mkhtemp.c
+ $(CC_MODE) $(CFLAGS_MODE) -c mkhtemp.c -o obj/mkhtemp.o
+
+obj/lottery.o: lottery.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lottery.c -o obj/lottery.o
+
+# library/helper objects
+
+obj/lib/state.o: lib/state.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/state.c -o obj/lib/state.o
+
+obj/lib/file.o: lib/file.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/file.c -o obj/lib/file.o
+
+obj/lib/string.o: lib/string.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/string.c -o obj/lib/string.o
+
+obj/lib/usage.o: lib/usage.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/usage.c -o obj/lib/usage.o
+
+obj/lib/command.o: lib/command.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/command.c -o obj/lib/command.o
+
+obj/lib/num.o: lib/num.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/num.c -o obj/lib/num.o
+
+obj/lib/io.o: lib/io.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/io.c -o obj/lib/io.o
+
+obj/lib/checksum.o: lib/checksum.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/checksum.c -o obj/lib/checksum.o
+
+obj/lib/word.o: lib/word.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/word.c -o obj/lib/word.o
+
+obj/lib/mkhtemp.o: lib/mkhtemp.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/mkhtemp.c -o obj/lib/mkhtemp.o
+
+obj/lib/rand.o: lib/rand.c
+ $(CC_MODE) $(CFLAGS_MODE) -c lib/rand.c -o obj/lib/rand.o
+
+# install
+
+install: $(PROG) $(PROGMKH) $(PROGLOT)
+ $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin
+ $(INSTALL) $(PROG) $(DESTDIR)$(PREFIX)/bin/$(PROG)
+ chmod 755 $(DESTDIR)$(PREFIX)/bin/$(PROG)
+ $(INSTALL) $(PROGMKH) $(DESTDIR)$(PREFIX)/bin/$(PROGMKH)
+ chmod 755 $(DESTDIR)$(PREFIX)/bin/$(PROGMKH)
+ $(INSTALL) $(PROGLOT) $(DESTDIR)$(PREFIX)/bin/$(PROGLOT)
+ chmod 755 $(DESTDIR)$(PREFIX)/bin/$(PROGLOT)
+
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG)
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(PROGMKH)
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(PROGLOT)
+
+clean:
+ rm -f $(PROG) $(PROGMKH) $(OBJS_NVMUTIL) $(OBJS_MKHTEMP) \
+ $(OBJS_LOTTERY) $(PROGLOT)
+
+distclean: clean
+
+# mode targets (portable replacement for ifeq)
+
+warn:
+ $(MAKE) CFLAGS_MODE="$(WARN)"
+
+strict:
+ $(MAKE) CFLAGS_MODE="$(STRICT)"
+
+hell:
+ $(MAKE) CFLAGS_MODE="$(HELLFLAGS)" CC_MODE="$(HELLCC)"
diff --git a/util/libreboot-utils/README.md b/util/libreboot-utils/README.md
new file mode 100644
index 00000000..dca1b92e
--- /dev/null
+++ b/util/libreboot-utils/README.md
@@ -0,0 +1,254 @@
+Mkhtemp - Hardened mktemp
+-------------------------
+
+Just like normal mktemp, but hardened.
+
+Create new files and directories randomly as determined by
+the user's TMPDIR, or fallback. These temporary files and
+directories can be generated from e.g. shell scripts, running
+mkhtemp. There is also a library that you could use in your
+program. Portable to Linux and BSD. **WORK IN PROGRESS.
+This is a very new project. Expect bugs - a stable release
+will be announced, when the code has matured.**
+
+A brief summary of *why* mkhtemp is more secure (more
+details provided later in this readme - please also
+read the source code):
+
+Detect and mitigate symlink attacks, directory access
+race conditions, unsecure TMPDIR (e.g. bad enforce sticky
+bit policy on world writeable dirs), implement in user
+space a virtual sandbox (block directory escape and resolve
+paths by walking from `/` manually instead of relying on
+the kernel/system), voluntarily error out (halt all
+operation) if accessing files you don't own - that's why
+sticky bits are checked for example, even when you're root.
+
+It... blocks symlinks, relative paths, attempts to prevent
+directory escape (outside of the directory that the file
+you're creating is in), basically implementing an analog
+of something like e.g. unveil, but in userspace!
+
+Mkhtemp is designed to be the most secure implementation
+possible, of mktemp, offering a heavy amount of hardening
+over traditional mktemp. Written in C99, and the plan is
+very much to keep this code portable over time - patches
+very much welcome.
+
+i.e. please read the source code
+
+```
+/*
+ * WARNING: WORK IN PROGRESS.
+ * Do not use this software in
+ * your distro yet. It's ready
+ * when it's ready. Read the src.
+ *
+ * What you see is an early beta.
+ *
+ * Please do not merge this in
+ * your Linux distro package repo
+ * yet (unless maybe you're AUR).
+ */
+```
+
+Supported mktemp flags:
+
+```
+mkhtemp: usage: mkhtemp [-d] [-p dir] [template]
+
+ -p DIR <-- set directory, overriding TMPDIR
+ -d <-- make a directory instead of a file
+ -q <-- silence errors (exit status unchanged)
+```
+
+The rest of them will be added later (the same ones
+that GNU and BSD mktemp implement). With these options,
+you can generate files/directories already.
+
+You can also write a template at the end. e.g.
+
+```
+mkhtemp -d -p path/to/directory vickysomething_XXXXXXXXXXX
+```
+
+On most sane/normal setups, the program should already
+actually work, but please know that it's very different
+internally than every other mktemp implementation.
+
+Read the source code if you're interested. As of this
+time of writing, mkhtemp is very new, and under
+development. A stable release will be announced when ready.
+
+### What does mkhtemp do differently?
+
+This software attempts to provide mitigation against
+several TOCTOU-based
+attacks e.g. directory rename / symlink / re-mount, and
+generally provides much higher strictness than previous
+implementations such as mktemp, mkstemp or even mkdtemp.
+It uses several modern features by default, e.g. openat2
+and `O_TMPFILE` (plus `O_EXCL`) on Linux, with additional
+hardening; BSD projects only have openat so the code uses
+that there, but some (not all) of the kinds of checks
+Openat2 enforces are done manually (in userspace).
+
+File system sandboxing in userspace (pathless discovery,
+and operations are done only with FDs). At startup, the
+root directory is opened, and then everything is relative
+to that.
+
+Many programs rely on mktemp, and they use TMPDIR in a way
+that is quite insecure. Mkhtemp intends to change that,
+quite dramatically, with: userspace sandbox (and use OS
+level options e.g. OBSD pledge where available), constant
+identity/ownership checks on files, MUCH stricter ownership
+restrictions (e.g. enforce sticky bit policy on world-
+writeable tmpdirs), preventing operation on other people's
+files (only your own files) - even root is restricted,
+depending on how the code is compiled. Please read the code.
+
+Basically, the gist of it is that normal mktemp *trusts*
+your system is set up properly. It will just run however
+you tell it to, on whatever directory you tell it to, and
+if you're able to write to it, it will write to it.
+Some implementations (e.g. OpenBSD one) do some checks,
+but not all of them do *all* checks. The purpose of
+mkhtemp is to be as strict as possible, while still being
+reliable enough that people can use it. Instead of catering
+to legacy requirements, mkhtemp says that systems should
+be secure. So if you're running in an insecure environment,
+the goal of mkhtemp is to *exit* when you run it; better
+this than files being corrupted.
+
+Security and reliability are the same thing. They both
+mean that your computer is behaving as it should, in a
+manner that you can predict.
+
+It doesn't matter how many containers you have, or how
+memory-safe your programming language is, the same has
+been true forever: code equals bugs, and code usually
+has the same percentage of bugs, so more code equals
+more bugs. Therefore, highly secure systems (such as
+OpenBSD) typically try to keep their code as small and
+clean as possible, so that they can audit it. Mkhtemp
+assumes that your system is hostile, and is designed
+accordingly.
+
+What?
+-----
+
+This is the utility version, which makes use of the also-
+included library. No docs yet - source code are the docs,
+and the (ever evolving, and hardening) specification.
+
+This was written from scratch, for use in nvmutil, and
+it is designed to be portable (BSD, Linux). Patches
+very much welcome.
+
+Caution
+-------
+
+This is a new utility. Expect bugs.
+
+```
+WARNING: This is MUCH stricter than every other mktemp
+ implementation, even more so than mkdtemp or
+ the OpenBSD version of mkstemp. It *will* break,
+ or more specifically, reveal the flaws in, almost
+ every major critical infrastructure, because most
+ people already use mktemp extremely insecurely.
+```
+
+This tool is written by me, for me, and also Libreboot, but
+it will be summitted for review to various Linux distros
+and BSD projects once it has reached maturity.
+
+### Why was this written?
+
+Atomic writes were implemented in nvmutil (Libreboot's
+Intel GbE NVM editor), but one element remained: the
+program mktemp, itself, which has virtually no securitty
+checks whatsoever. GNU and BSD implementations use
+mkstemp now, which is a bit more secure, and they offer
+additional hardening, but I wanted to be reasonably
+assured that my GbE files were not being corrupted in
+any way, and that naturally led to writing a hardened
+tool. It was originally just going to be for nvmutil,
+but then it became its own standard utility.
+
+Existing implementations of mktemp just simply do not
+have sufficient checks in place to prevent misuse. This
+tool, mkhtemp, intentionally focuses on being secure
+instead of easy. For individuals just running Linux on
+their personal machine, it might not make much difference,
+but corporations and projects running computers for lots
+of big infrastructure need something reliable, since
+mktemp is just one of those things everyone uses.
+Every big program needs to make temporary files.
+
+But the real reason I wrote this tool is because, it's
+fun, and because I wanted to challenge myself.
+
+Roadmap
+-------
+
+Some things that are in the near future for mkhtemp
+development:
+
+Thoroughly document every known case of CVEs in the wild,
+and major attacks against individuals/projects/corporations
+that were made possible by mktemp - that mkhtemp might
+have prevented. There are several.
+
+More hardening; still a lot more that can be done, depending
+on OS. E.g. integrate FreeBSD capsicum.
+
+Another example: although usually reliable, comparing the
+inode and device of a file/directory isn't by itself sufficient.
+There are other checks that mkhtemp does; for example I could
+implement it so that directories are more aggressively re-
+opened by mkhtemp itself, mid-operation. This re-opening
+would be quite expensive computationally, but it would then
+allow us to re-check everything, since we store state from
+when the program starts.
+
+Tidy up the code: the current code was thrown together in
+a week, and needs tidying. A proper specification should be
+written, to define how it works, and then the code should
+be auditted for compliance. A lot of the functions are
+also quite complex and do a lot; they could be split up.
+
+Right now, mkhtemp mainly returns a file descriptor and
+a path, after operation, ironic given the methods it uses
+while opening your file/dir. After it's done, you then have
+to handle everything again. Mkhtemp could keep everything
+open instead, and continue to provide verification; in
+other words, it could provide a completely unified way for
+Linux/BSD programs to open files, write to them atomically,
+and close. Programs like Vim will do this for example, or
+other text editors, but every program has its own way. So
+what mkhtemp could do is provide a well-defined API alongside
+its mktemp hardening. Efforts would be made to avoid
+feature creep, and ensure that the code remains small and
+nimble.
+
+Compatibility mode: another thing is that mkhtemp is a bit
+too strict for some users, so it may break some setups. What
+it could do is provide a compatibility mode, and in this
+mode, behave like regular mktemp. That way, it could become
+a drop-in replacement on Linux distros (and BSDs if they
+want it), while providing a more hardened version and
+recommending that where possible.
+
+~~Rewrite it in rust~~ (nothing against it though, I just like C99 for some reason)
+
+Also, generally document the history of mktemp, and how
+mkhtemp works in comparison.
+
+Also a manpage.
+
+Once all this is done, and the project is fully polished,
+then it will be ready for your Linux distro. For now, I
+just use it in nvmutil (and I also use it on my personal
+computer).
diff --git a/util/libreboot-utils/include/common.h b/util/libreboot-utils/include/common.h
new file mode 100644
index 00000000..8276d6da
--- /dev/null
+++ b/util/libreboot-utils/include/common.h
@@ -0,0 +1,640 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org>
+
+ TODO: this file should be split, into headers for each
+ C source file specifically. it was originally just
+ for nvmutil, until i added mkhtemp to the mix
+ */
+
+
+#ifndef COMMON_H
+#define COMMON_H
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <limits.h>
+#include <errno.h>
+
+/* for linux getrandom
+ */
+#if defined(__linux__)
+#include <sys/random.h>
+#include <sys/syscall.h>
+#endif
+
+#ifdef __OpenBSD__ /* for pledge */
+#include <unistd.h>
+#endif
+
+#define items(x) (sizeof((x)) / sizeof((x)[0]))
+
+/* system prototypes
+ */
+
+int fchmod(int fd, mode_t mode);
+
+#define MKHTEMP_RETRY_MAX 512
+#define MKHTEMP_SPIN_THRESHOLD 32
+
+#define MKHTEMP_FILE 0
+#define MKHTEMP_DIR 1
+
+
+/* if 1: on operations that
+ * check ownership, always
+ * permit root to access even
+ * if not the file/dir owner
+ */
+#ifndef ALLOW_ROOT_OVERRIDE
+#define ALLOW_ROOT_OVERRIDE 0
+#endif
+
+/*
+ */
+
+#ifndef SSIZE_MAX
+#define SSIZE_MAX ((ssize_t)(~((ssize_t)1 << (sizeof(ssize_t)*CHAR_BIT-1))))
+#endif
+
+
+/* build config
+ */
+
+#ifndef NVMUTIL_H
+#define NVMUTIL_H
+
+#define MAX_CMD_LEN 50
+
+#ifndef PATH_LEN
+#define PATH_LEN 4096
+#endif
+
+#define OFF_ERR 0
+#ifndef OFF_RESET
+#define OFF_RESET 1
+#endif
+
+#ifndef S_ISVTX
+#define S_ISVTX 01000
+#endif
+
+#if defined(S_IFMT) && ((S_ISVTX & S_IFMT) != 0)
+#error "Unexpected bit layout"
+#endif
+
+#ifndef MAX_ZERO_RW_RETRY
+#define MAX_ZERO_RW_RETRY 5
+#endif
+
+#ifndef REAL_POS_IO
+#define REAL_POS_IO 1
+#endif
+
+#ifndef LOOP_EAGAIN
+#define LOOP_EAGAIN 1
+#endif
+#ifndef LOOP_EINTR
+#define LOOP_EINTR 1
+#endif
+
+#ifndef _FILE_OFFSET_BITS
+#define _FILE_OFFSET_BITS 64
+#endif
+
+#ifndef EXIT_FAILURE
+#define EXIT_FAILURE 1
+#endif
+
+#ifndef EXIT_SUCCESS
+#define EXIT_SUCCESS 0
+#endif
+
+#ifndef O_NOCTTY
+#define O_NOCTTY 0
+#endif
+
+#ifndef O_ACCMODE
+#define O_ACCMODE (O_RDONLY | O_WRONLY | O_RDWR)
+#endif
+
+#ifndef O_BINARY
+#define O_BINARY 0
+#endif
+
+#ifndef O_EXCL
+#define O_EXCL 0
+#endif
+
+#ifndef O_CREAT
+#define O_CREAT 0
+#endif
+
+#ifndef O_NONBLOCK
+#define O_NONBLOCK 0
+#endif
+
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+
+#ifndef O_NOFOLLOW
+#define O_NOFOLLOW 0
+#endif
+
+#ifndef FD_CLOEXEC
+#define FD_CLOEXEC 0
+#endif
+
+/* Sizes in bytes:
+ */
+
+#define SIZE_1KB 1024
+#define SIZE_4KB (4 * SIZE_1KB)
+#define SIZE_8KB (8 * SIZE_1KB)
+#define SIZE_16KB (16 * SIZE_1KB)
+#define SIZE_128KB (128 * SIZE_1KB)
+
+#define GBE_BUF_SIZE (SIZE_128KB)
+
+/* First 128 bytes of gbe.bin is NVM.
+ * Then extended area. All of NVM must
+ * add up to BABA, truncated (LE)
+ *
+ * First 4KB of each half of the file
+ * contains NVM+extended.
+ */
+
+#define GBE_WORK_SIZE (SIZE_8KB)
+#define GBE_PART_SIZE (GBE_WORK_SIZE >> 1)
+#define NVM_CHECKSUM 0xBABA
+#define NVM_SIZE 128
+#define NVM_WORDS (NVM_SIZE >> 1)
+#define NVM_CHECKSUM_WORD (NVM_WORDS - 1)
+
+/* argc minimum (dispatch)
+ */
+
+#define ARGC_3 3
+#define ARGC_4 4
+
+#define NO_LOOP_EAGAIN 0
+#define NO_LOOP_EINTR 0
+
+/* For checking if an fd is a normal file.
+ * Portable for old Unix e.g. v7 (S_IFREG),
+ * 4.2BSD (S_IFMT), POSIX (S_ISREG).
+ *
+ * IFREG: assumed 0100000 (classic bitmask)
+ */
+
+#ifndef S_ISREG
+#if defined(S_IFMT) && defined(S_IFREG)
+#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
+#elif defined(S_IFREG)
+#define S_ISREG(m) (((m) & S_IFREG) != 0)
+#else
+#error "can't determine types with stat()"
+#endif
+#endif
+
+#define IO_READ 0
+#define IO_WRITE 1
+#define IO_PREAD 2
+#define IO_PWRITE 3
+
+/* for nvmutil commands
+ */
+
+#define CMD_DUMP 0
+#define CMD_SETMAC 1
+#define CMD_SWAP 2
+#define CMD_COPY 3
+#define CMD_CAT 4
+#define CMD_CAT16 5
+#define CMD_CAT128 6
+
+#define ARG_NOPART 0
+#define ARG_PART 1
+
+#define SKIP_CHECKSUM_READ 0
+#define CHECKSUM_READ 1
+
+#define SKIP_CHECKSUM_WRITE 0
+#define CHECKSUM_WRITE 1
+
+/* command table
+ */
+
+typedef void (*func_t)(void);
+
+struct commands {
+ size_t chk;
+ char *str;
+ func_t run;
+ int argc;
+ unsigned char arg_part;
+ unsigned char chksum_read;
+ unsigned char chksum_write;
+ size_t rw_size; /* within the 4KB GbE part */
+ int flags; /* e.g. O_RDWR or O_RDONLY */
+};
+
+/* mac address
+ */
+
+struct macaddr {
+ char *str; /* set to rmac, or argv string */
+ char rmac[18]; /* xx:xx:xx:xx:xx:xx */
+ unsigned short mac_buf[3];
+};
+
+/* gbe.bin and tmpfile
+ */
+
+struct xfile {
+ int gbe_fd;
+ struct stat gbe_st;
+
+ int tmp_fd;
+ struct stat tmp_st;
+
+ char *tname; /* path of tmp file */
+ char *fname; /* path of gbe file */
+
+ unsigned char *buf; /* work memory for files */
+
+ int io_err_gbe; /* intermediary write (verification) */
+ int io_err_gbe_bin; /* final write (real file) */
+ int rw_check_err_read[2];
+ int rw_check_partial_read[2];
+ int rw_check_bad_part[2];
+
+ int post_rw_checksum[2];
+
+ off_t gbe_file_size;
+ off_t gbe_tmp_size;
+
+ size_t part;
+ unsigned char part_modified[2];
+ unsigned char part_valid[2];
+
+ unsigned char real_buf[GBE_BUF_SIZE];
+ unsigned char bufcmp[GBE_BUF_SIZE]; /* compare gbe/tmp/reads */
+
+ unsigned char pad[GBE_WORK_SIZE]; /* the file that wouldn't die */
+
+ /* we later rename in-place, using old fd. renameat() */
+ int dirfd;
+ char *base;
+ char *tmpbase;
+};
+
+/* Command table, MAC address, files
+ *
+ * BE CAREFUL when editing this
+ * to ensure that you also update
+ * the tables in xstatus()
+ */
+
+struct xstate {
+ struct commands cmd[7];
+ struct macaddr mac;
+ struct xfile f;
+
+ size_t i; /* index to cmd[] for current command */
+ int no_cmd;
+
+ /* Cat commands set this.
+ the cat cmd helpers check it */
+ int cat;
+};
+
+struct filesystem {
+ int rootfd;
+};
+
+struct xstate *xstart(int argc, char *argv[]);
+struct xstate *xstatus(void);
+
+/* Sanitize command tables.
+ */
+
+void sanitize_command_list(void);
+void sanitize_command_index(size_t c);
+
+/* Argument handling (user input)
+ */
+
+void set_cmd(int argc, char *argv[]);
+void set_cmd_args(int argc, char *argv[]);
+size_t conv_argv_part_num(const char *part_str);
+
+/* Prep files for reading
+ */
+
+void open_gbe_file(void);
+int fd_verify_regular(int fd,
+ const struct stat *expected,
+ struct stat *out);
+int fd_verify_identity(int fd,
+ const struct stat *expected,
+ struct stat *out);
+int fd_verify_dir_identity(int fd,
+ const struct stat *expected);
+int is_owner(struct stat *st);
+int lock_file(int fd, int flags);
+int same_file(int fd, struct stat *st_old, int check_size);
+
+/* Read GbE file and verify checksums
+ */
+
+void copy_gbe(void);
+void read_file(void);
+void read_checksums(void);
+int good_checksum(size_t partnum);
+
+/* validate commands
+ */
+
+void check_command_num(size_t c);
+unsigned char valid_command(size_t c);
+
+/* Helper functions for command: setmac
+ */
+
+void cmd_helper_setmac(void);
+void parse_mac_string(void);
+void set_mac_byte(size_t mac_byte_pos);
+void set_mac_nib(size_t mac_str_pos,
+ size_t mac_byte_pos, size_t mac_nib_pos);
+void write_mac_part(size_t partnum);
+
+/* string functions
+ */
+
+size_t page_remain(const void *p);
+long pagesize(void);
+int xunveilx(const char *path, const char *permissions);
+int xpledgex(const char *promises, const char *execpromises);
+char *smalloc(char **buf, size_t size);
+void *vmalloc(void **buf, size_t size);
+size_t slen(const char *scmp, size_t maxlen,
+ size_t *rval);
+int vcmp(const void *s1, const void *s2, size_t n);
+int scmp(const char *a, const char *b,
+ size_t maxlen, int *rval);
+int ccmp(const char *a, const char *b, size_t i,
+ int *rval);
+char *sdup(const char *s,
+ size_t n, char **dest);
+char *scatn(ssize_t sc, const char **sv,
+ size_t max, char **rval);
+char *scat(const char *s1, const char *s2,
+ size_t n, char **dest);
+void dcat(const char *s, size_t n,
+ size_t off, char **dest1,
+ char **dest2);
+/* numerical functions
+ */
+
+unsigned short hextonum(char ch_s);
+void spew_hex(const void *data, size_t len);
+void *rmalloc(size_t n);
+void rset(void *buf, size_t n);
+void *rmalloc(size_t n);
+char *rchars(size_t n);
+size_t rsize(size_t n);
+
+/* Helper functions for command: dump
+ */
+
+void cmd_helper_dump(void);
+void print_mac_from_nvm(size_t partnum);
+
+/* Helper functions for command: swap
+ */
+
+void cmd_helper_swap(void);
+
+/* Helper functions for command: copy
+ */
+
+void cmd_helper_copy(void);
+
+/* Helper functions for commands:
+ * cat, cat16 and cat128
+ */
+
+void cmd_helper_cat(void);
+void cmd_helper_cat16(void);
+void cmd_helper_cat128(void);
+void cat(size_t nff);
+void cat_buf(unsigned char *b);
+
+/* Command verification/control
+ */
+
+void check_cmd(void (*fn)(void), const char *name);
+void cmd_helper_err(void);
+
+/* Write GbE files to disk
+ */
+
+void write_gbe_file(void);
+void set_checksum(size_t part);
+unsigned short calculated_checksum(size_t p);
+
+/* NVM read/write
+ */
+
+unsigned short nvm_word(size_t pos16, size_t part);
+void set_nvm_word(size_t pos16,
+ size_t part, unsigned short val16);
+void set_part_modified(size_t p);
+void check_nvm_bound(size_t pos16, size_t part);
+void check_bin(size_t a, const char *a_name);
+
+/* GbE file read/write
+ */
+
+void rw_gbe_file_part(size_t p, int rw_type,
+ const char *rw_type_str);
+void write_to_gbe_bin(void);
+int gbe_mv(void);
+void check_written_part(size_t p);
+void report_io_err_rw(void);
+unsigned char *gbe_mem_offset(size_t part, const char *f_op);
+off_t gbe_file_offset(size_t part, const char *f_op);
+off_t gbe_x_offset(size_t part, const char *f_op,
+ const char *d_type, off_t nsize, off_t ncmp);
+ssize_t rw_gbe_file_exact(int fd, unsigned char *mem, size_t nrw,
+ off_t off, int rw_type);
+
+/* Generic read/write
+ */
+
+int fsync_dir(const char *path);
+ssize_t rw_file_exact(int fd, unsigned char *mem, size_t len,
+ off_t off, int rw_type, int loop_eagain, int loop_eintr,
+ size_t max_retries, int off_reset);
+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);
+int io_args(int fd, void *mem, size_t nrw,
+ off_t off, int rw_type);
+int check_file(int fd, struct stat *st);
+ssize_t rw_over_nrw(ssize_t r, size_t nrw);
+off_t lseek_on_eintr(int fd, off_t off,
+ int whence, int loop_eagain, int loop_eintr);
+int try_err(int loop_err, int errval);
+ssize_t read_on_eintr(int fd,
+ void *buf, size_t count);
+ssize_t write_on_eintr(int fd,
+ void *buf, size_t count);
+ssize_t pread_on_eintr(int fd,
+ void *buf, size_t count, off_t off);
+ssize_t pwrite_on_eintr(int fd,
+ void *buf, size_t count, off_t off);
+
+/* Error handling and cleanup
+ */
+
+void usage(void);
+int set_errno(int saved_errno, int fallback);
+void err_exit(int nvm_errval, const char *msg, ...);
+func_t errhook(func_t ptr); /* hook function for cleanup on err */
+const char *lbgetprogname(void);
+void no_op(void);
+void err_mkhtemp(int errval, const char *msg, ...);
+
+/* libc hardening
+ */
+
+int new_tmpfile(int *fd, char **path, char *tmpdir,
+ const char *template);
+int new_tmpdir(int *fd, char **path, char *tmpdir,
+ const char *template);
+int new_tmp_common(int *fd, char **path, int type,
+ char *tmpdir, const char *template);
+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);
+int
+mkhtemp_tmpfile_linux(int dirfd,
+ struct stat *st_dir_initial,
+ char *fname_copy,
+ char *p,
+ size_t xc,
+ int *fd,
+ struct stat *st);
+int mkhtemp(int *fd, struct stat *st,
+ char *template, int dirfd, const char *fname,
+ struct stat *st_dir_initial, int type);
+int world_writeable_and_sticky(const char *s,
+ int sticky_allowed, int always_sticky);
+int same_dir(const char *a, const char *b);
+int tmpdir_policy(const char *path,
+ int *allow_noworld_unsticky);
+char *env_tmpdir(int always_sticky, char **tmpdir,
+ char *override_tmpdir);
+int secure_file(int *fd,
+ struct stat *st,
+ struct stat *expected,
+ int bad_flags,
+ int check_seek,
+ int do_lock,
+ mode_t mode);
+void close_on_eintr(int *fd);
+int fsync_on_eintr(int fd);
+int fs_rename_at(int olddirfd, const char *old,
+ int newdirfd, const char *new);
+int fs_open(const char *path, int flags);
+void free_and_set_null(char **buf);
+void open_on_eintr(const char *path, int *fd, int flags, mode_t mode,
+ struct stat *st);
+struct filesystem *rootfs(void);
+int fs_resolve_at(int dirfd, const char *path, int flags);
+int fs_next_component(const char **p,
+ char *name, size_t namesz);
+int fs_open_component(int dirfd, const char *name,
+ int flags, int is_last);
+int fs_dirname_basename(const char *path,
+ char **dir, char **base, int allow_relative);
+int openat2p(int dirfd, const char *path,
+ int flags, mode_t mode);
+int mkdirat_on_eintr(int dirfd,
+ const char *pathname, mode_t mode);
+int if_err(int condition, int errval);
+int if_err_sys(int condition);
+char *lbsetprogname(char *argv0);
+
+/* asserts */
+
+/* type asserts */
+typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1];
+typedef char static_assert_char_is_1[(sizeof(char) == 1) ? 1 : -1];
+typedef char static_assert_unsigned_char_is_1[
+ (sizeof(unsigned char) == 1) ? 1 : -1];
+typedef char static_assert_unsigned_short_is_2[
+ (sizeof(unsigned short) >= 2) ? 1 : -1];
+typedef char static_assert_short_is_2[(sizeof(short) >= 2) ? 1 : -1];
+typedef char static_assert_unsigned_int_is_4[
+ (sizeof(unsigned int) >= 4) ? 1 : -1];
+typedef char static_assert_unsigned_ssize_t_is_4[
+ (sizeof(size_t) >= 4) ? 1 : -1];
+typedef char static_assert_ssize_t_ussize_t[
+ (sizeof(size_t) == sizeof(ssize_t)) ? 1 : -1];
+typedef char static_assert_int_ge_32[(sizeof(int) >= 4) ? 1 : -1];
+typedef char static_assert_twos_complement[
+ ((-1 & 3) == 3) ? 1 : -1
+];
+typedef char assert_unsigned_ssize_t_ptr[
+ (sizeof(size_t) >= sizeof(void *)) ? 1 : -1
+];
+
+/*
+ * We set _FILE_OFFSET_BITS 64, but we only handle
+ * but we only need smaller files, so require 4-bytes.
+ * Some operating systems ignore the define, hence assert:
+ */
+typedef char static_assert_off_t_is_32[(sizeof(off_t) >= 4) ? 1 : -1];
+
+/*
+ * asserts (variables/defines sanity check)
+ */
+typedef char assert_argc3[(ARGC_3==3)?1:-1];
+typedef char assert_argc4[(ARGC_4==4)?1:-1];
+typedef char assert_read[(IO_READ==0)?1:-1];
+typedef char assert_write[(IO_WRITE==1)?1:-1];
+typedef char assert_pread[(IO_PREAD==2)?1:-1];
+typedef char assert_pwrite[(IO_PWRITE==3)?1:-1];
+typedef char assert_pathlen[(PATH_LEN>=256)?1:-1];
+/* commands */
+typedef char assert_cmd_dump[(CMD_DUMP==0)?1:-1];
+typedef char assert_cmd_setmac[(CMD_SETMAC==1)?1:-1];
+typedef char assert_cmd_swap[(CMD_SWAP==2)?1:-1];
+typedef char assert_cmd_copy[(CMD_COPY==3)?1:-1];
+typedef char assert_cmd_cat[(CMD_CAT==4)?1:-1];
+typedef char assert_cmd_cat16[(CMD_CAT16==5)?1:-1];
+typedef char assert_cmd_cat128[(CMD_CAT128==6)?1:-1];
+/* bool */
+typedef char bool_arg_nopart[(ARG_NOPART==0)?1:-1];
+typedef char bool_arg_part[(ARG_PART==1)?1:-1];
+typedef char bool_skip_checksum_read[(SKIP_CHECKSUM_READ==0)?1:-1];
+typedef char bool_checksum_read[(CHECKSUM_READ==1)?1:-1];
+typedef char bool_skip_checksum_write[(SKIP_CHECKSUM_WRITE==0)?1:-1];
+typedef char bool_checksum_write[(CHECKSUM_WRITE==1)?1:-1];
+typedef char bool_loop_eintr[(LOOP_EINTR==1||LOOP_EINTR==0)?1:-1];
+typedef char bool_loop_eagain[(LOOP_EAGAIN==1||LOOP_EAGAIN==0)?1:-1];
+typedef char bool_no_loop_eintr[(NO_LOOP_EINTR==0)?1:-1];
+typedef char bool_no_loop_eagain[(NO_LOOP_EAGAIN==0)?1:-1];
+typedef char bool_off_err[(OFF_ERR==0)?1:-1];
+typedef char bool_off_reset[(OFF_RESET==0||OFF_RESET==1)?1:-1];
+
+#endif
+#endif
diff --git a/util/libreboot-utils/lib/checksum.c b/util/libreboot-utils/lib/checksum.c
new file mode 100644
index 00000000..97b0efca
--- /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_exit(ECANCELED, "%s: part %lu has a bad checksum",
+ f->fname, (size_t)f->part);
+
+ err_exit(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..3ee75628
--- /dev/null
+++ b/util/libreboot-utils/lib/command.c
@@ -0,0 +1,525 @@
+/* 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_exit(EINVAL, "cmd index %lu: argc below 3, %d",
+ (size_t)c, cmd->argc);
+
+ if (cmd->str == NULL)
+ err_exit(EINVAL, "cmd index %lu: NULL str",
+ (size_t)c);
+
+ if (*cmd->str == '\0')
+ err_exit(EINVAL, "cmd index %lu: empty str",
+ (size_t)c);
+
+ if (slen(cmd->str, MAX_CMD_LEN +1, &rval) > MAX_CMD_LEN) {
+ err_exit(EINVAL, "cmd index %lu: str too long: %s",
+ (size_t)c, cmd->str);
+ }
+
+ if (cmd->run == NULL)
+ err_exit(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_exit(EINVAL, "Unsupported rw_size: %lu",
+ (size_t)gbe_rw_size);
+ }
+
+ if (gbe_rw_size > GBE_PART_SIZE)
+ err_exit(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_exit(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))
+ 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_exit(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_exit(EINVAL,
+ "arg_part set for command that needs argc4");
+
+ if (cmd->arg_part && i == CMD_SETMAC)
+ err_exit(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_exit(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_exit(EINVAL, "Bad part number (%c)", ch);
+
+ return (size_t)(ch - '0');
+}
+
+void
+check_command_num(size_t c)
+{
+ if (!valid_command(c))
+ err_exit(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_exit(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) != 17)
+ err_exit(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_exit(EINVAL, "Must not specify all-zeroes MAC address");
+
+ if (mac->mac_buf[0] & 1)
+ err_exit(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_exit(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_exit(EIO, "Randomisation failure");
+ else
+ err_exit(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);
+ spew_hex(f->buf + (p * GBE_PART_SIZE), NVM_SIZE);
+ }
+}
+
+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
+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_exit(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_exit(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_exit(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_exit(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_exit(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..b9d31ad7
--- /dev/null
+++ b/util/libreboot-utils/lib/file.c
@@ -0,0 +1,1070 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * Pathless i/o, and some stuff you
+ * probably never saw in userspace.
+ *
+ * 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:
+ return set_errno(saved_errno, ESTALE);
+}
+
+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 (if_err(slen(path, maxlen, &pathlen) == 0, EINVAL))
+ goto err_fsync_dir;
+
+ memcpy(smalloc(&dirbuf, pathlen + 1),
+ 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 (if_err_sys(dirfd < 0) ||
+ if_err_sys(fstat(dirfd, &st) < 0) ||
+ if_err(!S_ISDIR(st.st_mode), ENOTDIR)
+ ||
+ if_err_sys(fsync_on_eintr(dirfd) == -1))
+ goto err_fsync_dir;
+
+ close_on_eintr(&dirfd);
+
+ free_and_set_null(&dirbuf);
+
+ errno = saved_errno;
+ return 0;
+
+err_fsync_dir:
+
+
+ free_and_set_null(&dirbuf);
+ close_on_eintr(&dirfd);
+
+ return set_errno(saved_errno, EIO);
+}
+
+/* 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;
+ errno = 0;
+
+ 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:
+
+ return set_errno(saved_errno, EIO);
+}
+
+/* prw() - portable read-write with more
+ * safety checks than barebones libc
+ *
+ * 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
+ *
+ * REAL_POS_IO is enabled by default in common.h
+ * and the fallback version was written for fun.
+ * You should just use the real one (REAL_POS_IO 1),
+ * since it is generally more reliable.
+ */
+
+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;
+ errno = 0;
+
+ 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_on_eintr(fd, mem, nrw);
+ else if (rw_type == IO_READ)
+ r = read_on_eintr(fd, mem, nrw);
+#if defined(REAL_POS_IO) && \
+ REAL_POS_IO > 0
+ else if (rw_type == IO_PWRITE)
+ r = pwrite_on_eintr(fd, mem, nrw, off);
+ else if (rw_type == IO_PREAD)
+ r = pread_on_eintr(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_on_eintr(fd, mem, nrw);
+ else if (rw_type == IO_PWRITE)
+ r = write_on_eintr(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:
+ return set_errno(saved_errno, EIO);
+}
+
+int
+io_args(int fd, void *mem, size_t nrw,
+ off_t off, int rw_type)
+{
+ int saved_errno = errno;
+
+ if (if_err(mem == NULL, EFAULT) ||
+ if_err(fd < 0, EBADF) ||
+ if_err(off < 0, ERANGE) ||
+ if_err(!nrw, EPERM) || /* TODO: toggle zero-byte check */
+ if_err(nrw > (size_t)SSIZE_MAX, ERANGE) ||
+ if_err(((size_t)off + nrw) < (size_t)off, ERANGE) ||
+ if_err(rw_type > IO_PWRITE, EINVAL))
+ goto err_io_args;
+
+ errno = saved_errno;
+ return 0;
+
+err_io_args:
+ return set_errno(saved_errno, EINVAL);
+}
+
+int
+check_file(int fd, struct stat *st)
+{
+ int saved_errno = errno;
+
+ if (if_err(fd < 0, EBADF) ||
+ if_err(st == NULL, EFAULT) ||
+ if_err(fstat(fd, st) == -1, 0) ||
+ if_err(!S_ISREG(st->st_mode), EBADF))
+ goto err_is_file;
+
+ errno = saved_errno;
+ return 0;
+
+err_is_file:
+ return set_errno(saved_errno, EINVAL);
+}
+
+/* POSIX can say whatever it wants.
+ * specification != implementation
+ */
+
+ssize_t
+rw_over_nrw(ssize_t r, size_t nrw)
+{
+ int saved_errno = errno;
+
+ if (if_err(!nrw, 0) ||
+ if_err(r == -1, 0) ||
+ if_err((size_t)r > SSIZE_MAX, ERANGE) ||
+ if_err((size_t)r > nrw, ERANGE))
+ goto err_rw_over_nrw;
+
+ errno = saved_errno;
+ return r;
+
+err_rw_over_nrw:
+ return set_errno(saved_errno, EIO);
+}
+
+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;
+}
+
+/* two functions that reduce sloccount by
+ * two hundred lines */
+int
+if_err(int condition, int errval)
+{
+ if (!condition)
+ return 0;
+
+ if (errval)
+ errno = errval;
+
+ return 1;
+}
+int
+if_err_sys(int condition)
+{
+ if (!condition)
+ return 0;
+ return 1;
+}
+/* errno can never be -1, so you can
+ * use this to conditionally set an integer
+ * for comparison. see example in lseek_on_eintr
+ */
+int
+try_err(int loop_err, int errval)
+{
+ if (loop_err)
+ return errval;
+ return -1;
+}
+
+void
+open_on_eintr(const char *path,
+ int *fd, int flags, mode_t mode,
+ struct stat *st)
+{
+ int r = -1;
+ int saved_errno = errno;
+
+ if (path == NULL)
+ err_exit(EINVAL, "open_on_eintr: null path");
+
+ if (fd == NULL)
+ err_exit(EFAULT, "%s: open_on_eintr: null fd ptr", path);
+
+ if (*fd >= 0)
+ err_exit(EBADF, "%s: open_on_eintr: file already open", path);
+
+ do {
+ r = open(path, flags, mode);
+ } while (r == -1 && (
+ errno == EINTR || errno == EAGAIN ||
+ errno == EWOULDBLOCK || errno == ETXTBSY));
+
+ if (r < 0)
+ err_exit(errno, "%s: open_on_eintr: could not close", path);
+
+ *fd = r;
+
+ if (st != NULL) {
+ if (fstat(*fd, st) < 0)
+ err_exit(errno, "%s: stat", path);
+
+ if (!S_ISREG(st->st_mode))
+ err_exit(errno, "%s: not a regular file", path);
+ }
+
+ if (lseek_on_eintr(*fd, 0, SEEK_CUR, 1, 1) == (off_t)-1)
+ err_exit(errno, "%s: file not seekable", path);
+
+ errno = saved_errno;
+}
+
+void
+close_on_eintr(int *fd)
+{
+ int r;
+ int saved_errno = errno;
+
+ if (fd == NULL)
+ err_exit(EINVAL, "close_on_eintr: null pointer");
+
+ if (*fd < 0)
+ return;
+
+ do {
+ r = close(*fd);
+ } while (r == -1 && (
+ errno == EINTR || errno == EAGAIN ||
+ errno == EWOULDBLOCK || errno == ETXTBSY));
+
+ if (r < 0)
+ err_exit(errno, "close_on_eintr: could not close");
+
+ *fd = -1;
+
+ errno = saved_errno;
+}
+
+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 (if_err(new == NULL || old == NULL, EFAULT) ||
+ if_err(olddirfd < 0 || newdirfd < 0, EBADF))
+ return -1;
+
+ return renameat(olddirfd, old, newdirfd, new);
+}
+
+/* secure open, based on relative path to root
+ *
+ * always a fixed fd for / see: rootfs()
+ * and fs_resolve_at()
+ */
+int
+fs_open(const char *path, int flags)
+{
+ struct filesystem *fs;
+
+ if (if_err(path == NULL, EFAULT) ||
+ if_err(path[0] != '/', EINVAL) ||
+ if_err_sys((fs = rootfs()) == NULL))
+ return -1;
+
+ return fs_resolve_at(fs->rootfd, path + 1, 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 = -1;
+
+ open_on_eintr("/", &global_fs.rootfd,
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0400, NULL);
+
+ if (global_fs.rootfd < 0)
+ return NULL;
+
+ fs_initialised = 1;
+ }
+
+ return &global_fs;
+}
+
+/* filesystem sandboxing in userspace
+ * TODO:
+ missing length bound check.
+ potential CPU DoS on very long paths, spammed repeatedly.
+ perhaps cap at PATH_LEN?
+ */
+int
+fs_resolve_at(int dirfd, const char *path, int flags)
+{
+ int nextfd = -1;
+ int curfd;
+ const char *p;
+#if defined(PATH_LEN) && \
+ ((PATH_LEN) >= 256)
+ char name[PATH_LEN];
+#else
+ char name[4096];
+#endif
+ 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 not the original input */
+ if (curfd != dirfd)
+ close_on_eintr(&curfd);
+
+ curfd = nextfd;
+ nextfd = -1;
+ }
+
+ errno = saved_errno;
+ return curfd;
+
+err:
+ saved_errno = errno;
+
+ if (nextfd >= 0)
+ close_on_eintr(&nextfd);
+
+ /* close curfd only if it's not the original */
+ if (curfd != dirfd && curfd >= 0)
+ close_on_eintr(&curfd);
+
+ errno = saved_errno;
+ return -1;
+}
+
+/* NOTE:
+ rejects . and .. but not empty strings
+ after normalisation. edge case:
+ //////
+
+ normalised implicitly, but might be good
+ to add a defensive check regardless. code
+ probably not exploitable in current state.
+ */
+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);
+
+ if (!is_last) {
+
+ if (if_err(fd < 0, EBADF) ||
+ if_err_sys(fstat(fd, &st) < 0))
+ return -1;
+
+ if (!S_ISDIR(st.st_mode)) {
+
+ 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 = NULL;
+ 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 (if_err(path == NULL || dir == NULL || base == NULL, EFAULT))
+ return -1;
+
+ slen(path, maxlen, &len);
+ memcpy(smalloc(&buf, len + 1),
+ 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) {
+
+ sdup(".", maxlen, dir);
+ *base = buf;
+ } else {
+ errno = EINVAL;
+ free_and_set_null(&buf);
+ return -1;
+ }
+
+ return 0;
+}
+
+/* portable wrapper for use of openat2 on linux,
+ * with fallback for others e.g. openbsd
+ */
+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 (if_err(dirfd < 0, EBADF) ||
+ if_err(path == NULL, 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(int dirfd,
+ const char *path, mode_t mode)
+{
+ int saved_errno = errno;
+ int rval;
+
+ if (if_err(dirfd < 0, EBADF) ||
+ if_err(path == NULL, 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;
+}
+
+ssize_t
+read_on_eintr(int fd,
+ void *buf, size_t count)
+{
+ int saved_errno = errno;
+ int rval;
+
+ if (if_err(buf == NULL, EFAULT) ||
+ if_err(fd < 0, EBADF) ||
+ if_err(count == 0, EINVAL))
+ goto err;
+
+retry:
+ errno = 0;
+
+ if ((rval = read(fd, buf, count)) == -1 && (
+ errno == EINTR ||
+ errno == EAGAIN ||
+ errno == EWOULDBLOCK ||
+ errno == ETXTBSY))
+ goto retry;
+
+ errno = saved_errno;
+ return rval;
+err:
+ return set_errno(saved_errno, EIO);
+}
+
+ssize_t
+pread_on_eintr(int fd,
+ void *buf, size_t count,
+ off_t off)
+{
+ int saved_errno = errno;
+ int rval;
+
+ if (if_err(buf == NULL, EFAULT) ||
+ if_err(fd < 0, EBADF) ||
+ if_err(off < 0, EFAULT) ||
+ if_err(count == 0, EINVAL))
+ goto err;
+
+retry:
+ errno = 0;
+
+ if ((rval = pread(fd, buf, count, off)) == -1 && (
+ errno == EINTR ||
+ errno == EAGAIN ||
+ errno == EWOULDBLOCK ||
+ errno == ETXTBSY))
+ goto retry;
+
+ errno = saved_errno;
+ return rval;
+err:
+ return set_errno(saved_errno, EIO);
+}
+
+ssize_t
+write_on_eintr(int fd,
+ void *buf, size_t count)
+{
+ int saved_errno = errno;
+ int rval;
+
+ if (if_err(buf == NULL, EFAULT) ||
+ if_err(fd < 0, EBADF) ||
+ if_err(count == 0, EINVAL))
+ goto err;
+
+retry:
+ errno = 0;
+
+ if ((rval = write(fd, buf, count)) == -1 && (
+ errno == EINTR ||
+ errno == EAGAIN ||
+ errno == EWOULDBLOCK ||
+ errno == ETXTBSY))
+ goto retry;
+
+ errno = saved_errno;
+ return rval;
+err:
+ return set_errno(saved_errno, EIO);
+}
+
+ssize_t
+pwrite_on_eintr(int fd,
+ void *buf, size_t count,
+ off_t off)
+{
+ int saved_errno = errno;
+ int rval;
+
+ if (if_err(buf == NULL, EFAULT) ||
+ if_err(fd < 0, EBADF) ||
+ if_err(off < 0, EFAULT) ||
+ if_err(count == 0, EINVAL))
+ goto err;
+
+retry:
+ errno = 0;
+
+ if ((rval = pwrite(fd, buf, count, off)) == -1 && (
+ errno == EINTR ||
+ errno == EAGAIN ||
+ errno == EWOULDBLOCK ||
+ errno == ETXTBSY))
+ goto retry;
+
+ errno = saved_errno;
+ return rval;
+err:
+ return set_errno(saved_errno, EIO);
+}
diff --git a/util/libreboot-utils/lib/io.c b/util/libreboot-utils/lib/io.c
new file mode 100644
index 00000000..4938cdc8
--- /dev/null
+++ b/util/libreboot-utils/lib/io.c
@@ -0,0 +1,581 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * I/O functions specific to nvmutil.
+ */
+
+/* TODO: local tmpfiles not being deleted
+ when flags==O_RDONLY e.g. dump command
+ */
+
+#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;
+
+ f->gbe_fd = -1;
+
+ open_on_eintr(f->fname, &f->gbe_fd,
+ O_NOFOLLOW | O_CLOEXEC | O_NOCTTY,
+ ((cmd->flags & O_ACCMODE) == O_RDONLY) ? 0400 : 0600,
+ &f->gbe_st);
+
+ if (f->gbe_st.st_nlink > 1)
+ err_exit(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_exit(EIO, "%s: file unlinked while open", f->fname);
+
+ _flags = fcntl(f->gbe_fd, F_GETFL);
+ if (_flags == -1)
+ err_exit(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_exit(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_exit(EINVAL, "File size must be 8KB, 16KB or 128KB");
+ }
+
+/* currently fails (EBADF), locks are advisory anyway: */
+/*
+ if (lock_file(f->gbe_fd, cmd->flags) == -1)
+ err_exit(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_exit(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_exit(errno, "%s: %s: copy failed",
+ f->fname, f->tname);
+
+ /* file size comparison
+ */
+ if (fstat(f->tmp_fd, &_st) == -1)
+ err_exit(errno, "%s: stat", f->tname);
+
+ f->gbe_tmp_size = _st.st_size;
+
+ if (f->gbe_tmp_size != f->gbe_file_size)
+ err_exit(EIO, "%s: %s: not the same size",
+ f->fname, f->tname);
+
+ /* needs sync, for verification
+ */
+ if (fsync_on_eintr(f->tmp_fd) == -1)
+ err_exit(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_exit(errno, "%s: read failed (cmp)", f->tname);
+
+ if (vcmp(f->buf, f->bufcmp, f->gbe_file_size) != 0)
+ err_exit(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_exit(errno, "%s: file inode/device changed", f->tname);
+
+ if (same_file(f->gbe_fd, &f->gbe_st, 1) < 0)
+ err_exit(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_exit(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_exit(errno, "%s: %s: part %lu",
+ f->fname, rw_type_str, (size_t)p);
+
+ if ((size_t)rval != gbe_rw_size)
+ err_exit(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_exit(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_exit(EIO, "%s: bad write", f->fname);
+
+ saved_errno = errno;
+
+ close_on_eintr(&f->tmp_fd);
+ close_on_eintr(&f->gbe_fd);
+
+ 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
+ */
+ free_and_set_null(&f->tname);
+ }
+ }
+
+ 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_exit(errno, "%s: file inode/device changed", f->tname);
+
+ if (same_file(f->gbe_fd, &f->gbe_st, 1) < 0)
+ err_exit(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 (vcmp(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 = -1;
+
+ char *dir = NULL;
+ char *base = NULL;
+ char *dest_name = NULL;
+
+ int dirfd = -1;
+
+ struct stat st_dir;
+
+ /* will be set 0 if it doesn't
+ */
+ tmp_gbe_bin_exists = 1;
+
+ dest_tmp = NULL;
+ dest_fd = -1;
+
+ saved_errno = errno;
+
+ rval = fs_rename_at(f->dirfd, f->tmpbase,
+ f->dirfd, f->base);
+
+ if (rval > -1)
+ tmp_gbe_bin_exists = 0;
+
+ret_gbe_mv:
+ if (f->gbe_fd > -1) {
+ close_on_eintr(&f->gbe_fd);
+
+ if (fsync_dir(f->fname) < 0) {
+ f->io_err_gbe_bin = 1;
+ rval = -1;
+ }
+ }
+
+ close_on_eintr(&f->tmp_fd);
+
+ /* 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)
+ goto out;
+
+ return set_errno(saved_errno, EIO);
+out:
+ 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_exit(ECANCELED, "%s: GbE %s %s out of bounds",
+ f->fname, d_type, f_op);
+
+ if (off != 0 && off != ncmp >> 1)
+ err_exit(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..0560da47
--- /dev/null
+++ b/util/libreboot-utils/lib/mkhtemp.c
@@ -0,0 +1,999 @@
+/* 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 / fast path: */
+#ifdef __linux__
+#include <linux/openat2.h>
+#include <sys/syscall.h>
+#ifndef O_TMPFILE
+#define O_TMPFILE 020000000
+#endif
+#ifndef AT_EMPTY_PATH
+#define AT_EMPTY_PATH 0x1000
+#endif
+#endif
+
+#include "../include/common.h"
+
+/* note: tmpdir is an override of TMPDIR or /tmp or /var/tmp */
+int
+new_tmpfile(int *fd, char **path, char *tmpdir,
+ const char *template)
+{
+ return new_tmp_common(fd, path, MKHTEMP_FILE,
+ tmpdir, template);
+}
+
+/* note: tmpdir is an override of TMPDIR or /tmp or /var/tmp */
+int
+new_tmpdir(int *fd, char **path, char *tmpdir,
+ const char *template)
+{
+ return new_tmp_common(fd, path, MKHTEMP_DIR,
+ tmpdir, template);
+}
+
+int
+new_tmp_common(int *fd, char **path, int type,
+ char *tmpdir, const char *template)
+{
+#if defined(PATH_LEN) && \
+ (PATH_LEN) >= 256
+ size_t maxlen = PATH_LEN;
+#else
+ size_t maxlen = 4096;
+#endif
+ struct stat st;
+
+ const char *templatestr;
+ size_t templatestr_len;
+
+ 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;
+
+ char *fail_dir = NULL;
+
+ 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 (tmpdir == NULL) { /* no user override */
+#if defined(PERMIT_NON_STICKY_ALWAYS) && \
+ ((PERMIT_NON_STICKY_ALWAYS) > 0)
+ tmpdir = env_tmpdir(PERMIT_NON_STICKY_ALWAYS, &fail_dir, NULL);
+#else
+ tmpdir = env_tmpdir(0, &fail_dir, NULL);
+#endif
+ } else {
+
+#if defined(PERMIT_NON_STICKY_ALWAYS) && \
+ ((PERMIT_NON_STICKY_ALWAYS) > 0)
+ tmpdir = env_tmpdir(PERMIT_NON_STICKY_ALWAYS, &fail_dir,
+ tmpdir);
+#else
+ tmpdir = env_tmpdir(0, &fail_dir, tmpdir);
+#endif
+ }
+ if (tmpdir == NULL)
+ goto err;
+
+ if (*tmpdir == '\0')
+ goto err;
+ if (*tmpdir != '/')
+ goto err;
+
+ if (template != NULL)
+ templatestr = template;
+ else
+ templatestr = "tmp.XXXXXXXXXX";
+
+ /* may as well calculate in advance */
+ destlen = slen(tmpdir, maxlen, &dirlen) + 1
+ + slen(templatestr, maxlen, &templatestr_len);
+ /* full path: */
+ dest = scatn(3, (const char *[]) { tmpdir, "/", templatestr },
+ maxlen, &dest);
+
+ 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;
+
+ close_on_eintr(&dirfd);
+
+ errno = saved_errno;
+ *path = dest;
+
+ return 0;
+
+err:
+
+ if (errno != saved_errno)
+ saved_errno = errno;
+ else
+ saved_errno = errno = EIO;
+
+ free_and_set_null(&dest);
+
+ close_on_eintr(&dirfd);
+ close_on_eintr(fd);
+
+ /* where a TMPDIR isn't found, and we err,
+ * we pass this back through for the
+ * error message
+ */
+ if (fail_dir != NULL)
+ *path = fail_dir;
+
+ errno = saved_errno;
+ return -1;
+}
+
+
+/* hardened TMPDIR parsing
+ */
+
+char *
+env_tmpdir(int bypass_all_sticky_checks, char **tmpdir,
+ char *override_tmpdir)
+{
+ char *t;
+ int allow_noworld_unsticky;
+ int saved_errno = errno;
+
+ static const char tmp[] = "/tmp";
+ static const char vartmp[] = "/var/tmp";
+
+ /* tmpdir is a user override, if set */
+ if (override_tmpdir == NULL)
+ t = getenv("TMPDIR");
+ else
+ t = override_tmpdir;
+
+ if (t != NULL && *t != '\0') {
+
+ if (tmpdir_policy(t,
+ &allow_noworld_unsticky) < 0) {
+ if (tmpdir != NULL)
+ *tmpdir = t;
+ return NULL; /* errno already set */
+ }
+
+ if (!world_writeable_and_sticky(t,
+ allow_noworld_unsticky,
+ bypass_all_sticky_checks)) {
+ if (tmpdir != NULL)
+ *tmpdir = t;
+ return NULL;
+ }
+
+ errno = saved_errno;
+ return t;
+ }
+
+ allow_noworld_unsticky = 0;
+
+ if (world_writeable_and_sticky(tmp,
+ allow_noworld_unsticky,
+ bypass_all_sticky_checks)) {
+
+ if (tmpdir != NULL)
+ *tmpdir = (char *)tmp;
+
+ errno = saved_errno;
+ return (char *)tmp;
+ }
+
+ if (world_writeable_and_sticky(vartmp,
+ allow_noworld_unsticky,
+ bypass_all_sticky_checks)) {
+
+ if (tmpdir != NULL)
+ *tmpdir = (char *)vartmp;
+
+ errno = saved_errno;
+ return (char *)vartmp;
+ }
+
+ 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:
+ return set_errno(saved_errno, EIO);
+}
+
+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!
+ */
+ /* bonus: scmp checks null for us */
+ if (!scmp(a, b, maxlen, &rval_scmp))
+ goto success_same_dir;
+
+ fd_a = fs_open(a, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
+ if (fd_a < 0)
+ goto err_same_dir;
+
+ fd_b = fs_open(b, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
+ 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) {
+
+ close_on_eintr(&fd_a);
+ close_on_eintr(&fd_b);
+
+success_same_dir:
+
+ /* SUCCESS
+ */
+
+ errno = saved_errno;
+ return 1;
+ }
+
+ close_on_eintr(&fd_a);
+ close_on_eintr(&fd_b);
+
+ /* FAILURE (logical)
+ */
+
+ errno = saved_errno;
+ return 0;
+
+err_same_dir:
+
+ /* FAILURE (probably syscall)
+ */
+
+ close_on_eintr(&fd_a);
+ close_on_eintr(&fd_b);
+
+ return set_errno(saved_errno, EIO);
+}
+
+/* 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;
+ }
+
+ /* all of these checks are probably
+ * redundant (execution rights)
+ if (!(st.st_mode & S_IXUSR) ||
+ !(st.st_mode & S_IXGRP) ||
+ !(st.st_mode & S_IXOTH)) {
+ */
+ /* just require it for *you*, for now */
+ if (!(st.st_mode & S_IXUSR)) {
+ errno = EACCES;
+ goto sticky_hell;
+ }
+
+ /* *normal-**ish mode (libc):
+ */
+
+ if (bypass_all_sticky_checks)
+ goto sticky_heaven; /* normal == no security */
+
+ /* extremely not-libc 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 */
+ }
+
+ /* for good measure */
+ if (faccessat(dirfd, ".", X_OK, AT_EACCESS) < 0)
+ goto sticky_hell;
+
+ /* 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:
+ close_on_eintr(&dirfd);
+ errno = saved_errno;
+
+ return 1;
+
+sticky_hell:
+ close_on_eintr(&dirfd);
+
+ (void) set_errno(saved_errno, EPERM);
+ 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)
+ */
+/*
+ TODO:
+ some variables e.g. template vs suffix,
+ assumes they match.
+ we should test this explicitly,
+ but the way this is called is
+ currently safe - this would however
+ be nice for future library use
+ by outside projects.
+ this whole code needs to be reorganised
+*/
+int
+mkhtemp(int *fd,
+ struct stat *st,
+ char *template,
+ int dirfd,
+ const char *fname,
+ struct stat *st_dir_initial,
+ int type)
+{
+ size_t template_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 (if_err(fd == NULL || template == NULL || fname == NULL ||
+ st_dir_initial == NULL, EFAULT) ||
+ if_err(*fd >= 0, EEXIST) ||
+ if_err(dirfd < 0, EBADF))
+ return -1;
+
+ /* count X */
+ for (end = template + slen(template, max_len, &template_len);
+ end > template && *--end == 'X'; xc++);
+
+ fname_len = slen(fname, max_len, &fname_len);
+ if (if_err(strrchr(fname, '/') != NULL, EINVAL))
+ return -1;
+
+ if (if_err(xc < 3 || xc > template_len, EINVAL) ||
+ if_err(fname_len > template_len, EOVERFLOW))
+ return -1;
+
+ if (if_err(vcmp(fname, template + template_len - fname_len,
+ fname_len) != 0, EINVAL))
+ return -1;
+
+ /* fname_copy = templatestr region only; p points to trailing XXXXXX */
+ memcpy(smalloc(&fname_copy, fname_len + 1),
+ template + 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)
+ continue;
+ if (r < 0)
+ goto err;
+
+ /* success: copy final name back */
+ memcpy(template + template_len - fname_len,
+ fname_copy, fname_len);
+
+ errno = saved_errno;
+ goto success;
+ }
+
+ errno = EEXIST;
+
+err:
+ close_on_eintr(fd);
+
+success:
+ free_and_set_null(&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 rval = -1;
+ char *rstr = NULL;
+
+ int file_created = 0;
+ int dir_created = 0;
+
+ if (if_err(fd == NULL || st == NULL || p ==NULL || fname_copy ==NULL ||
+ st_dir_initial == NULL, EFAULT) ||
+ if_err(*fd >= 0, EEXIST))
+ goto err;
+
+ /* TODO: potential infinite loop under entropy failure.
+ * if attacker has control of rand - TODO: maybe add timeout
+ */
+ memcpy(p, rstr = rchars(xc), xc);
+ free_and_set_null(&rstr);
+
+ if (if_err_sys(fd_verify_dir_identity(dirfd, st_dir_initial) < 0))
+ goto err;
+
+ if (type == MKHTEMP_FILE) {
+#ifdef __linux__
+ /* try O_TMPFILE fast path */
+ if (mkhtemp_tmpfile_linux(dirfd,
+ st_dir_initial, fname_copy,
+ p, xc, fd, st) == 0) {
+
+ errno = saved_errno;
+ rval = 1;
+ goto out;
+ }
+#endif
+
+ *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;
+
+ if ((*fd = openat2p(dirfd, fname_copy,
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0)) < 0)
+ goto err;
+
+ if (if_err_sys(fstat(*fd, &st_open) < 0) ||
+ if_err(!S_ISDIR(st_open.st_mode), ENOTDIR))
+ goto err;
+
+ /* NOTE: pointless to check nlink here (only just opened) */
+ 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 (if_err(!S_ISDIR(st_open.st_mode), ENOTDIR) ||
+ if_err_sys(is_owner(&st_open) < 0) ||
+ if_err(st_open.st_mode & (S_IWGRP | S_IWOTH), EPERM))
+ goto err;
+ }
+
+ errno = saved_errno;
+ rval = 1;
+ goto out;
+
+err:
+ close_on_eintr(fd);
+
+ if (file_created)
+ (void) unlinkat(dirfd, fname_copy, 0);
+ if (dir_created)
+ (void) unlinkat(dirfd, fname_copy, AT_REMOVEDIR);
+
+ rval = -1;
+out:
+ return rval;
+}
+
+/* linux has its own special hardening
+ available specifically for tmpfiles,
+ which eliminates many race conditions.
+
+ we still use openat() on bsd, which is
+ still ok with our other mitigations
+ */
+#ifdef __linux__
+int
+mkhtemp_tmpfile_linux(int dirfd,
+ struct stat *st_dir_initial,
+ char *fname_copy,
+ char *p,
+ size_t xc,
+ int *fd,
+ struct stat *st)
+{
+ int saved_errno = errno;
+ int tmpfd = -1;
+ size_t retries;
+ int linked = 0;
+ char *rstr = NULL;
+
+ if (fd == NULL || st == NULL ||
+ fname_copy == NULL || p == NULL ||
+ st_dir_initial == NULL) {
+ errno = EFAULT;
+ return -1;
+ }
+
+ /* create unnamed tmpfile */
+ tmpfd = openat(dirfd, ".",
+ O_TMPFILE | O_RDWR | O_CLOEXEC, 0600);
+
+ if (tmpfd < 0)
+ return -1;
+
+ if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0)
+ goto err;
+
+ for (retries = 0; retries < MKHTEMP_RETRY_MAX; retries++) {
+
+ memcpy(p, rstr = rchars(xc), xc);
+ free_and_set_null(&rstr);
+
+ if (fd_verify_dir_identity(dirfd,
+ st_dir_initial) < 0)
+ goto err;
+
+ if (linkat(tmpfd, "",
+ dirfd, fname_copy,
+ AT_EMPTY_PATH) == 0) {
+
+ linked = 1; /* file created */
+
+ if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0)
+ goto err;
+
+ /* success */
+ *fd = tmpfd;
+
+ if (fstat(*fd, st) < 0)
+ goto err;
+
+ if (secure_file(fd, st, st,
+ O_APPEND, 1, 1, 0600) < 0)
+ goto err;
+
+ errno = saved_errno;
+ return 0;
+ }
+
+ if (errno != EEXIST)
+ goto err;
+
+ /* retry on collision */
+ }
+
+ errno = EEXIST;
+
+err:
+ if (linked)
+ (void) unlinkat(dirfd, fname_copy, 0);
+
+ close_on_eintr(&tmpfd);
+ return -1;
+}
+#endif
+
+/* WARNING: **ONCE** per file.
+ *
+ * some of these checks will trip up
+ * if you do them twice; all of them
+ * only need to be done once anyway.
+ */
+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;
+
+ if (if_err(fd == NULL || st == NULL, EFAULT) ||
+ if_err(*fd < 0, EBADF) ||
+ if_err_sys((flags = fcntl(*fd, F_GETFL)) == -1) ||
+ if_err(bad_flags > 0 && (flags & bad_flags), EPERM))
+ goto err_demons;
+
+ if (expected != NULL) {
+ if (fd_verify_regular(*fd, expected, st) < 0)
+ goto err_demons;
+ } else if (if_err_sys(fstat(*fd, &st_now) == -1) ||
+ if_err(!S_ISREG(st_now.st_mode), EBADF)) {
+ goto err_demons; /***********/
+ } else /* ( >:3 ) */
+ *st = st_now; /* /| |\ */ /* don't let him out */
+ /* / \ */
+ if (check_seek) { /***********/
+ if (lseek(*fd, 0, SEEK_CUR) == (off_t)-1)
+ goto err_demons;
+ } /* don't release the demon! */
+
+ if (if_err(st->st_nlink != 1, ELOOP) ||
+ if_err(st->st_uid != geteuid() && geteuid() != 0, EPERM) ||
+ if_err_sys(is_owner(st) < 0) ||
+ if_err(st->st_mode & (S_IWGRP | S_IWOTH), EPERM))
+ goto err_demons;
+
+ if (do_lock) {
+ if (lock_file(*fd, flags) == -1)
+ goto err_demons;
+
+ /* TODO: why would this be NULL? audit
+ * to find out. we should always verify! */
+ 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:
+ return set_errno(saved_errno, EIO);
+}
+
+int
+fd_verify_regular(int fd,
+ const struct stat *expected,
+ struct stat *out)
+{if (
+ if_err_sys(fd_verify_identity(fd, expected, out) < 0) ||
+ if_err(!S_ISREG(out->st_mode), EBADF)
+ ) return -1;
+ else
+ 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( if_err(fd < 0 || expected == NULL, EFAULT) ||
+ if_err_sys(fstat(fd, &st_now)) ||
+ if_err(st_now.st_dev != expected->st_dev ||
+ st_now.st_ino != expected->st_ino, 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 (if_err(fd < 0 || expected == NULL, EFAULT) ||
+ if_err_sys(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 (if_err(fd < 0, EBADF) ||
+ if_err(flags < 0, 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:
+ return set_errno(saved_errno, EIO);
+}
diff --git a/util/libreboot-utils/lib/num.c b/util/libreboot-utils/lib/num.c
new file mode 100644
index 00000000..e13a8853
--- /dev/null
+++ b/util/libreboot-utils/lib/num.c
@@ -0,0 +1,119 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * Non-randomisation-related numerical functions.
+ * For rand functions, see: rand.c
+ */
+
+#ifdef __OpenBSD__
+#include <sys/param.h>
+#endif
+#include <sys/types.h>
+
+#include <errno.h>
+#if !((defined(__OpenBSD__) && (OpenBSD) >= 201) || \
+ defined(__FreeBSD__) || \
+ defined(__NetBSD__) || defined(__APPLE__))
+#include <fcntl.h> /* if not arc4random: /dev/urandom */
+#endif
+#include <ctype.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "../include/common.h"
+
+unsigned short
+hextonum(char ch_s)
+{
+ int saved_errno = errno;
+
+ unsigned char ch;
+ size_t rval;
+
+ ch = (unsigned char)ch_s;
+
+ if ((unsigned int)(ch - '0') <= 9)
+ return ch - '0';
+
+ ch |= 0x20;
+
+ if ((unsigned int)(ch - 'a') <= 5)
+ return ch - 'a' + 10;
+
+ if (ch == '?' || ch == 'x')
+ return rsize(16); /* <-- with rejection sampling! */
+
+ return 16;
+}
+
+/* basically hexdump -C */
+/*
+ TODO: optimise this
+ write a full util for hexdump
+ how to optimise:
+ don't call print tens of thousands of times!
+ convert the numbers manually, and cache everything
+ in a BUFSIZ sized buffer, with everything properly
+ aligned. i worked out that i could fit 79 rows
+ in a 8KB buffer (1264 bytes of numbers represented
+ as strings in hex)
+ this depends on the OS, and would be calculated at
+ runtime.
+ then:
+ don't use printf. just write it to stdout (basically
+ a simple cat implementation)
+*/
+void
+spew_hex(const void *data, size_t len)
+{
+ const unsigned char *buf = (const unsigned char *)data;
+ unsigned char c;
+ size_t i;
+ size_t j;
+
+ if (buf == NULL ||
+ len == 0)
+ return;
+
+ for (i = 0; i < len; i += 16) {
+
+ if (len <= 4294967296) /* below 4GB */
+ printf("%08zx ", i);
+ else
+ printf("%0*zx ", sizeof(size_t) * 2, i);
+
+ for (j = 0; j < 16; j++) {
+
+ if (i + j < len)
+ printf("%02x ", buf[i + j]);
+ else
+ printf(" ");
+
+ if (j == 7)
+ printf(" ");
+ }
+
+ printf(" |");
+
+ for (j = 0; j < 16 && i + j < len; j++) {
+
+ c = buf[i + j];
+ printf("%c", isprint(c) ? c : '.');
+ }
+
+ printf("|\n");
+ }
+
+ printf("%08zx\n", len);
+}
+
+void
+check_bin(size_t a, const char *a_name)
+{
+ if (a > 1)
+ err_exit(EINVAL, "%s must be 0 or 1, but is %lu",
+ a_name, (size_t)a);
+}
diff --git a/util/libreboot-utils/lib/rand.c b/util/libreboot-utils/lib/rand.c
new file mode 100644
index 00000000..9da8d9eb
--- /dev/null
+++ b/util/libreboot-utils/lib/rand.c
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * Random number generation
+ */
+
+#ifndef RAND_H
+#define RAND_H
+
+#ifdef __OpenBSD__
+#include <sys/param.h>
+#endif
+#include <sys/types.h>
+
+#ifndef USE_URANDOM
+#define USE_URANDOM 0
+#endif
+
+#include <errno.h>
+#if defined(USE_URANDOM) && \
+ ((USE_URANDOM) > 0)
+#include <fcntl.h> /* if not arc4random: /dev/urandom */
+#elif defined(__linux__)
+#include <sys/random.h>
+#include <sys/syscall.h>
+#endif
+
+#include <fcntl.h>
+#include <limits.h>
+#include <stddef.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "../include/common.h"
+
+/* Regarding Linux getrandom/urandom:
+ *
+ * For maximum security guarantee, we *only*
+ * use getrandom via syscall, or /dev/urandom;
+ * use of urandom is ill advised. This is why
+ * we use the syscall, in case the libc version
+ * of getrandom() might defer to /dev/urandom
+ *
+ * We *abort* on error, for both /dev/urandom
+ * and getrandom(), because the BSD arc4random
+ * never returns with error; therefore, for the
+ * most parity in terms of behaviour, we abort,
+ * because otherwise the function would have two
+ * return modes: always successful (BSD), or only
+ * sometimes (Linux). The BSD arc4random could
+ * theoretically abort; it is extremely unlikely
+ * there, and just so on Linux, hence this design.
+ *
+ * This is important, because cryptographic code
+ * for example must not rely on weak randomness.
+ * We must therefore treat broken randomness as
+ * though the world is broken, and burn accordingly.
+ *
+ * Similarly, any invalid input (NULL, zero bytes
+ * requested) are treated as fatal errors; again,
+ * cryptographic code must be reliable. If your
+ * code erroneously requested zero bytes, you might
+ * then end up with a non-randomised buffer, where
+ * you likely intended otherwise.
+ *
+ * In other words: call rset() correctly, or your
+ * program dies, and rset will behave correctly,
+ * or your program dies.
+ */
+
+/* random string generator, with
+ * rejection sampling. NOTE: only
+ * uses ASCII-safe characters, for
+ * printing on a unix terminal
+ *
+ * you still shouldn't use this for
+ * password generation; open diceware
+ * passphrases are better for that
+ *
+ * NOTE: the generated strings must
+ * ALSO be safe for file/directory names
+ * on unix-like os e.g. linux/bsd
+ */
+char *
+rchars(size_t n) /* emulates spkmodem-decode */
+{
+ static char ch[] =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+ char *s = NULL;
+ size_t i;
+
+ smalloc(&s, n + 1);
+ for (i = 0; i < n; i++)
+ s[i] = ch[rsize(sizeof(ch) - 1)];
+
+ *(s + n) = '\0';
+ return s;
+}
+
+size_t
+rsize(size_t n)
+{
+ size_t rval = SIZE_MAX;
+ if (!n)
+ err_exit(EFAULT, "rsize: division by zero");
+
+ /* rejection sampling (clamp rand to eliminate modulo bias) */
+ for (; rval >= SIZE_MAX - (SIZE_MAX % n); rset(&rval, sizeof(rval)));
+
+ return rval % n;
+}
+
+void *
+rmalloc(size_t n)
+{
+ void *buf = NULL;
+ rset(vmalloc(&buf, n), n);
+ return buf; /* basically malloc() but with rand */
+}
+
+void
+rset(void *buf, size_t n)
+{
+ int saved_errno = errno;
+
+ if (if_err(buf == NULL, EFAULT))
+ goto err;
+
+ if (n == 0)
+ err_exit(EPERM, "rset: zero-byte request");
+
+#if (defined(__OpenBSD__) || defined(__FreeBSD__) || \
+ defined(__NetBSD__) || defined(__APPLE__) || \
+ defined(__DragonFly__)) && !(defined(USE_URANDOM) && \
+ ((USE_URANDOM) > 0))
+
+ arc4random_buf(buf, n);
+ goto out;
+#else
+ size_t off = 0;
+ ssize_t rc = 0;
+
+#if defined(USE_URANDOM) && \
+ ((USE_URANDOM) > 0)
+ int fd = -1;
+ open_on_eintr("/dev/urandom", &fd, O_RDONLY, 0400, NULL);
+retry_rand:
+ if ((rc = read_on_eintr(fd,
+ (unsigned char *)buf + off, n - off)) < 0) {
+#elif defined(__linux__)
+retry_rand:
+ if ((rc = (ssize_t)syscall(SYS_getrandom,
+ (unsigned char *)buf + off, n - off, 0)) < 0) {
+#else
+#error Unsupported operating system (possibly unsecure randomisation)
+#endif
+ if (errno == EINTR ||
+ errno == EAGAIN)
+ goto retry_rand;
+
+ goto err; /* possibly unsupported by kernel */
+ }
+
+ if (rc == 0)
+ goto err; /* prevent infinite loop on fatal err */
+
+ if ((off += (size_t)rc) < n)
+ goto retry_rand;
+
+#if defined(USE_URANDOM) && \
+ ((USE_URANDOM) > 0)
+ close_on_eintr(&fd);
+#endif
+ goto out;
+#endif
+out:
+ errno = saved_errno;
+ return;
+err:
+#if defined(USE_URANDOM) && \
+ ((USE_URANDOM) > 0)
+ close_on_eintr(&fd);
+#endif
+ err_exit(ECANCELED,
+ "Randomisation failure, possibly unsupported in your kernel");
+ exit(EXIT_FAILURE);
+}
+#endif
diff --git a/util/libreboot-utils/lib/state.c b/util/libreboot-utils/lib/state.c
new file mode 100644
index 00000000..f0be5656
--- /dev/null
+++ b/util/libreboot-utils/lib/state.c
@@ -0,0 +1,166 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org>
+ *
+ * State machine (singleton) for nvmutil data.
+ */
+
+#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[])
+{
+#if defined(PATH_LEN) && \
+ ((PATH_LEN) >= 256)
+ static size_t maxlen = PATH_LEN;
+#else
+ static size_t maxlen = 4096;
+#endif
+ static int first_run = 1;
+ static char *dir = NULL;
+ static char *base = NULL;
+ char *realdir = NULL;
+ char *tmpdir = NULL;
+ char *tmpbase_local = NULL;
+
+ static struct xstate us = {
+ {
+ /* be careful when modifying xstate. you
+ * must set everything precisely */
+ {
+ 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},
+
+ /* ->i (index to cmd[]) */
+ 0,
+
+ /* .no_cmd (set 0 when a command is found) */
+ 1,
+
+ /* .cat (cat helpers set this) */
+ -1
+
+ };
+
+ if (!first_run)
+ return &us;
+
+ if (argc < 3)
+ err_exit(EINVAL, "xstart: Too few arguments");
+ if (argv == NULL)
+ err_exit(EINVAL, "xstart: NULL argv");
+
+ first_run = 0;
+
+ us.f.buf = us.f.real_buf;
+
+ us.f.fname = argv[1];
+
+ us.f.tmp_fd = -1;
+ us.f.tname = NULL;
+
+ if ((realdir = realpath(us.f.fname, NULL)) == NULL)
+ err_exit(errno, "xstart: can't get realpath of %s",
+ us.f.fname);
+
+ if (fs_dirname_basename(realdir, &dir, &base, 0) < 0)
+ err_exit(errno, "xstart: don't know CWD of %s",
+ us.f.fname);
+
+ sdup(base, maxlen, &us.f.base);
+
+ us.f.dirfd = fs_open(dir,
+ O_RDONLY | O_DIRECTORY);
+ if (us.f.dirfd < 0)
+ err_exit(errno, "%s: open dir", dir);
+
+ if (new_tmpfile(&us.f.tmp_fd, &us.f.tname, dir, ".gbe.XXXXXXXXXX") < 0)
+ err_exit(errno, "%s", us.f.tname);
+
+ if (fs_dirname_basename(us.f.tname,
+ &tmpdir, &tmpbase_local, 0) < 0)
+ err_exit(errno, "tmp basename");
+
+ sdup(tmpbase_local, maxlen, &us.f.tmpbase);
+
+ free_and_set_null(&tmpdir);
+
+ if (us.f.tname == NULL)
+ err_exit(errno, "x->f.tname null");
+ if (*us.f.tname == '\0')
+ err_exit(errno, "x->f.tname empty");
+
+ if (fstat(us.f.tmp_fd, &us.f.tmp_st) < 0)
+ err_exit(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));
+
+ return &us;
+}
+
+struct xstate *
+xstatus(void)
+{
+ struct xstate *x = xstart(0, NULL);
+
+ if (x == NULL)
+ err_exit(EACCES, "NULL pointer to xstate");
+
+ return x;
+}
diff --git a/util/libreboot-utils/lib/string.c b/util/libreboot-utils/lib/string.c
new file mode 100644
index 00000000..c083bd6d
--- /dev/null
+++ b/util/libreboot-utils/lib/string.c
@@ -0,0 +1,636 @@
+/* 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 <limits.h>
+#include <stdint.h>
+
+#include "../include/common.h"
+
+/* for null detection inside
+ * word-optimised string functions
+ */
+#define ff ((size_t)-1 / 0xFF)
+#define high ((ff) * 0x80)
+/* NOTE:
+ * do not assume that a match means
+ * both words have null at the same
+ * location. see how this is handled
+ * e.g. in scmp.
+ */
+#define zeroes(x) (((x) - (ff)) & ~(x) & (high))
+
+size_t
+page_remain(const void *p)
+{
+ /* calling sysconf repeatedly
+ * is folly. cache it (static)
+ */
+ static size_t pagesz = 0;
+ if (!pagesz)
+ pagesz = (size_t)pagesize();
+
+ return pagesz - ((uintptr_t)p & (pagesz - 1));
+}
+
+long
+pagesize(void)
+{
+ static long rval = 0;
+ static int set = 0;
+
+ if (!set) {
+ if ((rval = sysconf(_SC_PAGESIZE)) < 0)
+ err_exit(errno, "could not determine page size");
+ set = 1;
+ }
+
+ return rval;
+}
+
+void
+free_and_set_null(char **buf)
+{
+ if (buf == NULL)
+ err_exit(EFAULT,
+ "null ptr (to ptr for freeing) in free_and_set_null");
+
+ if (*buf == NULL)
+ return;
+
+ free(*buf);
+ *buf = NULL;
+}
+
+/* safe(ish) malloc.
+
+ use this and free_and_set_null()
+ in your program, to reduce the
+ chance of use after frees!
+
+ if you use these functions in the
+ intended way, you will greatly reduce
+ the number of bugs in your code
+ */
+char *
+smalloc(char **buf, size_t size)
+{
+ return (char *)vmalloc((void **)buf, size);
+}
+void *
+vmalloc(void **buf, size_t size)
+{
+ void *rval = NULL;
+
+ if (size >= SIZE_MAX - 1)
+ err_exit(EOVERFLOW, "integer overflow in vmalloc");
+ if (buf == NULL)
+ err_exit(EFAULT, "Bad pointer passed to vmalloc");
+
+ /* lots of programs will
+ * re-initialise a buffer
+ * that was allocated, without
+ * freeing or NULLing it. this
+ * is here intentionally, to
+ * force the programmer to behave
+ */
+ if (*buf != NULL)
+ err_exit(EFAULT, "Non-null pointer given to vmalloc");
+
+ if (!size)
+ err_exit(EFAULT,
+ "Tried to vmalloc(0) and that is very bad. Fix it now");
+
+ if ((rval = malloc(size)) == NULL)
+ err_exit(errno, "malloc fail in vmalloc");
+
+ return *buf = rval;
+}
+
+/* strict word-based strcmp */
+int
+scmp(const char *a,
+ const char *b,
+ size_t maxlen,
+ int *rval)
+{
+ size_t i = 0;
+ size_t j;
+ size_t wa;
+ size_t wb;
+ int saved_errno = errno;
+
+ if (if_err(a == NULL || b == NULL || rval == NULL, EFAULT))
+ goto err;
+
+ for ( ; ((uintptr_t)(a + i) % sizeof(size_t)) != 0; i++) {
+
+ if (if_err(i >= maxlen, EOVERFLOW))
+ goto err;
+ else if (!ccmp(a, b, i, rval))
+ goto out;
+ }
+
+ for ( ; i + sizeof(size_t) <= maxlen;
+ i += sizeof(size_t)) {
+
+ /* prevent crossing page boundary on word check */
+ if (page_remain(a + i) < sizeof(size_t) ||
+ page_remain(b + i) < sizeof(size_t))
+ break;
+
+ memcpy(&wa, a + i, sizeof(size_t));
+ memcpy(&wb, b + i, sizeof(size_t));
+
+ if (wa != wb)
+ for (j = 0; j < sizeof(size_t); j++)
+ if (!ccmp(a, b, i + j, rval))
+ goto out;
+
+ if (!zeroes(wa))
+ continue;
+
+ *rval = 0;
+ goto out;
+ }
+
+ for ( ; i < maxlen; i++)
+ if (!ccmp(a, b, i, rval))
+ goto out;
+
+err:
+ (void) set_errno(saved_errno, EFAULT);
+ if (rval != NULL)
+ *rval = -1;
+
+ err_exit(errno, "scmp");
+ return -1;
+out:
+ errno = saved_errno;
+ return *rval;
+}
+
+int ccmp(const char *a, const char *b,
+ size_t i, int *rval)
+{
+ unsigned char ac;
+ unsigned char bc;
+
+ if (if_err(a == NULL || b == NULL || rval == NULL, EFAULT))
+ err_exit(errno, "ccmp");
+
+ ac = (unsigned char)a[i];
+ bc = (unsigned char)b[i];
+
+ if (ac != bc) {
+ *rval = ac - bc;
+ return 0;
+ } else if (ac == '\0') {
+ *rval = 0;
+ return 0;
+ }
+
+ return 1;
+}
+
+/* strict word-based strlen */
+size_t
+slen(const char *s,
+ size_t maxlen,
+ size_t *rval)
+{
+ int saved_errno = errno;
+ size_t i = 0;
+ size_t w;
+ size_t j;
+
+ if (if_err(s == NULL || rval == NULL, EFAULT))
+ goto err;
+
+ for ( ; ((uintptr_t)(s + i) % sizeof(size_t)) != 0; i++) {
+
+ if (i >= maxlen)
+ goto err;
+ if (s[i] == '\0') {
+ *rval = i;
+ goto out;
+ }
+ }
+
+ for ( ; i + sizeof(size_t) <= maxlen;
+ i += sizeof(size_t)) {
+
+ memcpy(&w, s + i, sizeof(size_t));
+ if (!zeroes(w))
+ continue;
+
+ for (j = 0; j < sizeof(size_t); j++) {
+ if (s[i + j] == '\0') {
+ *rval = i + j;
+ goto out;
+ }
+ }
+ }
+
+ for ( ; i < maxlen; i++) {
+ if (s[i] == '\0') {
+ *rval = i;
+ goto out;
+ }
+ }
+
+err:
+ (void) set_errno(saved_errno, EFAULT);
+ if (rval != NULL)
+ *rval = 0;
+
+ err_exit(errno, "slen"); /* abort */
+ return 0; /* gcc15 is happy */
+out:
+ errno = saved_errno;
+ return *rval;
+}
+
+/* strict word-based strdup */
+char *
+sdup(const char *s,
+ size_t max, char **dest)
+{
+ size_t j;
+ size_t w;
+ size_t i = 0;
+ char *out = NULL;
+ int saved_errno = errno;
+
+ if (if_err(dest == NULL || *dest != NULL || s == NULL, EFAULT))
+ goto err;
+
+ out = smalloc(dest, max);
+
+ for ( ; ((uintptr_t)(s + i) % sizeof(size_t)) != 0; i++) {
+
+ if (if_err(i >= max, EOVERFLOW))
+ goto err;
+
+ out[i] = s[i];
+ if (s[i] == '\0') {
+ *dest = out;
+ goto out;
+ }
+ }
+
+ for ( ; i + sizeof(size_t) <= max; i += sizeof(size_t)) {
+
+ if (page_remain(s + i) < sizeof(size_t))
+ break;
+
+ memcpy(&w, s + i, sizeof(size_t));
+ if (!zeroes(w)) {
+ memcpy(out + i, &w, sizeof(size_t));
+ continue;
+ }
+
+ for (j = 0; j < sizeof(size_t); j++) {
+
+ out[i + j] = s[i + j];
+ if (s[i + j] == '\0') {
+ *dest = out;
+ goto out;
+ }
+ }
+ }
+
+ for ( ; i < max; i++) {
+
+ out[i] = s[i];
+ if (s[i] == '\0') {
+ *dest = out;
+ goto out;
+ }
+ }
+
+err:
+ free_and_set_null(&out);
+ if (dest != NULL)
+ *dest = NULL;
+
+ (void) set_errno(saved_errno, EFAULT);
+ err_exit(errno, "sdup");
+
+ return NULL;
+out:
+ errno = saved_errno;
+ return *dest;
+}
+
+/* concatenate N number of strings */
+char *
+scatn(ssize_t sc, const char **sv,
+ size_t max, char **rval)
+{
+ int saved_errno = errno;
+ char *final = NULL;
+ char *rcur = NULL;
+ char *rtmp = NULL;
+ size_t i;
+
+ if (if_err(sc < 2, EINVAL) ||
+ if_err(sv == NULL, EFAULT) ||
+ if_err(rval == NULL || *rval != NULL, EFAULT))
+ goto err;
+
+ for (i = 0; i < sc; i++) {
+
+ if (if_err(sv[i] == NULL, EFAULT))
+ goto err;
+ else if (i == 0) {
+ (void) sdup(sv[0], max, &final);
+ continue;
+ }
+
+ rtmp = NULL;
+ scat(final, sv[i], max, &rtmp);
+
+ free_and_set_null(&final);
+ final = rtmp;
+ rtmp = NULL;
+ }
+
+ errno = saved_errno;
+ *rval = final;
+ return *rval;
+err:
+ free_and_set_null(&rcur);
+ free_and_set_null(&rtmp);
+ free_and_set_null(&final);
+
+ (void) set_errno(saved_errno, EFAULT);
+
+ err_exit(errno, "scatn");
+ return NULL;
+}
+
+/* strict strcat */
+char *
+scat(const char *s1, const char *s2,
+ size_t n, char **dest)
+{
+ size_t size1;
+ size_t size2;
+ char *rval = NULL;
+ int saved_errno = errno;
+
+ if (if_err(dest == NULL || *dest != NULL, EFAULT))
+ goto err;
+
+ slen(s1, n, &size1);
+ slen(s2, n, &size2);
+
+ if (if_err(size1
+ > SIZE_MAX - size2 - 1, EOVERFLOW))
+ goto err;
+
+ smalloc(&rval, size1 + size2 + 1);
+
+ memcpy(rval, s1, size1);
+ memcpy(rval + size1, s2, size2);
+ *(rval + size1 + size2) = '\0';
+
+ *dest = rval;
+ errno = saved_errno;
+ return *dest;
+err:
+ (void) set_errno(saved_errno, EINVAL);
+ if (dest != NULL)
+ *dest = NULL;
+ err_exit(errno, "scat");
+
+ return NULL;
+}
+
+/* strict split/de-cat - off is where
+ 2nd buffer will start from */
+void
+dcat(const char *s, size_t n,
+ size_t off, char **dest1,
+ char **dest2)
+{
+ size_t size;
+ char *rval1 = NULL;
+ char *rval2 = NULL;
+ int saved_errno = errno;
+
+ if (if_err(dest1 == NULL || dest2 == NULL, EFAULT))
+ goto err;
+
+ if (if_err(slen(s, n, &size) >= SIZE_MAX - 1, EOVERFLOW) ||
+ if_err(off >= size, EOVERFLOW))
+ goto err;
+
+ memcpy(smalloc(&rval1, off + 1),
+ s, off);
+ *(rval1 + off) = '\0';
+
+ memcpy(smalloc(&rval2, size - off +1),
+ s + off, size - off);
+ *(rval2 + size - off) = '\0';
+
+ *dest1 = rval1;
+ *dest2 = rval2;
+
+ errno = saved_errno;
+ return;
+
+err:
+ *dest1 = *dest2 = NULL;
+
+ free_and_set_null(&rval1);
+ free_and_set_null(&rval2);
+
+ (void) set_errno(saved_errno, EINVAL);
+ err_exit(errno, "dcat");
+}
+
+/* because no libc reimagination is complete
+ * without a reimplementation of memcmp. and
+ * no safe one is complete without null checks.
+ */
+int
+vcmp(const void *s1, const void *s2, size_t n)
+{
+ int saved_errno = errno;
+ size_t i = 0;
+ size_t a;
+ size_t b;
+
+ const unsigned char *x;
+ const unsigned char *y;
+
+ if (if_err(s1 == NULL || s2 == NULL, EFAULT))
+ err_exit(EFAULT, "vcmp: null input");
+
+ x = s1;
+ y = s2;
+
+ for ( ; i + sizeof(size_t) <= n; i += sizeof(size_t)) {
+
+ memcpy(&a, x + i, sizeof(size_t));
+ memcpy(&b, y + i, sizeof(size_t));
+
+ if (a != b)
+ break;
+ }
+
+ for ( ; i < n; i++)
+ if (x[i] != y[i])
+ return (int)x[i] - (int)y[i];
+
+ errno = saved_errno;
+ return 0;
+}
+
+/* on functions that return with errno,
+ * i sometimes have a default fallback,
+ * which is set if errno wasn't changed,
+ * under error condition.
+ */
+int
+set_errno(int saved_errno, int fallback)
+{
+ if (errno == saved_errno)
+ errno = fallback;
+ return -1;
+}
+
+/* the one for nvmutil state is in state.c */
+/* this one just exits */
+void
+err_exit(int nvm_errval, const char *msg, ...)
+{
+ va_list args;
+ int saved_errno = errno;
+ const char *p;
+
+ func_t err_cleanup = errhook(NULL);
+ err_cleanup();
+ errno = saved_errno;
+
+ if (!errno)
+ saved_errno = errno = ECANCELED;
+
+ fprintf(stderr, "%s: ", lbgetprogname());
+
+ va_start(args, msg);
+ vfprintf(stderr, msg, args);
+ va_end(args);
+
+ fprintf(stderr, ": %s\n", strerror(errno));
+
+ exit(EXIT_FAILURE);
+}
+
+/* the err function will
+ * call this upon exit, and
+ * cleanup will be performed
+ * e.g. you might want to
+ * close some files, depending
+ * on your program.
+ * see: err_exit()
+ */
+func_t errhook(func_t ptr)
+{
+ static int set = 0;
+ static func_t hook = NULL;
+
+ if (!set) {
+ set = 1;
+
+ if (ptr == NULL)
+ hook = no_op;
+ else
+ hook = ptr;
+ }
+
+ return hook;
+}
+
+void
+no_op(void)
+{
+ return;
+}
+
+const char *
+lbgetprogname(void)
+{
+ char *name = lbsetprogname(NULL);
+ char *p = NULL;
+ if (name)
+ p = strrchr(name, '/');
+ if (p)
+ return p + 1;
+ else if (name)
+ return name;
+ else
+ return "libreboot-utils";
+}
+
+/* singleton. if string not null,
+ sets the string. after set,
+ will not set anymore. either
+ way, returns the string
+ */
+char *
+lbsetprogname(char *argv0)
+{
+ static char *progname = NULL;
+ static int set = 0;
+
+ if (!set) {
+ if (argv0 == NULL)
+ return "libreboot-utils";
+ (void) sdup(argv0, 4096, &progname);
+ set = 1;
+ }
+
+ return progname;
+}
+
+/* https://man.openbsd.org/pledge.2
+ https://man.openbsd.org/unveil.2 */
+int
+xpledgex(const char *promises, const char *execpromises)
+{
+ int saved_errno = errno;
+ (void) promises, (void) execpromises, (void) saved_errno;
+#ifdef __OpenBSD__
+ if (pledge(promises, execpromises) == -1)
+ err_exit(errno, "pledge");
+#endif
+ errno = saved_errno;
+ return 0;
+}
+int
+xunveilx(const char *path, const char *permissions)
+{
+ int saved_errno = errno;
+ (void) path, (void) permissions, (void) saved_errno;
+#ifdef __OpenBSD__
+ if (pledge(promises, execpromises) == -1)
+ err_exit(errno, "pledge");
+#endif
+ errno = saved_errno;
+ return 0;
+}
diff --git a/util/libreboot-utils/lib/usage.c b/util/libreboot-utils/lib/usage.c
new file mode 100644
index 00000000..7c9fa34b
--- /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 = lbgetprogname();
+
+ 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_exit(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..85e1d88b
--- /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_exit(ECANCELED, "check_nvm_bound: out of bounds %lu",
+ (size_t)c);
+}
diff --git a/util/libreboot-utils/lottery.c b/util/libreboot-utils/lottery.c
new file mode 100644
index 00000000..7370de1b
--- /dev/null
+++ b/util/libreboot-utils/lottery.c
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: MIT ( >:3 )
+ * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> /| |\
+ Something something non-determinism / \ */
+
+#include <ctype.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include "include/common.h"
+
+static void
+exit_cleanup(void);
+
+int
+main(int argc, char **argv)
+{
+ int same = 0;
+ char *buf;
+ size_t size = BUFSIZ;
+ (void) argc, (void) argv;
+
+ (void) errhook(exit_cleanup);
+ (void) lbsetprogname(argv[0]);
+
+ /* https://man.openbsd.org/pledge.2 */
+ xpledgex("stdio", NULL);
+
+ buf = rmalloc(size);
+ if (!vcmp(buf, buf + (size >> 1), size >> 1))
+ same = 1;
+
+ if (argc < 2) /* no spew */
+ spew_hex(buf, size);
+ free_and_set_null(&buf);
+
+ fprintf(stderr, "\n%s\n", same ? "You win!" : "You lose!");
+ return same ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+static void
+exit_cleanup(void)
+{
+#if defined(__OpenBSD__)
+ fprintf(stderr, "OpenBSD wins\n");
+#elif defined(__FreeBSD__)
+ fprintf(stderr, "FreeBSD wins\n");
+#elif defined(__NetBSD__)
+ fprintf(stderr, "NetBSD wins\n");
+#elif defined(__APPLE__)
+ fprintf(stderr, "MacOS wins\n");
+#elif defined(__DragonFly__)
+ fprintf(stderr, "DragonFly BSD wins\n");
+#elif defined(__linux__)
+#if defined(__GLIBC__)
+ fprintf(stderr, "GNU/Linux wins\n");
+#elif defined(__MUSL__)
+ fprintf(stderr, "Rich Felker wins\n");
+#else
+ fprintf(stderr, "Linux wins\n");
+#endif
+#else
+ fprintf(stderr, "Your operating system wins\n");
+#endif
+ return;
+}
diff --git a/util/libreboot-utils/mkhtemp.c b/util/libreboot-utils/mkhtemp.c
new file mode 100644
index 00000000..f4c2b646
--- /dev/null
+++ b/util/libreboot-utils/mkhtemp.c
@@ -0,0 +1,152 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * Hardened mktemp (mkhtemp!)
+ *
+ * WORK IN PROGRESS (proof of concept), or, v0.0000001
+ * DO NOT PUT THIS IN YOUR LINUX DISTRO YET.
+ *
+ * I will remove this notice when the code is mature, and
+ * probably contact several of your projects myself.
+ *
+ * See README. This is an ongoing project; no proper docs
+ * yet, and no manpage (yet!) - the code is documentation,
+ * while the specification that it implements evolves.
+ */
+
+#if defined(__linux__) && !defined(_GNU_SOURCE)
+/* for openat2 on linux */
+#define _GNU_SOURCE 1
+#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"
+
+static void
+exit_cleanup(void);
+
+int
+main(int argc, char *argv[])
+{
+#if defined (PATH_LEN) && \
+ (PATH_LEN) >= 256
+ size_t maxlen = PATH_LEN;
+#else
+ size_t maxlen = 4096;
+#endif
+ size_t len;
+ size_t tlen;
+ size_t xc = 0;
+
+ char *tmpdir = NULL;
+ char *template = NULL;
+ char *p;
+ char *s = NULL;
+ char *rp;
+ char resolved[maxlen];
+ char c;
+
+ int fd = -1;
+ int type = MKHTEMP_FILE;
+
+ (void) errhook(exit_cleanup);
+ (void) lbsetprogname(argv[0]);
+
+ /* https://man.openbsd.org/pledge.2 */
+ xpledgex("stdio flock rpath wpath cpath", NULL);
+
+ while ((c =
+ getopt(argc, argv, "qdp:")) != -1) {
+
+ switch (c) {
+ case 'd':
+ type = MKHTEMP_DIR;
+ break;
+
+ case 'p':
+ tmpdir = optarg;
+ break;
+
+ case 'q': /* don't print errors */
+ /* (exit status unchanged) */
+ break;
+
+ default:
+ goto err_usage;
+ }
+ }
+
+ if (optind < argc)
+ template = argv[optind];
+ if (optind + 1 < argc)
+ goto err_usage;
+
+ /* custom template e.g. foo.XXXXXXXXXXXXXXXXXXXXX */
+ if (template != NULL) {
+ for (p = template + slen(template, maxlen, &tlen);
+ p > template && *--p == 'X'; xc++);
+
+ if (xc < 3) /* the gnu mktemp errs on less than 3 */
+ err_exit(EINVAL,
+ "template must have 3 X or more on end (12+ advised");
+ }
+
+ /* user supplied -p PATH - WARNING:
+ * this permits symlinks, but only here,
+ * not in the library, so they are resolved
+ * here first, and *only here*. the mkhtemp
+ * library blocks them. be careful
+ * when using -p
+ */
+ if (tmpdir != NULL) {
+ rp = realpath(tmpdir, resolved);
+ if (rp == NULL)
+ err_exit(errno, "%s", tmpdir);
+
+ tmpdir = resolved;
+ }
+
+ if (new_tmp_common(&fd, &s, type,
+ tmpdir, template) < 0)
+ err_exit(errno, "%s", s);
+
+ xpledgex("stdio", NULL);
+
+ if (s == NULL)
+ err_exit(EFAULT, "bad string initialisation");
+ if (*s == '\0')
+ err_exit(EFAULT, "empty string initialisation");
+
+ slen(s, maxlen, &len); /* Nullterminierung prüfen */
+ /* for good measure */
+
+ printf("%s\n", s);
+
+ return EXIT_SUCCESS;
+
+err_usage:
+ err_exit(EINVAL,
+ "usage: %s [-d] [-p dir] [template]\n", lbgetprogname());
+}
+
+static void
+exit_cleanup(void)
+{
+ return;
+}/*
+
+ ( >:3 )
+ /| |\
+ / \ */
diff --git a/util/libreboot-utils/nvmutil.c b/util/libreboot-utils/nvmutil.c
new file mode 100644
index 00000000..ec41371f
--- /dev/null
+++ b/util/libreboot-utils/nvmutil.c
@@ -0,0 +1,116 @@
+/* SPDX-License-Identifier: MIT
+ * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org>
+ *
+ * This tool lets you modify Intel GbE NVM (Gigabit Ethernet
+ * Non-Volatile Memory) images, e.g. change the MAC address.
+ * These images configure your Intel Gigabit Ethernet adapter.
+ */
+
+#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"
+
+static void
+exit_cleanup(void);
+
+int
+main(int argc, char *argv[])
+{
+ struct xstate *x;
+ struct commands *cmd;
+ struct xfile *f;
+ size_t c;
+
+ (void) errhook(exit_cleanup);
+ (void) lbsetprogname(argv[0]);
+
+ /* https://man.openbsd.org/pledge.2 */
+ /* https://man.openbsd.org/unveil.2 */
+ xpledgex("stdio flock rpath wpath cpath unveil", NULL);
+ xunveilx("/dev/urandom", "r");
+
+#ifndef S_ISREG
+ err_exit(ECANCELED,
+ "Can't determine file types (S_ISREG undefined)");
+#endif
+#if ((CHAR_BIT) != 8)
+ err_exit(ECANCELED, "Unsupported char size");
+#endif
+
+ if ((x = xstart(argc, argv)) == NULL)
+ err_exit(ECANCELED, "NULL state on init");
+
+ /* parse user command */
+/* TODO: CHECK ACCESSES VIA xstatus() */
+ set_cmd(argc, argv);
+ set_cmd_args(argc, argv);
+
+ cmd = &x->cmd[x->i];
+ f = &x->f;
+
+ if ((cmd->flags & O_ACCMODE) == O_RDONLY)
+ xunveilx(f->fname, "r");
+ else
+ xunveilx(f->fname, "rwc");
+
+ xunveilx(f->tname, "rwc");
+ xunveilx(NULL, NULL);
+ xpledgex("stdio flock rpath wpath cpath", NULL);
+
+ if (cmd->run == NULL)
+ err_exit(errno, "Command not set");
+
+ sanitize_command_list();
+ open_gbe_file();
+ copy_gbe();
+ read_checksums();
+ cmd->run();
+
+ for (c = 0; c < items(x->cmd); c++)
+ x->cmd[c].run = cmd_helper_err;
+
+ if ((cmd->flags & O_ACCMODE) == O_RDWR)
+ write_to_gbe_bin();
+
+ exit_cleanup();
+ if (f->io_err_gbe_bin)
+ err_exit(EIO, "%s: error writing final file");
+
+ free_and_set_null(&f->tname);
+
+ return EXIT_SUCCESS;
+}
+
+static void
+exit_cleanup(void)
+{
+ struct xstate *x;
+ struct xfile *f;
+
+ x = xstatus();
+ if (x == NULL)
+ return;
+
+ f = &x->f;
+
+ /* close fds if still open */
+ close_on_eintr(&f->tmp_fd);
+ close_on_eintr(&f->gbe_fd);
+
+ /* unlink tmpfile if it exists */
+ if (f->tname != NULL) {
+ (void) unlink(f->tname);
+ free_and_set_null(&f->tname);
+ }
+}
diff --git a/util/nvmutil/.gitignore b/util/nvmutil/.gitignore
deleted file mode 100644
index 802202a4..00000000
--- a/util/nvmutil/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/nvm
-/nvmutil
-*.bin
diff --git a/util/nvmutil/Makefile b/util/nvmutil/Makefile
deleted file mode 100644
index bef6f28c..00000000
--- a/util/nvmutil/Makefile
+++ /dev/null
@@ -1,30 +0,0 @@
-# SPDX-License-Identifier: MIT
-# SPDX-FileCopyrightText: 2022,2026 Leah Rowe <leah@libreboot.org>
-# SPDX-FileCopyrightText: 2023 Riku Viitanen <riku.viitanen@protonmail.com>
-
-CC?=cc
-CFLAGS?=-Os -Wall -Wextra -Werror -pedantic -std=c90
-DESTDIR?=
-PREFIX?=/usr/local
-INSTALL?=install
-
-PROG=nvmutil
-
-all: $(PROG)
-
-$(PROG): nvmutil.c
- $(CC) $(CFLAGS) nvmutil.c -o $(PROG)
-
-install: $(PROG)
- mkdir -p $(DESTDIR)$(PREFIX)/bin/
- install $(PROG) $(DESTDIR)$(PREFIX)/bin/
-
-uninstall:
- rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG)
-
-clean:
- rm -f $(PROG)
-
-distclean: clean
-
-.PHONY: all install uninstall clean distclean
diff --git a/util/nvmutil/nvmutil.c b/util/nvmutil/nvmutil.c
deleted file mode 100644
index 68e041a3..00000000
--- a/util/nvmutil/nvmutil.c
+++ /dev/null
@@ -1,1617 +0,0 @@
-/* SPDX-License-Identifier: MIT
- *
- * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org>
- * Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com>
- *
- * This tool lets you modify Intel GbE NVM (Gigabit Ethernet
- * Non-Volatile Memory) images, e.g. change the MAC address.
- * These images configure your Intel Gigabit Ethernet adapter.
- *
- * This code is designed to be portable, running on as many
- * Unix and Unix-like systems as possible (mainly BSD/Linux).
- *
- * Recommended CFLAGS for Clang/GCC:
- *
- * -Os -Wall -Wextra -Werror -pedantic -std=c99
- */
-
-#ifndef _XOPEN_SOURCE
-#define _XOPEN_SOURCE 500
-#endif
-
-#ifndef _FILE_OFFSET_BITS
-#define _FILE_OFFSET_BITS 64
-#endif
-
-#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>
-#if defined(__has_include)
-#if __has_include(<stdint.h>)
-#include <stdint.h>
-#else
-typedef unsigned char uint8_t;
-typedef unsigned short uint16_t;
-typedef unsigned int uint32_t;
-#endif
-#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
-#include <stdint.h>
-#else
-typedef unsigned char uint8_t;
-typedef unsigned short uint16_t;
-typedef unsigned int uint32_t;
-#endif
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1];
-typedef char static_assert_uint8_is_1[(sizeof(uint8_t) == 1) ? 1 : -1];
-typedef char static_assert_uint16_is_2[(sizeof(uint16_t) == 2) ? 1 : -1];
-typedef char static_assert_uint32_is_4[(sizeof(uint32_t) == 4) ? 1 : -1];
-typedef char static_assert_int_ge_32[(sizeof(int) >= 4) ? 1 : -1];
-typedef char static_assert_twos_complement[
- ((-1 & 3) == 3) ? 1 : -1
-];
-
-/*
- * We set _FILE_OFFSET_BITS 64, but we only handle
- * files that are 128KB in size at a maximum, so we
- * realistically only need 32-bit at a minimum.
- */
-typedef char static_assert_off_t_is_32[(sizeof(off_t) >= 4) ? 1 : -1];
-
-/*
- * Older versions of BSD to the early 2000s
- * could compile nvmutil, but pledge was
- * added in the 2010s. Therefore, for extra
- * portability, we will only pledge/unveil
- * on OpenBSD versions that have it.
- */
-#if defined(__OpenBSD__) && defined(OpenBSD)
-#if OpenBSD >= 604
-#ifndef NVMUTIL_UNVEIL
-#define NVMUTIL_UNVEIL 1
-#endif
-#endif
-#if OpenBSD >= 509
-#ifndef NVMUTIL_PLEDGE
-#define NVMUTIL_PLEDGE 1
-#endif
-#endif
-#endif
-
-#ifndef EXIT_FAILURE
-#define EXIT_FAILURE 1
-#endif
-
-#ifndef EXIT_SUCCESS
-#define EXIT_SUCCESS 0
-#endif
-
-#ifndef O_BINARY
-#define O_BINARY 0
-#endif
-
-#ifndef O_NONBLOCK
-#define O_NONBLOCK 0
-#endif
-
-/*
- * Sanitize command tables.
- */
-static void sanitize_command_list(void);
-static void sanitize_command_index(size_t c);
-static void check_enum_bin(size_t a, const char *a_name,
- size_t b, const char *b_name);
-
-/*
- * Argument handling (user input)
- */
-static void set_cmd(int argc, char *argv[]);
-static void set_cmd_args(int argc, char *argv[]);
-static size_t conv_argv_part_num(const char *part_str);
-static int xstrxcmp(const char *a, const char *b, size_t maxlen);
-
-/*
- * Prep files for reading
- *
- * Portability: /dev/urandom used
- * on Linux / old Unix, whereas
- * arc4random is used on BSD/MacOS.
- */
-static void open_dev_urandom(void);
-static void open_gbe_file(void);
-static void xopen(int *fd, const char *path, int flags, struct stat *st);
-
-/*
- * Read GbE file and verify
- * checksums.
- *
- * After this, we can run commands.
- */
-static void read_gbe_file(void);
-static void read_checksums(void);
-static int good_checksum(size_t partnum);
-
-/*
- * Execute user command on GbE data.
- * These are stubs that call helpers.
- */
-static void run_cmd(size_t c);
-static void check_command_num(size_t c);
-static uint8_t valid_command(size_t c);
-
-/*
- * Helper functions for command: setmac
- */
-static void cmd_helper_setmac(void);
-static void parse_mac_string(void);
-static size_t xstrxlen(const char *scmp, size_t maxlen);
-static void set_mac_byte(size_t mac_byte_pos);
-static void set_mac_nib(size_t mac_str_pos,
- size_t mac_byte_pos, size_t mac_nib_pos);
-static uint16_t hextonum(char ch_s);
-static uint16_t rhex(void);
-static void write_mac_part(size_t partnum);
-
-/*
- * Helper functions for command: dump
- */
-static void cmd_helper_dump(void);
-static void print_mac_from_nvm(size_t partnum);
-static void hexdump(size_t partnum);
-
-/*
- * Helper functions for commands:
- * cat, cat16 and cat128
- */
-static void cmd_helper_cat(void);
-static void gbe_cat_buf(uint8_t *b);
-
-/*
- * After command processing, write
- * the modified GbE file back.
- *
- * These are stub functions: check
- * below for the actual functions.
- */
-static void write_gbe_file(void);
-static void override_part_modified(void);
-static void set_checksum(size_t part);
-static uint16_t calculated_checksum(size_t p);
-
-/*
- * Helper functions for accessing
- * the NVM area during operation.
- */
-static uint16_t nvm_word(size_t pos16, size_t part);
-static void set_nvm_word(size_t pos16, size_t part, uint16_t val16);
-static void set_part_modified(size_t p);
-static void check_nvm_bound(size_t pos16, size_t part);
-static void check_bin(size_t a, const char *a_name);
-
-/*
- * Helper functions for stub functions
- * that handle GbE file reads/writes.
- */
-static void rw_gbe_file_part(size_t p, int rw_type,
- const char *rw_type_str);
-static uint8_t *gbe_mem_offset(size_t part, const char *f_op);
-static off_t gbe_file_offset(size_t part, const char *f_op);
-static off_t gbe_x_offset(size_t part, const char *f_op,
- const char *d_type, off_t nsize, off_t ncmp);
-static ssize_t rw_file_exact(int fd, uint8_t *mem, size_t len,
- off_t off, int rw_type);
-static ssize_t do_rw(int fd,
- uint8_t *mem, size_t len, off_t off, int rw_type);
-static ssize_t prw(int fd, void *mem, size_t nrw,
- off_t off, int rw_type);
-static off_t lseek_eintr(int fd, off_t off, int whence);
-
-/*
- * Error handling and cleanup
- */
-static void err(int nvm_errval, const char *msg, ...);
-static void close_files(void);
-static const char *getnvmprogname(void);
-static void set_err_if_unset(int errval);
-static void usage(uint8_t usage_exit);
-
-/*
- * Sizes in bytes:
- */
-#define SIZE_1KB 1024
-#define SIZE_4KB (4 * SIZE_1KB)
-#define SIZE_8KB (8 * SIZE_1KB)
-#define SIZE_16KB (16 * SIZE_1KB)
-#define SIZE_128KB (128 * SIZE_1KB)
-
-/*
- * First 128 bytes of a GbE part contains
- * the regular NVM (Non-Volatile-Memory)
- * area. All of these bytes must add up,
- * truncated to 0xBABA.
- *
- * The full GbE region is 4KB, but only
- * the first 128 bytes are used here.
- *
- * There is a second 4KB part with the same
- * rules, and it *should* be identical.
- */
-#define GBE_FILE_SIZE SIZE_8KB /* for buf */
-#define GBE_PART_SIZE (GBE_FILE_SIZE >> 1)
-#define NVM_CHECKSUM 0xBABA
-#define NVM_SIZE 128
-#define NVM_WORDS (NVM_SIZE >> 1)
-#define NVM_CHECKSUM_WORD (NVM_WORDS - 1)
-
-/*
- * Portable macro based on BSD nitems.
- * Used to count the number of commands (see below).
- */
-#define items(x) (sizeof((x)) / sizeof((x)[0]))
-
-static const char newrandom[] = "/dev/urandom";
-static const char oldrandom[] = "/dev/random"; /* fallback on OLD unix */
-static const char *rname = NULL;
-
-/*
- * GbE files can be 8KB, 16KB or 128KB,
- * but we only need the two 4KB parts
- * from offset zero and offset 64KB in
- * a 128KB file, or zero and 8KB in a 16KB
- * file, or zero and 4KB in an 8KB file.
- *
- * The code will handle this properly.
- */
-static uint8_t buf[GBE_FILE_SIZE];
-static uint8_t pad[GBE_PART_SIZE]; /* the file that wouldn't die */
-
-static uint16_t mac_buf[3];
-static off_t gbe_file_size;
-
-static int urandom_fd = -1;
-static int gbe_fd = -1;
-static size_t part;
-static uint8_t part_modified[2];
-static uint8_t part_valid[2];
-
-static const char rmac[] = "xx:xx:xx:xx:xx:xx";
-static const char *mac_str;
-static const char *fname;
-static const char *argv0;
-
-#ifndef SSIZE_MAX
-#define SSIZE_MAX ((ssize_t)(~((size_t)1 << (sizeof(ssize_t)*CHAR_BIT-1))))
-#endif
-
-/*
- * Use these for .invert in command[]:
- * If set to 1: read/write inverter (p0->p1, p1->p0)
- */
-#define PART_INVERT 1
-#define NO_INVERT 0
-
-/*
- * Use these for .argc in command[]:
- */
-#define ARGC_3 3
-#define ARGC_4 4
-
-enum {
- LESEN,
- PLESEN,
- SCHREIB,
- PSCHREIB
-};
-
-/*
- * Used as indices for command[]
- * MUST be in the same order as entries in command[]
- */
-enum {
- CMD_DUMP,
- CMD_SETMAC,
- CMD_SWAP,
- CMD_COPY,
- CMD_CAT,
- CMD_CAT16,
- CMD_CAT128
-};
-
-/*
- * If set, a given part will always be written.
- */
-enum {
- SET_MOD_OFF, /* don't manually set part modified */
- SET_MOD_0, /* set part 0 modified */
- SET_MOD_1, /* set part 1 modified */
- SET_MOD_N, /* set user-specified part modified */
- /* affected by command[].invert */
- SET_MOD_BOTH /* set both parts modified */
-};
-
-enum {
- ARG_NOPART,
- ARG_PART
-};
-
-enum {
- SKIP_CHECKSUM_READ,
- CHECKSUM_READ
-};
-
-enum {
- SKIP_CHECKSUM_WRITE,
- CHECKSUM_WRITE
-};
-
-struct commands {
- size_t chk;
- const char *str;
- void (*run)(void);
- int argc;
- uint8_t invert;
- uint8_t set_modified;
- uint8_t arg_part;
- uint8_t chksum_read;
- uint8_t chksum_write;
- size_t rw_size; /* within the 4KB GbE part */
- int flags; /* e.g. O_RDWR or O_RDONLY */
-};
-
-/*
- * Command table, for nvmutil commands
- */
-static const struct commands command[] = {
- { CMD_DUMP, "dump", cmd_helper_dump, ARGC_3,
- NO_INVERT, SET_MOD_OFF,
- ARG_NOPART,
- SKIP_CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
- NVM_SIZE, O_RDONLY },
-
- { CMD_SETMAC, "setmac", cmd_helper_setmac, ARGC_3,
- NO_INVERT, SET_MOD_OFF,
- ARG_NOPART,
- CHECKSUM_READ, CHECKSUM_WRITE,
- NVM_SIZE, O_RDWR },
-
- /*
- * OPTIMISATION: Read inverted, so no copying is needed.
- */
- { CMD_SWAP, "swap", NULL, ARGC_3,
- PART_INVERT, SET_MOD_BOTH,
- ARG_NOPART,
- CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
- GBE_PART_SIZE, O_RDWR },
-
- /*
- * OPTIMISATION: Read inverted, so no copying is needed.
- * The non-target part will not be read.
- */
- { CMD_COPY, "copy", NULL, ARGC_4,
- PART_INVERT, SET_MOD_N,
- ARG_PART,
- CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
- GBE_PART_SIZE, O_RDWR },
-
- { CMD_CAT, "cat", cmd_helper_cat, ARGC_3,
- NO_INVERT, SET_MOD_OFF,
- ARG_NOPART,
- CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
- GBE_PART_SIZE, O_RDONLY },
-
- { CMD_CAT16, "cat16", cmd_helper_cat, ARGC_3,
- NO_INVERT, SET_MOD_OFF,
- ARG_NOPART,
- CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
- GBE_PART_SIZE, O_RDONLY },
-
- { CMD_CAT128, "cat128", cmd_helper_cat, ARGC_3,
- NO_INVERT, SET_MOD_OFF,
- ARG_NOPART,
- CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
- GBE_PART_SIZE, O_RDONLY },
-};
-
-#define MAX_CMD_LEN 50
-#define N_COMMANDS items(command)
-#define CMD_NULL N_COMMANDS
-
-/*
- * Index in command[], will be set later
- */
-static size_t cmd_index = CMD_NULL;
-
-typedef char assert_argc3[(ARGC_3==3)?1:-1];
-typedef char assert_argc4[(ARGC_4==4)?1:-1];
-
-int
-main(int argc, char *argv[])
-{
- argv0 = argv[0];
- if (argc < 3)
- usage(1);
-
- fname = argv[1];
-
-#ifdef NVMUTIL_PLEDGE
-#ifdef NVMUTIL_UNVEIL
- if (pledge("stdio rpath wpath unveil", NULL) == -1)
- err(errno, "pledge");
- if (unveil("/dev/null", "r") == -1)
- err(errno, "unveil '/dev/null'");
-#else
- if (pledge("stdio rpath wpath", NULL) == -1)
- err(errno, "pledge");
-#endif
-#endif
-
- sanitize_command_list();
-
- set_cmd(argc, argv);
- set_cmd_args(argc, argv);
-
-#ifdef NVMUTIL_PLEDGE
-#ifdef NVMUTIL_UNVEIL
- if (command[cmd_index].flags == O_RDONLY) {
- if (unveil(fname, "r") == -1)
- err(errno, "%s: unveil ro", fname);
- if (unveil(NULL, NULL) == -1)
- err(errno, "unveil block (ro)");
- if (pledge("stdio rpath", NULL) == -1)
- err(errno, "pledge ro (kill unveil)");
- } else {
- if (unveil(fname, "rw") == -1)
- err(errno, "%s: unveil rw", fname);
- if (unveil(NULL, NULL) == -1)
- err(errno, "unveil block (rw)");
- if (pledge("stdio rpath wpath", NULL) == -1)
- err(errno, "pledge rw (kill unveil)");
- }
-#else
- if (command[cmd_index].flags == O_RDONLY) {
- if (pledge("stdio rpath", NULL) == -1)
- err(errno, "pledge ro");
- }
-#endif
-#endif
-
- open_dev_urandom();
-
- open_gbe_file();
-
-#ifdef NVMUTIL_PLEDGE
- if (pledge("stdio", NULL) == -1)
- err(errno, "pledge stdio (main)");
-#endif
-
- /*
- * Used by CMD_CAT, for padding
- */
- memset(pad, 0xff, sizeof(pad));
-
- read_gbe_file();
- read_checksums();
-
- errno = 0;
- run_cmd(cmd_index);
-
- if (errno && (!(part_valid[0] || part_valid[1])))
- err(errno, "%s: Unhandled error (WRITE SKIPPED)", fname);
-
- if (command[cmd_index].flags == O_RDWR)
- write_gbe_file();
-
- close_files();
-
- if (errno)
- err(errno, "Unhandled error on exit");
-
- return EXIT_SUCCESS;
-}
-
-/*
- * Guard against regressions by maintainers (command table)
- */
-static void
-sanitize_command_list(void)
-{
- size_t c;
-
- for (c = 0; c < N_COMMANDS; c++)
- sanitize_command_index(c);
-}
-
-static void
-sanitize_command_index(size_t c)
-{
- uint8_t mod_type;
- size_t gbe_rw_size;
-
- check_command_num(c);
-
- if (command[c].argc < 3)
- err(EINVAL, "cmd index %lu: argc below 3, %d",
- (unsigned long)c, command[c].argc);
-
- if (command[c].str == NULL)
- err(EINVAL, "cmd index %lu: NULL str",
- (unsigned long)c);
- if (*command[c].str == '\0')
- err(EINVAL, "cmd index %lu: empty str",
- (unsigned long)c);
-
- if (xstrxlen(command[c].str, MAX_CMD_LEN + 1) >
- MAX_CMD_LEN) {
- err(EINVAL, "cmd index %lu: str too long: %s",
- (unsigned long)c, command[c].str);
- }
-
- if (!((CMD_SETMAC > CMD_DUMP) && (CMD_SWAP > CMD_SETMAC) &&
- (CMD_COPY > CMD_SWAP) && (CMD_CAT > CMD_COPY) &&
- (CMD_CAT16 > CMD_CAT) && (CMD_CAT128 > CMD_CAT16)))
- err(EINVAL, "Some command integers are the same");
-
- if (!((SET_MOD_0 > SET_MOD_OFF) && (SET_MOD_1 > SET_MOD_0) &&
- (SET_MOD_N > SET_MOD_1) && (SET_MOD_BOTH > SET_MOD_N)))
- err(EINVAL, "Some modtype integers are the same");
-
- mod_type = command[c].set_modified;
- switch (mod_type) {
- case SET_MOD_0:
- case SET_MOD_1:
- case SET_MOD_N:
- case SET_MOD_BOTH:
- case SET_MOD_OFF:
- break;
- default:
- err(EINVAL, "Unsupported set_mod type: %u", mod_type);
- }
-
- check_bin(command[c].invert, "cmd.invert");
- check_bin(command[c].arg_part, "cmd.arg_part");
- check_bin(command[c].chksum_read, "cmd.chksum_read");
- check_bin(command[c].chksum_write, "cmd.chksum_write");
-
- check_enum_bin(ARG_NOPART, "ARG_NOPART", ARG_PART, "ARG_PART");
- check_enum_bin(SKIP_CHECKSUM_READ, "SKIP_CHECKSUM_READ",
- CHECKSUM_READ, "CHECKSUM_READ");
- check_enum_bin(SKIP_CHECKSUM_WRITE, "SKIP_CHECKSUM_WRITE",
- CHECKSUM_WRITE, "CHECKSUM_WRITE");
- check_enum_bin(NO_INVERT, "NO_INVERT", PART_INVERT, "PART_INVERT");
-
- gbe_rw_size = command[c].rw_size;
-
- switch (gbe_rw_size) {
- case GBE_PART_SIZE:
- case NVM_SIZE:
- break;
- default:
- err(EINVAL, "Unsupported rw_size: %lu",
- (unsigned long)gbe_rw_size);
- }
-
- if (gbe_rw_size > GBE_PART_SIZE)
- err(EINVAL, "rw_size larger than GbE part: %lu",
- (unsigned long)gbe_rw_size);
-
- if (command[c].flags != O_RDONLY &&
- command[c].flags != O_RDWR)
- err(EINVAL, "invalid cmd.flags setting");
-
- if (!((!LESEN) && (PLESEN == 1) && (SCHREIB == 2) && (PSCHREIB == 3)))
- err(EINVAL, "rw type integers are the wrong values");
-}
-
-static void
-check_enum_bin(size_t a, const char *a_name,
- size_t b, const char *b_name)
-{
- if (a)
- err(EINVAL, "%s is non-zero", a_name);
-
- if (b != 1)
- err(EINVAL, "%s is a value other than 1", b_name);
-}
-
-static void
-set_cmd(int argc, char *argv[])
-{
- const char *cmd_str;
-
- for (cmd_index = 0; valid_command(cmd_index); cmd_index++) {
- cmd_str = command[cmd_index].str;
-
- if (xstrxcmp(argv[2], cmd_str, MAX_CMD_LEN) != 0)
- continue;
- else if (argc >= command[cmd_index].argc)
- return;
-
- err(EINVAL, "Too few args on command '%s'", cmd_str);
- }
-
- cmd_index = CMD_NULL;
-}
-
-static void
-set_cmd_args(int argc, char *argv[])
-{
- uint8_t arg_part;
-
- if (!valid_command(cmd_index) || argc < 3)
- usage(1);
-
- arg_part = command[cmd_index].arg_part;
-
- /* Maintainer bugs */
- if (arg_part && argc < 4)
- err(EINVAL,
- "arg_part set for command that needs argc4");
- if (arg_part && cmd_index == CMD_SETMAC)
- err(EINVAL,
- "arg_part set on CMD_SETMAC");
-
- if (cmd_index == CMD_SETMAC)
- mac_str = argc >= 4 ? argv[3] : rmac;
- else if (arg_part)
- part = conv_argv_part_num(argv[3]);
-}
-
-static 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');
-}
-
-/*
- * Portable strcmp() but blocks NULL/empty/unterminated
- * strings. Even stricter than strncmp().
- */
-static int
-xstrxcmp(const char *a, const char *b, size_t maxlen)
-{
- size_t i;
-
- if (a == NULL || b == NULL)
- err(EINVAL, "NULL input to xstrxcmp");
-
- if (*a == '\0' || *b == '\0')
- err(EINVAL, "Empty string in xstrxcmp");
-
- for (i = 0; i < maxlen; i++) {
- if (a[i] != b[i])
- return (unsigned char)a[i] - (unsigned char)b[i];
-
- if (a[i] == '\0')
- return 0;
- }
-
- /*
- * We reached maxlen, so assume unterminated string.
- */
- err(EINVAL, "Unterminated string in xstrxcmp");
-
- /*
- * Should never reach here. This keeps compilers happy.
- */
- set_err_if_unset(EINVAL);
- return -1;
-}
-
-static void
-open_dev_urandom(void)
-{
- rname = newrandom;
- urandom_fd = open(rname, O_RDONLY | O_BINARY | O_NONBLOCK);
- if (urandom_fd != -1)
- return;
-
- /*
- * Fall back to /dev/random on very old Unix.
- *
- * We must reset errno, to remove stale state
- * set by reading /dev/urandom
- */
-
- fprintf(stderr, "Can't open %s (will use %s instead)\n",
- newrandom, oldrandom);
-
- errno = 0;
-
- rname = oldrandom;
- urandom_fd = open(rname, O_RDONLY | O_BINARY | O_NONBLOCK);
- if (urandom_fd == -1)
- err(errno, "%s: could not open", rname);
-}
-
-static void
-open_gbe_file(void)
-{
- struct stat gbe_st;
-
- xopen(&gbe_fd, fname, command[cmd_index].flags | O_BINARY, &gbe_st);
-
- gbe_file_size = gbe_st.st_size;
-
- switch (gbe_file_size) {
- case SIZE_8KB:
- case SIZE_16KB:
- case SIZE_128KB:
- break;
- default:
- err(EINVAL, "File size must be 8KB, 16KB or 128KB");
- }
-}
-
-static void
-xopen(int *fd_ptr, const char *path, int flags, struct stat *st)
-{
- if ((*fd_ptr = open(path, flags)) == -1)
- err(errno, "%s", path);
-
- if (fstat(*fd_ptr, st) == -1)
- err(errno, "%s", path);
-
- if (!S_ISREG(st->st_mode))
- err(errno, "%s: not a regular file", path);
-}
-
-static void
-read_gbe_file(void)
-{
- size_t p;
- uint8_t do_read[2] = {1, 1};
-
- /*
- * Commands specifying a partnum only
- * need the given GbE part to be read.
- */
- if (command[cmd_index].arg_part)
- do_read[part ^ 1] = 0;
-
- for (p = 0; p < 2; p++) {
- if (do_read[p])
- rw_gbe_file_part(p, PLESEN, "pread");
- }
-}
-
-static void
-read_checksums(void)
-{
- size_t p;
- size_t skip_part;
- uint8_t invert;
- uint8_t arg_part;
- uint8_t num_invalid;
- uint8_t max_invalid;
-
- part_valid[0] = 0;
- part_valid[1] = 0;
-
- if (!command[cmd_index].chksum_read)
- return;
-
- num_invalid = 0;
- max_invalid = 2;
-
- invert = command[cmd_index].invert;
- arg_part = command[cmd_index].arg_part;
- if (arg_part)
- max_invalid = 1;
-
- /*
- * Skip verification on this part,
- * but only when arg_part is set.
- */
- skip_part = part ^ 1 ^ invert;
-
- for (p = 0; p < 2; p++) {
- /*
- * Only verify a part if it was *read*
- */
- if (arg_part && (p == skip_part))
- continue;
-
- part_valid[p] = good_checksum(p);
- if (!part_valid[p])
- ++num_invalid;
- }
-
- if (num_invalid < max_invalid)
- errno = 0;
-
- if (num_invalid >= max_invalid) {
- if (max_invalid == 1)
- err(EINVAL, "%s: part %lu has a bad checksum",
- fname, (unsigned long)part);
- err(EINVAL, "%s: No valid checksum found in file",
- fname);
- }
-}
-
-static int
-good_checksum(size_t partnum)
-{
- uint16_t expected_checksum = calculated_checksum(partnum);
- uint16_t current_checksum = nvm_word(NVM_CHECKSUM_WORD, partnum);
-
- if (current_checksum == expected_checksum)
- return 1;
-
- set_err_if_unset(EINVAL);
- return 0;
-}
-
-static void
-run_cmd(size_t c)
-{
- check_command_num(c);
- if (command[c].run != NULL)
- command[c].run();
-}
-
-static void
-check_command_num(size_t c)
-{
- if (!valid_command(c))
- err(errno, "Invalid run_cmd arg: %lu",
- (unsigned long)c);
-}
-
-static uint8_t
-valid_command(size_t c)
-{
- if (c >= N_COMMANDS)
- return 0;
-
- if (c != command[c].chk)
- err(EINVAL, "Invalid cmd chk value (%lu) vs arg: %lu",
- (unsigned long)command[c].chk, (unsigned long)c);
-
- return 1;
-}
-
-static void
-cmd_helper_setmac(void)
-{
- size_t partnum;
-
- printf("MAC address to be written: %s\n", mac_str);
- parse_mac_string();
-
- for (partnum = 0; partnum < 2; partnum++)
- write_mac_part(partnum);
-}
-
-static void
-parse_mac_string(void)
-{
- size_t mac_byte;
-
- if (xstrxlen(mac_str, 18) != 17)
- err(EINVAL, "MAC address is the wrong length");
-
- memset(mac_buf, 0, sizeof(mac_buf));
-
- for (mac_byte = 0; mac_byte < 6; mac_byte++)
- set_mac_byte(mac_byte);
-
- if ((mac_buf[0] | mac_buf[1] | mac_buf[2]) == 0)
- err(EINVAL, "Must not specify all-zeroes MAC address");
-
- if (mac_buf[0] & 1)
- err(EINVAL, "Must not specify multicast MAC address");
-}
-
-/*
- * strnlen() but aborts on NULL input, and empty strings.
- * Our version also prohibits unterminated strings.
- * strnlen() was standardized in POSIX.1-2008 and is not
- * available on some older systems, so we provide our own.
- */
-static size_t
-xstrxlen(const char *scmp, size_t maxlen)
-{
- size_t xstr_index;
-
- if (scmp == NULL)
- err(EINVAL, "NULL input to xstrxlen");
-
- if (*scmp == '\0')
- err(EINVAL, "Empty string in xstrxlen");
-
- for (xstr_index = 0;
- xstr_index < maxlen && scmp[xstr_index] != '\0';
- xstr_index++);
-
- if (xstr_index == maxlen)
- err(EINVAL, "Unterminated string in xstrxlen");
-
- return xstr_index;
-}
-
-static void
-set_mac_byte(size_t mac_byte_pos)
-{
- size_t mac_str_pos = mac_byte_pos * 3;
- size_t mac_nib_pos;
- char separator;
-
- 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);
-}
-
-static void
-set_mac_nib(size_t mac_str_pos,
- size_t mac_byte_pos, size_t mac_nib_pos)
-{
- char mac_ch;
- uint16_t hex_num;
-
- mac_ch = mac_str[mac_str_pos + mac_nib_pos];
-
- if ((hex_num = hextonum(mac_ch)) > 15)
- 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_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? */
-}
-
-static uint16_t
-hextonum(char ch_s)
-{
- unsigned char ch = (unsigned char)ch_s;
-
- if ((unsigned)(ch - '0') <= 9)
- return ch - '0';
-
- ch |= 0x20;
-
- if ((unsigned)(ch - 'a') <= 5)
- return ch - 'a' + 10;
-
- if (ch == '?' || ch == 'x')
- return rhex(); /* random character */
-
- return 16; /* invalid character */
-}
-
-static uint16_t
-rhex(void)
-{
- static size_t n = 0;
- static uint8_t rnum[12];
-
- if (!n) {
- n = sizeof(rnum);
- if (rw_file_exact(urandom_fd, rnum, n, 0, LESEN) == -1)
- err(errno, "Randomisation failed");
- errno = 0;
- }
-
- return (uint16_t)(rnum[--n] & 0xf);
-}
-
-static void
-write_mac_part(size_t partnum)
-{
- size_t w;
-
- check_bin(partnum, "part number");
- if (!part_valid[partnum])
- return;
-
- for (w = 0; w < 3; w++)
- set_nvm_word(w, partnum, mac_buf[w]);
-
- printf("Wrote MAC address to part %lu: ",
- (unsigned long)partnum);
- print_mac_from_nvm(partnum);
-}
-
-static void
-cmd_helper_dump(void)
-{
- size_t partnum;
-
- part_valid[0] = good_checksum(0);
- part_valid[1] = good_checksum(1);
-
- if (part_valid[0] || part_valid[1])
- errno = 0;
-
- for (partnum = 0; partnum < 2; partnum++) {
- if (!part_valid[partnum])
- fprintf(stderr,
- "BAD checksum %04x in part %lu (expected %04x)\n",
- nvm_word(NVM_CHECKSUM_WORD, partnum),
- (unsigned long)partnum,
- calculated_checksum(partnum));
-
- printf("MAC (part %lu): ",
- (unsigned long)partnum);
- print_mac_from_nvm(partnum);
- hexdump(partnum);
- }
-}
-
-static void
-print_mac_from_nvm(size_t partnum)
-{
- size_t c;
-
- for (c = 0; c < 3; c++) {
- uint16_t val16 = nvm_word(c, partnum);
- printf("%02x:%02x", val16 & 0xff, val16 >> 8);
- if (c == 2)
- printf("\n");
- else
- printf(":");
- }
-}
-
-static void
-hexdump(size_t partnum)
-{
- size_t c;
- size_t row;
- uint16_t val16;
-
- for (row = 0; row < 8; row++) {
- printf("%08lx ", (unsigned long)((size_t)row << 4));
- for (c = 0; c < 8; c++) {
- val16 = nvm_word((row << 3) + c, partnum);
- if (c == 4)
- printf(" ");
- printf(" %02x %02x", val16 & 0xff, val16 >> 8);
- }
- printf("\n");
- }
-}
-
-static void
-cmd_helper_cat(void)
-{
- size_t p;
- size_t ff;
- size_t n = 0;
-
- if (cmd_index == CMD_CAT16)
- n = 1;
- else if (cmd_index == CMD_CAT128)
- n = 15;
- else if (cmd_index != CMD_CAT)
- err(EINVAL, "cmd_helper_cat called erroneously");
-
- fflush(NULL);
-
- for (p = 0; p < 2; p++) {
- gbe_cat_buf(buf + (p * GBE_PART_SIZE));
-
- for (ff = 0; ff < n; ff++)
- gbe_cat_buf(pad);
- }
-}
-
-static void
-gbe_cat_buf(uint8_t *b)
-{
- ssize_t rval;
-
- while (1) {
- rval = rw_file_exact(STDOUT_FILENO, b,
- GBE_PART_SIZE, 0, SCHREIB);
-
- if (rval >= 0) {
- /*
- * A partial write is especially
- * fatal, as it should already be
- * prevented in rw_file_exact().
- */
- if ((size_t)rval != GBE_PART_SIZE)
- err(EIO, "stdout: cat: Partial write");
- break;
- }
-
- if (errno != EAGAIN)
- err(errno, "stdout: cat");
-
- /*
- * We assume that no data
- * was written to stdout.
- */
- errno = 0;
- }
-
- /*
- * No errors here.
- * Avoid the warning in main()
- */
- errno = 0;
-}
-
-static void
-write_gbe_file(void)
-{
- size_t p;
- size_t partnum;
- uint8_t update_checksum;
-
- if (command[cmd_index].flags == O_RDONLY)
- return;
-
- update_checksum = command[cmd_index].chksum_write;
-
- override_part_modified();
-
- for (p = 0; p < 2; p++) {
- partnum = p ^ command[cmd_index].invert;
-
- if (!part_modified[partnum])
- continue;
-
- if (update_checksum)
- set_checksum(partnum);
-
- rw_gbe_file_part(partnum, PSCHREIB, "pwrite");
- }
-}
-
-static void
-override_part_modified(void)
-{
- uint8_t mod_type = command[cmd_index].set_modified;
-
- switch (mod_type) {
- case SET_MOD_0:
- set_part_modified(0);
- break;
- case SET_MOD_1:
- set_part_modified(1);
- break;
- case SET_MOD_N:
- set_part_modified(part ^ command[cmd_index].invert);
- break;
- case SET_MOD_BOTH:
- set_part_modified(0);
- set_part_modified(1);
- break;
- case SET_MOD_OFF:
- break;
- default:
- err(EINVAL, "Unsupported set_mod type: %u",
- mod_type);
- }
-}
-
-static void
-set_checksum(size_t p)
-{
- check_bin(p, "part number");
- set_nvm_word(NVM_CHECKSUM_WORD, p, calculated_checksum(p));
-}
-
-static uint16_t
-calculated_checksum(size_t p)
-{
- size_t c;
- uint32_t val16 = 0;
-
- for (c = 0; c < NVM_CHECKSUM_WORD; c++)
- val16 += (uint32_t)nvm_word(c, p);
-
- return (uint16_t)((NVM_CHECKSUM - val16) & 0xffff);
-}
-
-/*
- * GbE NVM files store 16-bit (2-byte) little-endian words.
- * We must therefore swap the order when reading or writing.
- *
- * NOTE: The MAC address words are stored big-endian in the
- * file, but we assume otherwise and adapt accordingly.
- */
-
-static uint16_t
-nvm_word(size_t pos16, size_t p)
-{
- size_t pos;
-
- check_nvm_bound(pos16, p);
- pos = (pos16 << 1) + (p * GBE_PART_SIZE);
-
- return (uint16_t)buf[pos] |
- ((uint16_t)buf[pos + 1] << 8);
-}
-
-static void
-set_nvm_word(size_t pos16, size_t p, uint16_t val16)
-{
- size_t pos;
-
- check_nvm_bound(pos16, p);
- pos = (pos16 << 1) + (p * GBE_PART_SIZE);
-
- buf[pos] = (uint8_t)(val16 & 0xff);
- buf[pos + 1] = (uint8_t)(val16 >> 8);
-
- set_part_modified(p);
-}
-
-static void
-set_part_modified(size_t p)
-{
- check_bin(p, "part number");
- part_modified[p] = 1;
-}
-
-static void
-check_nvm_bound(size_t c, size_t p)
-{
- /*
- * NVM_SIZE assumed as the limit, because this
- * current design assumes that we will only
- * ever modified the NVM area.
- */
-
- check_bin(p, "part number");
-
- if (c >= NVM_WORDS)
- err(ECANCELED, "check_nvm_bound: out of bounds %lu",
- (unsigned long)c);
-}
-
-static 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, (unsigned long)a);
-}
-
-static void
-rw_gbe_file_part(size_t p, int rw_type,
- const char *rw_type_str)
-{
- size_t gbe_rw_size = command[cmd_index].rw_size;
- uint8_t invert = command[cmd_index].invert;
-
- uint8_t *mem_offset;
-
- if (rw_type == SCHREIB || rw_type == PSCHREIB)
- invert = 0;
-
- /*
- * Inverted reads are used by copy/swap.
- * E.g. read from p0 (file) to p1 (mem).
- */
- mem_offset = gbe_mem_offset(p ^ invert, rw_type_str);
-
- if (rw_file_exact(gbe_fd, mem_offset,
- gbe_rw_size, gbe_file_offset(p, rw_type_str),
- rw_type) == -1)
- err(errno, "%s: %s: part %lu",
- fname, rw_type_str, (unsigned long)p);
-
- errno = 0;
-}
-
-/*
- * 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.
- */
-static uint8_t *
-gbe_mem_offset(size_t p, const char *f_op)
-{
- off_t gbe_off = gbe_x_offset(p, f_op, "mem",
- GBE_PART_SIZE, GBE_FILE_SIZE);
-
- return (uint8_t *)(buf + gbe_off);
-}
-
-/*
- * I/O operations filtered here. These operations must
- * only write from the 0th position or the half position
- * within the GbE file, and write 4KB of data.
- *
- * This check is called, to ensure just that.
- */
-static off_t
-gbe_file_offset(size_t p, const char *f_op)
-{
- off_t gbe_file_half_size = gbe_file_size >> 1;
-
- return gbe_x_offset(p, f_op, "file",
- gbe_file_half_size, gbe_file_size);
-}
-
-static off_t
-gbe_x_offset(size_t p, const char *f_op, const char *d_type,
- off_t nsize, off_t ncmp)
-{
- 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",
- fname, d_type, f_op);
-
- if (off != 0 && off != ncmp >> 1)
- err(ECANCELED, "%s: GbE %s %s at bad offset",
- fname, d_type, f_op);
-
- return off;
-}
-
-/*
- * Read or write the exact contents of a file,
- * along with a buffer, (if applicable) offset,
- * and number of bytes to be read. It unified
- * the functionality of read(), pread(), write()
- * and pwrite(), with retry-on-EINTR and also
- * prevents infinite loop on zero-reads.
- *
- * The pread() and pwrite() functionality are
- * provided by yet another portable function,
- * prw() - see notes below.
- *
- * This must only be used on files. It cannot
- * be used on sockets or pipes, because 0-byte
- * reads are treated like fatal errors. This
- * means that EOF is also considered fatal.
- */
-static ssize_t
-rw_file_exact(int fd, uint8_t *mem, size_t len,
- off_t off, int rw_type)
-{
- ssize_t rval = 0;
- size_t rc = 0;
-
- if (fd < 0 || !len || len > (size_t)SSIZE_MAX) {
- set_err_if_unset(EIO);
- return -1;
- }
-
- while (rc < len) {
- rval = do_rw(fd, mem + rc, len - rc, off + rc, rw_type);
-
- if (rval < 0 && errno == EINTR) {
- continue;
- } else if (rval < 0) {
- set_err_if_unset(EIO);
- return -1;
- }
- if ((size_t)rval > (len - rc) /* Prevent overflow */
- || rval == 0) { /* Prevent infinite 0-byte loop */
- set_err_if_unset(EIO);
- return -1;
- }
-
- rc += (size_t)rval;
- }
-
- return rc;
-}
-
-static ssize_t
-do_rw(int fd, uint8_t *mem,
- size_t len, off_t off, int rw_type)
-{
- if (rw_type == LESEN || rw_type == PLESEN << 2)
- return read(fd, mem, len);
-
- if (rw_type == SCHREIB || rw_type == PSCHREIB << 2)
- return write(fd, mem, len);
-
- if (rw_type == PLESEN || rw_type == PSCHREIB)
- return prw(fd, mem, len, off, rw_type);
-
- set_err_if_unset(EINVAL);
- return -1;
-}
-
-/*
- * This implements a portable analog of pwrite()
- * and pread() - note that this version is not
- * thread-safe (race conditions are possible on
- * shared file descriptors).
- *
- * This limitation is acceptable, since nvmutil is
- * single-threaded. Portability is the main goal.
- */
-static ssize_t
-prw(int fd, void *mem, size_t nrw,
- off_t off, int rw_type)
-{
- off_t off_orig;
- ssize_t r;
- int saved_errno;
-
- if ((off_orig = lseek_eintr(fd, (off_t)0, SEEK_CUR)) == (off_t)-1)
- return -1;
- if (lseek_eintr(fd, off, SEEK_SET) == (off_t)-1)
- return -1;
-
- do {
- r = do_rw(fd, mem, nrw, off, rw_type << 2);
- } while (r < 0 && errno == EINTR);
-
- saved_errno = errno;
- if (lseek_eintr(fd, off_orig, SEEK_SET) == (off_t)-1) {
- if (r < 0)
- errno = saved_errno;
- return -1;
- }
- errno = saved_errno;
-
- return r;
-}
-
-static off_t
-lseek_eintr(int fd, off_t off, int whence)
-{
- off_t old;
-
- do {
- old = lseek(fd, off, whence);
- } while (old == (off_t)-1 && errno == EINTR);
-
- return old;
-}
-
-static void
-err(int nvm_errval, const char *msg, ...)
-{
- va_list args;
-
- /*
- * We need to ensure that files are closed
- * on exit, including error exits. This
- * would otherwise recurse, because the
- * close_files() function also calls err(),
- * but with -1 on nvm_errval. It's the only
- * one that does this.
- *
- * Since the errval is for setting errno, -1
- * would be incorrect. Therefore, set_err_if_unset()
- * avoids overriding errno if the given value
- * is negative.
- *
- * Be careful modifying err() and close_files().
- */
- if (nvm_errval != -1)
- close_files();
-
- fprintf(stderr, "%s: ", getnvmprogname());
-
- va_start(args, msg);
- vfprintf(stderr, msg, args);
- va_end(args);
-
- set_err_if_unset(nvm_errval);
- fprintf(stderr, ": %s", strerror(errno));
-
- fprintf(stderr, "\n");
- exit(EXIT_FAILURE);
-}
-
-static void
-close_files(void)
-{
- if (gbe_fd > -1) {
- if (close(gbe_fd) == -1)
- err(-1, "%s: close failed", fname);
- gbe_fd = -1;
- }
-
- if (urandom_fd > -1) {
- if (close(urandom_fd) == -1)
- err(-1, "%s: close failed", rname);
- urandom_fd = -1;
- }
-}
-
-static const char *
-getnvmprogname(void)
-{
- const char *p;
-
- if (argv0 == NULL || *argv0 == '\0')
- return "";
-
- p = strrchr(argv0, '/');
-
- if (p)
- return p + 1;
- else
- return argv0;
-}
-
-/*
- * Set errno only if it hasn't already been set.
- * This prevents overriding real libc errors.
- *
- * We use errno for regular program state, while
- * being careful not to clobber what was set by
- * real libc function, or a minority of our stub
- * functions such as prw()
- */
-static void
-set_err_if_unset(int x)
-{
- if (errno)
- return;
- if (x > 0)
- errno = x;
- else
- errno = ECANCELED;
-}
-
-static void
-usage(uint8_t usage_exit)
-{
- const char *util = getnvmprogname();
-
-#ifdef NVMUTIL_PLEDGE
- if (pledge("stdio", NULL) == -1)
- err(errno, "pledge");
-#endif
- 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);
-
- if (usage_exit)
- err(EINVAL, "Too few arguments");
-}
diff --git a/util/spkmodem_decode/.gitignore b/util/spkmodem_decode/.gitignore
new file mode 100644
index 00000000..42814fe4
--- /dev/null
+++ b/util/spkmodem_decode/.gitignore
@@ -0,0 +1,2 @@
+spkmodem-recv
+spkmodem-decode
diff --git a/util/spkmodem_recv/Makefile b/util/spkmodem_decode/Makefile
index 293d7475..b00c4f43 100644
--- a/util/spkmodem_recv/Makefile
+++ b/util/spkmodem_decode/Makefile
@@ -8,12 +8,12 @@ DESTDIR?=
PREFIX?=/usr/local
INSTALL?=install
-PROG=spkmodem-recv
+PROG=spkmodem-decode
all: $(PROG)
-$(PROG): spkmodem-recv.c
- $(CC) $(CFLAGS) spkmodem-recv.c -o $(PROG)
+$(PROG): spkmodem-decode.c
+ $(CC) $(CFLAGS) spkmodem-decode.c -o $(PROG)
install: $(PROG)
mkdir -p $(DESTDIR)$(PREFIX)/bin/
diff --git a/util/spkmodem_decode/spkmodem-decode.c b/util/spkmodem_decode/spkmodem-decode.c
new file mode 100644
index 00000000..3b3b33f8
--- /dev/null
+++ b/util/spkmodem_decode/spkmodem-decode.c
@@ -0,0 +1,725 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright (c) 2013 Free Software Foundation, Inc.
+ * Copyright (c) 2023, 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * This program receives text encoded as pulses on the PC speaker,
+ * and decodes them via simple FSK (Frequency Shift Keying)
+ * demodulation and FIR (Finite Impulse Response) frequency
+ * discriminator.
+ *
+ * It waits for specific tones at specific intervals.
+ * It detects tones within the audio stream and reconstructs
+ * characters bit-by-bit as the encoded modem signal is received.
+ * This is performance-efficient on most CPUs, and has relatively
+ * high tolerance for noisy signals (similar to techniques used
+ * for data stored on audio cassette tapes).
+ *
+ * This is a special interface provided by coreboot and GNU GRUB,
+ * for computers that lack serial ports (useful for debugging).
+ * Note that GRUB and coreboot can both send these signals; this
+ * tool merely decodes them. This tool does not *encode*, only
+ * decode.
+ *
+ * Usage example (NOTE: little endian!):
+ * parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-decode
+ *
+ * Originally provided by GNU GRUB, this version is a heavily
+ * modified fork that complies with the OpenBSD Kernel Source
+ * File Style Guide (KNF) instead of GNU coding standards; it
+ * emphasises strict error handling, portability and code
+ * quality, as characterised by OpenBSD projects. Several magic
+ * numbers have been tidied up, calculated (not hardcoded) and
+ * thoroughly explained, unlike in the original version.
+ *
+ * The original version was essentially a blob, masquerading as
+ * source code. This forked source code is therefore the result
+ * of extensive reverse engineering (of the GNU source code)!
+ * This cleaned up code and extensive commenting will thoroughly
+ * explain how the decoding works. This was done as an academic
+ * exercise in 2023, continuing in 2026.
+ *
+ * This fork of spkmodem-recv, called spkmodem-decode, is provided
+ * with Libreboot releases:
+ * https://libreboot.org/
+ *
+ * The original GNU version is here, if you're morbidly curious:
+ * https://cgit.git.savannah.gnu.org/cgit/grub.git/plain/util/spkmodem-recv.c?id=3dce38eb196f47bdf86ab028de74be40e13f19fd
+ *
+ * Libreboot's version was renamed to spkmodem-decode on 12 March 2026,
+ * since Libreboot's version no longer closely resembles the GNU
+ * version at all; ergo, a full rename was in order. GNU's version
+ * was called spkmodem-recv.
+ */
+
+#define _POSIX_SOURCE
+
+/*
+ * For OpenBSD define, to detect version
+ * for deciding whether to use pledge(2)
+ */
+#ifdef __OpenBSD__
+#include <sys/param.h>
+#endif
+
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/*
+ * spkmodem is essentially using FSK (Frequency Shift Keying)
+ * with two primary tones representing encoded bits,
+ * separated by a framing tone.
+ * Very cheap on CPU cycles and avoids needing something more
+ * expensive like FFT or Goertzel filters, and tolerates
+ * weak/noisy signals.
+ */
+
+/*
+ * Frequency of audio in Hz
+ * WARNING: if changing, make sure to adjust
+ * SAMPLES_PER_FRAME accordingly (see maths below)
+ */
+#define SAMPLE_RATE 48000
+
+/*
+ * One analysis frame spans 5 ms.
+ *
+ * frame_time = SAMPLES_PER_FRAME / SAMPLE_RATE
+ *
+ * With the default sample rate (48 kHz):
+ *
+ * frame_time = N / 48000
+ * 0.005 s = N / 48000
+ * N = 0.005 × 48000 = 240 samples
+ */
+#define SAMPLES_PER_FRAME 240
+
+/*
+ * Number of analysis frames per second.
+ *
+ * Each increment in the frequency counters corresponds
+ * roughly to this many Hertz of tone frequency.
+ *
+ * With the default values:
+ * FRAME_RATE = 48000 / 240 = 200 Hz
+ */
+#define FRAME_RATE ((SAMPLE_RATE) / (SAMPLES_PER_FRAME))
+
+/*
+ * Two FIR windows are maintained; one for data tone,
+ * and one for the separator tone. They are positioned
+ * one frame apart in the ring buffer.
+ */
+#define MAX_SAMPLES (2 * (SAMPLES_PER_FRAME))
+
+/*
+ * Approx byte offset for ring buffer span, just for
+ * easier debug output correlating to the audio stream.
+ */
+#define SAMPLE_OFFSET ((MAX_SAMPLES) * (sizeof(short)))
+
+/*
+ * Expected tone ranges (approximate, derived from spkmodem).
+ * These values are intentionally wide because real-world setups
+ * often involve microphones, room acoustics, and cheap ADCs.
+ */
+#define SEP_TONE_MIN_HZ 1000
+#define SEP_TONE_MAX_HZ 3000
+
+#define SEP_TOLERANCE_PULSES \
+ (((SEP_TONE_MAX_HZ) - (SEP_TONE_MIN_HZ)) / (2 * (FRAME_RATE)))
+
+#define DATA_TONE_MIN_HZ 3000
+#define DATA_TONE_MAX_HZ 12000
+
+/* Mid point used to distinguish the two data tones. */
+#define DATA_TONE_THRESHOLD_HZ 5000
+
+/*
+ * Convert tone frequency ranges into pulse counts within the
+ * sliding analysis window.
+ *
+ * pulse_count = tone_frequency / FRAME_RATE
+ * where FRAME_RATE = SAMPLE_RATE / SAMPLES_PER_FRAME.
+ */
+#define FREQ_SEP_MIN ((SEP_TONE_MIN_HZ) / (FRAME_RATE))
+#define FREQ_SEP_MAX ((SEP_TONE_MAX_HZ) / (FRAME_RATE))
+
+#define FREQ_DATA_MIN ((DATA_TONE_MIN_HZ) / (FRAME_RATE))
+#define FREQ_DATA_MAX ((DATA_TONE_MAX_HZ) / (FRAME_RATE))
+
+#define FREQ_DATA_THRESHOLD ((DATA_TONE_THRESHOLD_HZ) / (FRAME_RATE))
+
+/*
+ * These determine how long the program will wait during
+ * tone auto-detection, before shifting to defaults.
+ * It is done every LEARN_FRAMES number of frames.
+ */
+#define LEARN_SECONDS 1
+#define LEARN_FRAMES ((LEARN_SECONDS) * (FRAME_RATE))
+
+/*
+ * Sample amplitude threshold used to convert the waveform
+ * into a pulse stream. Values near zero are regarded as noise.
+ */
+#define THRESHOLD 500
+
+#define READ_BUF 4096
+
+struct decoder_state {
+ unsigned char pulse[MAX_SAMPLES];
+
+ signed short inbuf[READ_BUF];
+ size_t inpos;
+ size_t inlen;
+
+ int ringpos;
+ int sep_pos;
+
+ /*
+ * Sliding window pulse counters
+ * used to detect modem tones
+ */
+ int freq_data;
+ int freq_separator;
+ int sample_count;
+
+ int ascii_bit;
+ unsigned char ascii;
+
+ int debug;
+ int swap_bytes;
+
+ /* dynamic separator calibration */
+ int sep_sum;
+ int sep_samples;
+ int sep_min;
+ int sep_max;
+
+ /* for automatic tone detection */
+ int freq_min;
+ int freq_max;
+ int freq_threshold;
+ int learn_frames;
+
+ /* previous sample used for edge detection */
+ signed short prev_sample;
+};
+
+static const char *argv0;
+
+/*
+ * 16-bit little endian words are read
+ * continuously. we will swap them, if
+ * the host cpu is big endian.
+ */
+static int host_is_big_endian(void);
+
+/* main loop */
+static void handle_audio(struct decoder_state *st);
+
+/* separate tone tolerances */
+static void select_separator_tone(struct decoder_state *st);
+static int is_valid_signal(struct decoder_state *st);
+
+/* output to terminal */
+static int set_ascii_bit(struct decoder_state *st);
+static void print_char(struct decoder_state *st);
+static void reset_char(struct decoder_state *st);
+
+/* process samples/frames */
+static void decode_pulse(struct decoder_state *st);
+static signed short read_sample(struct decoder_state *st);
+static void read_words(struct decoder_state *st);
+
+/* continually adjust tone */
+static void detect_tone(struct decoder_state *st);
+static int silent_signal(struct decoder_state *st);
+static void select_low_tone(struct decoder_state *st);
+
+/* debug */
+static void print_stats(struct decoder_state *st);
+
+/* error handling / usage */
+static void err(int errval, const char *msg, ...);
+static void usage(void);
+static const char *progname(void);
+
+/* portability (old systems) */
+int getopt(int, char * const *, const char *);
+extern char *optarg;
+extern int optind;
+extern int opterr;
+extern int optopt;
+
+#ifndef CHAR_BIT
+#define CHAR_BIT 8
+#endif
+
+typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1];
+typedef char static_assert_char_is_1[(sizeof(char) == 1) ? 1 : -1];
+typedef char static_assert_short[(sizeof(short) == 2) ? 1 : -1];
+typedef char static_assert_int_is_4[(sizeof(int) >= 4) ? 1 : -1];
+typedef char static_assert_twos_complement[
+ ((-1 & 3) == 3) ? 1 : -1
+];
+
+int
+main(int argc, char **argv)
+{
+ struct decoder_state st;
+ int c;
+
+ argv0 = argv[0];
+
+#if defined (__OpenBSD__) && defined(OpenBSD)
+#if OpenBSD >= 509
+ if (pledge("stdio", NULL) == -1)
+ err(errno, "pledge");
+#endif
+#endif
+
+ memset(&st, 0, sizeof(st));
+
+ while ((c = getopt(argc, argv, "d")) != -1) {
+ if (c != 'd')
+ usage();
+ st.debug = 1;
+ break;
+ }
+
+ /* fallback in case tone detection fails */
+ st.freq_min = 100000;
+ st.freq_max = 0;
+ st.freq_threshold = FREQ_DATA_THRESHOLD;
+
+ /*
+ * Used for separator calibration
+ */
+ st.sep_min = FREQ_SEP_MIN;
+ st.sep_max = FREQ_SEP_MAX;
+
+ st.ascii_bit = 7;
+
+ st.ringpos = 0;
+ st.sep_pos = SAMPLES_PER_FRAME;
+
+ if (host_is_big_endian())
+ st.swap_bytes = 1;
+
+ setvbuf(stdout, NULL, _IONBF, 0);
+
+ for (;;)
+ handle_audio(&st);
+
+ return EXIT_SUCCESS;
+}
+
+static int
+host_is_big_endian(void)
+{
+ unsigned int x = 1;
+ return (*(unsigned char *)&x == 0);
+}
+
+static void
+handle_audio(struct decoder_state *st)
+{
+ int sample;
+
+ /*
+ * If the modem signal disappears for several (read: 3)
+ * frames, discard the partially assembled character.
+ */
+ if (st->sample_count >= (3 * SAMPLES_PER_FRAME) ||
+ st->freq_separator <= 0)
+ reset_char(st);
+
+ st->sample_count = 0;
+
+ /* process exactly one frame */
+ for (sample = 0; sample < SAMPLES_PER_FRAME; sample++)
+ decode_pulse(st);
+
+ select_separator_tone(st);
+
+ if (set_ascii_bit(st) < 0)
+ print_char(st);
+
+ /* Detect tone per each frame */
+ detect_tone(st);
+}
+
+/*
+ * collect separator tone statistics
+ * (and auto-adjust tolerances)
+ */
+static void
+select_separator_tone(struct decoder_state *st)
+{
+ int avg;
+
+ if (!is_valid_signal(st))
+ return;
+
+ st->sep_sum += st->freq_separator;
+ st->sep_samples++;
+
+ if (st->sep_samples != 50)
+ return;
+
+ avg = st->sep_sum / st->sep_samples;
+
+ st->sep_min = avg - SEP_TOLERANCE_PULSES;
+ st->sep_max = avg + SEP_TOLERANCE_PULSES;
+
+ /* reset calibration accumulators */
+ st->sep_sum = 0;
+ st->sep_samples = 0;
+
+ if (st->debug)
+ printf("separator calibrated: %dHz\n",
+ avg * FRAME_RATE);
+}
+
+/*
+ * Verify that the observed pulse densities fall within the
+ * expected ranges for spkmodem tones. This prevents random noise
+ * from being misinterpreted as data.
+ */
+static int
+is_valid_signal(struct decoder_state *st)
+{
+ if (st->freq_data <= 0)
+ return 0;
+
+ if (st->freq_separator < st->sep_min ||
+ st->freq_separator > st->sep_max)
+ return 0;
+
+ return 1;
+}
+
+/*
+ * Each validated frame contributes one bit of modem data.
+ * Bits are accumulated MSB-first into the ASCII byte.
+ */
+static int
+set_ascii_bit(struct decoder_state *st)
+{
+ if (st->debug)
+ print_stats(st);
+
+ if (!is_valid_signal(st))
+ return st->ascii_bit;
+
+ if (st->freq_data < st->freq_threshold)
+ st->ascii |= (1 << st->ascii_bit);
+
+ st->ascii_bit--;
+
+ return st->ascii_bit;
+}
+
+static void
+print_char(struct decoder_state *st)
+{
+ if (st->debug)
+ printf("<%c,%x>", st->ascii, st->ascii);
+ else
+ putchar(st->ascii);
+
+ reset_char(st);
+}
+
+static void
+reset_char(struct decoder_state *st)
+{
+ st->ascii = 0;
+ st->ascii_bit = 7;
+}
+
+/*
+ * Main demodulation step (moving-sum FIR filter).
+ */
+static void
+decode_pulse(struct decoder_state *st)
+{
+ unsigned char old_ring, old_sep;
+ unsigned char new_pulse;
+ signed short sample;
+ int ringpos;
+ int sep_pos;
+ int diff_edge;
+ int diff_amp;
+
+ ringpos = st->ringpos;
+ sep_pos = st->sep_pos;
+
+ /*
+ * Sliding rectangular FIR (Finite Impulse Response) filter.
+ *
+ * After thresholding, the signal becomes a stream of 0/1 pulses.
+ * The decoder measures pulse density over two windows:
+ *
+ * freq_data: pulses in the "data" window
+ * freq_separator: pulses in the "separator" window
+ *
+ * Instead of calculating each window every time (O(N) per frame), we
+ * update the window sums incrementally:
+ *
+ * sum_new = sum_old - pulse_leaving + pulse_entering
+ *
+ * This keeps the filter O(1) per sample instead of O(N).
+ * The technique is equivalent to a rectangular FIR filter
+ * implemented as a sliding moving sum.
+ *
+ * The two windows are exactly SAMPLES_PER_FRAME apart in the ring
+ * buffer, so the pulse leaving the data window is simultaneously
+ * entering the separator window.
+ */
+ old_ring = st->pulse[ringpos];
+ old_sep = st->pulse[sep_pos];
+ st->freq_data -= old_ring;
+ st->freq_data += old_sep;
+ st->freq_separator -= old_sep;
+
+ sample = read_sample(st);
+
+ /*
+ * Avoid startup edge. Since
+ * it's zero at startup, this
+ * may wrongly produce a pulse
+ */
+ if (st->sample_count == 0)
+ st->prev_sample = sample;
+
+ /*
+ * Detect edges instead of amplitude.
+ * This is more tolerant of weak microphones
+ * and speaker distortion..
+ *
+ * However, we check both slope edges and
+ * amplitude, to mitagate noise.
+ */
+ diff_amp = sample;
+ diff_edge = sample - st->prev_sample;
+ if (diff_edge < 0)
+ diff_edge = -diff_edge;
+ if (diff_amp < 0)
+ diff_amp = -diff_amp;
+ if (diff_edge > THRESHOLD &&
+ diff_amp > THRESHOLD)
+ new_pulse = 1;
+ else
+ new_pulse = 0;
+ st->prev_sample = sample;
+
+ st->pulse[ringpos] = new_pulse;
+ st->freq_separator += new_pulse;
+
+ /*
+ * Advance both FIR windows through the ring buffer.
+ * The separator window always stays one frame ahead
+ * of the data window.
+ */
+ if (++ringpos >= MAX_SAMPLES)
+ ringpos = 0;
+ if (++sep_pos >= MAX_SAMPLES)
+ sep_pos = 0;
+
+ st->ringpos = ringpos;
+ st->sep_pos = sep_pos;
+
+ st->sample_count++;
+}
+
+static signed short
+read_sample(struct decoder_state *st)
+{
+ signed short sample;
+ unsigned short u;
+
+ while (st->inpos >= st->inlen)
+ read_words(st);
+
+ sample = st->inbuf[st->inpos++];
+
+ if (st->swap_bytes) {
+ u = (unsigned short)sample;
+ u = (u >> 8) | (u << 8);
+
+ sample = (signed short)u;
+ }
+
+ return sample;
+}
+
+static void
+read_words(struct decoder_state *st)
+{
+ size_t n;
+
+ n = fread(st->inbuf, sizeof(st->inbuf[0]),
+ READ_BUF, stdin);
+
+ if (n != 0) {
+ st->inpos = 0;
+ st->inlen = n;
+
+ return;
+ }
+
+ if (ferror(stdin))
+ err(errno, "stdin read");
+ if (feof(stdin))
+ exit(EXIT_SUCCESS);
+}
+
+/*
+ * Automatically detect spkmodem tone
+ */
+static void
+detect_tone(struct decoder_state *st)
+{
+ if (st->learn_frames >= LEARN_FRAMES)
+ return;
+
+ st->learn_frames++;
+
+ if (silent_signal(st))
+ return;
+
+ select_low_tone(st);
+
+ if (st->learn_frames != LEARN_FRAMES)
+ return;
+
+ /*
+ * If the observed frequencies are too close,
+ * learning likely failed (only one tone seen).
+ * Keep the default threshold.
+ */
+ if (st->freq_max - st->freq_min < 2)
+ return;
+
+ st->freq_threshold =
+ (st->freq_min + st->freq_max) / 2;
+
+ if (st->debug)
+ printf("auto threshold: %dHz\n",
+ st->freq_threshold * FRAME_RATE);
+}
+
+/*
+ * Ignore silence / near silence.
+ * Both FIR windows will be near zero when no signal exists.
+ */
+static int
+silent_signal(struct decoder_state *st)
+{
+ return (st->freq_data <= 2 &&
+ st->freq_separator <= 2);
+}
+
+/*
+ * Choose the lowest active tone.
+ * Separator frames carry tone in the separator window,
+ * data frames carry tone in the data window.
+ */
+static void
+select_low_tone(struct decoder_state *st)
+{
+ int f;
+
+ f = st->freq_data;
+
+ if (f <= 0 || (st->freq_separator > 0 &&
+ st->freq_separator < f))
+ f = st->freq_separator;
+
+ if (f <= 0)
+ return;
+
+ if (f < st->freq_min)
+ st->freq_min = f;
+
+ if (f > st->freq_max)
+ st->freq_max = f;
+}
+
+static void
+print_stats(struct decoder_state *st)
+{
+ long pos;
+
+ int data_hz = st->freq_data * FRAME_RATE;
+ int sep_hz = st->freq_separator * FRAME_RATE;
+ int sep_hz_min = st->sep_min * FRAME_RATE;
+ int sep_hz_max = st->sep_max * FRAME_RATE;
+
+ if ((pos = ftell(stdin)) == -1) {
+ printf("%d %d %d data=%dHz sep=%dHz(min %dHz %dHz)\n",
+ st->freq_data,
+ st->freq_separator,
+ st->freq_threshold,
+ data_hz,
+ sep_hz,
+ sep_hz_min,
+ sep_hz_max);
+ return;
+ }
+
+ printf("%d %d %d @%ld data=%dHz sep=%dHz(min %dHz %dHz)\n",
+ st->freq_data,
+ st->freq_separator,
+ st->freq_threshold,
+ pos - SAMPLE_OFFSET,
+ data_hz,
+ sep_hz,
+ sep_hz_min,
+ sep_hz_max);
+}
+
+static void
+err(int errval, const char *msg, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "%s: ", progname());
+
+ va_start(ap, msg);
+ vfprintf(stderr, msg, ap);
+ va_end(ap);
+
+ fprintf(stderr, ": %s\n", strerror(errval));
+ exit(EXIT_FAILURE);
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-d]\n", progname());
+ exit(EXIT_FAILURE);
+}
+
+static const char *
+progname(void)
+{
+ const char *p;
+
+ if (argv0 == NULL || *argv0 == '\0')
+ return "";
+
+ p = strrchr(argv0, '/');
+
+ if (p)
+ return p + 1;
+ else
+ return argv0;
+}
diff --git a/util/spkmodem_recv/.gitignore b/util/spkmodem_recv/.gitignore
deleted file mode 100644
index 2f5c946c..00000000
--- a/util/spkmodem_recv/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-spkmodem-recv
diff --git a/util/spkmodem_recv/spkmodem-recv.c b/util/spkmodem_recv/spkmodem-recv.c
deleted file mode 100644
index 238c10b2..00000000
--- a/util/spkmodem_recv/spkmodem-recv.c
+++ /dev/null
@@ -1,313 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* SPDX-FileCopyrightText: 2013 Free Software Foundation, Inc. */
-/* Usage: parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-recv */
-
-/* Forked from coreboot's version, at util/spkmodem_recv/ in coreboot.git,
- * revision 5c2b5fcf2f9c9259938fd03cfa3ea06b36a007f0 as of 3 January 2022.
- * This version is heavily modified, re-written based on OpenBSD Kernel Source
- * File Style Guide (KNF); this change is Copyright 2023,2026 Leah Rowe. */
-
-#define _POSIX_SOURCE
-
-/*
- * For OpenBSD define, to detect version
- * for deciding whether to use pledge(2)
- */
-#ifdef __OpenBSD__
-#include <sys/param.h>
-#endif
-
-#include <errno.h>
-#include <stdio.h>
-#include <stdarg.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#define SAMPLES_PER_FRAME 240
-#define MAX_SAMPLES (2 * SAMPLES_PER_FRAME)
-
-#define FREQ_SEP_MIN 5
-#define FREQ_SEP_MAX 15
-
-#define FREQ_DATA_MIN 15
-#define FREQ_DATA_THRESHOLD 25
-#define FREQ_DATA_MAX 60
-
-#define THRESHOLD 500
-
-#define READ_BUF 4096
-
-struct decoder_state {
- signed short frame[MAX_SAMPLES];
- unsigned char pulse[MAX_SAMPLES];
-
- signed short inbuf[READ_BUF];
- size_t inpos;
- size_t inlen;
-
- int ringpos;
- int sep_pos;
-
- /*
- * Sliding window pulse counters
- * used to detect modem tones
- */
- int freq_data;
- int freq_separator;
- int sample_count;
-
- int ascii_bit;
- unsigned char ascii;
-
- int debug;
-};
-
-static const char *argv0;
-
-static void handle_audio(struct decoder_state *st);
-static int valid_signal(struct decoder_state *st);
-static void decode_pulse(struct decoder_state *st);
-static signed short read_sample(struct decoder_state *st);
-static int set_ascii_bit(struct decoder_state *st);
-static void print_char(struct decoder_state *st);
-static void print_stats(struct decoder_state *st);
-static void reset_char(struct decoder_state *st);
-
-static void err(int errval, const char *msg, ...);
-static void usage(void);
-static const char *progname(void);
-
-int getopt(int, char * const *, const char *);
-extern char *optarg;
-extern int optind;
-extern int opterr;
-extern int optopt;
-
-#ifndef errno
-extern int errno;
-#endif
-
-int
-main(int argc, char **argv)
-{
- struct decoder_state st;
- int c;
-
-#if defined (__OpenBSD__) && defined(OpenBSD)
-#if OpenBSD >= 509
- if (pledge("stdio", NULL) == -1)
- err(errno, "pledge");
-#endif
-#endif
-
- memset(&st, 0, sizeof(st));
- st.ascii_bit = 7;
-
- st.ringpos = 0;
- st.sep_pos = SAMPLES_PER_FRAME;
-
- argv0 = argv[0];
-
- while ((c = getopt(argc, argv, "d")) != -1) {
- switch (c) {
- case 'd':
- st.debug = 1;
- break;
- default:
- usage();
- }
- }
-
- setvbuf(stdout, NULL, _IONBF, 0);
-
- for (;;)
- handle_audio(&st);
-
- return EXIT_SUCCESS;
-}
-
-static void
-handle_audio(struct decoder_state *st)
-{
- int sample;
-
- if (st->sample_count > (3 * SAMPLES_PER_FRAME))
- reset_char(st);
-
- if (valid_signal(st)) {
-
- if (set_ascii_bit(st) < 0)
- print_char(st);
-
- st->sample_count = 0;
-
- for (sample = 0; sample < SAMPLES_PER_FRAME; sample++)
- decode_pulse(st);
-
- } else {
- decode_pulse(st);
- }
-}
-
-static int
-valid_signal(struct decoder_state *st)
-{
- return (st->freq_separator > FREQ_SEP_MIN &&
- st->freq_separator < FREQ_SEP_MAX &&
- st->freq_data > FREQ_DATA_MIN &&
- st->freq_data < FREQ_DATA_MAX);
-}
-
-static void
-decode_pulse(struct decoder_state *st)
-{
- unsigned char old_ring, old_sep;
- unsigned char new_pulse;
-
- old_ring = st->pulse[st->ringpos];
- old_sep = st->pulse[st->sep_pos];
-
- st->freq_data -= old_ring;
- st->freq_data += old_sep;
- st->freq_separator -= old_sep;
-
- st->frame[st->ringpos] = read_sample(st);
-
- if ((unsigned)(st->frame[st->ringpos] + THRESHOLD)
- > (unsigned)(2 * THRESHOLD))
- new_pulse = 1;
- else
- new_pulse = 0;
-
- st->pulse[st->ringpos] = new_pulse;
- st->freq_separator += new_pulse;
-
- st->ringpos++;
- if (st->ringpos >= MAX_SAMPLES)
- st->ringpos = 0;
-
- st->sep_pos++;
- if (st->sep_pos >= MAX_SAMPLES)
- st->sep_pos = 0;
-
- st->sample_count++;
-}
-
-static signed short
-read_sample(struct decoder_state *st)
-{
- size_t n;
-
- while (st->inpos >= st->inlen) {
-
- n = fread(st->inbuf, sizeof(st->inbuf[0]),
- READ_BUF, stdin);
-
- if (n == 0) {
- if (feof(stdin))
- exit(EXIT_SUCCESS);
- err(errno, "stdin read");
- }
-
- st->inpos = 0;
- st->inlen = n;
- }
-
- return st->inbuf[st->inpos++];
-}
-
-static int
-set_ascii_bit(struct decoder_state *st)
-{
- if (st->debug)
- print_stats(st);
-
- if (st->freq_data < FREQ_DATA_THRESHOLD)
- st->ascii |= (1 << st->ascii_bit);
-
- st->ascii_bit--;
-
- return st->ascii_bit;
-}
-
-static void
-print_char(struct decoder_state *st)
-{
- if (st->debug)
- printf("<%c,%x>", st->ascii, st->ascii);
- else
- putchar(st->ascii);
-
- reset_char(st);
-}
-
-static void
-print_stats(struct decoder_state *st)
-{
- long pos;
-
- pos = ftell(stdin);
-
- if (pos == -1) {
- printf("%d %d %d\n",
- st->freq_data,
- st->freq_separator,
- FREQ_DATA_THRESHOLD);
- return;
- }
-
- printf("%d %d %d @%ld\n",
- st->freq_data,
- st->freq_separator,
- FREQ_DATA_THRESHOLD,
- pos - sizeof(st->frame));
-}
-
-static void
-reset_char(struct decoder_state *st)
-{
- st->ascii = 0;
- st->ascii_bit = 7;
-}
-
-static void
-err(int errval, const char *msg, ...)
-{
- va_list ap;
-
- fprintf(stderr, "%s: ", progname());
-
- va_start(ap, msg);
- vfprintf(stderr, msg, ap);
- va_end(ap);
-
- if (!errno)
- errno = errval;
-
- fprintf(stderr, ": %s\n", strerror(errno));
- exit(EXIT_FAILURE);
-}
-
-static void
-usage(void)
-{
- fprintf(stderr, "usage: %s [-d]\n", progname());
- exit(EXIT_FAILURE);
-}
-
-static const char *
-progname(void)
-{
- const char *p;
-
- if (argv0 == NULL || *argv0 == '\0')
- return "";
-
- p = strrchr(argv0, '/');
-
- if (p)
- return p + 1;
- else
- return argv0;
-}