mirror of
https://codeberg.org/libreboot/lbmk.git
synced 2026-03-25 13:29:03 +02:00
1131 lines
19 KiB
C
1131 lines
19 KiB
C
/* SPDX-License-Identifier: MIT
|
|
* Copyright (c) 2026 Leah Rowe <leah@libreboot.org>
|
|
*
|
|
* Hardened mktemp (be nice to the demon).
|
|
*/
|
|
|
|
#if defined(__linux__) && !defined(_GNU_SOURCE)
|
|
/* for openat2 syscall on linux */
|
|
#define _GNU_SOURCE 1
|
|
#endif
|
|
|
|
#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"
|
|
|
|
int
|
|
new_tmpfile(int *fd, char **path)
|
|
{
|
|
return new_tmp_common(fd, path, MKHTEMP_FILE);
|
|
}
|
|
|
|
int
|
|
new_tmpdir(int *fd, char **path)
|
|
{
|
|
return new_tmp_common(fd, path, MKHTEMP_DIR);
|
|
}
|
|
|
|
/* WARNING:
|
|
* on error, *path (at **path) may be
|
|
NULL, or if the error pertains to
|
|
an actual TMPDIR, set. if set, it
|
|
will be using *static* memory and
|
|
must not be freed. on success,
|
|
a pointer to heap memory is set
|
|
instead.
|
|
* see:
|
|
* env_tmpdir()
|
|
* this is for error reports if e.g.
|
|
* TMPDIR isn't found (but is set)
|
|
* if TMPDIR isn't set, it will
|
|
* default to /tmp or /var/tmp
|
|
*/
|
|
int
|
|
new_tmp_common(int *fd, char **path, int type)
|
|
{
|
|
#if defined(PATH_LEN) && \
|
|
(PATH_LEN) >= 256
|
|
size_t maxlen = PATH_LEN;
|
|
#else
|
|
size_t maxlen = 4096;
|
|
#endif
|
|
struct stat st;
|
|
|
|
char suffix[] = "tmp.XXXXXXXXXX";
|
|
char *tmpdir = NULL;
|
|
|
|
size_t dirlen;
|
|
size_t destlen;
|
|
char *dest = NULL; /* final path (will be written into "path") */
|
|
int saved_errno = errno;
|
|
int dirfd = -1;
|
|
const char *fname = NULL;
|
|
|
|
struct stat st_dir_initial;
|
|
|
|
char *fail_dir = NULL;
|
|
|
|
if (path == NULL || fd == NULL) {
|
|
errno = EFAULT;
|
|
goto err;
|
|
}
|
|
|
|
/* don't mess with someone elses file */
|
|
if (*fd >= 0) {
|
|
errno = EEXIST;
|
|
goto err;
|
|
}
|
|
|
|
/* regarding **path:
|
|
* the pointer (to the pointer)
|
|
* must nott be null, but we don't
|
|
* care about the pointer it points
|
|
* to. you should expect it to be
|
|
* replaced upon successful return
|
|
*
|
|
* (on error, it will not be touched)
|
|
*/
|
|
|
|
|
|
*fd = -1;
|
|
|
|
#if defined(PERMIT_NON_STICKY_ALWAYS) && \
|
|
((PERMIT_NON_STICKY_ALWAYS) > 0)
|
|
tmpdir = env_tmpdir(PERMIT_NON_STICKY_ALWAYS, &fail_dir);
|
|
#else
|
|
tmpdir = env_tmpdir(0, &fail_dir);
|
|
#endif
|
|
if (tmpdir == NULL)
|
|
goto err;
|
|
|
|
if (slen(tmpdir, maxlen, &dirlen) < 0)
|
|
goto err;
|
|
if (*tmpdir == '\0')
|
|
goto err;
|
|
if (*tmpdir != '/')
|
|
goto err;
|
|
|
|
/* sizeof adds an extra byte, useful
|
|
* because we also want '.' or '/'
|
|
*/
|
|
destlen = dirlen + sizeof(suffix);
|
|
if (destlen > maxlen - 1) {
|
|
errno = EOVERFLOW;
|
|
goto err;
|
|
}
|
|
|
|
dest = malloc(destlen + 1);
|
|
if (dest == NULL) {
|
|
errno = ENOMEM;
|
|
goto err;
|
|
}
|
|
|
|
memcpy(dest, tmpdir, dirlen);
|
|
*(dest + dirlen) = '/';
|
|
memcpy(dest + dirlen + 1, suffix, sizeof(suffix) - 1);
|
|
*(dest + destlen) = '\0';
|
|
|
|
fname = dest + dirlen + 1;
|
|
|
|
dirfd = fs_open(tmpdir,
|
|
O_RDONLY | O_DIRECTORY);
|
|
if (dirfd < 0)
|
|
goto err;
|
|
|
|
if (fstat(dirfd, &st_dir_initial) < 0)
|
|
goto err;
|
|
|
|
*fd = mkhtemp(fd, &st, dest, dirfd,
|
|
fname, &st_dir_initial, type);
|
|
if (*fd < 0)
|
|
goto err;
|
|
|
|
close_no_err(&dirfd);
|
|
|
|
errno = saved_errno;
|
|
*path = dest;
|
|
|
|
return 0;
|
|
|
|
err:
|
|
|
|
if (errno != saved_errno)
|
|
saved_errno = errno;
|
|
else
|
|
saved_errno = errno = EIO;
|
|
|
|
if (dest != NULL) {
|
|
free(dest);
|
|
dest = NULL;
|
|
}
|
|
|
|
close_no_err(&dirfd);
|
|
close_no_err(fd);
|
|
|
|
/* where a TMPDIR isn't found, and we err,
|
|
* we pass this back through for the
|
|
* error message
|
|
*/
|
|
if (fail_dir != NULL)
|
|
*path = fail_dir;
|
|
|
|
errno = saved_errno;
|
|
return -1;
|
|
}
|
|
|
|
|
|
/* hardened TMPDIR parsing
|
|
*/
|
|
|
|
char *
|
|
env_tmpdir(int bypass_all_sticky_checks, char **tmpdir)
|
|
{
|
|
char *t;
|
|
int allow_noworld_unsticky;
|
|
int saved_errno = errno;
|
|
|
|
char tmp[] = "/tmp";
|
|
char vartmp[] = "/var/tmp";
|
|
|
|
t = getenv("TMPDIR");
|
|
|
|
if (t != NULL && *t != '\0') {
|
|
|
|
if (tmpdir_policy(t,
|
|
&allow_noworld_unsticky) < 0) {
|
|
if (tmpdir != NULL)
|
|
*tmpdir = t;
|
|
return NULL; /* errno already set */
|
|
}
|
|
|
|
if (!world_writeable_and_sticky(t,
|
|
allow_noworld_unsticky,
|
|
bypass_all_sticky_checks)) {
|
|
if (tmpdir != NULL)
|
|
*tmpdir = t;
|
|
return NULL;
|
|
}
|
|
|
|
errno = saved_errno;
|
|
return t;
|
|
}
|
|
|
|
allow_noworld_unsticky = 0;
|
|
|
|
if (world_writeable_and_sticky("/tmp",
|
|
allow_noworld_unsticky,
|
|
bypass_all_sticky_checks)) {
|
|
|
|
if (tmpdir != NULL)
|
|
*tmpdir = tmp;
|
|
|
|
errno = saved_errno;
|
|
return "/tmp";
|
|
}
|
|
|
|
if (world_writeable_and_sticky("/var/tmp",
|
|
allow_noworld_unsticky,
|
|
bypass_all_sticky_checks)) {
|
|
|
|
if (tmpdir != NULL)
|
|
*tmpdir = vartmp;
|
|
|
|
errno = saved_errno;
|
|
return "/var/tmp";
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int
|
|
tmpdir_policy(const char *path,
|
|
int *allow_noworld_unsticky)
|
|
{
|
|
int saved_errno = errno;
|
|
int r;
|
|
|
|
if (path == NULL ||
|
|
allow_noworld_unsticky == NULL) {
|
|
|
|
errno = EFAULT;
|
|
return -1;
|
|
}
|
|
|
|
*allow_noworld_unsticky = 1;
|
|
|
|
r = same_dir(path, "/tmp");
|
|
if (r < 0)
|
|
goto err_tmpdir_policy;
|
|
if (r > 0)
|
|
*allow_noworld_unsticky = 0;
|
|
|
|
r = same_dir(path, "/var/tmp");
|
|
if (r < 0)
|
|
goto err_tmpdir_policy;
|
|
if (r > 0)
|
|
*allow_noworld_unsticky = 0;
|
|
|
|
errno = saved_errno;
|
|
return 0;
|
|
|
|
err_tmpdir_policy:
|
|
|
|
if (errno == saved_errno)
|
|
errno = EIO;
|
|
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
same_dir(const char *a, const char *b)
|
|
{
|
|
int fd_a = -1;
|
|
int fd_b = -1;
|
|
|
|
struct stat st_a;
|
|
struct stat st_b;
|
|
|
|
int saved_errno = errno;
|
|
int rval_scmp;
|
|
|
|
#if defined(PATH_LEN) && \
|
|
(PATH_LEN) >= 256
|
|
size_t maxlen = (PATH_LEN);
|
|
#else
|
|
size_t maxlen = 4096;
|
|
#endif
|
|
|
|
/* optimisation: if both dirs
|
|
are the same, we don't need
|
|
to check anything. sehr schnell:
|
|
*/
|
|
if (scmp(a, b, maxlen, &rval_scmp) < 0)
|
|
goto err_same_dir;
|
|
/* bonus: scmp checks null for us */
|
|
if (rval_scmp == 0)
|
|
goto success_same_dir;
|
|
|
|
fd_a = fs_open(a, O_RDONLY | O_DIRECTORY);
|
|
if (fd_a < 0)
|
|
goto err_same_dir;
|
|
|
|
fd_b = fs_open(b, O_RDONLY | O_DIRECTORY);
|
|
if (fd_b < 0)
|
|
goto err_same_dir;
|
|
|
|
if (fstat(fd_a, &st_a) < 0)
|
|
goto err_same_dir;
|
|
|
|
if (fstat(fd_b, &st_b) < 0)
|
|
goto err_same_dir;
|
|
|
|
if (st_a.st_dev == st_b.st_dev &&
|
|
st_a.st_ino == st_b.st_ino) {
|
|
|
|
close_no_err(&fd_a);
|
|
close_no_err(&fd_b);
|
|
|
|
success_same_dir:
|
|
|
|
/* SUCCESS
|
|
*/
|
|
|
|
errno = saved_errno;
|
|
return 1;
|
|
}
|
|
|
|
close_no_err(&fd_a);
|
|
close_no_err(&fd_b);
|
|
|
|
/* FAILURE (logical)
|
|
*/
|
|
|
|
errno = saved_errno;
|
|
return 0;
|
|
|
|
err_same_dir:
|
|
|
|
/* FAILURE (probably syscall)
|
|
*/
|
|
|
|
close_no_err(&fd_a);
|
|
close_no_err(&fd_b);
|
|
|
|
if (errno == saved_errno)
|
|
errno = EIO;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* bypass_all_sticky_checks: if set,
|
|
disable stickiness checks (libc behaviour)
|
|
(if not set: leah behaviour)
|
|
|
|
allow_noworld_unsticky:
|
|
allow non-sticky files if not world-writeable
|
|
(still block non-sticky in standard TMPDIR)
|
|
*/
|
|
int
|
|
world_writeable_and_sticky(
|
|
const char *s,
|
|
int allow_noworld_unsticky,
|
|
int bypass_all_sticky_checks)
|
|
{
|
|
struct stat st;
|
|
int dirfd = -1;
|
|
|
|
int saved_errno = errno;
|
|
|
|
if (s == NULL || *s == '\0') {
|
|
errno = EINVAL;
|
|
goto sticky_hell;
|
|
}
|
|
|
|
/* mitigate symlink attacks*
|
|
*/
|
|
dirfd = fs_open(s, O_RDONLY | O_DIRECTORY);
|
|
if (dirfd < 0)
|
|
goto sticky_hell;
|
|
|
|
if (fstat(dirfd, &st) < 0)
|
|
goto sticky_hell;
|
|
|
|
if (!S_ISDIR(st.st_mode)) {
|
|
errno = ENOTDIR;
|
|
goto sticky_hell;
|
|
}
|
|
|
|
/* must be fully executable
|
|
* by everyone, or openat2
|
|
* becomes unreliable**
|
|
*/
|
|
if (!(st.st_mode & S_IXUSR) ||
|
|
!(st.st_mode & S_IXGRP) ||
|
|
!(st.st_mode & S_IXOTH)) {
|
|
|
|
errno = EACCES;
|
|
goto sticky_hell;
|
|
}
|
|
|
|
/* *normal-**ish mode (libc):
|
|
*/
|
|
|
|
if (bypass_all_sticky_checks)
|
|
goto sticky_heaven; /* normal == no security */
|
|
|
|
/* unhinged leah mode:
|
|
*/
|
|
|
|
if (st.st_mode & S_IWOTH) { /* world writeable */
|
|
|
|
/* if world-writeable, only
|
|
* allow sticky files
|
|
*/
|
|
if (st.st_mode & S_ISVTX)
|
|
goto sticky_heaven; /* sticky */
|
|
|
|
errno = EPERM;
|
|
goto sticky_hell; /* not sticky */
|
|
}
|
|
|
|
/* non-world-writeable, so
|
|
* stickiness is do-not-care
|
|
*/
|
|
if (allow_noworld_unsticky)
|
|
goto sticky_heaven; /* sticky! */
|
|
|
|
goto sticky_hell; /* heaven visa denied */
|
|
|
|
sticky_heaven:
|
|
/* i like the one in hamburg better */
|
|
|
|
close_no_err(&dirfd);
|
|
errno = saved_errno;
|
|
|
|
return 1;
|
|
|
|
sticky_hell:
|
|
|
|
if (errno == saved_errno)
|
|
errno = EPERM;
|
|
|
|
saved_errno = errno;
|
|
|
|
close_no_err(&dirfd);
|
|
|
|
errno = saved_errno;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* mk(h)temp - hardened mktemp.
|
|
* like mkstemp, but (MUCH) harder.
|
|
*
|
|
* designed to resist TOCTOU attacks
|
|
* e.g. directory race / symlink attack
|
|
*
|
|
* extremely strict and even implements
|
|
* some limited userspace-level sandboxing,
|
|
* similar in spirit to openbsd unveil,
|
|
* though unveil is from kernel space.
|
|
*
|
|
* supports both files and directories.
|
|
* file: type = MKHTEMP_FILE (0)
|
|
* dir: type = MKHTEMP_DIR (1)
|
|
*
|
|
* DESIGN NOTES:
|
|
*
|
|
* caller is expected to handle
|
|
* cleanup e.g. free(), on *st,
|
|
* *template, *fname (all of the
|
|
* pointers). ditto fd cleanup.
|
|
*
|
|
* some limited cleanup is
|
|
* performed here, e.g. directory/file
|
|
* cleanup on error in mkhtemp_try_create
|
|
*
|
|
* we only check if these are not NULL,
|
|
* and the caller is expected to take
|
|
* care; without too many conditions,
|
|
* these functions are more flexible,
|
|
* but some precauttions are taken:
|
|
*
|
|
* when used via the function new_tmpfile
|
|
* or new_tmpdir, thtis is extremely strict,
|
|
* much stricter than previous mktemp
|
|
* variants. for example, it is much
|
|
* stricter about stickiness on world
|
|
* writeable directories, and it enforces
|
|
* file ownership under hardened mode
|
|
* (only lets you touch your own files/dirs)
|
|
*/
|
|
int
|
|
mkhtemp(int *fd,
|
|
struct stat *st,
|
|
char *template,
|
|
int dirfd,
|
|
const char *fname,
|
|
struct stat *st_dir_initial,
|
|
int type)
|
|
{
|
|
size_t len = 0;
|
|
size_t xc = 0;
|
|
size_t fname_len = 0;
|
|
|
|
char *fname_copy = NULL;
|
|
char *p;
|
|
|
|
size_t retries;
|
|
|
|
int close_errno;
|
|
int saved_errno = errno;
|
|
|
|
#if defined(PATH_LEN) && \
|
|
(PATH_LEN) >= 256
|
|
size_t max_len = PATH_LEN;
|
|
#else
|
|
size_t max_len = 4096;
|
|
#endif
|
|
int r;
|
|
char *end;
|
|
|
|
if (fd == NULL ||
|
|
template == NULL ||
|
|
fname == NULL ||
|
|
st_dir_initial == NULL) {
|
|
|
|
errno = EFAULT;
|
|
return -1;
|
|
}
|
|
|
|
/* we do not mess with an
|
|
open descriptor.
|
|
*/
|
|
if (*fd >= 0) {
|
|
errno = EEXIST; /* leave their file alone */
|
|
return -1;
|
|
}
|
|
|
|
if (dirfd < 0) {
|
|
errno = EBADF;
|
|
return -1;
|
|
}
|
|
|
|
if (slen(template, max_len, &len) < 0)
|
|
return -1;
|
|
|
|
if (len >= max_len) {
|
|
errno = EMSGSIZE;
|
|
return -1;
|
|
}
|
|
|
|
if (slen(fname, max_len, &fname_len) < 0)
|
|
return -1;
|
|
|
|
if (fname_len == 0) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
if (strrchr(fname, '/') != NULL) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
/* count trailing X */
|
|
end = template + len;
|
|
while (end > template && *--end == 'X')
|
|
xc++;
|
|
|
|
if (xc < 6 || xc > len) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
if (fname_len > len) {
|
|
errno = EOVERFLOW;
|
|
return -1;
|
|
}
|
|
|
|
if (memcmp(fname,
|
|
template + len - fname_len,
|
|
fname_len) != 0) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
fname_copy = malloc(fname_len + 1);
|
|
if (fname_copy == NULL) {
|
|
errno = ENOMEM;
|
|
goto err;
|
|
}
|
|
|
|
/* fname_copy = suffix region only; p points to trailing XXXXXX */
|
|
memcpy(fname_copy,
|
|
template + len - fname_len,
|
|
fname_len + 1);
|
|
p = fname_copy + fname_len - xc;
|
|
|
|
for (retries = 0; retries < MKHTEMP_RETRY_MAX; retries++) {
|
|
|
|
r = mkhtemp_try_create(
|
|
dirfd,
|
|
st_dir_initial,
|
|
fname_copy,
|
|
p,
|
|
xc,
|
|
fd,
|
|
st,
|
|
type);
|
|
|
|
if (r == 0) {
|
|
if (retries >= MKHTEMP_SPIN_THRESHOLD) {
|
|
/* usleep can return EINTR */
|
|
close_errno = errno;
|
|
usleep((useconds_t)rlong() & 0x3FF);
|
|
errno = close_errno;
|
|
}
|
|
continue;
|
|
}
|
|
if (r < 0)
|
|
goto err;
|
|
|
|
/* success: copy final name back */
|
|
memcpy(template + len - fname_len,
|
|
fname_copy, fname_len);
|
|
|
|
errno = saved_errno;
|
|
goto success;
|
|
}
|
|
|
|
errno = EEXIST;
|
|
|
|
err:
|
|
close_no_err(fd);
|
|
|
|
success:
|
|
|
|
if (fname_copy != NULL)
|
|
free(fname_copy);
|
|
|
|
return (*fd >= 0) ? *fd : -1;
|
|
}
|
|
|
|
int
|
|
mkhtemp_try_create(int dirfd,
|
|
struct stat *st_dir_initial,
|
|
char *fname_copy,
|
|
char *p,
|
|
size_t xc,
|
|
int *fd,
|
|
struct stat *st,
|
|
int type)
|
|
{
|
|
struct stat st_open;
|
|
int saved_errno = errno;
|
|
int rval = -1;
|
|
|
|
int file_created = 0;
|
|
int dir_created = 0;
|
|
|
|
if (fd == NULL || st == NULL || p == NULL || fname_copy == NULL ||
|
|
st_dir_initial == NULL) {
|
|
errno = EFAULT;
|
|
goto err;
|
|
} else if (*fd >= 0) { /* do not mess with someone else's file */
|
|
errno = EEXIST;
|
|
goto err;
|
|
}
|
|
|
|
if (mkhtemp_fill_random(p, xc) < 0)
|
|
goto err;
|
|
|
|
if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0)
|
|
goto err;
|
|
|
|
if (type == MKHTEMP_FILE) {
|
|
*fd = openat2p(dirfd, fname_copy,
|
|
O_RDWR | O_CREAT | O_EXCL |
|
|
O_NOFOLLOW | O_CLOEXEC | O_NOCTTY,
|
|
0600);
|
|
|
|
/* O_CREAT and O_EXCL guarantees
|
|
* creation upon success
|
|
*/
|
|
if (*fd >= 0)
|
|
file_created = 1;
|
|
|
|
} else { /* dir: MKHTEMP_DIR */
|
|
|
|
if (mkdirat_on_eintr(dirfd, fname_copy, 0700) < 0)
|
|
goto err;
|
|
|
|
/* ^ NOTE: opening the directory here
|
|
will never set errno=EEXIST,
|
|
since we're not creating it */
|
|
|
|
dir_created = 1;
|
|
|
|
/* do it again (mitigate directory race) */
|
|
if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0)
|
|
goto err;
|
|
|
|
*fd = openat2p(dirfd, fname_copy,
|
|
O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0);
|
|
if (*fd < 0)
|
|
goto err;
|
|
|
|
if (fstat(*fd, &st_open) < 0)
|
|
goto err;
|
|
|
|
if (!S_ISDIR(st_open.st_mode)) {
|
|
errno = ENOTDIR;
|
|
goto err;
|
|
}
|
|
|
|
/* NOTE: could check nlink count here,
|
|
* but it's not very reliable here. skipped.
|
|
*/
|
|
|
|
if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0)
|
|
goto err;
|
|
|
|
}
|
|
|
|
/* NOTE: openat2p and mkdirat_on_eintr
|
|
* already handled EINTR/EAGAIN looping
|
|
*/
|
|
|
|
if (*fd < 0) {
|
|
if (errno == EEXIST) {
|
|
|
|
rval = 0;
|
|
goto out;
|
|
}
|
|
goto err;
|
|
}
|
|
|
|
if (fstat(*fd, &st_open) < 0)
|
|
goto err;
|
|
|
|
if (type == MKHTEMP_FILE) {
|
|
|
|
if (fd_verify_dir_identity(dirfd, st_dir_initial) < 0)
|
|
goto err;
|
|
|
|
if (secure_file(fd, st, &st_open,
|
|
O_APPEND, 1, 1, 0600) < 0) /* WARNING: only once */
|
|
goto err;
|
|
|
|
} else { /* dir: MKHTEMP_DIR */
|
|
|
|
if (fd_verify_identity(*fd, &st_open, st_dir_initial) < 0)
|
|
goto err;
|
|
|
|
if (!S_ISDIR(st_open.st_mode)) {
|
|
errno = ENOTDIR;
|
|
goto err;
|
|
}
|
|
|
|
if (is_owner(&st_open) < 0)
|
|
goto err;
|
|
|
|
/* group/world writeable */
|
|
if (st_open.st_mode & (S_IWGRP | S_IWOTH)) {
|
|
errno = EPERM;
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
errno = saved_errno;
|
|
rval = 1;
|
|
goto out;
|
|
|
|
err:
|
|
close_no_err(fd);
|
|
|
|
if (file_created)
|
|
(void) unlinkat(dirfd, fname_copy, 0);
|
|
|
|
if (dir_created)
|
|
(void) unlinkat(dirfd, fname_copy, AT_REMOVEDIR);
|
|
|
|
rval = -1;
|
|
out:
|
|
return rval;
|
|
}
|
|
|
|
int
|
|
mkhtemp_fill_random(char *p, size_t xc)
|
|
{
|
|
size_t chx = 0;
|
|
int rand_failures = 0;
|
|
|
|
size_t r;
|
|
|
|
int saved_rand_error = 0;
|
|
static char ch[] =
|
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
|
|
/* clamp rand to prevent modulo bias
|
|
* (reduced risk of entropy leak)
|
|
*/
|
|
size_t limit = ((size_t)-1) - (((size_t)-1) % (sizeof(ch) - 1));
|
|
|
|
int saved_errno = errno;
|
|
|
|
if (p == NULL) {
|
|
errno = EFAULT;
|
|
goto err_mkhtemp_fill_random;
|
|
}
|
|
|
|
for (chx = 0; chx < xc; chx++) {
|
|
|
|
do {
|
|
saved_rand_error = errno;
|
|
rand_failures = 0;
|
|
retry_rand:
|
|
errno = 0;
|
|
|
|
/* on bsd: uses arc4random
|
|
on linux: uses getrandom
|
|
on OLD linux: /dev/urandom
|
|
on old/other unix: /dev/urandom
|
|
*/
|
|
r = rlong();
|
|
|
|
if (errno > 0) {
|
|
if (++rand_failures <= 8)
|
|
goto retry_rand;
|
|
|
|
goto err_mkhtemp_fill_random;
|
|
}
|
|
|
|
rand_failures = 0;
|
|
errno = saved_rand_error;
|
|
|
|
} while (r >= limit);
|
|
|
|
p[chx] = ch[r % (sizeof(ch) - 1)];
|
|
}
|
|
|
|
errno = saved_errno;
|
|
return 0;
|
|
|
|
err_mkhtemp_fill_random:
|
|
|
|
if (errno == saved_errno)
|
|
errno = ECANCELED;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* WARNING: **ONCE** per file.
|
|
*
|
|
* !!! DO NOT RUN TWICE PER FILE. BEWARE OF THE DEMON !!!
|
|
* watch out for spikes!
|
|
*/
|
|
int secure_file(int *fd,
|
|
struct stat *st,
|
|
struct stat *expected,
|
|
int bad_flags,
|
|
int check_seek,
|
|
int do_lock,
|
|
mode_t mode)
|
|
{
|
|
int flags;
|
|
struct stat st_now;
|
|
int saved_errno = errno;
|
|
/* you have been warned */
|
|
if (fd == NULL) {
|
|
errno = EFAULT;
|
|
goto err_demons;
|
|
}
|
|
if (*fd < 0) {
|
|
errno = EBADF;
|
|
goto err_demons;
|
|
}
|
|
|
|
if (st == NULL) {
|
|
errno = EFAULT;
|
|
goto err_demons;
|
|
}
|
|
|
|
flags = fcntl(*fd, F_GETFL);
|
|
|
|
if (flags == -1)
|
|
goto err_demons;
|
|
|
|
if (bad_flags > 0) {
|
|
|
|
/* e.g. O_APPEND breaks pwrite/pread
|
|
* by allowing offsets to be ignored */
|
|
if (flags & bad_flags) {
|
|
errno = EPERM;
|
|
goto err_demons;
|
|
}
|
|
}
|
|
|
|
if (expected != NULL) {
|
|
if (fd_verify_regular(*fd, expected, st) < 0)
|
|
goto err_demons;
|
|
} else {
|
|
if (fstat(*fd, &st_now) == -1)
|
|
goto err_demons;
|
|
|
|
if (!S_ISREG(st_now.st_mode)) {
|
|
errno = EBADF;
|
|
goto err_demons;
|
|
}
|
|
|
|
*st = st_now;
|
|
}
|
|
|
|
if (check_seek) {
|
|
|
|
/* check if it's seekable */
|
|
if (lseek(*fd, 0, SEEK_CUR) == (off_t)-1)
|
|
goto err_demons;
|
|
}
|
|
|
|
/* don't release the demon
|
|
*/
|
|
if (st->st_nlink != 1) { /***********/
|
|
/* ( >:3 ) */
|
|
errno = ELOOP; /* /| |\ */ /* don't let him out */
|
|
goto err_demons; /* / \ */
|
|
/***********/
|
|
}
|
|
|
|
if (st->st_uid != geteuid() && /* someone else's file */
|
|
geteuid() != 0) { /* override for root */
|
|
|
|
errno = EPERM;
|
|
goto err_demons;
|
|
}
|
|
if (is_owner(st) < 0)
|
|
goto err_demons;
|
|
|
|
/* world-writeable or group-ownership.
|
|
* if these are set, then others could
|
|
* modify the file (not secure)
|
|
*/
|
|
if (st->st_mode & (S_IWGRP | S_IWOTH)) {
|
|
errno = EPERM;
|
|
goto err_demons;
|
|
}
|
|
|
|
if (do_lock) {
|
|
if (lock_file(*fd, flags) == -1)
|
|
goto err_demons;
|
|
|
|
if (expected != NULL) {
|
|
if (fd_verify_identity(*fd, expected, &st_now) < 0)
|
|
goto err_demons;
|
|
}
|
|
}
|
|
|
|
if (fchmod(*fd, mode) == -1)
|
|
goto err_demons;
|
|
|
|
errno = saved_errno;
|
|
return 0;
|
|
|
|
err_demons:
|
|
|
|
if (errno == saved_errno)
|
|
errno = EIO;
|
|
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
fd_verify_regular(int fd,
|
|
const struct stat *expected,
|
|
struct stat *out)
|
|
{
|
|
if (fd_verify_identity(fd, expected, out) < 0)
|
|
return -1;
|
|
|
|
if (!S_ISREG(out->st_mode)) {
|
|
errno = EBADF;
|
|
return -1;
|
|
}
|
|
|
|
return 0; /* regular file */
|
|
}
|
|
|
|
int
|
|
fd_verify_identity(int fd,
|
|
const struct stat *expected,
|
|
struct stat *out)
|
|
{
|
|
struct stat st_now;
|
|
int saved_errno = errno;
|
|
|
|
if (fd < 0 || expected == NULL) {
|
|
errno = EFAULT;
|
|
return -1;
|
|
}
|
|
|
|
if (fstat(fd, &st_now) < 0)
|
|
return -1;
|
|
|
|
if (st_now.st_dev != expected->st_dev ||
|
|
st_now.st_ino != expected->st_ino) {
|
|
errno = ESTALE;
|
|
return -1;
|
|
}
|
|
|
|
if (out != NULL)
|
|
*out = st_now;
|
|
|
|
errno = saved_errno;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
fd_verify_dir_identity(int fd,
|
|
const struct stat *expected)
|
|
{
|
|
struct stat st_now;
|
|
int saved_errno = errno;
|
|
|
|
if (fd < 0 || expected == NULL) {
|
|
errno = EFAULT;
|
|
return -1;
|
|
}
|
|
|
|
if (fstat(fd, &st_now) < 0)
|
|
return -1;
|
|
|
|
if (st_now.st_dev != expected->st_dev ||
|
|
st_now.st_ino != expected->st_ino) {
|
|
errno = ESTALE;
|
|
return -1;
|
|
}
|
|
|
|
if (!S_ISDIR(st_now.st_mode)) {
|
|
errno = ENOTDIR;
|
|
return -1;
|
|
}
|
|
|
|
errno = saved_errno;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
is_owner(struct stat *st)
|
|
{
|
|
if (st == NULL) {
|
|
|
|
errno = EFAULT;
|
|
return -1;
|
|
}
|
|
|
|
#if ALLOW_ROOT_OVERRIDE
|
|
if (st->st_uid != geteuid() && /* someone else's file */
|
|
geteuid() != 0) { /* override for root */
|
|
#else
|
|
if (st->st_uid != geteuid()) { /* someone else's file */
|
|
#endif /* and no root override */
|
|
errno = EPERM;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
lock_file(int fd, int flags)
|
|
{
|
|
struct flock fl;
|
|
int saved_errno = errno;
|
|
|
|
if (fd < 0) {
|
|
errno = EBADF;
|
|
goto err_lock_file;
|
|
}
|
|
|
|
if (flags < 0) {
|
|
errno = EINVAL;
|
|
goto err_lock_file;
|
|
}
|
|
|
|
memset(&fl, 0, sizeof(fl));
|
|
|
|
if ((flags & O_ACCMODE) == O_RDONLY)
|
|
fl.l_type = F_RDLCK;
|
|
else
|
|
fl.l_type = F_WRLCK;
|
|
|
|
fl.l_whence = SEEK_SET;
|
|
|
|
if (fcntl(fd, F_SETLK, &fl) == -1)
|
|
goto err_lock_file;
|
|
|
|
saved_errno = errno;
|
|
return 0;
|
|
|
|
err_lock_file:
|
|
|
|
if (errno == saved_errno)
|
|
errno = EIO;
|
|
|
|
return -1;
|
|
}
|