WIP: pathless resolution

Signed-off-by: Leah Rowe <leah@libreboot.org>
This commit is contained in:
Leah Rowe
2026-03-22 14:36:33 +00:00
parent 6838db4647
commit 825d520575
2 changed files with 263 additions and 241 deletions

View File

@@ -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 */

View File

@@ -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;
}