summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeah Rowe <leah@libreboot.org>2026-03-23 00:27:05 +0000
committerLeah Rowe <leah@libreboot.org>2026-03-23 04:43:47 +0000
commitc87e425442f17a70651e05d1deab2a9e50f7625e (patch)
treea44e55ba0f37e4db1074bafd0f45cc56ebf9012d
parent1684f475cda9219db7a593b6fa5345c7185117f3 (diff)
WIP dir support (also demons)
Signed-off-by: Leah Rowe <leah@libreboot.org>
-rw-r--r--util/nvmutil/include/common.h14
-rw-r--r--util/nvmutil/lib/file.c361
2 files changed, 294 insertions, 81 deletions
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;
}