WIP: file system sandboxing

Signed-off-by: Leah Rowe <leah@libreboot.org>
This commit is contained in:
Leah Rowe
2026-03-22 18:29:22 +00:00
parent 5818ec11bf
commit 535ab29deb
2 changed files with 161 additions and 158 deletions

View File

@@ -299,6 +299,10 @@ struct xstate {
int cat;
};
struct filesystem {
int rootfd;
};
struct xstate *xstart(int argc, char *argv[]);
struct xstate *xstatus(void);
@@ -489,10 +493,13 @@ int secure_file(int *fd, struct stat *st,
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_mkdir_p_at(int dirfd, const char *path, mode_t mode,
struct stat *st_dir_initial);
int fs_rename_at(int olddirfd, const char *old,
int newdirfd, const char *new);
int fs_rm_rf_at(int dirfd, const char *path);
int fs_mkdir_p(const char *path, mode_t mode);
int fs_mkdir_p_at(int dirfd, const char *path, mode_t mode);
int fs_open(const char *path, int flags);
struct filesystem *rootfs(void);
int fs_resolve_at(int dirfd, const char *path, int flags);
int fs_next_component(const char **p,
char *name, size_t namesz);

View File

@@ -164,14 +164,15 @@ fsync_dir(const char *path)
dirbuf[1] = '\0';
}
dirfd = fs_resolve(dirbuf, O_RDONLY | O_CLOEXEC | O_NOCTTY
dirfd = fs_open(dirbuf,
O_RDONLY | O_CLOEXEC | O_NOCTTY
#ifdef O_DIRECTORY
| O_DIRECTORY
#endif
#ifdef O_NOFOLLOW
| O_NOFOLLOW
#endif
);
);
if (dirfd < 0)
goto err_fsync_dir;
@@ -404,7 +405,8 @@ new_tmpfile(int *fd, int local,
/* ALWAYS set this right after
* split path, to avoid leaking fd:
*/
dirfd = fs_resolve(dir, O_RDONLY | O_DIRECTORY);
dirfd = fs_open(dir, O_RDONLY | O_DIRECTORY);
if (dirfd < 0)
goto err_new_tmpfile;
@@ -426,7 +428,7 @@ new_tmpfile(int *fd, int local,
memcpy(dest + dirlen + 1, suffix,
sizeof(suffix) - 1);
dirfd = fs_resolve(tmpdir,
dirfd = fs_open(tmpdir,
O_RDONLY | O_DIRECTORY);
if (dirfd < 0)
goto err_new_tmpfile;
@@ -614,11 +616,11 @@ same_dir(const char *a, const char *b)
if (rval_scmp == 0)
goto success_same_dir;
fd_a = fs_resolve(a, O_RDONLY | O_DIRECTORY);
fd_a = fs_open(a, O_RDONLY | O_DIRECTORY);
if (fd_a < 0)
goto err_same_dir;
fd_b = fs_resolve(b, O_RDONLY | O_DIRECTORY);
fd_b = fs_open(b, O_RDONLY | O_DIRECTORY);
if (fd_b < 0)
goto err_same_dir;
@@ -694,7 +696,7 @@ world_writeable_and_sticky(
/* mitigate symlink attacks*
*/
dirfd = fs_resolve(s, O_RDONLY | O_DIRECTORY);
dirfd = fs_open(s, O_RDONLY | O_DIRECTORY);
if (dirfd < 0)
goto sticky_hell;
@@ -1854,128 +1856,98 @@ 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)
fs_rename_at(int olddirfd, const char *old,
int newdirfd, const char *new)
{
#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;
if (new == NULL || old == NULL) {
const char *p;
char name[256];
errno = EFAULT;
return -1;
}
int saved_errno = errno;
int close_errno;
int r;
if (olddirfd < 0 || newdirfd < 0) {
int is_last;
errno = EBADF;
return -1;
}
if (path == NULL || *path != '/') {
return renameat(olddirfd, old, newdirfd, new);
}
int
fs_rm_rf_at(int dirfd, const char *path)
{
int fd = -1;
struct stat st;
if (path == NULL) {
errno = EFAULT;
return -1;
}
fd = fs_resolve_at(dirfd, path, O_RDONLY | O_DIRECTORY);
if (fd >= 0) {
/* directory */
/* iterate entries */
/* recurse */
/* unlinkat(dirfd, path, AT_REMOVEDIR) */
return 0;
}
/* fallback: file */
return unlinkat(dirfd, path, 0);
}
int
fs_mkdir_p(const char *path, mode_t mode)
{
struct filesystem *fs;
if (path == NULL) {
errno = EFAULT;
return -1;
}
if (path[0] != '/') {
errno = EINVAL;
return -1;
}
/* block "/" if flags pertain to creation/write.
don't mess with the user's root!
*/
if (path[0] == '/' && path[1] == '\0') {
if (flags & (O_CREAT | O_TRUNC | O_RDWR | O_WRONLY)) {
errno = EISDIR;
return -1;
}
}
if (slen(path, maxlen, &len) < 0)
fs = rootfs();
if (fs == NULL)
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_errno = errno;
(void) close_on_eintr(dirfd);
errno = close_errno;
dirfd = nextfd;
nextfd = -1;
}
errno = saved_errno;
return dirfd;
err:
saved_errno = errno;
if (dirfd >= 0)
(void) close_on_eintr(dirfd);
if (nextfd >= 0)
(void) close_on_eintr(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);
return fs_mkdir_p_at(fs->rootfd, path + 1, mode);
}
/* implementation of: mkdir -p
*/
int
fs_mkdir_p_at(int dirfd, const char *path, mode_t mode,
struct stat *st_dir_initial)
fs_mkdir_p_at(int dirfd, const char *path, mode_t mode)
{
const char *p;
const char *p = path;
char name[256];
int nextfd = -1;
struct stat st_parent_initial;
struct stat st_parent_now;
int saved_errno = errno;
int close_errno;
int r;
int is_last;
struct stat st_dir_now;
if (dirfd < 0 || path == NULL ||
st_dir_initial == NULL || *path == '\0') {
if (path == NULL) {
errno = EFAULT;
return -1;
}
if (dirfd < 0 || *path == '\0') {
errno = EINVAL;
return -1;
}
p = path;
if (fstat(dirfd, &st_parent_initial) < 0)
return -1;
for (;;) {
@@ -1985,88 +1957,112 @@ fs_mkdir_p_at(int dirfd, const char *path, mode_t mode,
if (r == 0)
break;
is_last = (*p == '\0');
/* check parent integrity */
if (fstat(dirfd, &st_parent_now) < 0)
goto err;
if (st_parent_now.st_dev != st_parent_initial.st_dev ||
st_parent_now.st_ino != st_parent_initial.st_ino) {
errno = ESTALE;
goto err;
}
/* TODO: consider more flags
* or make configurable
*/
nextfd = openat2p(dirfd, name,
O_RDONLY | O_DIRECTORY | O_CLOEXEC, mode);
O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0);
if (nextfd < 0) {
if (errno != ENOENT)
goto err;
if (mkdirat(dirfd, name, mode) < 0)
goto err;
/*
* use the file descriptor instead.
* (danach prüfen wir die Sicherheit des Verzeichnisses)
*/
if (fstat(dirfd, &st_dir_now) < 0)
goto err;
/*
* mitigate symlink attacks before open
*/
if (st_dir_now.st_dev != st_dir_initial->st_dev ||
st_dir_now.st_ino != st_dir_initial->st_ino) {
errno = ESTALE;
goto err;
}
/* TODO: consider more flags
* or make configurable?
*/
nextfd = openat2p(dirfd, name,
O_RDONLY | O_DIRECTORY | O_CLOEXEC,
mode);
O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0);
if (nextfd < 0)
goto err;
}
close_errno = errno;
(void) close_on_eintr(dirfd);
errno = close_errno;
(void)close_on_eintr(dirfd);
dirfd = nextfd;
nextfd = -1;
st_parent_initial = st_parent_now;
}
errno = saved_errno;
return 0;
return (0);
err:
saved_errno = errno;
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);
errno = saved_errno;
return -1;
return (-1);
}
/* use in conjunction with fs_resolve. example:
* int rootfd = fs_resolve("/safe/root", O_RDONLY | O_DIRECTORY);
* now, you can resolve everything relatively, for instance:
* fd = fs_resolve_at(rootfd, "file.txt", O_RDONLY);
* if a user then tries e.g. ../etc/password ??????
* BLOCKED
* basically userspace sandboxing, similar to e.g.
* openbsd unveil
* freebsd capsicum
* openat2 RESOLVE_BENEATH
* ...but in ****userspace****
* no need for chroot (you should still use one in critical code)
* cannot escape the relative root that you set.
* no dependence on CWD
* probably safe in multi-threaded code (with some care)
/* secure open, based on
* relative path to root
*
* ps: you should still use unveil. unveil is awesome.
* always a fixed fd for /
* see: rootfs()
*/
int
fs_open(const char *path, int flags)
{
struct filesystem *fs;
const char *rel;
if (path == NULL) {
errno = EFAULT;
return -1;
}
if (path[0] != '/') {
errno = EINVAL;
return -1;
}
fs = rootfs();
if (!fs)
return -1;
rel = path + 1;
return fs_resolve_at(fs->rootfd, rel, 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 =
open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (global_fs.rootfd < 0)
return NULL;
fs_initialised = 1;
}
return &global_fs;
}
/* filesystem sandboxing.
* (in userspace)
*/
int
fs_resolve_at(int dirfd, const char *path, int flags)