mirror of
https://codeberg.org/libreboot/lbmk.git
synced 2026-03-25 13:29:03 +02:00
WIP: file system sandboxing
Signed-off-by: Leah Rowe <leah@libreboot.org>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user