diff options
Diffstat (limited to 'util')
28 files changed, 6623 insertions, 1875 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 95683df2..b00c4f43 100644 --- a/util/spkmodem_recv/Makefile +++ b/util/spkmodem_decode/Makefile @@ -8,16 +8,16 @@ 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/ - install $(PROG) $(DESTDIR)$(PREFIX)/bin/ + install -c $(PROG) $(DESTDIR)$(PREFIX)/bin/ uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG) 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 9307ac12..00000000 --- a/util/spkmodem_recv/spkmodem-recv.c +++ /dev/null @@ -1,220 +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 - -#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> - -static int set_ascii_bit(void); -static void handle_audio(void), decode_pulse(void), print_char(void), - print_stats(void); -static void err(int nvm_errval, const char *msg, ...); -static const char * getnvmprogname(void); -static void set_err_if_unset(int x); - -int getopt(int, char * const *, const char *); -extern char *optarg; -extern int optind, opterr, optopt; - -#if defined(__OpenBSD__) && defined(OpenBSD) -#if OpenBSD >= 509 -#ifndef HAVE_PLEDGE -#define HAVE_PLEDGE 1 -#endif -#endif -#endif - -#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 reset_char() ascii = 0, ascii_bit = 7 - -signed short frame[MAX_SAMPLES], pulse[MAX_SAMPLES]; -int ringpos, debug, freq_data, freq_separator, sample_count, ascii_bit = 7; -char ascii = 0; - -static const char *argv0; - -int -main(int argc, char *argv[]) -{ - int c = 0; - - argv0 = argv[0]; - -#ifdef HAVE_PLEDGE -#if HAVE_PLEDGE > 0 - if (pledge("stdio", NULL) == -1) - err(errno, "pledge"); -#endif -#endif - while (c != -1) { - c = getopt(argc, argv, "d"); - - if (c == 'd') - debug = 1; - else if (c != -1) - err(EINVAL, "Invalid argument"); - } - - setvbuf(stdout, NULL, _IONBF, 0); - - while (1) - handle_audio(); - - return EXIT_SUCCESS; -} - -static void -handle_audio(void) -{ - int sample; - - if (sample_count > (3 * SAMPLES_PER_FRAME)) - sample_count = reset_char(); - - if ((freq_separator <= FREQ_SEP_MIN) || (freq_separator >= FREQ_SEP_MAX) - || (freq_data <= FREQ_DATA_MIN) || (freq_data >= FREQ_DATA_MAX)) { - decode_pulse(); - return; - } - - if (set_ascii_bit() < 0) - print_char(); - - sample_count = 0; - - for (sample = 0; sample < SAMPLES_PER_FRAME; sample++) - decode_pulse(); -} - -static void -decode_pulse(void) -{ - size_t n; - size_t frame_size; - - int next_ringpos = (ringpos + SAMPLES_PER_FRAME) % MAX_SAMPLES; - - freq_data -= pulse[ringpos]; - freq_data += pulse[next_ringpos]; - freq_separator -= pulse[next_ringpos]; - - frame_size = sizeof(frame[0]); - n = fread(&frame[ringpos], 1, frame_size, stdin); - - if (n != frame_size) { - if (feof(stdin)) - exit(EXIT_SUCCESS); - if (ferror(stdin)) - err(errno, "Could not read from frame."); - } - - pulse[ringpos] = (abs(frame[ringpos]) > THRESHOLD) ? 1 : 0; - freq_separator += pulse[ringpos++]; - ringpos %= MAX_SAMPLES; - ++sample_count; -} - -static int -set_ascii_bit(void) -{ - if (debug) - print_stats(); - - if (freq_data < FREQ_DATA_THRESHOLD) - ascii |= (1 << ascii_bit); - - --ascii_bit; - return ascii_bit; -} - -static void -print_stats(void) -{ - long stdin_pos; - - if ((stdin_pos = ftell(stdin)) == -1) - err(errno, "ftell stdin"); - - printf ("%d %d %d @%ld\n", freq_data, freq_separator, - FREQ_DATA_THRESHOLD, stdin_pos - sizeof(frame)); -} - -static void -print_char(void) -{ - if (debug) - printf("<%c, %x>", ascii, ascii); - else - printf("%c", ascii); - - reset_char(); -} - -static void -err(int nvm_errval, const char *msg, ...) -{ - va_list args; - - 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 const char * -getnvmprogname(void) -{ - const char *p; - - if (argv0 == NULL || *argv0 == '\0') - return ""; - - p = strrchr(argv0, '/'); - - if (p) - return p + 1; - else - return argv0; -} - -static void -set_err_if_unset(int x) -{ - if (errno) - return; - if (x > 0) - errno = x; - else - errno = ECANCELED; -} |
