diff options
| author | Leah Rowe <leah@libreboot.org> | 2026-03-20 04:02:51 +0000 |
|---|---|---|
| committer | Leah Rowe <leah@libreboot.org> | 2026-03-25 12:32:57 +0000 |
| commit | 210922bc9174bcce3444f9bc2782b033622b4c70 (patch) | |
| tree | 7153f7b848bf1ebc60baae09f4384c7a43d83d0a /util/libreboot-utils | |
| parent | f50ffd6bb13c04cb185fb6311f8875582bf18388 (diff) | |
util/mkhtemp: extremely hardened mkhtemp
This will also be used in lbmk itself at some point,
which currently just uses regular mktemp, for tmpdir
handling during the build process.
Renamed util/nvmutil to util/libreboot-utils, which
now contains two tools. The new tool, mkhtemp, is a
hardened implementation of mktemp, which nvmutil
also uses now. Still experimental, but good enough
for nvmutil.
Mkhtemp attempts to provide TOCTOU resistance on
Linux, by using modern features in Linux such as
Openat2 (syscall) with O_EXCL and O_TMPFILE,
and many various security checks e.g.
inode/dev during creation. Checks are done constantly,
to try to detect race conditions. The code is very
strict about things like sticky bits in world writeable
directories, also ownership (it can be made to bar even
root access on files and directories it doesn't own).
It's a security-first implementation of mktemp, likely
even more secure than the OpenBSD mkstemp, but more
auditing and testing is needed - more features are
also planned, including a compatibility mode to make
it also work like traditional mktemp/mkstemp. The
intention, once this becomes stable, is that it will
become a modern drop-in replacement for mkstemp on
Linux and BSD systems.
Some legacy code has been removed, and in general
cleaned up. I wrote mkhtemp for nvmutil, as part of
its atomic write behaviour, but mktemp was the last
remaining liability, so I rewrote that too!
Docs/manpage/website will be made for mkhtemp once
the code is mature.
Other changes have also been made. This is from another
experimental branch of Libreboot, that I'm pushing
early. For example, nvmutil's state machine has been
tidied up, moving more logic back into main.
Mktemp is historically prone to race conditions,
e.g. symlink attacks, directory replacement, remounting
during operation, all sorts of things. Mkhtemp has
been written to solve, or otherwise mitigate, that
problem. Mkhtemp is currently experimental and will
require a major cleanup at some point, but it
already works well enough, and you can in fact use
it; at this time, the -d, -p and -q flags are
supported, and you can add a custom template at
the end, e.g.
mkhtemp -p test -d
Eventually, I will make this have complete parity
with the GNU and BSD implementations, so that it is
fully useable on existing setups, while optionally
providing the hardening as well.
A lot of code has also been tidied up. I didn't
track the changes I made with this one, because
it was a major re-write of nvmutil; it is now
libreboot-utils, and I will continue to write
more programs in here over time. It's basically
now a bunch of hardened wrappers around various
libc functions, e.g. there is also a secure I/O
wrapper for read/write.
There is a custom randomisation function, rlong,
which simply uses arc4random or getrandom, on
BSD and Linux respectively. Efforts are made to
make it as reliable as possible, to the extent
that it never returns with failure; in the unlikely
event that it fails, it aborts. It also sleeps
between failure, to mitigate certain DoS attacks.
You can just go in util/libreboot-utils and
type make, then you will have the nvmutil and
mkhtemp binaries, which you can just use. It
all works. Everything was massively rewritten.
Signed-off-by: Leah Rowe <leah@libreboot.org>
Diffstat (limited to 'util/libreboot-utils')
| -rw-r--r-- | util/libreboot-utils/.gitignore | 6 | ||||
| -rw-r--r-- | util/libreboot-utils/AUTHORS | 2 | ||||
| -rw-r--r-- | util/libreboot-utils/COPYING | 21 | ||||
| -rw-r--r-- | util/libreboot-utils/Makefile | 141 | ||||
| -rw-r--r-- | util/libreboot-utils/include/common.h | 618 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/checksum.c | 108 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/command.c | 564 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/file.c | 1065 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/io.c | 591 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/mkhtemp.c | 1103 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/num.c | 119 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/rand.c | 114 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/state.c | 233 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/string.c | 284 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/usage.c | 30 | ||||
| -rw-r--r-- | util/libreboot-utils/lib/word.c | 68 | ||||
| -rw-r--r-- | util/libreboot-utils/mkhtemp.c | 211 | ||||
| -rw-r--r-- | util/libreboot-utils/nvmutil.c | 132 |
18 files changed, 5410 insertions, 0 deletions
diff --git a/util/libreboot-utils/.gitignore b/util/libreboot-utils/.gitignore new file mode 100644 index 00000000..fbf110f9 --- /dev/null +++ b/util/libreboot-utils/.gitignore @@ -0,0 +1,6 @@ +/nvm +/nvmutil +/mkhtemp +*.bin +*.o +*.d diff --git a/util/libreboot-utils/AUTHORS b/util/libreboot-utils/AUTHORS new file mode 100644 index 00000000..f38ea210 --- /dev/null +++ b/util/libreboot-utils/AUTHORS @@ -0,0 +1,2 @@ +Leah Rowe +Riku Viitanen diff --git a/util/libreboot-utils/COPYING b/util/libreboot-utils/COPYING new file mode 100644 index 00000000..47c35a86 --- /dev/null +++ b/util/libreboot-utils/COPYING @@ -0,0 +1,21 @@ +Copyright (C) 2022-2026 Leah Rowe <leah@libreboot.org> +Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/util/libreboot-utils/Makefile b/util/libreboot-utils/Makefile new file mode 100644 index 00000000..692ebf0f --- /dev/null +++ b/util/libreboot-utils/Makefile @@ -0,0 +1,141 @@ +# 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=c90 -pedantic -Werror +HELLFLAGS = $(STRICT) -Weverything + +PROG = nvmutil +PROGMKH = mkhtemp + +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 + +# default mode +CFLAGS_MODE = $(PORTABLE) +CC_MODE = $(CC) + +all: $(PROG) $(PROGMKH) + +$(PROG): $(OBJS_NVMUTIL) + $(CC_MODE) $(OBJS_NVMUTIL) -o $(PROG) $(LDFLAGS) + +$(PROGMKH): $(OBJS_MKHTEMP) + $(CC_MODE) $(OBJS_MKHTEMP) -o $(PROGMKH) $(LDFLAGS) + +# ensure obj directory exists +$(OBJS_NVMUTIL): obj +$(OBJS_MKHTEMP): 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 + +# 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) + $(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) + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG) + rm -f $(DESTDIR)$(PREFIX)/bin/$(PROGMKH) + +clean: + rm -f $(PROG) $(PROGMKH) $(OBJS_NVMUTIL) $(OBJS_MKHTEMP) + +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/include/common.h b/util/libreboot-utils/include/common.h new file mode 100644 index 00000000..fb3aa886 --- /dev/null +++ b/util/libreboot-utils/include/common.h @@ -0,0 +1,618 @@ +/* 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 + +#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 0 +#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 + */ + +struct commands { + size_t chk; + char *str; + void (*run)(void); + 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; + + char *argv0; + + 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); +void xopen(int *fd, const char *path, int flags, struct stat *st); + +/* 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 + */ + +int slen(const char *scmp, size_t maxlen, + size_t *rval); +int scmp(const char *a, const char *b, + size_t maxlen, int *rval); +int sdup(const char *s, + size_t n, char **dest); +int scat(const char *s1, const char *s2, + size_t n, char **dest); +int dcat(const char *s, size_t n, + size_t off, char **dest1, + char **dest2); +/* numerical functions + */ + +unsigned short hextonum(char ch_s); +size_t rlong(void); + +/* Helper functions for command: dump + */ + +void cmd_helper_dump(void); +void print_mac_from_nvm(size_t partnum); +void hexdump(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); +#if !defined(REAL_POS_IO) || \ + REAL_POS_IO < 1 +off_t lseek_on_eintr(int fd, off_t off, + int whence, int loop_eagain, int loop_eintr); +#endif +int try_err(int loop_err, int errval); + +/* Error handling and cleanup + */ + +void usage(void); +void err_no_cleanup(int stfu, int nvm_errval, const char *msg, ...); +void b0rk(int nvm_errval, const char *msg, ...); +int exit_cleanup(void); +const char *getnvmprogname(void); + +void err_mkhtemp(int stfu, 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 mkhtemp_fill_random(char *p, size_t xc); +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); +int 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 close_no_err(int *fd); +void free_if_null(char **buf); +int close_warn(int *fd, char *s); +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 *lbgetprogname(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..9a041989 --- /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) + b0rk(ECANCELED, "%s: part %lu has a bad checksum", + f->fname, (size_t)f->part); + + b0rk(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..c7048a23 --- /dev/null +++ b/util/libreboot-utils/lib/command.c @@ -0,0 +1,564 @@ +/* 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) + b0rk(EINVAL, "cmd index %lu: argc below 3, %d", + (size_t)c, cmd->argc); + + if (cmd->str == NULL) + b0rk(EINVAL, "cmd index %lu: NULL str", + (size_t)c); + + if (*cmd->str == '\0') + b0rk(EINVAL, "cmd index %lu: empty str", + (size_t)c); + + if (slen(cmd->str, MAX_CMD_LEN +1, &rval) < 0) + b0rk(errno, "Could not get command length"); + + if (rval > MAX_CMD_LEN) { + b0rk(EINVAL, "cmd index %lu: str too long: %s", + (size_t)c, cmd->str); + } + + if (cmd->run == NULL) + b0rk(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: + b0rk(EINVAL, "Unsupported rw_size: %lu", + (size_t)gbe_rw_size); + } + + if (gbe_rw_size > GBE_PART_SIZE) + b0rk(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) + b0rk(EINVAL, "invalid cmd.flags setting"); +} + +void +set_cmd(int argc, char *argv[]) +{ + struct xstate *x = xstatus(); + const char *cmd; + + int rval; + + size_t c; + + for (c = 0; c < items(x->cmd); c++) { + + cmd = x->cmd[c].str; + + if (scmp(argv[2], cmd, MAX_CMD_LEN, &rval) < 0) + err_no_cleanup(0, EINVAL, + "could not compare command strings"); + if (rval != 0) + continue; /* not the right command */ + + /* valid command found */ + if (argc >= x->cmd[c].argc) { + x->no_cmd = 0; + x->i = c; /* set command */ + + return; + } + + err_no_cleanup(0, 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) + b0rk(EINVAL, + "arg_part set for command that needs argc4"); + + if (cmd->arg_part && i == CMD_SETMAC) + b0rk(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') + b0rk(EINVAL, "Partnum string '%s' wrong length", part_str); + + /* char signedness is implementation-defined + */ + ch = (unsigned char)part_str[0]; + if (ch < '0' || ch > '1') + b0rk(EINVAL, "Bad part number (%c)", ch); + + return (size_t)(ch - '0'); +} + +void +check_command_num(size_t c) +{ + if (!valid_command(c)) + b0rk(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) + b0rk(EINVAL, + "Invalid cmd chk value (%lu) vs arg: %lu", + cmd->chk, c); + + return 1; +} + +void +cmd_helper_setmac(void) +{ + struct xstate *x = xstatus(); + struct macaddr *mac = &x->mac; + + size_t partnum; + + check_cmd(cmd_helper_setmac, "setmac"); + + printf("MAC address to be written: %s\n", mac->str); + parse_mac_string(); + + for (partnum = 0; partnum < 2; partnum++) + write_mac_part(partnum); +} + +void +parse_mac_string(void) +{ + struct xstate *x = xstatus(); + struct macaddr *mac = &x->mac; + + size_t mac_byte; + + size_t rval; + + if (slen(x->mac.str, 18, &rval) < 0) + b0rk(EINVAL, "Could not determine MAC length"); + + if (rval != 17) + b0rk(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) + b0rk(EINVAL, "Must not specify all-zeroes MAC address"); + + if (mac->mac_buf[0] & 1) + b0rk(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]) != ':') + b0rk(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) + b0rk(EIO, "Randomisation failure"); + else + b0rk(EINVAL, "Invalid character '%c'", + mac->str[mac_str_pos + mac_nib_pos]); + } + + /* If random, ensure that local/unicast bits are set. + */ + if ((mac_byte_pos == 0) && (mac_nib_pos == 1) && + ((mac_ch | 0x20) == 'x' || + (mac_ch == '?'))) + hex_num = (hex_num & 0xE) | 2; /* local, unicast */ + + /* MAC words stored big endian in-file, little-endian + * logically, so we reverse the order. + */ + mac->mac_buf[mac_byte_pos >> 1] |= hex_num << + (((mac_byte_pos & 1) << 3) /* left or right byte? */ + | ((mac_nib_pos ^ 1) << 2)); /* left or right nib? */ +} + +void +write_mac_part(size_t partnum) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + struct macaddr *mac = &x->mac; + + size_t w; + + check_bin(partnum, "part number"); + if (!f->part_valid[partnum]) + return; + + for (w = 0; w < 3; w++) + set_nvm_word(w, partnum, mac->mac_buf[w]); + + printf("Wrote MAC address to part %lu: ", + (size_t)partnum); + print_mac_from_nvm(partnum); +} + +void +cmd_helper_dump(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t p; + + check_cmd(cmd_helper_dump, "dump"); + + f->part_valid[0] = good_checksum(0); + f->part_valid[1] = good_checksum(1); + + for (p = 0; p < 2; p++) { + + if (!f->part_valid[p]) { + + fprintf(stderr, + "BAD checksum %04x in part %lu (expected %04x)\n", + nvm_word(NVM_CHECKSUM_WORD, p), + (size_t)p, + calculated_checksum(p)); + } + + printf("MAC (part %lu): ", + (size_t)p); + + print_mac_from_nvm(p); + + hexdump(p); + } +} + +void +print_mac_from_nvm(size_t partnum) +{ + size_t c; + unsigned short val16; + + for (c = 0; c < 3; c++) { + + val16 = nvm_word(c, partnum); + + printf("%02x:%02x", + (unsigned int)(val16 & 0xff), + (unsigned int)(val16 >> 8)); + + if (c == 2) + printf("\n"); + else + printf(":"); + } +} + +void +hexdump(size_t partnum) +{ + size_t c; + size_t row; + unsigned short val16; + + for (row = 0; row < 8; row++) { + + printf("%08lx ", + (size_t)((size_t)row << 4)); + + for (c = 0; c < 8; c++) { + + val16 = nvm_word((row << 3) + c, partnum); + + if (c == 4) + printf(" "); + + printf(" %02x %02x", + (unsigned int)(val16 & 0xff), + (unsigned int)(val16 >> 8)); + + } + + printf("\n"); + } +} + +void +cmd_helper_swap(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + check_cmd(cmd_helper_swap, "swap"); + + memcpy( + f->buf + (size_t)GBE_WORK_SIZE, + f->buf, + GBE_PART_SIZE); + + memcpy( + f->buf, + f->buf + (size_t)GBE_PART_SIZE, + GBE_PART_SIZE); + + memcpy( + f->buf + (size_t)GBE_PART_SIZE, + f->buf + (size_t)GBE_WORK_SIZE, + GBE_PART_SIZE); + + set_part_modified(0); + set_part_modified(1); +} + +void +cmd_helper_copy(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + check_cmd(cmd_helper_copy, "copy"); + + memcpy( + f->buf + (size_t)((f->part ^ 1) * GBE_PART_SIZE), + f->buf + (size_t)(f->part * GBE_PART_SIZE), + GBE_PART_SIZE); + + set_part_modified(f->part ^ 1); +} + +void +cmd_helper_cat(void) +{ + struct xstate *x = xstatus(); + + check_cmd(cmd_helper_cat, "cat"); + + x->cat = 0; + cat(0); +} + +void +cmd_helper_cat16(void) +{ + struct xstate *x = xstatus(); + + check_cmd(cmd_helper_cat16, "cat16"); + + x->cat = 1; + cat(1); +} + +void +cmd_helper_cat128(void) +{ + struct xstate *x = xstatus(); + + check_cmd(cmd_helper_cat128, "cat128"); + + x->cat = 15; + cat(15); +} + +void +cat(size_t nff) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t p; + size_t ff; + + p = 0; + ff = 0; + + if ((size_t)x->cat != nff) { + + b0rk(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) + b0rk(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) + b0rk(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) + b0rk(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) +{ + b0rk(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..552618d6 --- /dev/null +++ b/util/libreboot-utils/lib/file.c @@ -0,0 +1,1065 @@ +/* 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: + + if (errno == saved_errno) + errno = ESTALE; + + return -1; +} + +/* open() but with abort traps + */ +/* TODO: also support other things here than files. + and then use, throughout the program. + in particular, use of openat might help + (split the path) + (see: link attack mitigations throughout nvmutil) + + make it return, and handle the return value/errno + + (this could return e.g. EINTR) + + TODO: this function is not used by mkhtemp, nor will + it probably be, it's currently used by nvmutil, + for opening intel gbe nvm config files. i can + probably remove it though and unify witth some + of the verification code now used for mkhtemp + +TODO: and don't abort. return -1. and handle in the caller. + +minor obstacle: the mkhtemp code always requires absolute +paths, whereas the gbe editor takes relative paths. + */ +void +xopen(int *fd_ptr, const char *path, int flags, struct stat *st) +{ + if ((*fd_ptr = open(path, flags)) < 0) + err_no_cleanup(0, errno, "%s", path); + + if (fstat(*fd_ptr, st) < 0) + err_no_cleanup(0, errno, "%s: stat", path); + + if (!S_ISREG(st->st_mode)) + err_no_cleanup(0, errno, "%s: not a regular file", path); + + if (lseek_on_eintr(*fd_ptr, 0, SEEK_CUR, 1, 1) == (off_t)-1) + err_no_cleanup(0, errno, "%s: file not seekable", path); +} + +/* fsync() the directory of a file, + * useful for atomic writes + */ + +int +fsync_dir(const char *path) +{ + int saved_errno = errno; + + size_t pathlen = 0; + size_t maxlen = 0; + + char *dirbuf = NULL; + int dirfd = -1; + + char *slash = NULL; + struct stat st = {0}; + + int close_errno; + +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + maxlen = PATH_LEN; +#else + maxlen = 4096; +#endif + + if (if_err(path == NULL, EFAULT) || + if_err_sys(slen(path, maxlen, &pathlen) < 0) || + if_err(pathlen >= maxlen || pathlen < 0, EMSGSIZE) || + if_err(pathlen == 0, EINVAL) + || + if_err_sys((dirbuf = malloc(pathlen + 1)) == NULL)) + goto err_fsync_dir; + + memcpy(dirbuf, path, pathlen + 1); + slash = strrchr(dirbuf, '/'); + + if (slash != NULL) { + *slash = '\0'; + if (*dirbuf == '\0') { + dirbuf[0] = '/'; + dirbuf[1] = '\0'; + } + } else { + dirbuf[0] = '.'; + dirbuf[1] = '\0'; + } + + dirfd = fs_open(dirbuf, + O_RDONLY | O_CLOEXEC | O_NOCTTY +#ifdef O_DIRECTORY + | O_DIRECTORY +#endif +#ifdef O_NOFOLLOW + | O_NOFOLLOW +#endif +); + + if (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; + + if (close_on_eintr(dirfd) == -1) { + + dirfd = -1; + goto err_fsync_dir; + } + + free_if_null(&dirbuf); + + errno = saved_errno; + return 0; + +err_fsync_dir: + + if (errno == saved_errno) + errno = EIO; + + free_if_null(&dirbuf); + close_no_err(&dirfd); + + return -1; +} + +/* + * Safe I/O functions wrapping around + * read(), write() and providing a portable + * analog of both pread() and pwrite(). + * These functions are designed for maximum + * robustness, checking NULL inputs, overflowed + * outputs, and all kinds of errors that the + * standard libc functions don't. + * + * Looping on EINTR and EAGAIN is supported. + * EINTR/EAGAIN looping is done indefinitely. + */ + +/* rw_file_exact() - Read perfectly or die + * + * Read/write, and absolutely insist on an + * absolute read; e.g. if 100 bytes are + * requested, this MUST return 100. + * + * This function will never return zero. + * It will only return below (error), + * or above (success). On error, -1 is + * returned and errno is set accordingly. + * + * Zero-byte returns are not allowed. + * It will re-spin a finite number of + * times upon zero-return, to recover, + * otherwise it will return an error. + */ + +ssize_t +rw_file_exact(int fd, unsigned char *mem, size_t nrw, + off_t off, int rw_type, int loop_eagain, + int loop_eintr, size_t max_retries, + int off_reset) +{ + ssize_t rval; + ssize_t rc; + + size_t nrw_cur; + + off_t off_cur; + void *mem_cur; + + size_t retries_on_zero; + + int saved_errno = errno; + + rval = 0; + + rc = 0; + retries_on_zero = 0; + + if (io_args(fd, mem, nrw, off, rw_type) == -1) + goto err_rw_file_exact; + + while (1) { + + /* Prevent theoretical overflow */ + if (rval >= 0 && (size_t)rval > (nrw - rc)) { + errno = EOVERFLOW; + goto err_rw_file_exact; + } + + rc += rval; + if ((size_t)rc >= nrw) + break; + + mem_cur = (void *)(mem + (size_t)rc); + nrw_cur = (size_t)(nrw - (size_t)rc); + + if (off < 0) { + errno = EOVERFLOW; + goto err_rw_file_exact; + } + + off_cur = off + (off_t)rc; + + rval = prw(fd, mem_cur, nrw_cur, off_cur, + rw_type, loop_eagain, loop_eintr, + off_reset); + + if (rval < 0) + goto err_rw_file_exact; + + if (rval == 0) { + if (retries_on_zero++ < max_retries) + continue; + + errno = EIO; + goto err_rw_file_exact; + } + + retries_on_zero = 0; + } + + if ((size_t)rc != nrw) { + + errno = EIO; + goto err_rw_file_exact; + } + + rval = rw_over_nrw(rc, nrw); + if (rval < 0) + goto err_rw_file_exact; + + errno = saved_errno; + + return rval; + +err_rw_file_exact: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +/* prw() - portable read-write with more + * safety checks than barebones libc + * + * portable pwrite/pread on request, or real + * pwrite/pread libc functions can be used. + * the portable (non-libc) pread/pwrite is not + * thread-safe, because it does not prevent or + * mitigate race conditions on file descriptors + * + * If you need real pwrite/pread, just compile + * with flag: REAL_POS_IO=1 + * + * A fallback is provided for regular read/write. + * rw_type can be IO_READ (read), IO_WRITE (write), + * IO_PREAD (pread) or IO_PWRITE + * + * loop_eagain does a retry loop on EAGAIN if set + * loop_eintr does a retry loop on EINTR if set + * + * race conditions on non-libc pread/pwrite: + * if a file offset changes, abort, to mitage. + * + * off_reset 1: reset the file offset *once* if + * a change was detected, assuming + * nothing else is touching it now + * off_reset 0: never reset if changed + */ + +ssize_t +prw(int fd, void *mem, size_t nrw, + off_t off, int rw_type, + int loop_eagain, int loop_eintr, + int off_reset) +{ + ssize_t rval; + ssize_t r; + int positional_rw; + struct stat st; +#if !defined(REAL_POS_IO) || \ + REAL_POS_IO < 1 + off_t verified; + off_t off_orig; + off_t off_last; +#endif + int saved_errno = errno; + + if (io_args(fd, mem, nrw, off, rw_type) + == -1) + goto err_prw; + + r = -1; + + /* do not use loop_eagain on + * normal files + */ + + if (!loop_eagain) { + /* check whether the file + * changed + */ + + if (check_file(fd, &st) == -1) + goto err_prw; + } + + if (rw_type >= IO_PREAD) + positional_rw = 1; /* pread/pwrite */ + else + positional_rw = 0; /* read/write */ + +try_rw_again: + + if (!positional_rw) { +#if defined(REAL_POS_IO) && \ + REAL_POS_IO > 0 +real_pread_pwrite: +#endif + if (rw_type == IO_WRITE) + r = write(fd, mem, nrw); + else if (rw_type == IO_READ) + r = read(fd, mem, nrw); +#if defined(REAL_POS_IO) && \ + REAL_POS_IO > 0 + else if (rw_type == IO_PWRITE) + r = pwrite(fd, mem, nrw, off); + else if (rw_type == IO_PREAD) + r = pread(fd, mem, nrw, off); +#endif + + if (r == -1 && (errno == try_err(loop_eintr, EINTR) + || errno == try_err(loop_eagain, EAGAIN))) + goto try_rw_again; + + rval = rw_over_nrw(r, nrw); + if (rval < 0) + goto err_prw; + + errno = saved_errno; + + return rval; + } + +#if defined(REAL_POS_IO) && \ + REAL_POS_IO > 0 + goto real_pread_pwrite; +#else + if ((off_orig = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, + loop_eagain, loop_eintr)) == (off_t)-1) { + r = -1; + } else if (lseek_on_eintr(fd, off, SEEK_SET, + loop_eagain, loop_eintr) == (off_t)-1) { + r = -1; + } else { + verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, + loop_eagain, loop_eintr); + + /* abort if the offset changed, + * indicating race condition. if + * off_reset enabled, reset *ONCE* + */ + + if (off_reset && off != verified) + lseek_on_eintr(fd, off, SEEK_SET, + loop_eagain, loop_eintr); + + do { + /* check offset again, repeatedly. + * even if off_reset is set, this + * aborts if offsets change again + */ + + verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, + loop_eagain, loop_eintr); + + if (off != verified) { + + errno = EBUSY; + goto err_prw; + } + + if (rw_type == IO_PREAD) + r = read(fd, mem, nrw); + else if (rw_type == IO_PWRITE) + r = write(fd, mem, nrw); + + if (rw_over_nrw(r, nrw) == -1) + break; + + } while (r == -1 && + (errno == try_err(loop_eintr, EINTR) || + errno == try_err(loop_eagain, EAGAIN))); + } + + saved_errno = errno; + + off_last = lseek_on_eintr(fd, off_orig, SEEK_SET, + loop_eagain, loop_eintr); + + if (off_last != off_orig) { + errno = saved_errno; + goto err_prw; + } + + errno = saved_errno; + + rval = rw_over_nrw(r, nrw); + if (rval < 0) + goto err_prw; + + errno = saved_errno; + + return rval; + +#endif + +err_prw: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +int +io_args(int fd, void *mem, size_t nrw, + off_t off, int rw_type) +{ + int saved_errno = errno; + + 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: + if (errno == saved_errno) + errno = EINVAL; + + return -1; +} + +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: + if (errno == saved_errno) + errno = EINVAL; + + return -1; +} + +/* POSIX can say whatever it wants. + * specification != implementation + */ + +ssize_t +rw_over_nrw(ssize_t r, size_t nrw) +{ + int saved_errno = errno; + + 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: + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +#if !defined(REAL_POS_IO) || \ + REAL_POS_IO < 1 +off_t +lseek_on_eintr(int fd, off_t off, int whence, + int loop_eagain, int loop_eintr) +{ + off_t old; + + old = -1; + + do { + old = lseek(fd, off, whence); + } while (old == (off_t)-1 && ( + errno == try_err(loop_eintr, EINTR) || + errno == try_err(loop_eintr, ETXTBSY) || + errno == try_err(loop_eagain, EAGAIN) || + errno == try_err(loop_eagain, EWOULDBLOCK))); + + return old; +} +#endif + +/* two functions that reduce sloccount by + * two hundred lines... no, now three. */ +int +if_err(int condition, int errval) +{ + if (!condition) + return 0; + + if (errval) + errno = errval; + + return 1; +} +/* technically pointless, but stylistically + * pleasing alongside if_err chains. + * use this one for syscalls that are + * expected to set errno + * also use it for non-system calls + * that act like them, e.g. prw() or + * rw_write_exact() */ +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 +free_if_null(char **buf) +{ + if (buf == NULL || *buf == NULL) + return; + + free(*buf); + *buf = NULL; +} + +/* also returns error code */ +int +close_warn(int *fd, char *s) +{ + int saved_errno = errno; + + if (fd == NULL) { + if (s != NULL) + fprintf(stderr, "FAIL: %s: bad fd ptr\n", s); + return -1; + } + + if (*fd < 0 && s != NULL) { + fprintf(stderr, "WARN: %s: already closed\n", s); + } else if (close(*fd) < 0) { + if (s != NULL) + fprintf(stderr, "FAIL: %s: close\n", s); + return -1; + } + + *fd = -1; + errno = saved_errno; + + return 0; +} + +/* TODO: remove this. giant liability. + make close calls always err instead, + when they fail. otherwise we hide bugs! + */ +void +close_no_err(int *fd) +{ + int saved_errno = errno; + + if (fd == NULL || *fd < 0) + return; + + (void) close_on_eintr(*fd); + *fd = -1; + + errno = saved_errno; +} + +/* TODO: make fd a pointer insttead + and automatically reset -1 here */ +/* BUT DO NOT reset -1 on error */ +int +close_on_eintr(int fd) +{ + int r; + int saved_errno = errno; + + do { + r = close(fd); + } while (r == -1 && ( + errno == EINTR || errno == EAGAIN || + errno == EWOULDBLOCK || errno == ETXTBSY)); + + if (r >= 0) + errno = saved_errno; + + return r; +} + +int +fsync_on_eintr(int fd) +{ + int r; + int saved_errno = errno; + + do { + r = fsync(fd); + } while (r == -1 && (errno == EINTR || errno == EAGAIN || + errno == ETXTBSY || errno == EWOULDBLOCK)); + + if (r >= 0) + errno = saved_errno; + + return r; +} + +int +fs_rename_at(int olddirfd, const char *old, + int newdirfd, const char *new) +{ + if (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() + */ +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 = + open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + + if (global_fs.rootfd < 0) + return NULL; + + fs_initialised = 1; + } + + return &global_fs; +} + +/* filesystem sandboxing. + * (in userspace) + */ +int +fs_resolve_at(int dirfd, const char *path, int flags) +{ + int nextfd = -1; + int curfd; + const char *p; +#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 it is not the original input */ + if (curfd != dirfd) { + (void) close_on_eintr(curfd); + } + + curfd = nextfd; + nextfd = -1; + } + + errno = saved_errno; + return curfd; + +err: + saved_errno = errno; + + if (nextfd >= 0) + (void) close_on_eintr(nextfd); + + /* close curfd only if it's not the original */ + if (curfd != dirfd && curfd >= 0) + (void) close_on_eintr(curfd); + + errno = saved_errno; + return -1; +} + +int +fs_next_component(const char **p, + char *name, size_t namesz) +{ + const char *s = *p; + size_t len = 0; +#if defined(PATH_LEN) && \ +(PATH_LEN) >= 256 + size_t maxlen = PATH_LEN; +#else + size_t maxlen = 4096; +#endif + + while (*s == '/') + s++; + + if (*s == '\0') { + *p = s; + return 0; + } + + while (s[len] != '/' && s[len] != '\0') + len++; + + if (len == 0 || len >= namesz || + len >= maxlen) { + errno = ENAMETOOLONG; + return -1; + } + + memcpy(name, s, len); + name[len] = '\0'; + + /* reject . and .. */ + if ((name[0] == '.' && name[1] == '\0') || + (name[0] == '.' && name[1] == '.' && name[2] == '\0')) { + errno = EPERM; + return -1; + } + + *p = s + len; + return 1; +} + +int +fs_open_component(int dirfd, const char *name, + int flags, int is_last) +{ + int fd; + struct stat st; + + fd = openat2p(dirfd, name, + (is_last ? flags : (O_RDONLY | O_DIRECTORY)) | + O_NOFOLLOW | O_CLOEXEC, (flags & O_CREAT) ? 0600 : 0); + + /* the patient always lies + */ + if (!is_last) { + + if (if_err(fd < 0, EBADF) || + if_err_sys(fstat(fd, &st) < 0)) + return -1; + + if (!S_ISDIR(st.st_mode)) { + + (void) close_on_eintr(fd); + errno = ENOTDIR; + return -1; + } + } + + return fd; +} + +int +fs_dirname_basename(const char *path, + char **dir, char **base, + int allow_relative) +{ + char *buf; + char *slash; + size_t len; + int rval; +#if defined(PATH_LEN) && \ +(PATH_LEN) >= 256 + size_t maxlen = PATH_LEN; +#else + size_t maxlen = 4096; +#endif + + if (path == NULL || dir == NULL || base == NULL || + if_err_sys(slen(path, maxlen, &len) < 0) || + if_err_sys((buf = malloc(len + 1)) == NULL)) + return -1; + + memcpy(buf, path, len + 1); + + /* strip trailing slashes */ + while (len > 1 && buf[len - 1] == '/') + buf[--len] = '\0'; + + slash = strrchr(buf, '/'); + + if (slash) { + + *slash = '\0'; + *dir = buf; + *base = slash + 1; + + if (**dir == '\0') { + (*dir)[0] = '/'; + (*dir)[1] = '\0'; + } + } else if (allow_relative) { + + *dir = strdup("."); + *base = buf; + } else { + errno = EINVAL; + free_if_null(&buf); + return -1; + } + + return 0; +} + +/* portable wrapper for use of openat2 on linux, + * with fallback for others e.g. openbsd + * + * BONUS: arg checks + * TODO: consider EINTR/EAGAIN retry loop + */ +int +openat2p(int dirfd, const char *path, + int flags, mode_t mode) +{ +#ifdef __linux__ + struct open_how how = { + .flags = flags, + .mode = mode, + .resolve = + RESOLVE_BENEATH | + RESOLVE_NO_SYMLINKS | + RESOLVE_NO_MAGICLINKS + }; + int saved_errno = errno; + int rval; +#endif + + if (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( /* <-- say that 10 times to please the demon */ + 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; +} + + + + + + + + + + + diff --git a/util/libreboot-utils/lib/io.c b/util/libreboot-utils/lib/io.c new file mode 100644 index 00000000..d05adbcc --- /dev/null +++ b/util/libreboot-utils/lib/io.c @@ -0,0 +1,591 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * I/O functions specific to nvmutil. + */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "../include/common.h" + +void +open_gbe_file(void) +{ + struct xstate *x = xstatus(); + struct commands *cmd = &x->cmd[x->i]; + struct xfile *f = &x->f; + + int _flags; + + xopen(&f->gbe_fd, f->fname, + cmd->flags | O_BINARY | + O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, &f->gbe_st); + + if (f->gbe_st.st_nlink > 1) + b0rk(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) + b0rk(EIO, "%s: file unlinked while open", f->fname); + + _flags = fcntl(f->gbe_fd, F_GETFL); + if (_flags == -1) + b0rk(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) + b0rk(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: + b0rk(EINVAL, "File size must be 8KB, 16KB or 128KB"); + } + + if (lock_file(f->gbe_fd, cmd->flags) == -1) + b0rk(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) + b0rk(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) + b0rk(errno, "%s: %s: copy failed", + f->fname, f->tname); + + /* file size comparison + */ + if (fstat(f->tmp_fd, &_st) == -1) + b0rk(errno, "%s: stat", f->tname); + + f->gbe_tmp_size = _st.st_size; + + if (f->gbe_tmp_size != f->gbe_file_size) + b0rk(EIO, "%s: %s: not the same size", + f->fname, f->tname); + + /* needs sync, for verification + */ + if (fsync_on_eintr(f->tmp_fd) == -1) + b0rk(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) + b0rk(errno, "%s: read failed (cmp)", f->tname); + + if (memcmp(f->buf, f->bufcmp, f->gbe_file_size) != 0) + b0rk(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) + b0rk(errno, "%s: file inode/device changed", f->tname); + + if (same_file(f->gbe_fd, &f->gbe_st, 1) < 0) + b0rk(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) + b0rk(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) + b0rk(errno, "%s: %s: part %lu", + f->fname, rw_type_str, (size_t)p); + + if ((size_t)rval != gbe_rw_size) + b0rk(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) + b0rk(errno, "%s: fsync (pre-verification)", + f->tname); + + check_written_part(0); + check_written_part(1); + + report_io_err_rw(); + + if (f->io_err_gbe) + b0rk(EIO, "%s: bad write", f->fname); + + saved_errno = errno; + + f->io_err_gbe_bin |= -close_warn(&f->tmp_fd, f->tname); + f->io_err_gbe_bin |= -close_warn(&f->gbe_fd, f->fname); + + 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_if_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) + b0rk(errno, "%s: file inode/device changed", f->tname); + + if (same_file(f->gbe_fd, &f->gbe_st, 1) < 0) + b0rk(errno, "%s: file changed during write", f->fname); + + rval = rw_gbe_file_exact(f->tmp_fd, f->pad, + gbe_rw_size, file_offset, IO_PREAD); + + if (rval == -1) + f->rw_check_err_read[p] = f->io_err_gbe = 1; + else if ((size_t)rval != gbe_rw_size) + f->rw_check_partial_read[p] = f->io_err_gbe = 1; + else if (memcmp(mem_offset, f->pad, gbe_rw_size) != 0) + f->rw_check_bad_part[p] = f->io_err_gbe = 1; + + if (f->rw_check_err_read[p] || + f->rw_check_partial_read[p]) + return; + + /* We only load one part on-file, into memory but + * always at offset zero, for post-write checks. + * That's why we hardcode good_checksum(0) + */ + + buf_restore = f->buf; + + /* good_checksum works on f->buf + * so let's change f->buf for now + */ + + f->buf = f->pad; + + if (good_checksum(0)) + f->post_rw_checksum[p] = 1; + + f->buf = buf_restore; +} + +void +report_io_err_rw(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + size_t p; + + if (!f->io_err_gbe) + return; + + for (p = 0; p < 2; p++) { + if (!f->part_modified[p]) + continue; + + if (f->rw_check_err_read[p]) + fprintf(stderr, + "%s: pread: p%lu (post-verification)\n", + f->fname, (size_t)p); + if (f->rw_check_partial_read[p]) + fprintf(stderr, + "%s: partial pread: p%lu (post-verification)\n", + f->fname, (size_t)p); + if (f->rw_check_bad_part[p]) + fprintf(stderr, + "%s: pwrite: corrupt write on p%lu\n", + f->fname, (size_t)p); + + if (f->rw_check_err_read[p] || + f->rw_check_partial_read[p]) { + fprintf(stderr, + "%s: p%lu: skipped checksum verification " + "(because read failed)\n", + f->fname, (size_t)p); + + continue; + } + + fprintf(stderr, "%s: ", f->fname); + + if (f->post_rw_checksum[p]) + fprintf(stderr, "GOOD"); + else + fprintf(stderr, "BAD"); + + fprintf(stderr, " checksum in p%lu on-disk.\n", + (size_t)p); + + if (f->post_rw_checksum[p]) { + fprintf(stderr, + " This does NOT mean it's safe. it may be\n" + " salvageable if you use the cat feature.\n"); + } + } +} + +int +gbe_mv(void) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + int rval; + + int saved_errno; + int tmp_gbe_bin_exists; + + char *dest_tmp; + int dest_fd = -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: + + /* TODO: this whole section is bloat. + it can be generalised + */ + + if (f->gbe_fd > -1) { + if (close_on_eintr(f->gbe_fd) < 0) { + f->gbe_fd = -1; + rval = -1; + } + f->gbe_fd = -1; + + if (fsync_dir(f->fname) < 0) { + f->io_err_gbe_bin = 1; + rval = -1; + } + } + + if (f->tmp_fd > -1) { + if (close_on_eintr(f->tmp_fd) < 0) { + f->tmp_fd = -1; + rval = -1; + } + f->tmp_fd = -1; + } + + /* before this function is called, + * tmp_fd may have been moved + */ + if (tmp_gbe_bin_exists) { + if (unlink(f->tname) < 0) + rval = -1; + else + tmp_gbe_bin_exists = 0; + } + + if (rval < 0) { + /* if nothing set errno, + * we assume EIO, or we + * use what was set + */ + if (errno == saved_errno) + errno = EIO; + } else { + errno = saved_errno; + } + + return rval; +} + +/* This one is similar to gbe_file_offset, + * but used to check Gbe bounds in memory, + * and it is *also* used during file I/O. + */ +unsigned char * +gbe_mem_offset(size_t p, const char *f_op) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + off_t gbe_off; + + gbe_off = gbe_x_offset(p, f_op, "mem", + GBE_PART_SIZE, GBE_WORK_SIZE); + + return (unsigned char *) + (f->buf + (size_t)gbe_off); +} + +/* I/O operations filtered here. These operations must + * only write from the 0th position or the half position + * within the GbE file, and write 4KB of data. + */ +off_t +gbe_file_offset(size_t p, const char *f_op) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + off_t gbe_file_half_size; + + gbe_file_half_size = f->gbe_file_size >> 1; + + return gbe_x_offset(p, f_op, "file", + gbe_file_half_size, f->gbe_file_size); +} + +off_t +gbe_x_offset(size_t p, const char *f_op, const char *d_type, + off_t nsize, off_t ncmp) +{ + struct xstate *x = xstatus(); + struct xfile *f = &x->f; + + off_t off; + + check_bin(p, "part number"); + + off = ((off_t)p) * (off_t)nsize; + + if (off > ncmp - GBE_PART_SIZE) + b0rk(ECANCELED, "%s: GbE %s %s out of bounds", + f->fname, d_type, f_op); + + if (off != 0 && off != ncmp >> 1) + b0rk(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..191d657c --- /dev/null +++ b/util/libreboot-utils/lib/mkhtemp.c @@ -0,0 +1,1103 @@ +/* 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); +} + +/* note: tmpdir is an override of TMPDIR or /tmp or /var/tmp */ +/* WARNING: + * on error, *path (at **path) may be + NULL, or if the error pertains to + an actual TMPDIR, set. if set, it + will be using *static* memory and + must not be freed. on success, + a pointer to heap memory is set + instead. + * see: + * env_tmpdir() + * this is for error reports if e.g. + * TMPDIR isn't found (but is set) + * if TMPDIR isn't set, it will + * default to /tmp or /var/tmp + */ +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 (slen(tmpdir, maxlen, &dirlen) < 0) + goto err; + if (*tmpdir == '\0') + goto err; + if (*tmpdir != '/') + goto err; + + if (template != NULL) + templatestr = template; + else + templatestr = "tmp.XXXXXXXXXX"; + + if (slen(templatestr, maxlen, &templatestr_len) < 0) + goto err; + + /* sizeof adds an extra byte, useful + * because we also want '.' or '/' + */ + destlen = dirlen + 1 + templatestr_len; + if (destlen > maxlen - 1) { + errno = EOVERFLOW; + goto err; + } + + dest = malloc(destlen + 1); + if (dest == NULL) { + errno = ENOMEM; + goto err; + } + + memcpy(dest, tmpdir, dirlen); + *(dest + dirlen) = '/'; + memcpy(dest + dirlen + 1, templatestr, templatestr_len); + *(dest + destlen) = '\0'; + + fname = dest + dirlen + 1; + + dirfd = fs_open(tmpdir, + O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + goto err; + + if (fstat(dirfd, &st_dir_initial) < 0) + goto err; + + *fd = mkhtemp(fd, &st, dest, dirfd, + fname, &st_dir_initial, type); + if (*fd < 0) + goto err; + + close_no_err(&dirfd); + + errno = saved_errno; + *path = dest; + + return 0; + +err: + + if (errno != saved_errno) + saved_errno = errno; + else + saved_errno = errno = EIO; + + free_if_null(&dest); + + close_no_err(&dirfd); + close_no_err(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; + + char tmp[] = "/tmp"; + 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 = tmp; + + errno = saved_errno; + return "/tmp"; + } + + if (world_writeable_and_sticky("/var/tmp", + allow_noworld_unsticky, + bypass_all_sticky_checks)) { + + if (tmpdir != NULL) + *tmpdir = vartmp; + + errno = saved_errno; + return "/var/tmp"; + } + + return NULL; +} + +int +tmpdir_policy(const char *path, + int *allow_noworld_unsticky) +{ + int saved_errno = errno; + int r; + + if (path == NULL || + allow_noworld_unsticky == NULL) { + + errno = EFAULT; + return -1; + } + + *allow_noworld_unsticky = 1; + + r = same_dir(path, "/tmp"); + if (r < 0) + goto err_tmpdir_policy; + if (r > 0) + *allow_noworld_unsticky = 0; + + r = same_dir(path, "/var/tmp"); + if (r < 0) + goto err_tmpdir_policy; + if (r > 0) + *allow_noworld_unsticky = 0; + + errno = saved_errno; + return 0; + +err_tmpdir_policy: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +int +same_dir(const char *a, const char *b) +{ + int fd_a = -1; + int fd_b = -1; + + struct stat st_a; + struct stat st_b; + + int saved_errno = errno; + int rval_scmp; + +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + size_t maxlen = (PATH_LEN); +#else + size_t maxlen = 4096; +#endif + + /* optimisation: if both dirs + are the same, we don't need + to check anything. sehr schnell: + */ + if (scmp(a, b, maxlen, &rval_scmp) < 0) + goto err_same_dir; + /* bonus: scmp checks null for us */ + if (rval_scmp == 0) + goto success_same_dir; + + fd_a = fs_open(a, O_RDONLY | O_DIRECTORY); + if (fd_a < 0) + goto err_same_dir; + + fd_b = fs_open(b, O_RDONLY | O_DIRECTORY); + if (fd_b < 0) + goto err_same_dir; + + if (fstat(fd_a, &st_a) < 0) + goto err_same_dir; + + if (fstat(fd_b, &st_b) < 0) + goto err_same_dir; + + if (st_a.st_dev == st_b.st_dev && + st_a.st_ino == st_b.st_ino) { + + close_no_err(&fd_a); + close_no_err(&fd_b); + +success_same_dir: + + /* SUCCESS + */ + + errno = saved_errno; + return 1; + } + + close_no_err(&fd_a); + close_no_err(&fd_b); + + /* FAILURE (logical) + */ + + errno = saved_errno; + return 0; + +err_same_dir: + + /* FAILURE (probably syscall) + */ + + close_no_err(&fd_a); + close_no_err(&fd_b); + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +/* bypass_all_sticky_checks: if set, + disable stickiness checks (libc behaviour) + (if not set: leah behaviour) + + allow_noworld_unsticky: + allow non-sticky files if not world-writeable + (still block non-sticky in standard TMPDIR) +*/ +int +world_writeable_and_sticky( + const char *s, + int allow_noworld_unsticky, + int bypass_all_sticky_checks) +{ + struct stat st; + int dirfd = -1; + + int saved_errno = errno; + + if (s == NULL || *s == '\0') { + errno = EINVAL; + goto sticky_hell; + } + + /* mitigate symlink attacks* + */ + dirfd = fs_open(s, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + goto sticky_hell; + + if (fstat(dirfd, &st) < 0) + goto sticky_hell; + + if (!S_ISDIR(st.st_mode)) { + errno = ENOTDIR; + goto sticky_hell; + } + + /* must be fully executable + * by everyone, or openat2 + * becomes unreliable** + * + * TODO: loosen these, as a toggle. + * execution rights isn't + * really a requirement for + * TMPDIR, except maybe search, + * but this function will be + * generalised at some point + * for use in other tools + * besides just mkhtemp. + */ + /* + 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 */ + + /* unhinged leah mode: + */ + + if (st.st_mode & S_IWOTH) { /* world writeable */ + + /* if world-writeable, only + * allow sticky files + */ + if (st.st_mode & S_ISVTX) + goto sticky_heaven; /* sticky */ + + errno = EPERM; + goto sticky_hell; /* not sticky */ + } + + /* if anyone even looks at you funny, drop + * everything on the floor and refuse to function + */ + 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: +/* i like the one in hamburg better */ + + close_no_err(&dirfd); + errno = saved_errno; + + return 1; + +sticky_hell: + + if (errno == saved_errno) + errno = EPERM; + + saved_errno = errno; + + close_no_err(&dirfd); + + errno = saved_errno; + + return 0; +} + +/* mk(h)temp - hardened mktemp. + * like mkstemp, but (MUCH) harder. + * + * designed to resist TOCTOU attacks + * e.g. directory race / symlink attack + * + * extremely strict and even implements + * some limited userspace-level sandboxing, + * similar in spirit to openbsd unveil, + * though unveil is from kernel space. + * + * supports both files and directories. + * file: type = MKHTEMP_FILE (0) + * dir: type = MKHTEMP_DIR (1) + * + * DESIGN NOTES: + * + * caller is expected to handle + * cleanup e.g. free(), on *st, + * *template, *fname (all of the + * pointers). ditto fd cleanup. + * + * some limited cleanup is + * performed here, e.g. directory/file + * cleanup on error in mkhtemp_try_create + * + * we only check if these are not NULL, + * and the caller is expected to take + * care; without too many conditions, + * these functions are more flexible, + * but some precauttions are taken: + * + * when used via the function new_tmpfile + * or new_tmpdir, thtis is extremely strict, + * much stricter than previous mktemp + * variants. for example, it is much + * stricter about stickiness on world + * writeable directories, and it enforces + * file ownership under hardened mode + * (only lets you touch your own files/dirs) + */ +int +mkhtemp(int *fd, + struct stat *st, + char *template, + int dirfd, + const char *fname, + struct stat *st_dir_initial, + int type) +{ + size_t len = 0; + size_t xc = 0; + size_t fname_len = 0; + + char *fname_copy = NULL; + char *p; + + size_t retries; + + int close_errno; + int saved_errno = errno; + +#if defined(PATH_LEN) && \ + (PATH_LEN) >= 256 + size_t max_len = PATH_LEN; +#else + size_t max_len = 4096; +#endif + int r; + char *end; + + if (if_err(fd == NULL || template == NULL || fname == NULL || + st_dir_initial == NULL, EFAULT) || + if_err(*fd >= 0, EEXIST) || + if_err(dirfd < 0, EBADF) + || + if_err_sys(slen(template, max_len, &len) < 0) || + if_err(len >= max_len, EMSGSIZE) + || + if_err_sys(slen(fname, max_len, &fname_len) < 0) || + if_err(fname == NULL, EINVAL) || + if_err(strrchr(fname, '/') != NULL, EINVAL)) + return -1; + + for (end = template + len; /* count X */ + end > template && *--end == 'X'; xc++); + + if (if_err(xc < 3 || xc > len, EINVAL) || + if_err(fname_len > len, EOVERFLOW)) + return -1; + + if (if_err(memcmp(fname, template + len - fname_len, + fname_len) != 0, EINVAL)) + return -1; + + if((fname_copy = malloc(fname_len + 1)) == NULL) + goto err; + + /* fname_copy = templatestr region only; p points to trailing XXXXXX */ + memcpy(fname_copy, + template + len - fname_len, + fname_len + 1); + p = fname_copy + fname_len - xc; + + for (retries = 0; retries < MKHTEMP_RETRY_MAX; retries++) { + + r = mkhtemp_try_create(dirfd, + st_dir_initial, fname_copy, + p, xc, fd, st, type); + + if (r == 0) { + if (retries >= MKHTEMP_SPIN_THRESHOLD) { + /* usleep can return EINTR */ + close_errno = errno; + usleep((useconds_t)rlong() & 0x3FF); + errno = close_errno; + } + continue; + } + if (r < 0) + goto err; + + /* success: copy final name back */ + memcpy(template + len - fname_len, + fname_copy, fname_len); + + errno = saved_errno; + goto success; + } + + errno = EEXIST; + +err: + close_no_err(fd); + +success: + free_if_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; + + 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; + + if (if_err_sys(mkhtemp_fill_random(p, xc) < 0) || + 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_no_err(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; + + 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++) { + + if (mkhtemp_fill_random(p, xc) < 0) + goto err; + + 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_no_err(&tmpfd); + return -1; +} +#endif + +int +mkhtemp_fill_random(char *p, size_t xc) +{ + static char ch[] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + size_t chx = 0; + size_t r; + + /* clamp rand to prevent modulo bias + */ + size_t limit = ((size_t)-1) - (((size_t)-1) % (sizeof(ch) - 1)); + int saved_errno = errno; + + if (if_err(p == NULL, EFAULT)) + return -1; + + for (chx = 0; chx < xc; chx++) { + +retry_rand: + /* on bsd: uses arc4random + on linux: uses getrandom + *never returns error* + */ + r = rlong(); /* always returns successful */ + if (r >= limit) + goto retry_rand; + + p[chx] = ch[r % (sizeof(ch) - 1)]; + } + + errno = saved_errno; + return 0; + +} + +/* WARNING: **ONCE** per file. + * + * !!! DO NOT RUN TWICE PER FILE. BEWARE OF THE DEMON !!! + * watch out for spikes! + */ +/* TODO: bad_flags can be negative, and is + * ignored if it is. should we err instead? + */ +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; + + if (expected != NULL) + if (fd_verify_identity(*fd, expected, &st_now) < 0) + goto err_demons; + } + + if (fchmod(*fd, mode) == -1) + goto err_demons; + + errno = saved_errno; + return 0; + +err_demons: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} + +int +fd_verify_regular(int fd, + const struct stat *expected, + struct stat *out) +{if ( + 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: + + if (errno == saved_errno) + errno = EIO; + + return -1; +} diff --git a/util/libreboot-utils/lib/num.c b/util/libreboot-utils/lib/num.c new file mode 100644 index 00000000..41a08f0b --- /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> + * + * Numerical functions. + * NOTE: randomness was moved to rand.c + */ + +/* +TODO: properly handle errno in this file + */ + +#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 <limits.h> +#include <stddef.h> +#include <string.h> +#include <unistd.h> + +#include "../include/common.h" + +/* TODO: + * make this and errno handling more + * flexible + + in particular: + hextonum could be modified to + write into a buffer instead, + with the converted numbers, + of an arbitrary length + */ +unsigned short +hextonum(char ch_s) +{ + int saved_errno = errno; + + /* rlong() can return error, + but preserves errno if no + error. we need to detect + this because it handles + /dev/urandom sometimes + + therefore, if it's zero + at start, we know if there + was an err at the end, by + return value zero, if errno + was set; this is technically + valid, since zero is also + a valid random number! + + it's an edge case that i had + to fix. i'll rewrite the code + better later. for now, it + should be ok. + */ + errno = 0; + + unsigned char ch; + size_t rval; + + ch = (unsigned char)ch_s; + + if ((unsigned int)(ch - '0') <= 9) { + + rval = ch - '0'; + goto hextonum_success; + } + + ch |= 0x20; + + if ((unsigned int)(ch - 'a') <= 5) { + + rval = ch - 'a' + 10; + goto hextonum_success; + } + + if (ch == '?' || ch == 'x') { + + rval = rlong(); + if (errno > 0) + goto err_hextonum; + + goto hextonum_success; + } + + goto err_hextonum; + +hextonum_success: + + errno = saved_errno; + return (unsigned short)rval & 0xf; + +err_hextonum: + + if (errno == saved_errno) + errno = EINVAL; + else + return 17; /* 17 indicates getrandom/urandom fail */ + + return 16; /* invalid character */ + + /* caller just checks >15. */ +} + +void +check_bin(size_t a, const char *a_name) +{ + if (a > 1) + err_no_cleanup(0, 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..c4cf008c --- /dev/null +++ b/util/libreboot-utils/lib/rand.c @@ -0,0 +1,114 @@ +/* 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> + +#include <errno.h> +#if !((defined(__OpenBSD__) && (OpenBSD) >= 201) || \ + defined(__FreeBSD__) || \ + defined(__NetBSD__) || defined(__APPLE__)) +#include <fcntl.h> /* if not arc4random: /dev/urandom */ +#endif +#include <limits.h> +#include <stddef.h> +#include <string.h> +#include <unistd.h> +#include <stdlib.h> + +#include "../include/common.h" + +/* Random numbers + */ + +/* when calling this: save errno + * first, then set errno to zero. + * on error, this function will + * set errno and possibly return + * + * rlong also preserves errno + * and leaves it unchanged on + * success, so if you do it + * right, you can detect error. + * this is because it uses + * /dev/urandom which can err. + * ditto getrandom (EINTR), + * theoretically. + */ + +/* for the linux version: we use only the + * syscall, because we cannot trust /dev/urandom + * to be as robust, and some libc implementations + * may default to /dev/urandom under fault conditions. + * + * for general high reliability, we must abort on + * failure. in practise, it will likely never fail. + * the arc4random call on bsd never returns error. + */ + +size_t +rlong(void) +{ + size_t rval; + int saved_errno = errno; + errno = 0; + +#if (defined(__OpenBSD__) || defined(__FreeBSD__) || \ + defined(__NetBSD__) || defined(__APPLE__) || \ + defined(__DragonFly__)) + + arc4random_buf(&rval, sizeof(size_t)); + goto out; + +#elif defined(__linux__) + + size_t off = 0; + size_t len = sizeof(rval); + ssize_t rc; + +retry_rand: + rc = (ssize_t)syscall(SYS_getrandom, + (char *)&rval + off, len - off, 0); + + if (rc < 0) { + if (errno == EINTR || errno == EAGAIN) { + usleep(100); + goto retry_rand; + } + + goto err; /* possibly unsupported by kernel */ + } + + if ((off += (size_t)rc) < len) + goto retry_rand; + + goto out; +err: + /* + * getrandom can return with error, but arc4random + * doesn't. generally, getrandom will be reliable, + * but we of course have to maintain parity with + * BSD. So a rand failure is to be interpreted as + * a major systems failure, and we act accordingly. + */ + err_no_cleanup(1, ECANCELED, + "Randomisation failure, possibly unsupported in your kernel."); + exit(EXIT_FAILURE); + +#else +#error Unsupported operating system (possibly unsecure randomisation) +#endif + +out: + errno = saved_errno; + return rval; +} +#endif diff --git a/util/libreboot-utils/lib/state.c b/util/libreboot-utils/lib/state.c new file mode 100644 index 00000000..42d060b7 --- /dev/null +++ b/util/libreboot-utils/lib/state.c @@ -0,0 +1,233 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org> + * + * State machine (singleton) for nvmutil data. + */ + +#ifdef __OpenBSD__ +#include <sys/param.h> +#endif +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "../include/common.h" + +struct xstate * +xstart(int argc, char *argv[]) +{ + static int first_run = 1; + static 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}, + + /* .argv0 (for our getprogname implementation) */ + NULL, + + /* ->i (index to cmd[]) */ + 0, + + /* .no_cmd (set 0 when a command is found) */ + 1, + + /* .cat (cat helpers set this) */ + -1 + + }; + + if (!first_run) + return &us; + + if (argc < 3) + err_no_cleanup(0, EINVAL, "xstart: Too few arguments"); + if (argv == NULL) + err_no_cleanup(0, EINVAL, "xstart: NULL argv"); + + first_run = 0; + + us.f.buf = us.f.real_buf; + + us.argv0 = argv[0]; + us.f.fname = argv[1]; + + us.f.tmp_fd = -1; + us.f.tname = NULL; + + if ((realdir = realpath(us.f.fname, NULL)) == NULL) + err_no_cleanup(0, errno, "xstart: can't get realpath of %s", + us.f.fname); + + if (fs_dirname_basename(realdir, &dir, &base, 0) < 0) + err_no_cleanup(0, errno, "xstart: don't know CWD of %s", + us.f.fname); + + if ((us.f.base = strdup(base)) == NULL) + err_no_cleanup(0, errno, "strdup base"); + + us.f.dirfd = fs_open(dir, + O_RDONLY | O_DIRECTORY); + if (us.f.dirfd < 0) + err_no_cleanup(0, errno, "%s: open dir", dir); + + if (new_tmpfile(&us.f.tmp_fd, &us.f.tname, dir, ".gbe.XXXXXXXXXX") < 0) + err_no_cleanup(0, errno, "%s", us.f.tname); + + if (fs_dirname_basename(us.f.tname, + &tmpdir, &tmpbase_local, 0) < 0) + err_no_cleanup(0, errno, "tmp basename"); + + us.f.tmpbase = strdup(tmpbase_local); + if (us.f.tmpbase == NULL) + err_no_cleanup(0, errno, "strdup tmpbase"); + + free_if_null(&tmpdir); + + if (us.f.tname == NULL) + err_no_cleanup(0, errno, "x->f.tname null"); + if (*us.f.tname == '\0') + err_no_cleanup(0, errno, "x->f.tname empty"); + + if (fstat(us.f.tmp_fd, &us.f.tmp_st) < 0) + err_no_cleanup(0, 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_no_cleanup(0, EACCES, "NULL pointer to xstate"); + + return x; +} + +void +b0rk(int nvm_errval, const char *msg, ...) +{ + struct xstate *x = xstatus(); + + va_list args; + + if (errno == 0) + errno = nvm_errval; + if (!errno) + errno = ECANCELED; + + (void)exit_cleanup(); + + if (x != NULL) + fprintf(stderr, "%s: ", getnvmprogname()); + + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + + fprintf(stderr, ": %s\n", strerror(errno)); + + exit(EXIT_FAILURE); +} + +int +exit_cleanup(void) +{ + struct xstate *x = xstatus(); + struct xfile *f; + + int close_err; + int saved_errno; + + close_err = 0; + saved_errno = errno; + + if (x != NULL) { + f = &x->f; + + close_no_err(&f->gbe_fd); + close_no_err(&f->tmp_fd); + close_no_err(&f->tmp_fd); + + if (f->tname != NULL) + if (unlink(f->tname) == -1) + close_err = 1; + + close_no_err(&f->dirfd); + free_if_null(&f->base); + free_if_null(&f->tmpbase); + } + + if (saved_errno) + errno = saved_errno; + + if (close_err) + return -1; + + return 0; +} diff --git a/util/libreboot-utils/lib/string.c b/util/libreboot-utils/lib/string.c new file mode 100644 index 00000000..0329c6c3 --- /dev/null +++ b/util/libreboot-utils/lib/string.c @@ -0,0 +1,284 @@ +/* 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" + +/* strict strcmp */ +int +scmp(const char *a, + const char *b, + size_t maxlen, + int *rval) +{ + size_t ch; + unsigned char ac; + unsigned char bc; + + if (a == NULL || + b == NULL || + rval == NULL) { + errno = EFAULT; + goto err; + } + + for (ch = 0; ch < maxlen; ch++) { + + ac = (unsigned char)a[ch]; + bc = (unsigned char)b[ch]; + + if (ac != bc) { + *rval = ac - bc; + return 0; + } + + if (ac == '\0') { + *rval = 0; + return 0; + } + } + +err: + errno = EFAULT; + if (rval != NULL) + *rval = -1; + return -1; +} + +/* strict strlen */ +int +slen(const char *s, + size_t maxlen, + size_t *rval) +{ + size_t ch; + + if (s == NULL || + rval == NULL) { + errno = EFAULT; + goto err; + } + + for (ch = 0; + ch < maxlen && s[ch] != '\0'; + ch++); + + if (ch == maxlen) { + /* unterminated */ + errno = EFAULT; + goto err; + } + + *rval = ch; + return 0; +err: + if (rval != NULL) + *rval = 0; + return -1; +} + +/* strict strdup */ +int +sdup(const char *s, + size_t n, char **dest) +{ + size_t size; + char *rval; + + if (dest == NULL || + slen(s, n, &size) < 0 || + if_err(size == SIZE_MAX, EOVERFLOW) || + (rval = malloc(size + 1)) == NULL) { + + if (dest != NULL) + *dest = NULL; + return -1; + } + + memcpy(rval, s, size); + *(rval + size) = '\0'; + + *dest = rval; + return 0; +} + +/* strict strcat */ +int +scat(const char *s1, const char *s2, + size_t n, char **dest) +{ + size_t size1; + size_t size2; + char *rval; + + if (dest == NULL || + slen(s1, n, &size1) < 0 || + slen(s2, n, &size2) < 0 || + if_err(size1 > SIZE_MAX - size2 - 1, EOVERFLOW) || + (rval = malloc(size1 + size2 + 1)) == NULL) { + + if (dest != NULL) + *dest = NULL; + return -1; + } + + memcpy(rval, s1, size1); + memcpy(rval + size1, s2, size2); + *(rval + size1 + size2) = '\0'; + + *dest = rval; + return 0; +} + +/* strict split/de-cat - off is where + 2nd buffer will start from */ +int +dcat(const char *s, size_t n, + size_t off, char **dest1, + char **dest2) +{ + size_t size; + char *rval1 = NULL; + char *rval2 = NULL; + + if (dest1 == NULL || dest2 == NULL || + slen(s, n, &size) < 0 || + if_err(size == SIZE_MAX, EOVERFLOW) || + if_err(off >= size, EOVERFLOW) || + (rval1 = malloc(off + 1)) == NULL || + (rval2 = malloc(size - off + 1)) == NULL) { + + goto err; + } + + memcpy(rval1, s, off); + *(rval1 + off) = '\0'; + + memcpy(rval2, s + off, size - off); + *(rval2 + size - off) = '\0'; + + *dest1 = rval1; + *dest2 = rval2; + + return 0; + +err: + if (rval1 != NULL) + free(rval1); + if (rval2 != NULL) + free(rval2); + + if (dest1 != NULL) + *dest1 = NULL; + if (dest2 != NULL) + *dest2 = NULL; + + return -1; +} + +/* the one for nvmutil state is in state.c */ +/* this one just exits */ +void +err_no_cleanup(int stfu, int nvm_errval, const char *msg, ...) +{ + va_list args; + int saved_errno = errno; + const char *p; + +#if defined(__OpenBSD__) && defined(OpenBSD) +#if (OpenBSD) >= 509 + if (pledge("stdio", NULL) == -1) + fprintf(stderr, "pledge failure during exit"); +#endif +#endif + if (!errno) + saved_errno = errno = ECANCELED; + + if ((p = getnvmprogname()) != NULL) + fprintf(stderr, "%s: ", p); + + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + + if (p != NULL) + fprintf(stderr, ": %s\n", strerror(errno)); + else + fprintf(stderr, "%s\n", strerror(errno)); + + exit(EXIT_FAILURE); +} + +const char * +getnvmprogname(void) +{ + static char *rval = NULL; + static char *p; + static int setname = 0; + + if (!setname) { + if ((rval = lbgetprogname(NULL)) == NULL) + return NULL; + + p = strrchr(rval, '/'); + if (p) + rval = p + 1; + + setname = 1; + } + + return rval; +} + +/* singleton. if string not null, + sets the string. after set, + will not set anymore. either + way, returns the string + */ +char * +lbgetprogname(char *argv0) +{ + static int setname = 0; + static char *progname = NULL; + size_t len; + + if (!setname) { + if (if_err(argv0 == NULL || *argv0 == '\0', EFAULT) || + slen(argv0, 4096, &len) < 0 || + (progname = malloc(len + 1)) == NULL) + return NULL; + + memcpy(progname, argv0, len + 1); + setname = 1; + } + + return progname; +} + + + + + + + + + + + + + diff --git a/util/libreboot-utils/lib/usage.c b/util/libreboot-utils/lib/usage.c new file mode 100644 index 00000000..2b5a93ca --- /dev/null +++ b/util/libreboot-utils/lib/usage.c @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com> + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + */ + +#include <errno.h> +#include <stdio.h> + +#include "../include/common.h" + +void +usage(void) +{ + const char *util = getnvmprogname(); + + fprintf(stderr, + "Modify Intel GbE NVM images e.g. set MAC\n" + "USAGE:\n" + "\t%s FILE dump\n" + "\t%s FILE setmac [MAC]\n" + "\t%s FILE swap\n" + "\t%s FILE copy 0|1\n" + "\t%s FILE cat\n" + "\t%s FILE cat16\n" + "\t%s FILE cat128\n", + util, util, util, util, + util, util, util); + + b0rk(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..6563e67a --- /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) + b0rk(ECANCELED, "check_nvm_bound: out of bounds %lu", + (size_t)c); +} diff --git a/util/libreboot-utils/mkhtemp.c b/util/libreboot-utils/mkhtemp.c new file mode 100644 index 00000000..261227cb --- /dev/null +++ b/util/libreboot-utils/mkhtemp.c @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * WORK IN PROGRESS (proof of concept), or, v0.0000001 + * + * Mkhtemp - Hardened mktemp. Create files and directories + * randomly as determined by user's TMPDIR, or fallback. It + * attemps to provide mitigation against several TOCTOU-based + * attacks e.g. directory rename / symlink attacks, and it + * 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 on Linux, with additional hardening; BSD + * projects only have openat so the code uses that there. + * + * 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. + * + * 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. + * + * 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. + */ + +#if defined(__linux__) && !defined(_GNU_SOURCE) +/* for openat2 on linux */ +#define _GNU_SOURCE 1 +#endif + +#ifdef __OpenBSD__ +#include <sys/param.h> /* pledge(2) */ +#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" + +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; + int stfu = 0; /* -q option */ + + if (lbgetprogname(argv[0]) == NULL) + err_no_cleanup(stfu, errno, "could not set progname"); + +/* https://man.openbsd.org/pledge.2 */ +#if defined(__OpenBSD__) && defined(OpenBSD) +#if (OpenBSD) >= 509 + if (pledge("stdio flock rpath wpath cpath", NULL) == -1) + goto err_usage; +#endif +#endif + + 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) */ + stfu = 1; + 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) { + if (slen(template, maxlen, &tlen) < 0) + err_no_cleanup(stfu, EINVAL, + "invalid template"); + + for (p = template + tlen; + p > template && *--p == 'X'; xc++); + + if (xc < 3) /* the gnu mktemp errs on less than 3 */ + err_no_cleanup(stfu, 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_no_cleanup(stfu, errno, "%s", tmpdir); + + tmpdir = resolved; + } + + if (new_tmp_common(&fd, &s, type, + tmpdir, template) < 0) + err_no_cleanup(stfu, errno, "%s", s); + +#if defined(__OpenBSD__) && defined(OpenBSD) +#if (OpenBSD) >= 509 + if (pledge("stdio", NULL) == -1) + err_no_cleanup(stfu, errno, "pledge, exit"); +#endif +#endif + + if (s == NULL) + err_no_cleanup(stfu, EFAULT, "bad string initialisation"); + if (*s == '\0') + err_no_cleanup(stfu, EFAULT, "empty string initialisation"); + if (slen(s, maxlen, &len) < 0) + err_no_cleanup(stfu, EFAULT, "unterminated string initialisiert"); + + printf("%s\n", s); + + return EXIT_SUCCESS; + +err_usage: + err_no_cleanup(stfu, EINVAL, + "usage: %s [-d] [-p dir] [template]\n", getnvmprogname()); +}/* + + + ( >:3 ) + /| |\ + / \ + + + + + + */ + + + + + + + + + + + + diff --git a/util/libreboot-utils/nvmutil.c b/util/libreboot-utils/nvmutil.c new file mode 100644 index 00000000..e02f60af --- /dev/null +++ b/util/libreboot-utils/nvmutil.c @@ -0,0 +1,132 @@ +/* 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. + */ + +#ifdef __OpenBSD__ +/* for pledge/unveil test: + */ +#include <sys/param.h> +#endif + +#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 "include/common.h" + +int +main(int argc, char *argv[]) +{ + struct xstate *x; + + struct commands *cmd; + struct xfile *f; + + size_t c; + + if (lbgetprogname(argv[0]) == NULL) + err_no_cleanup(0, errno, "could not set progname"); + +/* https://man.openbsd.org/pledge.2 + https://man.openbsd.org/unveil.2 */ +#if defined(__OpenBSD__) && defined(OpenBSD) +#if (OpenBSD) >= 604 + if (pledge("stdio flock rpath wpath cpath unveil", NULL) == -1) + err_no_cleanup(0, errno, "pledge plus unveil, main"); + if (unveil("/dev/null", "r") == -1) + err_no_cleanup(0, errno, "unveil r: /dev/null"); +#elif (OpenBSD) >= 509 + if (pledge("stdio flock rpath wpath cpath", NULL) == -1) + err_no_cleanup(0, errno, "pledge, main"); +#endif +#endif + +#ifndef S_ISREG + err_no_cleanup(0, ECANCELED, + "Can't determine file types (S_ISREG undefined)"); +#endif +#if ((CHAR_BIT) != 8) + err_no_cleanup(0, ECANCELED, "Unsupported char size"); +#endif + + x = xstart(argc, argv); + + if (x == NULL) + err_no_cleanup(0, 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; + +/* https://man.openbsd.org/pledge.2 + https://man.openbsd.org/unveil.2 */ +#if defined(__OpenBSD__) && defined(OpenBSD) +#if (OpenBSD) >= 604 + + if ((us.cmd[i].flags & O_ACCMODE) == O_RDONLY) { + if (unveil(us.f.fname, "r") == -1) + b0rk(errno, "%s: unveil r", us.f.fname); + } else { + if (unveil(us.f.fname, "rwc") == -1) + b0rk(errno, "%s: unveil rw", us.f.fname); + } + + if (unveil(us.f.tname, "rwc") == -1) + b0rk(errno, "unveil rwc: %s", us.f.tname); + + if (unveil(NULL, NULL) == -1) + b0rk(errno, "unveil block (rw)"); + + if (pledge("stdio flock rpath wpath cpath", NULL) == -1) + b0rk(errno, "pledge (kill unveil)"); + +#elif (OpenBSD) >= 509 + if (pledge("stdio flock rpath wpath cpath", NULL) == -1) + b0rk(errno, "pledge"); +#endif +#endif + + if (cmd->run == NULL) + b0rk(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(); + + if (exit_cleanup() == -1) + b0rk(EIO, "%s: close", f->fname); + + if (f->io_err_gbe_bin) + b0rk(EIO, "%s: error writing final file"); + + free_if_null(&f->tname); + + return EXIT_SUCCESS; +} |
