diff options
Diffstat (limited to 'util/libreboot-utils/lib/file.c')
| -rw-r--r-- | util/libreboot-utils/lib/file.c | 893 |
1 files changed, 893 insertions, 0 deletions
diff --git a/util/libreboot-utils/lib/file.c b/util/libreboot-utils/lib/file.c new file mode 100644 index 00000000..805db726 --- /dev/null +++ b/util/libreboot-utils/lib/file.c @@ -0,0 +1,893 @@ +/* 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. + */ + +/* +TODO: putting it here just so it's somewhere: +PATH_MAX is not reliable as a limit for paths, +because the real length depends on mount point, +and specific file systems. +more correct usage example: +long max = pathconf("/", _PC_PATH_MAX); + */ + + +#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; + int rval = 0; + errno = 0; + + if (if_err(st_old == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + (rval = fstat(fd, &st)) < 0 || + (rval = fd_verify_regular(fd, st_old, &st)) < 0 || + if_err(check_size && st.st_size != st_old->st_size, ESTALE)) + return with_fallback_errno(ESTALE); + + reset_caller_errno(rval); + return 0; +} + +int +fsync_dir(const char *path) +{ + int saved_errno = errno; + size_t pathlen = 0; + char *dirbuf = NULL; + int dirfd = -1; + char *slash = NULL; + struct stat st = {0}; + int rval = 0; + errno = 0; + + if (if_err(slen(path, PATH_MAX, &pathlen) == 0, EINVAL)) + goto err_fsync_dir; + + memcpy(smalloc(&dirbuf, pathlen + 1), + path, pathlen + 1); + slash = strrchr(dirbuf, '/'); + + if (slash != NULL) { + *slash = '\0'; + if (*dirbuf == '\0') { + dirbuf[0] = '/'; + dirbuf[1] = '\0'; + } + } else { + dirbuf[0] = '.'; + dirbuf[1] = '\0'; + } + + dirfd = fs_open(dirbuf, + O_RDONLY | O_CLOEXEC | O_NOCTTY +#ifdef O_DIRECTORY + | O_DIRECTORY +#endif +#ifdef O_NOFOLLOW + | O_NOFOLLOW +#endif +); + + if (if_err_sys(dirfd < 0) || + if_err_sys((rval = fstat(dirfd, &st)) < 0) || + if_err(!S_ISDIR(st.st_mode), ENOTDIR) + || + if_err_sys((rval = fsync_on_eintr(dirfd)) == -1)) + goto err_fsync_dir; + + close_on_eintr(&dirfd); + free_and_set_null(&dirbuf); + + reset_caller_errno(rval); + return 0; + +err_fsync_dir: + free_and_set_null(&dirbuf); + close_on_eintr(&dirfd); + + return with_fallback_errno(EIO); +} + +/* rw_file_exact() - Read perfectly or die + * + * Read/write, and absolutely insist on an + * absolute read; e.g. if 100 bytes are + * requested, this MUST return 100. + * + * This function will never return zero. + * It will only return below (error), + * or above (success). On error, -1 is + * returned and errno is set accordingly. + * + * Zero-byte returns are not allowed. + * It will re-spin a finite number of + * times upon zero-return, to recover, + * otherwise it will return an error. + */ + +ssize_t +rw_file_exact(int fd, unsigned char *mem, size_t nrw, + off_t off, int rw_type, size_t max_retries, int off_reset) +{ + int saved_errno = errno; + ssize_t rval = 0; + ssize_t rc = 0; + size_t nrw_cur; + off_t off_cur; + void *mem_cur; + size_t retries_on_zero = 0; + errno = 0; + + if (io_args(fd, mem, nrw, off, rw_type) == -1) + goto err_rw_file_exact; + + while (1) { + + /* Prevent theoretical overflow */ + if (if_err(rval >= 0 && (size_t)rval > (nrw - rc), 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 (if_err(off < 0, EOVERFLOW)) + goto err_rw_file_exact; + + off_cur = off + (off_t)rc; + + if ((rval = rw(fd, mem_cur, nrw_cur, off_cur, rw_type)) < 0) + goto err_rw_file_exact; + + if (rval == 0) { + if (retries_on_zero++ < max_retries) + continue; + + goto err_rw_file_exact; + } + + retries_on_zero = 0; + } + + if (if_err((size_t)rc != nrw, EIO) || + (rval = rw_over_nrw(rc, nrw)) < 0) + goto err_rw_file_exact; + + reset_caller_errno(rval); + return rval; + +err_rw_file_exact: + return with_fallback_errno(EIO); +} + +/* rw() - read-write but with more + * safety checks than barebones libc + * + * A fallback is provided for regular read/write. + * rw_type can be IO_READ (read), IO_WRITE (write), + * IO_PREAD (pread) or IO_PWRITE + */ + +ssize_t +rw(int fd, void *mem, size_t nrw, + off_t off, int rw_type) +{ + ssize_t rval = 0; + ssize_t r = -1; + struct stat st; + int saved_errno = errno; + errno = 0; + + if (io_args(fd, mem, nrw, off, rw_type) == -1) + return with_fallback_errno(EINVAL); + + switch (rw_type) { + case IO_WRITE: + r = write_on_eintr(fd, mem, nrw); + break; + case IO_READ: + r = read_on_eintr(fd, mem, nrw); + break; + case IO_PWRITE: + r = pwrite_on_eintr(fd, mem, nrw, off); + break; + case IO_PREAD: + r = pread_on_eintr(fd, mem, nrw, off); + break; + default: + errno = EINVAL; + break; + } + + if ((rval = rw_over_nrw(r, nrw)) < 0) + return with_fallback_errno(EIO); + + reset_caller_errno(rval); + return rval; +} + +int +io_args(int fd, void *mem, size_t nrw, + off_t off, int rw_type) +{ + int saved_errno = errno; + errno = 0; + + 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; + + reset_caller_errno(0); + return 0; + +err_io_args: + return with_fallback_errno(EINVAL); +} + +int +check_file(int fd, struct stat *st) +{ + int saved_errno = errno; + int rval = 0; + errno = 0; + + if (if_err(fd < 0, EBADF) || + if_err(st == NULL, EFAULT) || + ((rval = fstat(fd, st)) == -1) || + if_err(!S_ISREG(st->st_mode), EBADF)) + goto err_is_file; + + reset_caller_errno(rval); + return 0; + +err_is_file: + return with_fallback_errno(EINVAL); +} + +/* POSIX can say whatever it wants. + * specification != implementation + */ +ssize_t +rw_over_nrw(ssize_t r, size_t nrw) +{ + if (if_err(!nrw, EIO) || + (r == -1) || + if_err((size_t)r > SSIZE_MAX, ERANGE) || + if_err((size_t)r > nrw, ERANGE)) + return with_fallback_errno(EIO); + + return r; +} + +/* two functions that reduce sloccount by + * two hundred lines */ +int +if_err(int condition, int errval) +{ + if (!condition) + return 0; + if (errval) + errno = errval; + return 1; +} +int +if_err_sys(int condition) +{ + if (!condition) + return 0; + return 1; +} + +int +fs_rename_at(int olddirfd, const char *old, + int newdirfd, const char *new) +{ + if (if_err(new == NULL || old == NULL, EFAULT) || + if_err(olddirfd < 0 || newdirfd < 0, EBADF)) + return -1; + + return renameat(olddirfd, old, newdirfd, new); +} + +/* secure open, based on relative path to root + * + * always a fixed fd for / see: rootfs() + * and fs_resolve_at() + */ +int +fs_open(const char *path, int flags) +{ + struct filesystem *fs; + + if (if_err(path == NULL, EFAULT) || + if_err(path[0] != '/', EINVAL) || + if_err_sys((fs = rootfs()) == NULL)) + return -1; + + return fs_resolve_at(fs->rootfd, path + 1, flags); +} + +/* singleton function that returns a fixed descriptor of / + * used throughout, for repeated integrity checks + */ +struct filesystem * +rootfs(void) +{ + static struct filesystem global_fs; + static int fs_initialised = 0; + + if (!fs_initialised) { + + global_fs.rootfd = -1; + + open_file_on_eintr("/", &global_fs.rootfd, + O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0400, NULL); + + if (global_fs.rootfd < 0) + return NULL; + + fs_initialised = 1; + } + + return &global_fs; +} + +/* filesystem sandboxing in userspace + * TODO: + missing length bound check. + potential CPU DoS on very long paths, spammed repeatedly. + perhaps cap at MAX_PATH? + */ +int +fs_resolve_at(int dirfd, const char *path, int flags) +{ + int nextfd = -1; + int curfd; + const char *p; + char name[PATH_MAX]; + int saved_errno = errno; + int r; + int is_last; + errno = 0; + + if (dirfd < 0 || path == NULL || *path == '\0') { + errno = EINVAL; + return -1; + } + + p = path; + curfd = dirfd; /* start here */ + + for (;;) { + r = fs_next_component(&p, name, sizeof(name)); + if (r < 0) + goto err; + if (r == 0) + break; + + is_last = (*p == '\0'); + + nextfd = fs_open_component(curfd, name, flags, is_last); + if (nextfd < 0) + goto err; + + /* close previous fd if not the original input */ + if (curfd != dirfd) + close_on_eintr(&curfd); + + curfd = nextfd; + nextfd = -1; + } + + reset_caller_errno(0); + return curfd; + +err: + saved_errno = errno; + + if (nextfd >= 0) + close_on_eintr(&nextfd); + + /* close curfd only if it's not the original */ + if (curfd != dirfd && curfd >= 0) + close_on_eintr(&curfd); + + errno = saved_errno; + return with_fallback_errno(EIO); +} + +/* NOTE: + rejects . and .. but not empty strings + after normalisation. edge case: + ////// + + normalised implicitly, but might be good + to add a defensive check regardless. code + probably not exploitable in current state. + */ +int +fs_next_component(const char **p, + char *name, size_t namesz) +{ + const char *s = *p; + size_t len = 0; + + while (*s == '/') + s++; + + if (*s == '\0') { + *p = s; + return 0; + } + + while (s[len] != '/' && s[len] != '\0') + len++; + + if (len == 0 || len >= namesz || + len >= PATH_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + memcpy(name, s, len); + name[len] = '\0'; + + /* reject . and .. */ + if (if_err((name[0] == '.' && name[1] == '\0') || + (name[0] == '.' && name[1] == '.' && name[2] == '\0'), EPERM)) + goto err; + + *p = s + len; + return 1; +err: + return with_fallback_errno(EPERM); +} + +int +fs_open_component(int dirfd, const char *name, + int flags, int is_last) +{ + int saved_errno = errno; + int fd; + struct stat st; + errno = 0; + + fd = openat_on_eintr(dirfd, name, + (is_last ? flags : (O_RDONLY | O_DIRECTORY)) | + O_NOFOLLOW | O_CLOEXEC, (flags & O_CREAT) ? 0600 : 0); + + if (!is_last && + (if_err(fd < 0, EBADF) || + if_err_sys(fstat(fd, &st) < 0) || + if_err(!S_ISDIR(st.st_mode), ENOTDIR))) + return with_fallback_errno(EIO); + + reset_caller_errno(fd); + return fd; +} + +int +fs_dirname_basename(const char *path, + char **dir, char **base, + int allow_relative) +{ + int saved_errno = errno; + char *buf = NULL; + char *slash; + size_t len; + errno = 0; + + if (if_err(path == NULL || dir == NULL || base == NULL, EFAULT)) + goto err; + + slen(path, PATH_MAX, &len); + memcpy(smalloc(&buf, len + 1), + path, len + 1); + + /* strip trailing slashes */ + while (len > 1 && buf[len - 1] == '/') + buf[--len] = '\0'; + + slash = strrchr(buf, '/'); + + if (slash) { + + *slash = '\0'; + *dir = buf; + *base = slash + 1; + + if (**dir == '\0') { + (*dir)[0] = '/'; + (*dir)[1] = '\0'; + } + } else if (allow_relative) { + + sdup(".", PATH_MAX, dir); + *base = buf; + } else { + free_and_set_null(&buf); + goto err; + } + + reset_caller_errno(0); + return 0; +err: + return with_fallback_errno(EINVAL); +} + +/* TODO: why does this abort, but others + e.g. open_file_on_eintr, don't??? + */ +void +open_file_on_eintr(const char *path, + int *fd, int flags, mode_t mode, + struct stat *st) +{ + int saved_errno = errno; + int rval = 0; + errno = 0; + + if (path == NULL) + err_exit(EINVAL, "open_file_on_eintr: null path"); + if (fd == NULL) + err_exit(EFAULT, "%s: open_file_on_eintr: null fd ptr", path); + if (*fd >= 0) + err_exit(EBADF, + "%s: open_file_on_eintr: file already open", path); + + errno = 0; + while (fs_retry(saved_errno, + rval = open(path, flags, mode))); + + if (rval < 0) + err_exit(errno, + "%s: open_file_on_eintr: could not close", path); + + reset_caller_errno(rval); + *fd = rval; + + /* we don't care about edge case behaviour here, + even if the next operation sets errno on success, + because the open() call is our main concern. + however, we also must preserve the new errno, + assuming it changed above under the same edge case */ + + saved_errno = errno; + + if (st != NULL) { + if (fstat(*fd, st) < 0) + err_exit(errno, "%s: stat", path); + + if (!S_ISREG(st->st_mode)) + err_exit(errno, "%s: not a regular file", path); + } + + if (lseek_on_eintr(*fd, 0, SEEK_CUR, 1, 1) == (off_t)-1) + err_exit(errno, "%s: file not seekable", path); + + errno = saved_errno; /* see previous comment */ +} + + +#ifdef __linux__ /* we use openat2 on linux */ +int +openat_on_eintr(int dirfd, const char *path, + int flags, mode_t mode) +{ + struct open_how how = { + .flags = flags, + .mode = mode, + .resolve = + RESOLVE_BENEATH | + RESOLVE_NO_SYMLINKS | + RESOLVE_NO_MAGICLINKS + }; + int saved_errno = errno; + long rval = 0; + errno = 0; + + if (if_err(dirfd < 0, EBADF) || + if_err(path == NULL, EFAULT)) + goto err; + + errno = 0; + while (sys_retry(saved_errno, + rval = syscall(SYS_openat2, dirfd, path, &how, sizeof(how)))); + + if (rval == -1) /* avoid long->int UB for -1 */ + goto err; + + reset_caller_errno(rval); + return (int)rval; +err: + return with_fallback_errno(EIO); /* -1 */ +} +#else /* regular openat on non-linux e.g. openbsd */ +int +openat_on_eintr(int dirfd, const char *path, + int flags, mode_t mode) +{ + int saved_errno = errno; + int rval = 0; + errno = 0; + + if (if_err(dirfd < 0, EBADF) || + if_err(path == NULL, EFAULT)) + return with_fallback_errno(EIO); + + while (fs_retry(saved_errno, + rval = openat(dirfd, path, flags, mode))); + + reset_caller_errno(rval); + return rval; +} +#endif + +off_t +lseek_on_eintr(int fd, off_t off, int whence, + int loop_eagain, int loop_eintr) +{ + int saved_errno = errno; + off_t rval = 0; + errno = 0; + + while (off_retry(saved_errno, + rval = lseek(fd, off, whence))); + + reset_caller_errno(rval); + return rval; +} + +int +mkdirat_on_eintr(int dirfd, + const char *path, mode_t mode) +{ + int saved_errno = errno; + int rval = 0; + errno = 0; + + if (if_err(dirfd < 0, EBADF) || + if_err(path == NULL, EFAULT)) + return with_fallback_errno(EIO); + + while (fs_retry(saved_errno, + rval = mkdirat(dirfd, path, mode))); + + reset_caller_errno(rval); + return rval; +} + +ssize_t +read_on_eintr(int fd, + void *buf, size_t count) +{ + int saved_errno = errno; + ssize_t rval = 0; + errno = 0; + + if (if_err(buf == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + if_err(count == 0, EINVAL)) + return with_fallback_errno(EIO); + + while (rw_retry(saved_errno, + rval = read(fd, buf, count))); + + reset_caller_errno(rval); + return rval; +} + +ssize_t +pread_on_eintr(int fd, + void *buf, size_t count, + off_t off) +{ + int saved_errno = errno; + ssize_t rval = 0; + errno = 0; + + if (if_err(buf == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + if_err(off < 0, EFAULT) || + if_err(count == 0, EINVAL)) + return with_fallback_errno(EIO); + + while (rw_retry(saved_errno, + rval = pread(fd, buf, count, off))); + + reset_caller_errno(rval); + return rval; +} + +ssize_t +write_on_eintr(int fd, + void *buf, size_t count) +{ + int saved_errno = errno; + ssize_t rval = 0; + errno = 0; + + if (if_err(buf == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + if_err(count == 0, EINVAL)) + return with_fallback_errno(EIO); + + while (rw_retry(saved_errno, + rval = write(fd, buf, count))); + + reset_caller_errno(rval); + return rval; +} + +ssize_t +pwrite_on_eintr(int fd, + void *buf, size_t count, + off_t off) +{ + int saved_errno = errno; + ssize_t rval = 0; + errno = 0; + + if (if_err(buf == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + if_err(off < 0, EFAULT) || + if_err(count == 0, EINVAL)) + return with_fallback_errno(EIO); + + while (rw_retry(saved_errno, + rval = pwrite(fd, buf, count, off))); + + reset_caller_errno(rval); + return rval; +} + +int +fsync_on_eintr(int fd) +{ + int saved_errno = errno; + int rval = 0; + errno = 0; + + if (if_err(fd < 0, EBADF)) + return with_fallback_errno(EIO); + + while (fs_retry(saved_errno, + rval = fsync(fd))); + + reset_caller_errno(rval); + return rval; +} + +void +close_on_eintr(int *fd) +{ + int saved_errno = errno; + int rval = 0; + + if (fd == NULL) + err_exit(EINVAL, "close_on_eintr: null pointer"); + if (*fd < 0) + return; + + errno = 0; + while (fs_retry(saved_errno, + rval = close(*fd))); + + if (rval < 0) + err_exit(errno, "close_on_eintr: could not close"); + + *fd = -1; + + reset_caller_errno(rval); +} + +/* unified eintr looping. + * differently typed functions + * to avoid potential UB + * + * ONE MACRO TO RULE THEM ALL: + */ +#define fs_err_retry() \ + if ((rval == -1) && \ + (errno == EINTR)) \ + return 1; \ + if (rval >= 0 && !errno) \ + errno = saved_errno; \ + return 0 +/* + * Regarding the errno logic above: + * on success, it is permitted that + * a syscall could still set errno. + * We reset errno after storingit + * for later preservation, in functions + * that call *_retry() functions. + * + * They rely ultimately on this + * macro for errno restoration. We + * assume therefore that errno was + * reset to zero before the retry + * loop. If errno is then *set* on + * success, we leave it alone. Otherwise, + * we restore the caller's saved errno. + * + * This offers some consistency, while + * complying with POSIX specification. + */ + + +/* retry switch for offset-based + * functions e.g. lseek + */ +/* retry switch for functions that + return long status e.g. linux syscall + */ +int +off_retry(int saved_errno, off_t rval) +{ + fs_err_retry(); +} + +/* retry switch for functions that + return long status e.g. linux syscall + */ +int +sys_retry(int saved_errno, long rval) +{ + fs_err_retry(); +} + +/* retry switch for functions that + return int status e.g. mkdirat + */ +int +fs_retry(int saved_errno, int rval) +{ + fs_err_retry(); +} + +/* retry switch for functions that + return rw count in ssize_t e.g. read() + */ +int +rw_retry(int saved_errno, ssize_t rval) +{ + fs_err_retry(); +} |
