From afcd535816b45fd7b0e0d07c1a8580f6f462f5e4 Mon Sep 17 00:00:00 2001 From: Leah Rowe Date: Mon, 23 Mar 2026 23:31:00 +0000 Subject: mkhtemp: split library into its own file Signed-off-by: Leah Rowe --- util/nvmutil/lib/file.c | 1150 -------------------------------------------- util/nvmutil/lib/mkhtemp.c | 1133 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1133 insertions(+), 1150 deletions(-) create mode 100644 util/nvmutil/lib/mkhtemp.c (limited to 'util/nvmutil/lib') diff --git a/util/nvmutil/lib/file.c b/util/nvmutil/lib/file.c index 6546c252..ea2bcd0b 100644 --- a/util/nvmutil/lib/file.c +++ b/util/nvmutil/lib/file.c @@ -5,10 +5,6 @@ * Be nice to the demon. */ -#if defined(__linux__) && !defined(_GNU_SOURCE) -/* for openat2 syscall on linux */ -#define _GNU_SOURCE 1 -#endif #include #include @@ -242,1152 +238,6 @@ err_fsync_dir: return -1; } -/* hardened tmpfile and tmpdir creation. - * obsessively hardened, to the point that - * you could even say it is unhinged. - * - * much stricter than mkstemp. - * - * userspace sandboxing. TOCTOU-aware, - * much stricter than typical libc/mkstemp. - * TODO: write docs! right now the documentation - * is both tthe code and the specification. - * this code will likely be hardened more, over - * time, until a matured stable release can be - * made. i intend for this to go in linux distros - * and BSD source trees eventually, as a standalone - * utility. - * - * NOTE TO LINUX DISTROS / BSDs: more testing - * and auditing needed before putting this - * in your project. this comment will be removed - * after the code has matured for a while. this - * is a rough implementation, already carefully - * audited by one person: me - * - * expect bugs. one day, when this is ready, - * i will make a lot more of the options configurable - * at runtime, and add a special compatibility mode - * so that it can also run like regular mktemp, - * for compatibility; running mkhtemp with all the - * hardening means you get very non-standard behaviour, - * e.g. it's much stricter about ownership/permissions, - * sticky bits, and regards as fault conditions, what - * would be considered normal in mkstemp/mktemp. - * - * a full manual and specification will be written when - * this code is fully matured, and then i will probably - * start spamming your mailing lists myself ;) - */ - -/* returns opened fd, sets fd and *path at pointers *fd and *path */ -/* also sets external stat */ - -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); -} - -static 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[] = - "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; - char *tmpdir = NULL; - - int close_errno; - 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; - - 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); -#else - tmpdir = env_tmpdir(0); -#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; - - if (dirfd >= 0) { - close_errno = errno; - (void) close_on_eintr(dirfd); - errno = close_errno; - dirfd = -1; - } - - 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; - } - - if (dirfd >= 0) { - close_errno = errno; - (void) close_on_eintr(dirfd); - errno = close_errno; - dirfd = -1; - } - - if (*fd >= 0) { - close_errno = errno; - (void) close_on_eintr(*fd); - errno = close_errno; - *fd = -1; - } - - errno = saved_errno; - return -1; -} - - -/* 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) { - errno = EPERM; - return NULL; /* errno already set */ - } - - if (!world_writeable_and_sticky(t, - allow_noworld_unsticky, - bypass_all_sticky_checks)) { - errno = EPERM; - return NULL; - } - - 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 = 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) { - - (void) close_on_eintr(fd_a); - (void) close_on_eintr(fd_b); - -success_same_dir: - - /* SUCCESS - */ - - errno = saved_errno; - return 1; - } - - (void) close_on_eintr(fd_a); - (void) close_on_eintr(fd_b); - - /* FAILURE (logical) - */ - - errno = saved_errno; - return 0; - -err_same_dir: - - /* FAILURE (probably syscall) - */ - - 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 = 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 */ - - 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. - * - * designed to resist TOCTOU attacsk - * e.g. directory race / symlink attack - * - * extremely strict and even implements - * some limited userspace-level sandboxing, - * similar to openbsd unveil (which you - * can also use with this in your program) - * - * 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 < 12 || 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: - if (*fd >= 0) { - close_errno = errno; - (void)close_on_eintr(*fd); - errno = close_errno; - *fd = -1; - } - -success: - - if (fname_copy != NULL) - free(fname_copy); - - return (*fd >= 0) ? *fd : -1; -} - -static 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 close_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_errno = errno; - - if (fd != NULL && *fd >= 0) { - (void) close_on_eintr(*fd); - *fd = -1; - } - - if (file_created) - (void) unlinkat(dirfd, fname_copy, 0); - - if (dir_created) - (void) unlinkat(dirfd, fname_copy, AT_REMOVEDIR); - - errno = close_errno; - 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; -} - /* * Safe I/O functions wrapping around * read(), write() and providing a portable diff --git a/util/nvmutil/lib/mkhtemp.c b/util/nvmutil/lib/mkhtemp.c new file mode 100644 index 00000000..2fcb894e --- /dev/null +++ b/util/nvmutil/lib/mkhtemp.c @@ -0,0 +1,1133 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2026 Leah Rowe + * + * Hardened mktemp (be nice to the demon). + */ + +#if defined(__linux__) && !defined(_GNU_SOURCE) +/* for openat2 syscall on linux */ +#define _GNU_SOURCE 1 +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include + +/* for openat2: */ +#ifdef __linux__ +#include +#include +#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); +} + +static 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[] = + "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + char *tmpdir = NULL; + + int close_errno; + 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; + + 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); +#else + tmpdir = env_tmpdir(0); +#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; + + if (dirfd >= 0) { + close_errno = errno; + (void) close_on_eintr(dirfd); + errno = close_errno; + dirfd = -1; + } + + 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; + } + + if (dirfd >= 0) { + close_errno = errno; + (void) close_on_eintr(dirfd); + errno = close_errno; + dirfd = -1; + } + + if (*fd >= 0) { + close_errno = errno; + (void) close_on_eintr(*fd); + errno = close_errno; + *fd = -1; + } + + errno = saved_errno; + return -1; +} + + +/* 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) { + errno = EPERM; + return NULL; /* errno already set */ + } + + if (!world_writeable_and_sticky(t, + allow_noworld_unsticky, + bypass_all_sticky_checks)) { + errno = EPERM; + return NULL; + } + + 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 = 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) { + + (void) close_on_eintr(fd_a); + (void) close_on_eintr(fd_b); + +success_same_dir: + + /* SUCCESS + */ + + errno = saved_errno; + return 1; + } + + (void) close_on_eintr(fd_a); + (void) close_on_eintr(fd_b); + + /* FAILURE (logical) + */ + + errno = saved_errno; + return 0; + +err_same_dir: + + /* FAILURE (probably syscall) + */ + + 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 = 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 */ + + 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. + * + * designed to resist TOCTOU attacsk + * e.g. directory race / symlink attack + * + * extremely strict and even implements + * some limited userspace-level sandboxing, + * similar to openbsd unveil (which you + * can also use with this in your program) + * + * 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 < 12 || 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: + if (*fd >= 0) { + close_errno = errno; + (void)close_on_eintr(*fd); + errno = close_errno; + *fd = -1; + } + +success: + + if (fname_copy != NULL) + free(fname_copy); + + return (*fd >= 0) ? *fd : -1; +} + +static 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 close_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_errno = errno; + + if (fd != NULL && *fd >= 0) { + (void) close_on_eintr(*fd); + *fd = -1; + } + + if (file_created) + (void) unlinkat(dirfd, fname_copy, 0); + + if (dir_created) + (void) unlinkat(dirfd, fname_copy, AT_REMOVEDIR); + + errno = close_errno; + 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; +} -- cgit v1.2.1