From c87e425442f17a70651e05d1deab2a9e50f7625e Mon Sep 17 00:00:00 2001 From: Leah Rowe Date: Mon, 23 Mar 2026 00:27:05 +0000 Subject: WIP dir support (also demons) Signed-off-by: Leah Rowe --- util/nvmutil/include/common.h | 14 +- util/nvmutil/lib/file.c | 361 +++++++++++++++++++++++++++++++++--------- 2 files changed, 294 insertions(+), 81 deletions(-) (limited to 'util/nvmutil') diff --git a/util/nvmutil/include/common.h b/util/nvmutil/include/common.h index 0fedeedd..67969832 100644 --- a/util/nvmutil/include/common.h +++ b/util/nvmutil/include/common.h @@ -36,6 +36,13 @@ 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 @@ -493,10 +500,11 @@ static int mkhtemp_try_create(int dirfd, char *p, size_t xc, int *fd, - struct stat *st); + struct stat *st, + int type); int mkhtemp(int *fd, struct stat *st, char *template, int dirfd, const char *fname, - struct stat *st_dir_initial); + 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); @@ -529,6 +537,8 @@ 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); /* asserts */ diff --git a/util/nvmutil/lib/file.c b/util/nvmutil/lib/file.c index f0cb5ad4..a490b358 100644 --- a/util/nvmutil/lib/file.c +++ b/util/nvmutil/lib/file.c @@ -30,9 +30,6 @@ /* check that a file changed */ -#define MKHTEMP_RETRY_MAX 512 -#define MKHTEMP_SPIN_THRESHOLD 32 - int same_file(int fd, struct stat *st_old, int check_size) @@ -109,7 +106,7 @@ xopen(int *fd_ptr, const char *path, int flags, struct stat *st) if (!S_ISREG(st->st_mode)) err(errno, "%s: not a regular file", path); - if (lseek(*fd_ptr, 0, SEEK_CUR) == (off_t)-1) + if (lseek_on_eintr(*fd_ptr, 0, SEEK_CUR, 1, 1) == (off_t)-1) err(errno, "%s: file not seekable", path); } @@ -131,6 +128,8 @@ fsync_dir(const char *path) char *slash = NULL; struct stat st = {0}; + int close_errno; + #if defined(PATH_LEN) && \ (PATH_LEN) >= 256 maxlen = PATH_LEN; @@ -233,7 +232,9 @@ err_fsync_dir: if (dirfd >= 0) { + close_errno = errno; (void) close_on_eintr(dirfd); + errno = close_errno; dirfd = -1; } @@ -263,6 +264,7 @@ new_tmpfile(int *fd, char **path) "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; char *tmpdir = NULL; + int close_errno; size_t dirlen; size_t destlen; char *dest = NULL; /* final path */ @@ -275,15 +277,26 @@ new_tmpfile(int *fd, char **path) if (path == NULL || fd == NULL) { errno = EFAULT; goto err_new_tmpfile; - } else if (*path == NULL) { - errno = EFAULT; - goto err_new_tmpfile; } - if (*fd >= 0) { /* file already opend */ + /* don't mess with someone's file + * if already opened + */ + if (*fd >= 0) { errno = EEXIST; goto err_new_tmpfile; } + + /* 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 defined(PERMIT_NON_STICKY_ALWAYS) && \ @@ -335,12 +348,15 @@ new_tmpfile(int *fd, char **path) if (fstat(dirfd, &st_dir_initial) < 0) goto err_new_tmpfile; - *fd = mkhtemp(fd, &st, dest, dirfd, fname, &st_dir_initial); + *fd = mkhtemp(fd, &st, dest, dirfd, + fname, &st_dir_initial, MKHTEMP_FILE); if (*fd < 0) goto err_new_tmpfile; if (dirfd >= 0) { + close_errno = errno; (void) close_on_eintr(dirfd); + errno = close_errno; dirfd = -1; } @@ -363,14 +379,16 @@ err_new_tmpfile: } if (dirfd >= 0) { - + close_errno = errno; (void) close_on_eintr(dirfd); + errno = close_errno; dirfd = -1; } if (*fd >= 0) { - + close_errno = errno; (void) close_on_eintr(*fd); + errno = close_errno; *fd = -1; } @@ -661,18 +679,53 @@ sticky_hell: /* mk(h)temp - hardened mktemp. * like mkstemp, but (MUCH) harder. - * TODO: - * directory support (currently only - * generates files) + * + * designed to resist TOCTOU attacsk + * e.g. directory race / symlink attack + * + * extremely strict and even implements + * some limited userspace-level sandboxing, + * similar to openbsd unveil (which you + * can also use with this in your program) + * + * 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) + struct stat *st_dir_initial, + int type) { size_t len = 0; size_t xc = 0; @@ -683,6 +736,7 @@ mkhtemp(int *fd, size_t retries; + int close_errno; int saved_errno = errno; #if defined(PATH_LEN) && \ @@ -703,8 +757,11 @@ mkhtemp(int *fd, return -1; } + /* we do not mess with an + open descriptor. + */ if (*fd >= 0) { - errno = EEXIST; + errno = EEXIST; /* leave their file alone */ return -1; } @@ -778,13 +835,20 @@ mkhtemp(int *fd, p, xc, fd, - st); + st, + type); - if (r != 1) { - if (retries >= MKHTEMP_SPIN_THRESHOLD) + 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, @@ -798,7 +862,9 @@ mkhtemp(int *fd, err: if (*fd >= 0) { + close_errno = errno; (void)close_on_eintr(*fd); + errno = close_errno; *fd = -1; } @@ -817,34 +883,75 @@ mkhtemp_try_create(int dirfd, char *p, size_t xc, int *fd, - struct stat *st) + struct stat *st, + int type) { - struct stat st_dir_now; struct stat st_open; int saved_errno = errno; int close_errno; int rval = -1; - if (mkhtemp_fill_random(p, xc) < 0) + int file_created = 0; + int dir_created = 0; + + if (fd == NULL || st == NULL || p == NULL || fname_copy == NULL || + st_dir_initial == NULL) { + errno = EFAULT; + goto err; + } else if (*fd >= 0) { /* do not mess with someone else's file */ + errno = EEXIST; goto err; + } - if (fstat(dirfd, &st_dir_now) < 0) + if (mkhtemp_fill_random(p, xc) < 0) goto err; if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0) goto err; - *fd = openat2p(dirfd, fname_copy, - O_RDWR | O_CREAT | O_EXCL | - O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, - 0600); + if (type == MKHTEMP_FILE) { + *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; + + *fd = openat2p(dirfd, fname_copy, + O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0); + if (*fd < 0) + goto err; + + 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 || - errno == EINTR || - errno == EAGAIN || - errno == EWOULDBLOCK || - errno == ETXTBSY) { + if (errno == EEXIST) { rval = 0; goto out; @@ -852,17 +959,37 @@ mkhtemp_try_create(int dirfd, goto err; } - /* fd is now live */ - if (fstat(*fd, &st_open) < 0) goto err; - if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0) - goto err; + if (type == MKHTEMP_FILE) { - if (secure_file(fd, st, &st_open, - O_APPEND, 1, 1, 0600) < 0) - goto err; + 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 (!S_ISDIR(st_open.st_mode)) { + errno = ENOTDIR; + goto err; + } + + if (is_owner(&st_open) < 0) + goto err; + + /* group/world writeable */ + if (st_open.st_mode & (S_IWGRP | S_IWOTH)) { + errno = EPERM; + goto err; + } + } errno = saved_errno; rval = 1; @@ -876,6 +1003,12 @@ err: *fd = -1; } + if (file_created) + (void) unlinkat(dirfd, fname_copy, 0); + + if (dir_created) + (void) unlinkat(dirfd, fname_copy, AT_REMOVEDIR); + errno = close_errno; rval = -1; out: @@ -947,6 +1080,11 @@ err_mkhtemp_fill_random: return -1; } +/* WARNING: **ONCE** per file. + * + * !!! DO NOT RUN TWICE PER FILE. BEWARE OF THE DEMON !!! + * watch out for spikes! + */ int secure_file(int *fd, struct stat *st, struct stat *expected, @@ -958,25 +1096,25 @@ int secure_file(int *fd, int flags; struct stat st_now; int saved_errno = errno; - + /* you have been warned */ if (fd == NULL) { errno = EFAULT; - goto err_secure_file; + goto err_demons; } if (*fd < 0) { errno = EBADF; - goto err_secure_file; + goto err_demons; } if (st == NULL) { errno = EFAULT; - goto err_secure_file; + goto err_demons; } flags = fcntl(*fd, F_GETFL); if (flags == -1) - goto err_secure_file; + goto err_demons; if (bad_flags > 0) { @@ -984,49 +1122,49 @@ int secure_file(int *fd, * by allowing offsets to be ignored */ if (flags & bad_flags) { errno = EPERM; - goto err_secure_file; + goto err_demons; } } - if (fstat(*fd, &st_now) == -1) - goto err_secure_file; - if (expected != NULL) { - if (fd_verify_identity(*fd, expected, &st_now) < 0) - goto err_secure_file; - } + if (fd_verify_regular(*fd, expected, st) < 0) + goto err_demons; + } else { + if (fstat(*fd, &st_now) == -1) + goto err_demons; - *st = st_now; + if (!S_ISREG(st_now.st_mode)) { + errno = EBADF; + goto err_demons; + } - if (!S_ISREG(st->st_mode)) { - errno = EBADF; - goto err_secure_file; + *st = st_now; } - if (fd_verify_regular(*fd, expected, st) < 0) - goto err_secure_file; - if (check_seek) { /* check if it's seekable */ if (lseek(*fd, 0, SEEK_CUR) == (off_t)-1) - goto err_secure_file; + goto err_demons; } - /* tmpfile re/un linked while open */ - if (st->st_nlink != 1) { - errno = ELOOP; - goto err_secure_file; + /* don't release the demon + */ + if (st->st_nlink != 1) { /***********/ + /* ( >:3 ) */ + errno = ELOOP; /* /| |\ */ /* don't let him out */ + goto err_demons; /* / \ */ + /***********/ } if (st->st_uid != geteuid() && /* someone else's file */ geteuid() != 0) { /* override for root */ errno = EPERM; - goto err_secure_file; + goto err_demons; } if (is_owner(st) < 0) - goto err_secure_file; + goto err_demons; /* world-writeable or group-ownership. * if these are set, then others could @@ -1034,26 +1172,26 @@ int secure_file(int *fd, */ if (st->st_mode & (S_IWGRP | S_IWOTH)) { errno = EPERM; - goto err_secure_file; + goto err_demons; } if (do_lock) { if (lock_file(*fd, flags) == -1) - goto err_secure_file; + goto err_demons; if (expected != NULL) { if (fd_verify_identity(*fd, expected, &st_now) < 0) - goto err_secure_file; + goto err_demons; } } if (fchmod(*fd, mode) == -1) - goto err_secure_file; + goto err_demons; errno = saved_errno; return 0; -err_secure_file: +err_demons: if (errno == saved_errno) errno = EIO; @@ -1674,7 +1812,9 @@ lseek_on_eintr(int fd, off_t off, int whence, old = lseek(fd, off, whence); } while (old == (off_t)-1 && ( errno == try_err(loop_eintr, EINTR) || - errno == try_err(loop_eagain, EAGAIN))); + errno == try_err(loop_eintr, ETXTBSY) || + errno == try_err(loop_eagain, EAGAIN) || + errno == try_err(loop_eagain, EWOULDBLOCK))); return old; } @@ -1697,7 +1837,9 @@ close_on_eintr(int fd) do { r = close(fd); - } while (r == -1 && (errno == EINTR || errno == EAGAIN)); + } while (r == -1 && ( + errno == EINTR || errno == EAGAIN || + errno == EWOULDBLOCK || errno == ETXTBSY)); if (r >= 0) errno = saved_errno; @@ -1713,7 +1855,8 @@ fsync_on_eintr(int fd) do { r = fsync(fd); - } while (r == -1 && (errno == EINTR || errno == EAGAIN)); + } while (r == -1 && (errno == EINTR || errno == EAGAIN || + errno == ETXTBSY || errno == EWOULDBLOCK)); if (r >= 0) errno = saved_errno; @@ -1797,6 +1940,8 @@ fs_mkdir_p_at(int dirfd, const char *path, mode_t mode) struct stat st_parent_initial; struct stat st_parent_now; int saved_errno = errno; + int close_errno; + int dir_created = 0; int r; if (path == NULL) { @@ -1834,16 +1979,20 @@ fs_mkdir_p_at(int dirfd, const char *path, mode_t mode) if (errno != ENOENT) goto err; - if (mkdirat(dirfd, name, mode) < 0) + if (mkdirat_on_eintr(dirfd, name, mode) < 0) goto err; + dir_created = 1; + nextfd = openat2p(dirfd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0); if (nextfd < 0) goto err; } + close_errno = errno; (void)close_on_eintr(dirfd); + errno = close_errno; dirfd = nextfd; nextfd = -1; @@ -1855,9 +2004,11 @@ fs_mkdir_p_at(int dirfd, const char *path, mode_t mode) err: if (dirfd >= 0) - (void)close_on_eintr(dirfd); + (void) close_on_eintr(dirfd); if (nextfd >= 0) - (void)close_on_eintr(nextfd); + (void) close_on_eintr(nextfd); + if (dir_created) + (void) unlinkat(dirfd, name, AT_REMOVEDIR); errno = saved_errno; return (-1); @@ -2122,6 +2273,7 @@ fs_dirname_basename(const char *path, * with fallback for others e.g. openbsd * * BONUS: arg checks + * TODO: consider EINTR/EAGAIN retry loop */ int openat2p(int dirfd, const char *path, @@ -2137,6 +2289,8 @@ openat2p(int dirfd, const char *path, RESOLVE_NO_MAGICLINKS | RESOLVE_NO_XDEV }; + int saved_errno = errno; + int rval; #endif if (dirfd < 0) { @@ -2149,15 +2303,64 @@ openat2p(int dirfd, const char *path, return -1; } +retry: + errno = 0; + #ifdef __linux__ /* more secure than regular openat, * but linux-only at the time of writing */ - return syscall(SYS_openat2, dirfd, path, &how, sizeof(how)); + rval = syscall(SYS_openat2, dirfd, path, &how, sizeof(how)); #else /* less secure, but e.g. openbsd * doesn't have openat2 yet */ - return openat(dirfd, path, flags, mode); + 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 (dirfd < 0) { + errno = EBADF; + return -1; + } + + if (path == NULL) { + errno = 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; } -- cgit v1.2.1