/* we've been doing strings wrong since 1982 */ /* WIP!!!! do not use this. just a proof of concept. * under development for nvmutil, and also a separate * util called mkhtemp */ /* SPDX-License-Identifier: MIT * Copyright (c) 2026 Leah Rowe */ #include #include #include #include #include #include #include #include #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 (st.st_dev != st_old->st_dev || st.st_ino != st_old->st_ino || !S_ISREG(st.st_mode)) { errno = ESTALE; 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: if (errno == saved_errno) errno = EIO; return -1; } /* open() but with abort traps */ /* TODO: also support other things here than files. and then use, throughout the program. in particular, use of openat might help (split the path) (see: link attack mitigations throughout nvmutil) make it return, and handle the return value/errno (this could return e.g. EINTR) */ void xopen(int *fd_ptr, const char *path, int flags, struct stat *st) { if ((*fd_ptr = open(path, flags)) == -1) err(errno, "%s", path); if (fstat(*fd_ptr, st) == -1) err(errno, "%s: stat", path); if (!S_ISREG(st->st_mode)) err(errno, "%s: not a regular file", path); if (lseek(*fd_ptr, 0, SEEK_CUR) == (off_t)-1) err(errno, "%s: file not seekable", path); } /* fsync() the directory of a file, * useful for atomic writes */ 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}; #if defined(PATH_LEN) && \ (PATH_LEN) >= 256 maxlen = PATH_LEN; #else maxlen = 4096; #endif if (path == NULL) { errno = EFAULT; goto err_fsync_dir; } if (slen(path, maxlen, &pathlen) < 0) goto err_fsync_dir; if (pathlen >= maxlen || pathlen < 0) { errno = EMSGSIZE; goto err_fsync_dir; } if (pathlen == 0) { errno = EINVAL; goto err_fsync_dir; } dirbuf = malloc(pathlen + 1); if (dirbuf == NULL) { errno = ENOMEM; goto err_fsync_dir; } memcpy(dirbuf, 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 = 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; /* 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; if (!S_ISDIR(st.st_mode)) { errno = ENOTDIR; goto err_fsync_dir; } /* sync file on disk */ if (fsync_on_eintr(dirfd) == -1) goto err_fsync_dir; if (close_on_eintr(dirfd) == -1) { dirfd = -1; goto err_fsync_dir; } if (dirbuf != NULL) { free(dirbuf); dirbuf = NULL; } dirbuf = NULL; errno = saved_errno; return 0; err_fsync_dir: if (errno == saved_errno) errno = EIO; if (dirbuf != NULL) { free(dirbuf); dirbuf = NULL; } if (dirfd >= 0) { (void) close_on_eintr(dirfd); dirfd = -1; } return -1; } /* hardened tmpfile creation * * if not local, a standard world * writeable directory (e.g. /tmp) * will be used. otherwise, * the path is simply suffixed for * local tmp file (no world check) * * sets a file descriptor by pointer * fd, and returns the path as a * string (for your new tmp file) * * on error, the descriptor will be * set to -1 and errno will be set, * to indicate the error, and then * a NULL pointer will be returned. */ char * new_tmpfile(int *fd, int local, const char *path) { /* TODO: * directory support (currently only files) */ size_t maxlen; struct stat st; char suffix[] = "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; char *tmpdir = NULL; size_t dirlen; size_t pathlen; size_t destlen; size_t baselen; char *dest = NULL; /* final path */ /* for stat() in if (local) */ int fd_local_dir = -1; int saved_errno = errno; #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation in mkhtemp() */ int dirfd = -1; const char *fname = NULL; #endif struct path_split ps; if (fd == NULL) { errno = EFAULT; goto err_new_tmpfile; } /* block operating on * someone elses file */ if (*fd >= 0) { errno = EEXIST; goto err_new_tmpfile; /* they might * want it later! */ } /* but now it's *mine*: */ *fd = -1; #if defined(PATH_LEN) && \ (PATH_LEN) >= 256 maxlen = PATH_LEN; #else maxlen = 4096; #endif /* base dir e.g. /tmp */ if (!local) { #if defined(PERMIT_NON_STICKY_ALWAYS) && \ ((PERMIT_NON_STICKY_ALWAYS) > 0) tmpdir = env_tmpdir(PERMIT_NON_STICKY_ALWAYS); #else tmpdir = env_tmpdir(0); #endif if (tmpdir == NULL) goto err_new_tmpfile; } /* local means we want * e.g. hello.txt to then * have a tmpfile created * for it. useful for * atomic writes */ if (local) { if (path == NULL) { errno = EFAULT; goto err_new_tmpfile; } if (*path == '\0') { errno = EINVAL; goto err_new_tmpfile; } fd_local_dir = open_verified_dir(path); if (fd_local_dir < 0) goto err_new_tmpfile; if (fstat(fd_local_dir, &st) == -1) goto err_new_tmpfile; if (!S_ISREG(st.st_mode)) { errno = EBADF; goto err_new_tmpfile; } (void) close_on_eintr(fd_local_dir); fd_local_dir = -1; if (slen(path, maxlen, &pathlen) < 0) goto err_new_tmpfile; dirlen = 0; } else { if (slen(tmpdir, maxlen, &dirlen) < 0) goto err_new_tmpfile; pathlen = 0; } /* now we want the base dir, * with the file appended, * and the XXXXXXXXXX suffix */ /* using sizeof (not slen) adds an extra byte, * useful because we either want '.' or '/' */ destlen = dirlen + pathlen + sizeof(suffix); if (destlen > maxlen - 1) { /* -1 for NULL */ errno = EOVERFLOW; goto err_new_tmpfile; } dest = malloc(destlen + 1); /* +1 for NULL */ if (dest == NULL) { errno = ENOMEM; goto err_new_tmpfile; } /* As you see above, we only allow * either a base tmpdir and suffix, * or a user-supplied file and we * suffix that. */ if (dirlen > 0 && pathlen > 0) { errno = EINVAL; goto err_new_tmpfile; /* pre-emptive fix */ } if (local) { if (split_path(path, &ps) < 0) goto err_new_tmpfile; if (slen(ps.base, maxlen, &baselen) < 0) goto err_new_tmpfile; /* ALWAYS set this right after * split path, to avoid leaking fd: */ dirfd = ps.dirfd; *(dest) = '.'; memcpy(dest + 1, ps.base, baselen); memcpy(dest + 1 + baselen, suffix, sizeof(suffix) - 1); #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation * in mkhtemp( */ fname = dest + 1; #endif if (ps.buf != NULL) { free(ps.buf); ps.buf = NULL; } } else { memcpy(dest, tmpdir, dirlen); *(dest + dirlen) = '/'; memcpy(dest + dirlen + 1, suffix, sizeof(suffix) - 1); #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation in mkhtemp() */ dirfd = open_verified_dir(tmpdir); if (dirfd < 0) goto err_new_tmpfile; fname = dest + dirlen + 1; #endif } *(dest + destlen) = '\0'; #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation in mkhtemp() */ *fd = mkhtemp(fd, &st, dest, dirfd, fname); #else *fd = mkhtemp(fd, &st, dest); #endif if (*fd < 0) goto err_new_tmpfile; #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation in mkhtemp() */ if (dirfd >= 0) { (void) close_on_eintr(dirfd); dirfd = -1; } #endif errno = saved_errno; return dest; err_new_tmpfile: if (errno != saved_errno) saved_errno = errno; else saved_errno = errno = EIO; if (dest != NULL) { free(dest); dest = NULL; } #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* for openat dir replacement mitigation in mkhtemp() */ if (dirfd >= 0) { (void) close_on_eintr(dirfd); dirfd = -1; } #endif if (*fd >= 0) { (void) close_on_eintr(*fd); *fd = -1; } if (fd_local_dir >= 0) { (void) close_on_eintr(fd_local_dir); fd_local_dir = -1; } errno = saved_errno; return NULL; } /* hardened TMPDIR parsing */ char * env_tmpdir(int bypass_all_sticky_checks) { char *t; int allow_noworld_unsticky; int saved_errno = errno; t = getenv("TMPDIR"); if (t != NULL && *t != '\0') { if (tmpdir_policy(t, &allow_noworld_unsticky) == 0) { if (world_writeable_and_sticky(t, allow_noworld_unsticky, bypass_all_sticky_checks)) { errno = saved_errno; return t; } } } allow_noworld_unsticky = 0; if (world_writeable_and_sticky("/tmp", allow_noworld_unsticky, bypass_all_sticky_checks)) { errno = saved_errno; return "/tmp"; } if (world_writeable_and_sticky("/var/tmp", allow_noworld_unsticky, bypass_all_sticky_checks)) { errno = saved_errno; return "/var/tmp"; } if (errno == saved_errno) errno = EPERM; 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 = open_verified_dir(a); if (fd_a < 0) goto err_same_dir; fd_b = open_verified_dir(b); 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) { (void) close_on_eintr(fd_a); (void) close_on_eintr(fd_b); /* match */ errno = saved_errno; return 1; } (void) close_on_eintr(fd_a); (void) close_on_eintr(fd_b); success_same_dir: errno = saved_errno; return 0; err_same_dir: if (fd_a >= 0) (void) close_on_eintr(fd_a); if (fd_b >= 0) (void) close_on_eintr(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 = open_verified_dir(s); 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 openat(2) * 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 (is_owner(&st) < 0) goto sticky_hell; 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; /* definitely not sticky */ sticky_purgatory: /* maintainer caused regression */ err(errno, "sticky purgatory"); errno = EACCES; return -666; sticky_heaven: /* i like the one in hamburg better */ if (dirfd >= 0) (void) close_on_eintr(dirfd); errno = saved_errno; return 1; sticky_hell: if (errno == saved_errno) errno = EPERM; saved_errno = errno; if (dirfd >= 0) (void) close_on_eintr(dirfd); errno = saved_errno; return 0; } /* mk(h)temp - hardened mktemp. * like mkstemp, but (MUCH) harder. * TODO: * directory support (currently only * generates files) */ #if defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0) int mkhtemp(int *fd, struct stat *st, char *template) #else int mkhtemp(int *fd, struct stat *st, char *template, int dirfd, const char *fname) #endif { /* NOTE: this function currently * only supports *files*. * it doesn't make tmp*dirs* */ size_t retries = 0; #if !(defined(MAX_MKHTEMP_RETRIES) && \ (MAX_MKHTEMP_RETRIES) >= 128) size_t max_retries = 200; #else size_t max_retries = 128; #endif size_t chx = 0; size_t len = 0; char *p = NULL; char *template_copy = NULL; size_t xc = 0; static char ch[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; size_t limit = ((size_t)-1) - (((size_t)-1) % (sizeof(ch) - 1)); int rand_failures = 0; size_t r; #if defined(PATH_LEN) && \ (PATH_LEN) >= 256 size_t max_len = PATH_LEN; #else size_t max_len = 4096; #endif int file_created = 0; int saved_errno = errno; struct stat st_tmp; mode_t old_umask; int saved_rand_error = 0; #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) char *fname_copy = NULL; size_t fname_len = 0; /* for ctrl char check */ unsigned char ctrl = 0; size_t ctrl_pos = 0; if (fname == NULL) { errno = EFAULT; goto err_mkhtemp; } if (slen(fname, max_len, &fname_len) < 0) goto err_mkhtemp; if (fname_len == 0) { errno = EINVAL; goto err_mkhtemp; } if (dirfd < 0) { errno = EBADF; goto err_mkhtemp; } #endif if (fd == NULL || template == NULL) { errno = EFAULT; goto err_mkhtemp; } if (*fd >= 0) { errno = EEXIST; goto err_mkhtemp; } if (slen(template, max_len, &len) < 0) goto err_mkhtemp; /* bounds check */ if (len >= max_len) { errno = EMSGSIZE; goto err_mkhtemp; } #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) if (strrchr(fname, '/') != NULL) { /* otherwise, a mangled path could leave the directory, defeating the purpose of openat */ errno = EINVAL; goto err_mkhtemp; } /* reject dangerous basenames */ if (fname[0] == '\0' || (fname[0] == '.' && fname[1] == '\0') || (fname[0] == '.' && fname[1] == '.' && fname[2] == '\0')) { errno = EINVAL; goto err_mkhtemp; } /* block control chars */ for (ctrl_pos = 0; ctrl_pos < fname_len; ctrl_pos++) { ctrl = (unsigned char)fname[ctrl_pos]; if (ctrl < 32 || ctrl == 127) { errno = EINVAL; goto err_mkhtemp; } } #endif p = template + len; while (p > template && *--p == 'X') ++xc; if (xc < 6 || xc > len) { errno = EINVAL; goto err_mkhtemp; } #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) if (fname_len > len || fname_len > (len - xc)) { /* prevent overflow */ errno = EOVERFLOW; goto err_mkhtemp; } if (memcmp(fname, template + len - fname_len, fname_len) != 0) { errno = EINVAL; goto err_mkhtemp; } #endif template_copy = malloc(len + 1); if (template_copy == NULL) { errno = ENOMEM; goto err_mkhtemp; } /* we work on a cached copy first, * to avoid partial writes of the * input under fault conditions */ memcpy(template_copy, template, len + 1); #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) /* redundant copy, reduce chance of * mangling (regression mitigation) */ fname_copy = malloc(fname_len + 1); if (fname_copy == NULL) { errno = ENOMEM; goto err_mkhtemp; } memcpy(fname_copy, template_copy + len - fname_len, fname_len + 1); p = fname_copy + fname_len - xc; #else p = template_copy + len - xc; #endif for (retries = 0; retries < max_retries; retries++) { for (chx = 0; chx < xc; chx++) { /* clamp rand to prevent modulo bias * (reduced risk of entropy leak) */ 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; } rand_failures = 0; errno = saved_rand_error; } while (r >= limit); p[chx] = ch[r % (sizeof(ch) - 1)]; } #if defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0) /* openat(2) added to linux in 2006, BSDs later */ *fd = open(template_copy, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, 0600); #else /* use the file descriptor instead. (mitigate directory replacement attacks) */ *fd = openat(dirfd, fname_copy, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, 0600); #endif if (*fd >= 0) { file_created = 1; #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) memcpy(template_copy + len - fname_len, fname_copy, fname_len); #endif if (secure_file(fd, st, O_APPEND, 1, 1, 0600) < 0) goto err_mkhtemp; memcpy( template + len + 1 - xc, template_copy + len + 1 - xc, xc); if (template_copy != NULL) { free(template_copy); template_copy = NULL; } errno = saved_errno; /* thunder room secure */ return *fd; } if (errno != EEXIST && errno != EINTR && errno != EAGAIN) goto err_mkhtemp; } err_mkhtemp: saved_errno = errno; if (*fd >= 0) { (void) close_on_eintr(*fd); *fd = -1; /* ^^^^^ the caller gives us a dir, for use and we write in it but *we* create their file touch their *fd, not dirfd as we only initialised *fd */ } if (template_copy != NULL) { /* we created it, so *we* nuke it */ #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) if (file_created && fname_copy != NULL) (void) unlinkat(dirfd, fname_copy, 0); #else if (file_created) (void) unlink(template_copy); #endif free(template_copy); template_copy = NULL; } #if !(defined(DISABLE_OPENAT) && \ ((DISABLE_OPENAT) > 0)) if (fname_copy != NULL) { free(fname_copy); fname_copy = NULL; } #endif errno = saved_errno; /* returning EINTR/EAGAIN ourselves means that a caller could implement the same wait loop */ if (errno != EEXIST && errno != EINTR && errno != EAGAIN) { if (errno == saved_errno) errno = ECANCELED; } 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? TODO: consider set_flags, complementing bad_flags, for setting new flags; with another option to set it before the bad_flags check, or after it (because some callers may be setting flags given to them with theirs OR'd in, yet still want to filter bad flags, whereas others may not want to modify flags on a file that already contains specific flags) */ int secure_file(int *fd, struct stat *st, int bad_flags, int check_seek, int do_lock, mode_t mode) { int flags; int saved_errno = errno; if (fd == NULL) { errno = EFAULT; goto err_secure_file; } if (*fd < 0) { errno = EBADF; goto err_secure_file; } if (st == NULL) { errno = EFAULT; goto err_secure_file; } flags = fcntl(*fd, F_GETFL); if (flags == -1) goto err_secure_file; if (bad_flags > 0) { /* * For example: * O_APPEND would permit offsets * to be ignored, which breaks * positional read/write * * You might provide that as * the mask to this function, * before doing positional i/o */ if (flags & bad_flags) { errno = EPERM; goto err_secure_file; } } if (fstat(*fd, st) == -1) goto err_secure_file; /* * Extremely defensive * likely pointless checks */ /* check if it's a file */ if (!S_ISREG(st->st_mode)) { errno = EBADF; goto err_secure_file; } if (check_seek) { /* check if it's seekable */ if (lseek(*fd, 0, SEEK_CUR) == (off_t)-1) goto err_secure_file; } /* tmpfile re-linked, or unlinked, while opened */ if (st->st_nlink != 1) { errno = ELOOP; goto err_secure_file; } /* block if we don't own the file * (exception made for root) */ if (st->st_uid != geteuid() && /* someone else's file */ geteuid() != 0) { /* override for root */ errno = EPERM; goto err_secure_file; } if (is_owner(st) < 0) goto err_secure_file; /* 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_secure_file; } if (do_lock) { if (lock_file(*fd, flags) == -1) goto err_secure_file; } if (fchmod(*fd, mode) == -1) goto err_secure_file; errno = saved_errno; return 0; err_secure_file: if (errno == saved_errno) errno = EIO; return -1; } 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; } /* * Safe I/O functions wrapping around * read(), write() and providing a portable * analog of both pread() and pwrite(). * These functions are designed for maximum * robustness, checking NULL inputs, overflowed * outputs, and all kinds of errors that the * standard libc functions don't. * * Looping on EINTR and EAGAIN is supported. * EINTR/EAGAIN looping is done indefinitely. */ /* 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; 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: if (errno == saved_errno) errno = EIO; return -1; } /* prw() - portable read-write with more * safety checks than barebones libc * * portable pwrite/pread on request, or real * pwrite/pread libc functions can be used. * the portable (non-libc) pread/pwrite is not * thread-safe, because it does not prevent or * mitigate race conditions on file descriptors * * 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 */ 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; 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(fd, mem, nrw); else if (rw_type == IO_READ) r = read(fd, mem, nrw); #if defined(REAL_POS_IO) && \ REAL_POS_IO > 0 else if (rw_type == IO_PWRITE) r = pwrite(fd, mem, nrw, off); else if (rw_type == IO_PREAD) r = pread(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(fd, mem, nrw); else if (rw_type == IO_PWRITE) r = write(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: if (errno == saved_errno) errno = EIO; return -1; } int io_args(int fd, void *mem, size_t nrw, off_t off, int rw_type) { int saved_errno = errno; /* obviously */ if (mem == NULL) { errno = EFAULT; goto err_io_args; } /* uninitialised fd */ if (fd < 0) { errno = EBADF; goto err_io_args; } /* negative offset */ if (off < 0) { errno = ERANGE; goto err_io_args; } /* prevent zero-byte rw */ if (!nrw) goto err_io_args; /* prevent overflow */ if (nrw > (size_t)SSIZE_MAX) { errno = ERANGE; goto err_io_args; } /* prevent overflow */ if (((size_t)off + nrw) < (size_t)off) { errno = ERANGE; goto err_io_args; } if (rw_type > IO_PWRITE) { errno = EINVAL; goto err_io_args; } errno = saved_errno; return 0; err_io_args: if (errno == saved_errno) errno = EINVAL; return -1; } int check_file(int fd, struct stat *st) { int saved_errno = errno; if (fstat(fd, st) == -1) goto err_is_file; if (!S_ISREG(st->st_mode)) { errno = EBADF; goto err_is_file; } errno = saved_errno; return 0; err_is_file: if (errno == saved_errno) errno = EINVAL; return -1; } /* POSIX can say whatever it wants. * specification != implementation */ ssize_t rw_over_nrw(ssize_t r, size_t nrw) { int saved_errno = errno; /* not a libc bug, but we * don't like the number zero */ if (!nrw) goto err_rw_over_nrw; if (r == -1) return r; if ((size_t) r > SSIZE_MAX) { /* Theoretical buggy libc * check. Extremely academic. * * Specifications never * allow this return value * to exceed SSIZE_T, but * spec != implementation * * Check this after using * [p]read() or [p]write() * * NOTE: here, we assume * ssize_t integers are the * same size as SSIZE_T */ errno = ERANGE; goto err_rw_over_nrw; } /* Theoretical buggy libc: * Should never return a number of * bytes above the requested length. */ if ((size_t)r > nrw) { errno = ERANGE; goto err_rw_over_nrw; } errno = saved_errno; return r; err_rw_over_nrw: if (errno == saved_errno) errno = EIO; return -1; } #if !defined(REAL_POS_IO) || \ REAL_POS_IO < 1 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_eagain, EAGAIN))); return old; } #endif int try_err(int loop_err, int errval) { if (loop_err) return errval; return -1; } int close_on_eintr(int fd) { int r; int saved_errno = errno; do { r = close(fd); } while (r == -1 && (errno == EINTR || errno == EAGAIN)); if (r >= 0) errno = saved_errno; return r; } int fsync_on_eintr(int fd) { int r; int saved_errno = errno; do { r = fsync(fd); } while (r == -1 && (errno == EINTR || errno == EAGAIN)); if (r >= 0) errno = saved_errno; return r; }