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/nvmutil/lib/file.c | |
| 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/nvmutil/lib/file.c')
| -rw-r--r-- | util/nvmutil/lib/file.c | 890 |
1 files changed, 0 insertions, 890 deletions
diff --git a/util/nvmutil/lib/file.c b/util/nvmutil/lib/file.c deleted file mode 100644 index b4925ccd..00000000 --- a/util/nvmutil/lib/file.c +++ /dev/null @@ -1,890 +0,0 @@ -/* SPDX-License-Identifier: MIT - * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> - */ - -#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> - -#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; - - if (st_old == NULL || fd < 0) - goto err_same_file; - - if (fstat(fd, &st) == -1) - return -1; - - if (st.st_dev != st_old->st_dev || - st.st_ino != st_old->st_ino || - !S_ISREG(st.st_mode)) - 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: - - errno = EIO; - return -1; -} - -/* open() but with abort traps - */ - -void -xopen(int *fd_ptr, const char *path, int flags, struct stat *st) -{ - if ((*fd_ptr = open(path, flags)) == -1) - err(errno, "%s", path); - - if (fstat(*fd_ptr, st) == -1) - err(errno, "%s: stat", path); - - if (!S_ISREG(st->st_mode)) - err(errno, "%s: not a regular file", path); - - if (lseek(*fd_ptr, 0, SEEK_CUR) == (off_t)-1) - err(errno, "%s: file not seekable", path); -} - -/* fsync() the directory of a file, - * useful for atomic writes - */ - -int -fsync_dir(const char *path) -{ - int saved_errno = errno; - - unsigned long pathlen; - unsigned long maxlen; - - char *dirbuf; - int dirfd; - - char *slash; - - struct stat st; - -#if defined(PATH_LEN) && \ - (PATH_LEN) >= 256 - maxlen = PATH_LEN; -#else - maxlen = 1024; -#endif - - dirbuf = NULL; - dirfd = -1; - - pathlen = xstrxlen(path, maxlen); - - if (pathlen >= maxlen) { - fprintf(stderr, "Path too long for fsync_parent_dir\n"); - goto err_fsync_dir; - } - - if (pathlen == 0) - { - errno = EINVAL; - goto err_fsync_dir; - } - - dirbuf = malloc(pathlen + 1); - if (dirbuf == 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 = open(dirbuf, O_RDONLY -#ifdef O_DIRECTORY - | O_DIRECTORY -#endif -#ifdef O_NOFOLLOW - | O_NOFOLLOW -#endif - ); - if (dirfd == -1) - goto err_fsync_dir; - - if (fstat(dirfd, &st) < 0) - goto err_fsync_dir; - - if (!S_ISDIR(st.st_mode)) { - fprintf(stderr, "%s: not a directory\n", dirbuf); - goto err_fsync_dir; - } - - /* sync file on disk */ - if (fsync_on_eintr(dirfd) == -1) - goto err_fsync_dir; - - if (close_on_eintr(dirfd) == -1) - goto err_fsync_dir; - - if (dirbuf != NULL) - free(dirbuf); - - errno = saved_errno; - return 0; - -err_fsync_dir: - if (!errno) - errno = EIO; - - if (errno != saved_errno) - fprintf(stderr, "%s: %s\n", path, strerror(errno)); - - if (dirbuf != NULL) - free(dirbuf); - - if (dirfd > -1) - close_on_eintr(dirfd); - - errno = saved_errno; - - return -1; -} - -/* returns ptr to path (string). if local>0: - * make tmpfile in the same directory as the - * file. if local==0, use TMPDIR - * - * if local==0, the 3rd argument is ignored - */ - -char * -new_tmpfile(int *fd, int local, const char *path) -{ - unsigned long maxlen; - struct stat st; - - /* please do not modify the - * strings or I will get mad - */ - char tmp_none[] = ""; - char tmp_default[] = "/tmp"; - char default_tmpname[] = "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; - char *tmpname; - - char *base = NULL; - char *dest = NULL; - - unsigned long tmpdir_len = 0; - unsigned long tmpname_len = 0; - unsigned long tmppath_len = 0; - - int fd_tmp = -1; - int flags; - -#if defined(PATH_LEN) && \ - (PATH_LEN) >= 256 - maxlen = PATH_LEN; -#else - maxlen = 1024; -#endif - - tmpname = default_tmpname; - if (local) { - if (path == NULL) - goto err_new_tmpfile; - if (*path == '\0') - goto err_new_tmpfile; - - if (stat(path, &st) == -1) - goto err_new_tmpfile; - - if (!S_ISREG(st.st_mode)) - goto err_new_tmpfile; - - tmpname = (char *)path; - } - - if (local) { - base = tmp_none; - - /* appended to filename for tmp: - */ - tmpdir_len = xstrxlen(default_tmpname, maxlen); - } else { - base = get_tmpdir(); - - if (base == NULL) - base = tmp_default; - if (*base == '\0') - base = tmp_default; - - tmpdir_len = xstrxlen(base, maxlen); - } - - tmpname_len = xstrxlen(tmpname, maxlen); - - tmppath_len = tmpdir_len + tmpname_len; - ++tmppath_len; /* for '/' or '.' */ - - /* max length -1 of maxlen - * for termination - */ - if (tmpdir_len > maxlen - tmpname_len - 1) - goto err_new_tmpfile; - - /* +1 for NULL */ - dest = malloc(tmppath_len + 1); - if (dest == NULL) - goto err_new_tmpfile; - - if (local) { - - *dest = '.'; /* hidden file */ - - memcpy(dest + (unsigned long)1, tmpname, tmpname_len); - - memcpy(dest + (unsigned long)1 + tmpname_len, - default_tmpname, tmpdir_len); - } else { - - memcpy(dest, base, tmpdir_len); - - dest[tmpdir_len] = '/'; - - memcpy(dest + tmpdir_len + 1, tmpname, tmpname_len); - } - - dest[tmppath_len] = '\0'; - - fd_tmp = mkstemp_n(dest); - if (fd_tmp == -1) - goto err_new_tmpfile; - - if (fchmod(fd_tmp, 0600) == -1) - goto err_new_tmpfile; - - flags = fcntl(fd_tmp, F_GETFL); - - if (flags == -1) - goto err_new_tmpfile; - - /* - * O_APPEND would permit offsets - * to be ignored, which breaks - * positional read/write - */ - if (flags & O_APPEND) - goto err_new_tmpfile; - - if (lock_file(fd_tmp, flags) == -1) - goto err_new_tmpfile; - - if (fstat(fd_tmp, &st) == -1) - goto err_new_tmpfile; - - /* - * Extremely defensive - * likely pointless checks - */ - - /* check if it's a file */ - if (!S_ISREG(st.st_mode)) - goto err_new_tmpfile; - - /* check if it's seekable */ - if (lseek(fd_tmp, 0, SEEK_CUR) == (off_t)-1) - goto err_new_tmpfile; - - /* tmpfile has >1 hardlinks */ - if (st.st_nlink > 1) - goto err_new_tmpfile; - - /* tmpfile unlinked while opened */ - if (st.st_nlink == 0) - goto err_new_tmpfile; - - *fd = fd_tmp; - - return dest; - -err_new_tmpfile: - - if (dest != NULL) - free(dest); - - if (fd_tmp > -1) - close_on_eintr(fd_tmp); - - return NULL; -} - -int -lock_file(int fd, int flags) -{ - struct flock fl; - - 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) - return -1; - - return 0; -} - -/* return TMPDIR, or fall back - * to portable defaults - */ - -char * -get_tmpdir(void) -{ - char *t; - struct stat st; - - t = getenv("TMPDIR"); - - if (t && *t) { - - if (stat(t, &st) == 0 && S_ISDIR(st.st_mode)) { - - if ((st.st_mode & S_IWOTH) && !(st.st_mode & S_ISVTX)) - return NULL; - - return t; - } - } - - if (stat("/tmp", &st) == 0 && S_ISDIR(st.st_mode)) - return "/tmp"; - - if (stat("/var/tmp", &st) == 0 && S_ISDIR(st.st_mode)) - return "/var/tmp"; - - return "."; -} - -/* portable mkstemp - */ - -int -mkstemp_n(char *template) -{ - int fd; - unsigned long i, j; - unsigned long len; - char *p; - - unsigned long xc = 0; - - static char ch[] = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - unsigned long r; -#if defined(PATH_LEN) && \ - (PATH_LEN) >= 256 - unsigned long max_len = PATH_LEN; -#else - unsigned long max_len = 4096; -#endif - - len = xstrxlen(template, max_len); - - if (len < 6) { - errno = EINVAL; - return -1; - } - - p = template + len; - - while (p > template && p[-1] == 'X') { - --p; - ++xc; - } - - if (xc < 6) { - errno = EINVAL; - return -1; - } - - for (i = 0; i < 200; i++) { - - for (j = 0; j < xc; j++) { - - r = rlong(); - - p[j] = ch[(unsigned long)(r >> 1) % (sizeof(ch) - 1)]; - } - - fd = open(template, - O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); - - if (fd >= 0) - return fd; - - if (errno != EEXIST) - return -1; - } - - errno = EEXIST; - 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. - */ - -long -rw_file_exact(int fd, unsigned char *mem, unsigned long nrw, - off_t off, int rw_type, int loop_eagain, - int loop_eintr, unsigned long max_retries, - int off_reset) -{ - long rval; - long rc; - - unsigned long nrw_cur; - - off_t off_cur; - void *mem_cur; - - unsigned long retries_on_zero; - - rval = 0; - - rc = 0; - retries_on_zero = 0; - - if (io_args(fd, mem, nrw, off, rw_type) == -1) - return -1; - - while (1) { - - /* Prevent theoretical overflow */ - if (rval >= 0 && (unsigned long)rval > (nrw - rc)) - goto err_rw_file_exact; - - rc += rval; - if ((unsigned long)rc >= nrw) - break; - - mem_cur = (void *)(mem + (unsigned long)rc); - nrw_cur = (unsigned long)(nrw - (unsigned long)rc); - if (off < 0) - 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) - return -1; - - if (rval == 0) { - if (retries_on_zero++ < max_retries) - continue; - goto err_rw_file_exact; - } - - retries_on_zero = 0; - } - - if ((unsigned long)rc != nrw) - goto err_rw_file_exact; - - return rw_over_nrw(rc, nrw); - -err_rw_file_exact: - 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: HAVE_REAL_PREAD_PWRITE=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 - */ - -long -prw(int fd, void *mem, unsigned long nrw, - off_t off, int rw_type, - int loop_eagain, int loop_eintr, - int off_reset) -{ - long r; - int positional_rw; - struct stat st; -#if !defined(HAVE_REAL_PREAD_PWRITE) || \ - HAVE_REAL_PREAD_PWRITE < 1 - int saved_errno; - off_t verified; - off_t off_orig; - off_t off_last; -#endif - - if (io_args(fd, mem, nrw, off, rw_type) - == -1) { - return -1; - } - - r = -1; - - /* do not use loop_eagain on - * normal files - */ - - if (!loop_eagain) { - /* check whether the file - * changed - */ - - if (check_file(fd, &st) == -1) - return -1; - } - - if (rw_type >= IO_PREAD) - positional_rw = 1; /* pread/pwrite */ - else - positional_rw = 0; /* read/write */ - -try_rw_again: - - if (!positional_rw) { -#if defined(HAVE_REAL_PREAD_PWRITE) && \ - HAVE_REAL_PREAD_PWRITE > 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(HAVE_REAL_PREAD_PWRITE) && \ - HAVE_REAL_PREAD_PWRITE > 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; - - return rw_over_nrw(r, nrw); - } - -#if defined(HAVE_REAL_PREAD_PWRITE) && \ - HAVE_REAL_PREAD_PWRITE > 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) - 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) { - errno = EIO; - 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; - - return rw_over_nrw(r, nrw); -#endif - -err_prw: - errno = EIO; - return -1; -} - -int -io_args(int fd, void *mem, unsigned long nrw, - off_t off, int rw_type) -{ - /* obviously */ - if (mem == NULL) - goto err_io_args; - - /* uninitialised fd */ - if (fd < 0) - goto err_io_args; - - /* negative offset */ - if (off < 0) - goto err_io_args; - - /* prevent zero-byte rw */ - if (!nrw) - goto err_io_args; - - /* prevent overflow */ - if (nrw > (unsigned long)X_LONG_MAX) - goto err_io_args; - - /* prevent overflow */ - if (((unsigned long)off + nrw) < (unsigned long)off) - goto err_io_args; - - if (rw_type > IO_PWRITE) - goto err_io_args; - - return 0; - -err_io_args: - errno = EIO; - return -1; -} - -int -check_file(int fd, struct stat *st) -{ - if (fstat(fd, st) == -1) - goto err_is_file; - - if (!S_ISREG(st->st_mode)) - goto err_is_file; - - return 0; - -err_is_file: - errno = EIO; - return -1; -} - -/* POSIX can say whatever it wants. - * specification != implementation - */ - -long -rw_over_nrw(long r, unsigned long nrw) -{ - /* not a libc bug, but we - * don't like the number zero - */ - if (!nrw) - goto err_rw_over_nrw; - - if (r == -1) - return r; - - if ((unsigned long) - r > X_LONG_MAX) { - - /* Theoretical buggy libc - * check. Extremely academic. - * - * Specifications never - * allow this return value - * to exceed SSIZE_T, but - * spec != implementation - * - * Check this after using - * [p]read() or [p]write() - * - * NOTE: here, we assume - * long integers are the - * same size as SSIZE_T - */ - - goto err_rw_over_nrw; - } - - /* Theoretical buggy libc: - * Should never return a number of - * bytes above the requested length. - */ - if ((unsigned long)r > nrw) - goto err_rw_over_nrw; - - return r; - -err_rw_over_nrw: - - errno = EIO; - return -1; -} - -#if !defined(HAVE_REAL_PREAD_PWRITE) || \ - HAVE_REAL_PREAD_PWRITE < 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_eagain, EAGAIN))); - - return old; -} -#endif - -int -try_err(int loop_err, int errval) -{ - if (loop_err) - return errval; - - return -1; -} - -int -close_on_eintr(int fd) -{ - int r; - int saved_errno = errno; - - do { - r = close(fd); - } while (r == -1 && errno == EINTR); - - if (r > -1) - errno = saved_errno; - - return r; -} - -int -fsync_on_eintr(int fd) -{ - int r; - - do { - r = fsync(fd); - } while (r == -1 && errno == EINTR); - - return r; -} |
