Files
lbmk/util/libreboot-utils/lib/file.c
T
Leah Rowe da20b75bea libreboot-utils: more flexible string usage
i previously used error status and set return values
indirectly. i still do that, but where possible, i
also now return the real value.

this is because these string functions can no longer
return with error status; on error, they all abort.
this forces the program maintainer to keep their code
reliable, and removes the need to check the error status
after using syscalls, because these libc wrappers mitigate
that and make use of libc for you, including errors.

this is part of a general effort to promote safe use
of the C programming language, especially in libreboot!

Signed-off-by: Leah Rowe <leah@libreboot.org>
2026-03-30 06:25:52 +01:00

1071 lines
19 KiB
C

/* SPDX-License-Identifier: MIT
* Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
*
* Pathless i/o, and some stuff you
* probably never saw in userspace.
*
* Be nice to the demon.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* for openat2: */
#ifdef __linux__
#include <linux/openat2.h>
#include <sys/syscall.h>
#endif
#include "../include/common.h"
/* check that a file changed
*/
int
same_file(int fd, struct stat *st_old,
int check_size)
{
struct stat st;
int saved_errno = errno;
/* TODO: null/-1 checks
* like this can be
* generalised
*/
if (st_old == NULL) {
errno = EFAULT;
goto err_same_file;
}
if (fd < 0) {
errno = EBADF;
goto err_same_file;
}
if (fstat(fd, &st) == -1)
goto err_same_file;
if (fd_verify_regular(fd, st_old, &st) < 0)
goto err_same_file;
if (check_size &&
st.st_size != st_old->st_size)
goto err_same_file;
errno = saved_errno;
return 0;
err_same_file:
return set_errno(saved_errno, ESTALE);
}
int
fsync_dir(const char *path)
{
int saved_errno = errno;
size_t pathlen = 0;
size_t maxlen = 0;
char *dirbuf = NULL;
int dirfd = -1;
char *slash = NULL;
struct stat st = {0};
int close_errno;
#if defined(PATH_LEN) && \
(PATH_LEN) >= 256
maxlen = PATH_LEN;
#else
maxlen = 4096;
#endif
if (if_err(slen(path, maxlen, &pathlen) == 0, EINVAL))
goto err_fsync_dir;
memcpy(smalloc(&dirbuf, pathlen + 1),
path, pathlen + 1);
slash = strrchr(dirbuf, '/');
if (slash != NULL) {
*slash = '\0';
if (*dirbuf == '\0') {
dirbuf[0] = '/';
dirbuf[1] = '\0';
}
} else {
dirbuf[0] = '.';
dirbuf[1] = '\0';
}
dirfd = fs_open(dirbuf,
O_RDONLY | O_CLOEXEC | O_NOCTTY
#ifdef O_DIRECTORY
| O_DIRECTORY
#endif
#ifdef O_NOFOLLOW
| O_NOFOLLOW
#endif
);
if (if_err_sys(dirfd < 0) ||
if_err_sys(fstat(dirfd, &st) < 0) ||
if_err(!S_ISDIR(st.st_mode), ENOTDIR)
||
if_err_sys(fsync_on_eintr(dirfd) == -1))
goto err_fsync_dir;
close_on_eintr(&dirfd);
free_and_set_null(&dirbuf);
errno = saved_errno;
return 0;
err_fsync_dir:
free_and_set_null(&dirbuf);
close_on_eintr(&dirfd);
return set_errno(saved_errno, EIO);
}
/* rw_file_exact() - Read perfectly or die
*
* Read/write, and absolutely insist on an
* absolute read; e.g. if 100 bytes are
* requested, this MUST return 100.
*
* This function will never return zero.
* It will only return below (error),
* or above (success). On error, -1 is
* returned and errno is set accordingly.
*
* Zero-byte returns are not allowed.
* It will re-spin a finite number of
* times upon zero-return, to recover,
* otherwise it will return an error.
*/
ssize_t
rw_file_exact(int fd, unsigned char *mem, size_t nrw,
off_t off, int rw_type, int loop_eagain,
int loop_eintr, size_t max_retries,
int off_reset)
{
ssize_t rval;
ssize_t rc;
size_t nrw_cur;
off_t off_cur;
void *mem_cur;
size_t retries_on_zero;
int saved_errno = errno;
errno = 0;
rval = 0;
rc = 0;
retries_on_zero = 0;
if (io_args(fd, mem, nrw, off, rw_type) == -1)
goto err_rw_file_exact;
while (1) {
/* Prevent theoretical overflow */
if (rval >= 0 && (size_t)rval > (nrw - rc)) {
errno = EOVERFLOW;
goto err_rw_file_exact;
}
rc += rval;
if ((size_t)rc >= nrw)
break;
mem_cur = (void *)(mem + (size_t)rc);
nrw_cur = (size_t)(nrw - (size_t)rc);
if (off < 0) {
errno = EOVERFLOW;
goto err_rw_file_exact;
}
off_cur = off + (off_t)rc;
rval = prw(fd, mem_cur, nrw_cur, off_cur,
rw_type, loop_eagain, loop_eintr,
off_reset);
if (rval < 0)
goto err_rw_file_exact;
if (rval == 0) {
if (retries_on_zero++ < max_retries)
continue;
errno = EIO;
goto err_rw_file_exact;
}
retries_on_zero = 0;
}
if ((size_t)rc != nrw) {
errno = EIO;
goto err_rw_file_exact;
}
rval = rw_over_nrw(rc, nrw);
if (rval < 0)
goto err_rw_file_exact;
errno = saved_errno;
return rval;
err_rw_file_exact:
return set_errno(saved_errno, EIO);
}
/* prw() - portable read-write with more
* safety checks than barebones libc
*
* If you need real pwrite/pread, just compile
* with flag: REAL_POS_IO=1
*
* A fallback is provided for regular read/write.
* rw_type can be IO_READ (read), IO_WRITE (write),
* IO_PREAD (pread) or IO_PWRITE
*
* loop_eagain does a retry loop on EAGAIN if set
* loop_eintr does a retry loop on EINTR if set
*
* race conditions on non-libc pread/pwrite:
* if a file offset changes, abort, to mitage.
*
* off_reset 1: reset the file offset *once* if
* a change was detected, assuming
* nothing else is touching it now
* off_reset 0: never reset if changed
*
* REAL_POS_IO is enabled by default in common.h
* and the fallback version was written for fun.
* You should just use the real one (REAL_POS_IO 1),
* since it is generally more reliable.
*/
ssize_t
prw(int fd, void *mem, size_t nrw,
off_t off, int rw_type,
int loop_eagain, int loop_eintr,
int off_reset)
{
ssize_t rval;
ssize_t r;
int positional_rw;
struct stat st;
#if !defined(REAL_POS_IO) || \
REAL_POS_IO < 1
off_t verified;
off_t off_orig;
off_t off_last;
#endif
int saved_errno = errno;
errno = 0;
if (io_args(fd, mem, nrw, off, rw_type)
== -1)
goto err_prw;
r = -1;
/* do not use loop_eagain on
* normal files
*/
if (!loop_eagain) {
/* check whether the file
* changed
*/
if (check_file(fd, &st) == -1)
goto err_prw;
}
if (rw_type >= IO_PREAD)
positional_rw = 1; /* pread/pwrite */
else
positional_rw = 0; /* read/write */
try_rw_again:
if (!positional_rw) {
#if defined(REAL_POS_IO) && \
REAL_POS_IO > 0
real_pread_pwrite:
#endif
if (rw_type == IO_WRITE)
r = write_on_eintr(fd, mem, nrw);
else if (rw_type == IO_READ)
r = read_on_eintr(fd, mem, nrw);
#if defined(REAL_POS_IO) && \
REAL_POS_IO > 0
else if (rw_type == IO_PWRITE)
r = pwrite_on_eintr(fd, mem, nrw, off);
else if (rw_type == IO_PREAD)
r = pread_on_eintr(fd, mem, nrw, off);
#endif
if (r == -1 && (errno == try_err(loop_eintr, EINTR)
|| errno == try_err(loop_eagain, EAGAIN)))
goto try_rw_again;
rval = rw_over_nrw(r, nrw);
if (rval < 0)
goto err_prw;
errno = saved_errno;
return rval;
}
#if defined(REAL_POS_IO) && \
REAL_POS_IO > 0
goto real_pread_pwrite;
#else
if ((off_orig = lseek_on_eintr(fd, (off_t)0, SEEK_CUR,
loop_eagain, loop_eintr)) == (off_t)-1) {
r = -1;
} else if (lseek_on_eintr(fd, off, SEEK_SET,
loop_eagain, loop_eintr) == (off_t)-1) {
r = -1;
} else {
verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR,
loop_eagain, loop_eintr);
/* abort if the offset changed,
* indicating race condition. if
* off_reset enabled, reset *ONCE*
*/
if (off_reset && off != verified)
lseek_on_eintr(fd, off, SEEK_SET,
loop_eagain, loop_eintr);
do {
/* check offset again, repeatedly.
* even if off_reset is set, this
* aborts if offsets change again
*/
verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR,
loop_eagain, loop_eintr);
if (off != verified) {
errno = EBUSY;
goto err_prw;
}
if (rw_type == IO_PREAD)
r = read_on_eintr(fd, mem, nrw);
else if (rw_type == IO_PWRITE)
r = write_on_eintr(fd, mem, nrw);
if (rw_over_nrw(r, nrw) == -1)
break;
} while (r == -1 &&
(errno == try_err(loop_eintr, EINTR) ||
errno == try_err(loop_eagain, EAGAIN)));
}
saved_errno = errno;
off_last = lseek_on_eintr(fd, off_orig, SEEK_SET,
loop_eagain, loop_eintr);
if (off_last != off_orig) {
errno = saved_errno;
goto err_prw;
}
errno = saved_errno;
rval = rw_over_nrw(r, nrw);
if (rval < 0)
goto err_prw;
errno = saved_errno;
return rval;
#endif
err_prw:
return set_errno(saved_errno, EIO);
}
int
io_args(int fd, void *mem, size_t nrw,
off_t off, int rw_type)
{
int saved_errno = errno;
if (if_err(mem == NULL, EFAULT) ||
if_err(fd < 0, EBADF) ||
if_err(off < 0, ERANGE) ||
if_err(!nrw, EPERM) || /* TODO: toggle zero-byte check */
if_err(nrw > (size_t)SSIZE_MAX, ERANGE) ||
if_err(((size_t)off + nrw) < (size_t)off, ERANGE) ||
if_err(rw_type > IO_PWRITE, EINVAL))
goto err_io_args;
errno = saved_errno;
return 0;
err_io_args:
return set_errno(saved_errno, EINVAL);
}
int
check_file(int fd, struct stat *st)
{
int saved_errno = errno;
if (if_err(fd < 0, EBADF) ||
if_err(st == NULL, EFAULT) ||
if_err(fstat(fd, st) == -1, 0) ||
if_err(!S_ISREG(st->st_mode), EBADF))
goto err_is_file;
errno = saved_errno;
return 0;
err_is_file:
return set_errno(saved_errno, EINVAL);
}
/* POSIX can say whatever it wants.
* specification != implementation
*/
ssize_t
rw_over_nrw(ssize_t r, size_t nrw)
{
int saved_errno = errno;
if (if_err(!nrw, 0) ||
if_err(r == -1, 0) ||
if_err((size_t)r > SSIZE_MAX, ERANGE) ||
if_err((size_t)r > nrw, ERANGE))
goto err_rw_over_nrw;
errno = saved_errno;
return r;
err_rw_over_nrw:
return set_errno(saved_errno, EIO);
}
off_t
lseek_on_eintr(int fd, off_t off, int whence,
int loop_eagain, int loop_eintr)
{
off_t old;
old = -1;
do {
old = lseek(fd, off, whence);
} while (old == (off_t)-1 && (
errno == try_err(loop_eintr, EINTR) ||
errno == try_err(loop_eintr, ETXTBSY) ||
errno == try_err(loop_eagain, EAGAIN) ||
errno == try_err(loop_eagain, EWOULDBLOCK)));
return old;
}
/* two functions that reduce sloccount by
* two hundred lines */
int
if_err(int condition, int errval)
{
if (!condition)
return 0;
if (errval)
errno = errval;
return 1;
}
int
if_err_sys(int condition)
{
if (!condition)
return 0;
return 1;
}
/* errno can never be -1, so you can
* use this to conditionally set an integer
* for comparison. see example in lseek_on_eintr
*/
int
try_err(int loop_err, int errval)
{
if (loop_err)
return errval;
return -1;
}
void
open_on_eintr(const char *path,
int *fd, int flags, mode_t mode,
struct stat *st)
{
int r = -1;
int saved_errno = errno;
if (path == NULL)
err_exit(EINVAL, "open_on_eintr: null path");
if (fd == NULL)
err_exit(EFAULT, "%s: open_on_eintr: null fd ptr", path);
if (*fd >= 0)
err_exit(EBADF, "%s: open_on_eintr: file already open", path);
do {
r = open(path, flags, mode);
} while (r == -1 && (
errno == EINTR || errno == EAGAIN ||
errno == EWOULDBLOCK || errno == ETXTBSY));
if (r < 0)
err_exit(errno, "%s: open_on_eintr: could not close", path);
*fd = r;
if (st != NULL) {
if (fstat(*fd, st) < 0)
err_exit(errno, "%s: stat", path);
if (!S_ISREG(st->st_mode))
err_exit(errno, "%s: not a regular file", path);
}
if (lseek_on_eintr(*fd, 0, SEEK_CUR, 1, 1) == (off_t)-1)
err_exit(errno, "%s: file not seekable", path);
errno = saved_errno;
}
void
close_on_eintr(int *fd)
{
int r;
int saved_errno = errno;
if (fd == NULL)
err_exit(EINVAL, "close_on_eintr: null pointer");
if (*fd < 0)
return;
do {
r = close(*fd);
} while (r == -1 && (
errno == EINTR || errno == EAGAIN ||
errno == EWOULDBLOCK || errno == ETXTBSY));
if (r < 0)
err_exit(errno, "close_on_eintr: could not close");
*fd = -1;
errno = saved_errno;
}
int
fsync_on_eintr(int fd)
{
int r;
int saved_errno = errno;
do {
r = fsync(fd);
} while (r == -1 && (errno == EINTR || errno == EAGAIN ||
errno == ETXTBSY || errno == EWOULDBLOCK));
if (r >= 0)
errno = saved_errno;
return r;
}
int
fs_rename_at(int olddirfd, const char *old,
int newdirfd, const char *new)
{
if (if_err(new == NULL || old == NULL, EFAULT) ||
if_err(olddirfd < 0 || newdirfd < 0, EBADF))
return -1;
return renameat(olddirfd, old, newdirfd, new);
}
/* secure open, based on relative path to root
*
* always a fixed fd for / see: rootfs()
* and fs_resolve_at()
*/
int
fs_open(const char *path, int flags)
{
struct filesystem *fs;
if (if_err(path == NULL, EFAULT) ||
if_err(path[0] != '/', EINVAL) ||
if_err_sys((fs = rootfs()) == NULL))
return -1;
return fs_resolve_at(fs->rootfd, path + 1, 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 = -1;
open_on_eintr("/", &global_fs.rootfd,
O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0400, NULL);
if (global_fs.rootfd < 0)
return NULL;
fs_initialised = 1;
}
return &global_fs;
}
/* filesystem sandboxing in userspace
* TODO:
missing length bound check.
potential CPU DoS on very long paths, spammed repeatedly.
perhaps cap at PATH_LEN?
*/
int
fs_resolve_at(int dirfd, const char *path, int flags)
{
int nextfd = -1;
int curfd;
const char *p;
#if defined(PATH_LEN) && \
((PATH_LEN) >= 256)
char name[PATH_LEN];
#else
char name[4096];
#endif
int saved_errno = errno;
int r;
int is_last;
if (dirfd < 0 || path == NULL || *path == '\0') {
errno = EINVAL;
return -1;
}
p = path;
curfd = dirfd; /* start here */
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(curfd, name, flags, is_last);
if (nextfd < 0)
goto err;
/* close previous fd if not the original input */
if (curfd != dirfd)
close_on_eintr(&curfd);
curfd = nextfd;
nextfd = -1;
}
errno = saved_errno;
return curfd;
err:
saved_errno = errno;
if (nextfd >= 0)
close_on_eintr(&nextfd);
/* close curfd only if it's not the original */
if (curfd != dirfd && curfd >= 0)
close_on_eintr(&curfd);
errno = saved_errno;
return -1;
}
/* NOTE:
rejects . and .. but not empty strings
after normalisation. edge case:
//////
normalised implicitly, but might be good
to add a defensive check regardless. code
probably not exploitable in current state.
*/
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;
struct stat st;
fd = openat2p(dirfd, name,
(is_last ? flags : (O_RDONLY | O_DIRECTORY)) |
O_NOFOLLOW | O_CLOEXEC, (flags & O_CREAT) ? 0600 : 0);
if (!is_last) {
if (if_err(fd < 0, EBADF) ||
if_err_sys(fstat(fd, &st) < 0))
return -1;
if (!S_ISDIR(st.st_mode)) {
close_on_eintr(&fd);
errno = ENOTDIR;
return -1;
}
}
return fd;
}
int
fs_dirname_basename(const char *path,
char **dir, char **base,
int allow_relative)
{
char *buf = NULL;
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 (if_err(path == NULL || dir == NULL || base == NULL, EFAULT))
return -1;
slen(path, maxlen, &len);
memcpy(smalloc(&buf, len + 1),
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) {
sdup(".", maxlen, dir);
*base = buf;
} else {
errno = EINVAL;
free_and_set_null(&buf);
return -1;
}
return 0;
}
/* portable wrapper for use of openat2 on linux,
* with fallback for others e.g. openbsd
*/
int
openat2p(int dirfd, const char *path,
int flags, mode_t mode)
{
#ifdef __linux__
struct open_how how = {
.flags = flags,
.mode = mode,
.resolve =
RESOLVE_BENEATH |
RESOLVE_NO_SYMLINKS |
RESOLVE_NO_MAGICLINKS
};
int saved_errno = errno;
int rval;
#endif
if (if_err(dirfd < 0, EBADF) ||
if_err(path == NULL, EFAULT))
return -1;
retry:
errno = 0;
#ifdef __linux__
/* more secure than regular openat,
* but linux-only at the time of writing
*/
rval = syscall(SYS_openat2, dirfd, path, &how, sizeof(how));
#else
/* less secure, but e.g. openbsd
* doesn't have openat2 yet
*/
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(int dirfd,
const char *path, mode_t mode)
{
int saved_errno = errno;
int rval;
if (if_err(dirfd < 0, EBADF) ||
if_err(path == NULL, 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;
}
ssize_t
read_on_eintr(int fd,
void *buf, size_t count)
{
int saved_errno = errno;
int rval;
if (if_err(buf == NULL, EFAULT) ||
if_err(fd < 0, EBADF) ||
if_err(count == 0, EINVAL))
goto err;
retry:
errno = 0;
if ((rval = read(fd, buf, count)) == -1 && (
errno == EINTR ||
errno == EAGAIN ||
errno == EWOULDBLOCK ||
errno == ETXTBSY))
goto retry;
errno = saved_errno;
return rval;
err:
return set_errno(saved_errno, EIO);
}
ssize_t
pread_on_eintr(int fd,
void *buf, size_t count,
off_t off)
{
int saved_errno = errno;
int rval;
if (if_err(buf == NULL, EFAULT) ||
if_err(fd < 0, EBADF) ||
if_err(off < 0, EFAULT) ||
if_err(count == 0, EINVAL))
goto err;
retry:
errno = 0;
if ((rval = pread(fd, buf, count, off)) == -1 && (
errno == EINTR ||
errno == EAGAIN ||
errno == EWOULDBLOCK ||
errno == ETXTBSY))
goto retry;
errno = saved_errno;
return rval;
err:
return set_errno(saved_errno, EIO);
}
ssize_t
write_on_eintr(int fd,
void *buf, size_t count)
{
int saved_errno = errno;
int rval;
if (if_err(buf == NULL, EFAULT) ||
if_err(fd < 0, EBADF) ||
if_err(count == 0, EINVAL))
goto err;
retry:
errno = 0;
if ((rval = write(fd, buf, count)) == -1 && (
errno == EINTR ||
errno == EAGAIN ||
errno == EWOULDBLOCK ||
errno == ETXTBSY))
goto retry;
errno = saved_errno;
return rval;
err:
return set_errno(saved_errno, EIO);
}
ssize_t
pwrite_on_eintr(int fd,
void *buf, size_t count,
off_t off)
{
int saved_errno = errno;
int rval;
if (if_err(buf == NULL, EFAULT) ||
if_err(fd < 0, EBADF) ||
if_err(off < 0, EFAULT) ||
if_err(count == 0, EINVAL))
goto err;
retry:
errno = 0;
if ((rval = pwrite(fd, buf, count, off)) == -1 && (
errno == EINTR ||
errno == EAGAIN ||
errno == EWOULDBLOCK ||
errno == ETXTBSY))
goto retry;
errno = saved_errno;
return rval;
err:
return set_errno(saved_errno, EIO);
}