diff options
Diffstat (limited to 'util')
| -rw-r--r-- | util/nvmutil/include/common.h | 18 | ||||
| -rw-r--r-- | util/nvmutil/lib/file.c | 486 |
2 files changed, 263 insertions, 241 deletions
diff --git a/util/nvmutil/include/common.h b/util/nvmutil/include/common.h index 6cdeba18..cec06b25 100644 --- a/util/nvmutil/include/common.h +++ b/util/nvmutil/include/common.h @@ -306,12 +306,6 @@ struct xstate { int cat; }; -struct path_split { - int dirfd; - char *buf; - const char *base; -}; - struct xstate *xstart(int argc, char *argv[]); struct xstate *xstatus(void); @@ -505,15 +499,19 @@ 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); -int split_path(const char *path, - struct path_split *ps); -int open_verified_dir(const char *path); -int check_dirfd(int dirfd, const char *path); int secure_file(int *fd, struct stat *st, 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_resolve(const char *path, int flags); +int fs_root_fd(void); +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); /* asserts */ diff --git a/util/nvmutil/lib/file.c b/util/nvmutil/lib/file.c index e722cb6a..ec45ac3f 100644 --- a/util/nvmutil/lib/file.c +++ b/util/nvmutil/lib/file.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: MIT * Copyright (c) 2026 Leah Rowe <leah@libreboot.org> + * + * Pathless i/o */ #include <sys/types.h> @@ -14,6 +16,7 @@ #include "../include/common.h" + /* check that a file changed */ @@ -78,10 +81,10 @@ err_same_file: void xopen(int *fd_ptr, const char *path, int flags, struct stat *st) { - if ((*fd_ptr = open(path, flags)) == -1) + if ((*fd_ptr = open(path, flags)) < 0) err(errno, "%s", path); - if (fstat(*fd_ptr, st) == -1) + if (fstat(*fd_ptr, st) < 0) err(errno, "%s: stat", path); if (!S_ISREG(st->st_mode)) @@ -156,7 +159,7 @@ fsync_dir(const char *path) dirbuf[1] = '\0'; } - dirfd = open(dirbuf, O_RDONLY | O_CLOEXEC | O_NOCTTY + dirfd = fs_resolve(dirbuf, O_RDONLY | O_CLOEXEC | O_NOCTTY #ifdef O_DIRECTORY | O_DIRECTORY #endif @@ -167,17 +170,6 @@ fsync_dir(const char *path) if (dirfd < 0) goto err_fsync_dir; - /* symlink/directory replacement - attack mitigation - */ - if (check_dirfd(dirfd, dirbuf) < 0) { - - (void) close_on_eintr(dirfd); - - dirfd = -1; - goto err_fsync_dir; - } - if (fstat(dirfd, &st) < 0) goto err_fsync_dir; @@ -284,7 +276,8 @@ new_tmpfile(int *fd, int local, struct stat st_dir_initial; #endif - struct path_split ps; + char *dir = NULL; + char *base = NULL; if (fd == NULL) { @@ -402,20 +395,22 @@ new_tmpfile(int *fd, int local, if (local) { - if (split_path(path, &ps) < 0) + if (fs_dirname_basename(path, &dir, &base, 0) < 0) goto err_new_tmpfile; - if (slen(ps.base, maxlen, &baselen) < 0) + if (slen(base, maxlen, &baselen) < 0) goto err_new_tmpfile; /* ALWAYS set this right after * split path, to avoid leaking fd: */ - dirfd = ps.dirfd; + dirfd = fs_resolve(dir, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) + goto err_new_tmpfile; *(dest) = '.'; - memcpy(dest + 1, ps.base, baselen); + memcpy(dest + 1, base, baselen); memcpy(dest + 1 + baselen, suffix, sizeof(suffix) - 1); @@ -424,14 +419,9 @@ new_tmpfile(int *fd, int local, ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation * in mkhtemp( */ - fname = dest + 1; + fname = base; #endif - if (ps.buf != NULL) { - free(ps.buf); - ps.buf = NULL; - } - } else { memcpy(dest, tmpdir, dirlen); @@ -445,7 +435,8 @@ new_tmpfile(int *fd, int local, ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation in mkhtemp() */ - dirfd = open_verified_dir(tmpdir); + dirfd = fs_resolve(tmpdir, + O_RDONLY | O_DIRECTORY); if (dirfd < 0) goto err_new_tmpfile; @@ -639,11 +630,11 @@ same_dir(const char *a, const char *b) if (rval_scmp == 0) goto success_same_dir; - fd_a = open_verified_dir(a); + fd_a = fs_resolve(a, O_RDONLY | O_DIRECTORY); if (fd_a < 0) goto err_same_dir; - fd_b = open_verified_dir(b); + fd_b = fs_resolve(b, O_RDONLY | O_DIRECTORY); if (fd_b < 0) goto err_same_dir; @@ -719,7 +710,7 @@ world_writeable_and_sticky( /* mitigate symlink attacks* */ - dirfd = open_verified_dir(s); + dirfd = fs_resolve(s, O_RDONLY | O_DIRECTORY); if (dirfd < 0) goto sticky_hell; @@ -773,7 +764,7 @@ world_writeable_and_sticky( if (allow_noworld_unsticky) goto sticky_heaven; /* sticky! */ - goto sticky_hell; /* definitely not sticky */ + goto sticky_hell; /* heaven visa denied */ sticky_heaven: /* i like the one in hamburg better */ @@ -1212,207 +1203,6 @@ err_mkhtemp: return -1; } -/* split up a given - * path into directory - * and file name. used - * for e.g. openat - */ -int -split_path(const char *path, - struct path_split *ps) -{ - size_t maxlen; - size_t len; - - char *slash; - - int saved_errno = errno; - - if (path == NULL || ps == NULL) - goto err_split_path; - -#if defined(PATH_LEN) && \ -(PATH_LEN) >= 256 - maxlen = PATH_LEN; -#else - maxlen = 4096; -#endif - - if (slen(path, maxlen, &len) < 0) - goto err_split_path; - - if (len == 0 || len >= maxlen) { - - errno = ERANGE; - goto err_split_path; - } - - ps->buf = malloc(len + 1); - if (ps->buf == NULL) { - - errno = ENOMEM; - goto err_split_path; - } - - memcpy(ps->buf, path, len + 1); - - for ( ; len > 1 && - ps->buf[len - 1] == '/'; len--) - ps->buf[len - 1] = '\0'; - - slash = strrchr(ps->buf, '/'); - - if (slash) { - - *slash = '\0'; - ps->base = slash + 1; - - if (*ps->buf == '\0') { - - ps->buf[0] = '/'; - ps->buf[1] = '\0'; - } - } else { - - ps->base = ps->buf; - - ps->buf[0] = '.'; - ps->buf[1] = '\0'; - } - - ps->dirfd = open_verified_dir(ps->buf); - if (ps->dirfd < 0) - goto err_split_path; - - errno = saved_errno; - - return 0; - -err_split_path: - - saved_errno = errno; - - if (ps->buf != NULL) { - free(ps->buf); - ps->buf = NULL; - } - - if (ps->dirfd >= 0) { - (void) close_on_eintr(ps->dirfd); - ps->dirfd = -1; - } - - errno = saved_errno; - - if (errno == saved_errno) - errno = EIO; /* likely open/check_dirfd */ - - return -1; -} - -/* when we open a directory, - * we need to secure it each - * time against replacement - * attacks (e.g. symlinks) - */ -int -open_verified_dir(const char *path) -{ - int fd; - int saved_errno = errno; - - fd = open(path, O_RDONLY | O_DIRECTORY | - O_CLOEXEC | O_NOCTTY -#ifdef O_NOFOLLOW - | O_NOFOLLOW -#endif - ); - - if (fd < 0) - return -1; - - errno = saved_errno; - - if (check_dirfd(fd, path) < 0) { - - saved_errno = errno; - (void) close_on_eintr(fd); - - errno = saved_errno; - return -1; - } - - errno = saved_errno; - return fd; -} - -/* useful for mitigating directory - * replacement attacks; call this - * before e.g. stat(), right after - * calling open/openat(). compares - * the inode/device of a given path - * relative to the file descriptor, - * which if changed would indicate - * a possible attack / race condition - */ -int -check_dirfd(int dirfd, const char *path) -{ - struct stat st_fd; - struct stat st_path; - - int saved_errno = errno; - - if (dirfd < 0) { - errno = EBADF; - goto err_check_dirfd; - } - - if (path == NULL) { - errno = EFAULT; - goto err_check_dirfd; - } - - if (fstat(dirfd, &st_fd) < 0) - goto err_check_dirfd; - -#if !(defined(DISABLE_OPENAT) && \ - ((DISABLE_OPENAT) > 0)) - /* - * mitigate symlink / directory replacement - * attacks (fstatat added to linux in 2006, - * and the BSDs added it later on) - * - * on older/weird unix, you'll see stat(2), - * and would therefore be vulnerable. - */ - if (fstatat(AT_FDCWD, path, &st_path, - AT_SYMLINK_NOFOLLOW) != 0) - goto err_check_dirfd; -#else - if (stat(path, &st_path) != 0) - goto err_check_dirfd; -#endif - - if (st_fd.st_dev != st_path.st_dev || - st_fd.st_ino != st_path.st_ino) { - - errno = ENOENT; - goto err_check_dirfd; - } - - errno = saved_errno; - - return 0; - -err_check_dirfd: - - if (errno == saved_errno) - errno = EPERM; /* context: symlink attack */ - - return -1; -} - /* why doesn't literally every libc have this? @@ -2121,3 +1911,237 @@ fsync_on_eintr(int fd) return r; } + +/* pathless resolution. we + * walk directories ourselves; + * will also be used for a portable + * fallback against openat2 if unavailable + * + * only accepts global paths, from / because + * we don't want relative tmpdirs! + */ +int +fs_resolve(const char *path, int flags) +{ +#if defined(PATH_LEN) && \ +(PATH_LEN) >= 256 + size_t maxlen = PATH_LEN; +#else + size_t maxlen = 4096; +#endif + size_t len; + int dirfd = -1; + int nextfd = -1; + + const char *p; + char name[256]; + + int saved_errno = errno; + int r; + + int is_last; + + if (path == NULL || *path != '/') { + errno = EINVAL; + return -1; + } + + if (slen(path, maxlen, &len) < 0) + return -1; + + dirfd = fs_root_fd(); + if (dirfd < 0) + return -1; + + p = path; + + 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(dirfd, + name, flags, is_last); + if (nextfd < 0) + goto err; + + close(dirfd); + dirfd = nextfd; + nextfd = -1; + } + + errno = saved_errno; + return dirfd; + +err: + saved_errno = errno; + + if (dirfd >= 0) + close(dirfd); + if (nextfd >= 0) + close(nextfd); + + errno = saved_errno; + return -1; +} + +int +fs_root_fd(void) +{ + /* TODO: consider more flags here (and/or make configurable */ + return open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); +} + +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; + +/* TODO: + open() fallback if DISABLE_OPENAT > 0 + change function signature + and ditto any callers using + the same ifdefs. this would + allow us to implement somewhat + openat-like functionality with + openat2-like features, even on + systems that lack openat(2), + let alone openat2 + */ + + fd = openat(dirfd, name, + (is_last ? flags : (O_RDONLY | O_DIRECTORY)) | + O_NOFOLLOW | O_CLOEXEC); + + /* on some systems, O_DIRECTORY is + * ignored or unreliable. We must + * assume that your operating system + * is lying. the OS always lies. + */ + if (!is_last) { + + struct stat st; + + if (fstat(fd, &st) < 0) + return -1; + + if (!S_ISDIR(st.st_mode)) { + + close(fd); + errno = ENOTDIR; + return -1; + } + } + + return fd; +} + +int +fs_dirname_basename(const char *path, + char **dir, char **base, + int allow_relative) +{ +/* TODO: audit maxlen use vs PATH_LEN + -- should implement length checks + */ + 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) + return -1; + + if (slen(path, maxlen, &len) < 0) + return -1; + + buf = malloc(len + 1); + if (buf == 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(buf); + return -1; + } + + return 0; +} |
