mirror of
https://codeberg.org/libreboot/lbmk.git
synced 2026-03-25 13:29:03 +02:00
@@ -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 */
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user