From 6838db4647b600bf5b356429f54850bf801e7ba4 Mon Sep 17 00:00:00 2001 From: Leah Rowe Date: Fri, 20 Mar 2026 04:02:51 +0000 Subject: WIP: hardened mktemp i'm pretty much nearly there. still no dir support, only files. i won't keep amending now - will do more, then squash later. Signed-off-by: Leah Rowe --- util/nvmutil/lib/file.c | 1799 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1516 insertions(+), 283 deletions(-) (limited to 'util/nvmutil/lib/file.c') diff --git a/util/nvmutil/lib/file.c b/util/nvmutil/lib/file.c index b4925ccd..e722cb6a 100644 --- a/util/nvmutil/lib/file.c +++ b/util/nvmutil/lib/file.c @@ -24,16 +24,29 @@ same_file(int fd, struct stat *st_old, struct stat st; int saved_errno = errno; - if (st_old == NULL || fd < 0) + /* 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) - return -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)) + !S_ISREG(st.st_mode)) { + + errno = ESTALE; goto err_same_file; + } if (check_size && st.st_size != st_old->st_size) @@ -44,13 +57,24 @@ same_file(int fd, struct stat *st_old, err_same_file: - errno = EIO; + if (errno == saved_errno) + errno = ESTALE; + 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) { @@ -76,30 +100,32 @@ fsync_dir(const char *path) { int saved_errno = errno; - unsigned long pathlen; - unsigned long maxlen; - - char *dirbuf; - int dirfd; + size_t pathlen = 0; + size_t maxlen = 0; - char *slash; + char *dirbuf = NULL; + int dirfd = -1; - struct stat st; + char *slash = NULL; + struct stat st = {0}; #if defined(PATH_LEN) && \ (PATH_LEN) >= 256 maxlen = PATH_LEN; #else - maxlen = 1024; + maxlen = 4096; #endif - dirbuf = NULL; - dirfd = -1; + if (path == NULL) { + errno = EFAULT; + goto err_fsync_dir; + } - pathlen = xstrxlen(path, maxlen); + if (slen(path, maxlen, &pathlen) < 0) + goto err_fsync_dir; - if (pathlen >= maxlen) { - fprintf(stderr, "Path too long for fsync_parent_dir\n"); + if (pathlen >= maxlen || pathlen < 0) { + errno = EMSGSIZE; goto err_fsync_dir; } @@ -110,8 +136,11 @@ fsync_dir(const char *path) } dirbuf = malloc(pathlen + 1); - if (dirbuf == NULL) + if (dirbuf == NULL) { + + errno = ENOMEM; goto err_fsync_dir; + } memcpy(dirbuf, path, pathlen + 1); slash = strrchr(dirbuf, '/'); @@ -127,7 +156,7 @@ fsync_dir(const char *path) dirbuf[1] = '\0'; } - dirfd = open(dirbuf, O_RDONLY + dirfd = open(dirbuf, O_RDONLY | O_CLOEXEC | O_NOCTTY #ifdef O_DIRECTORY | O_DIRECTORY #endif @@ -135,14 +164,26 @@ fsync_dir(const char *path) | O_NOFOLLOW #endif ); - if (dirfd == -1) + 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)) { - fprintf(stderr, "%s: not a directory\n", dirbuf); + + errno = ENOTDIR; goto err_fsync_dir; } @@ -150,316 +191,1420 @@ fsync_dir(const char *path) if (fsync_on_eintr(dirfd) == -1) goto err_fsync_dir; - if (close_on_eintr(dirfd) == -1) + if (close_on_eintr(dirfd) == -1) { + + dirfd = -1; goto err_fsync_dir; + } + + if (dirbuf != NULL) { - if (dirbuf != NULL) free(dirbuf); + dirbuf = NULL; + } + + dirbuf = NULL; errno = saved_errno; return 0; err_fsync_dir: - if (!errno) + + if (errno == saved_errno) errno = EIO; - if (errno != saved_errno) - fprintf(stderr, "%s: %s\n", path, strerror(errno)); + if (dirbuf != NULL) { - if (dirbuf != NULL) free(dirbuf); + dirbuf = NULL; + } - if (dirfd > -1) - close_on_eintr(dirfd); + if (dirfd >= 0) { - errno = saved_errno; + (void) close_on_eintr(dirfd); + dirfd = -1; + } return -1; } -/* returns ptr to path (string). if local>0: - * make tmpfile in the same directory as the - * file. if local==0, use TMPDIR +/* 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) * - * if local==0, the 3rd argument is ignored + * 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) +new_tmpfile(int *fd, int local, + const char *path) { - unsigned long maxlen; +/* TODO: + * directory support (currently only files) + */ + size_t maxlen; struct stat st; - /* please do not modify the - * strings or I will get mad - */ - char tmp_none[] = ""; - char tmp_default[] = "/tmp"; - char default_tmpname[] = "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; - char *tmpname; + char suffix[] = + "tmpXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + char *tmpdir = NULL; + + size_t dirlen; + size_t pathlen; + size_t destlen; + size_t baselen; + + char *dest = NULL; /* final path */ + + 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; + + /* open the directory early, + * check via fd throughout, + * for directory replacement + * attack mitigation + */ + struct stat st_dir_initial; +#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; + } + + if (slen(path, maxlen, &pathlen) < 0) + goto err_new_tmpfile; + + if (path[pathlen - 1] == '/') { + errno = EINVAL; + 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; + + /* we will use this later, throughout, + * for detecting **directory replacement** + */ + if (fstat(dirfd, &st_dir_initial) < 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, &st_dir_initial); +#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; + } + + 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); + +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 = 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_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, + struct stat *st_dir_initial) +#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; + + /* in openat mode, we re-check + directory against previous + check done by the caller, + * mitigating symlink attacks + */ + struct stat st_dir_now; + + if (fname == NULL || + st_dir_initial == 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. + * (danach prüfen wir die Sicherheit des Verzeichnisses) + */ + if (fstat(dirfd, &st_dir_now) < 0) + goto err_mkhtemp; + /* + * mitigate symlink attacks before open + */ + if (st_dir_now.st_dev != st_dir_initial->st_dev || + st_dir_now.st_ino != st_dir_initial->st_ino) { + errno = ESTALE; + goto err_mkhtemp; + } + + *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; + + /* copy replaced characters + */ + memcpy( + template + len + xc, + template_copy + len + 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); - char *base = NULL; - char *dest = NULL; + for ( ; len > 1 && + ps->buf[len - 1] == '/'; len--) + ps->buf[len - 1] = '\0'; - unsigned long tmpdir_len = 0; - unsigned long tmpname_len = 0; - unsigned long tmppath_len = 0; + slash = strrchr(ps->buf, '/'); - int fd_tmp = -1; - int flags; + if (slash) { -#if defined(PATH_LEN) && \ - (PATH_LEN) >= 256 - maxlen = PATH_LEN; -#else - maxlen = 1024; -#endif + *slash = '\0'; + ps->base = slash + 1; - tmpname = default_tmpname; - if (local) { - if (path == NULL) - goto err_new_tmpfile; - if (*path == '\0') - goto err_new_tmpfile; + if (*ps->buf == '\0') { - if (stat(path, &st) == -1) - goto err_new_tmpfile; + ps->buf[0] = '/'; + ps->buf[1] = '\0'; + } + } else { - if (!S_ISREG(st.st_mode)) - goto err_new_tmpfile; + ps->base = ps->buf; - tmpname = (char *)path; + ps->buf[0] = '.'; + ps->buf[1] = '\0'; } - if (local) { - base = tmp_none; + ps->dirfd = open_verified_dir(ps->buf); + if (ps->dirfd < 0) + goto err_split_path; - /* appended to filename for tmp: - */ - tmpdir_len = xstrxlen(default_tmpname, maxlen); - } else { - base = get_tmpdir(); + errno = saved_errno; + + return 0; + +err_split_path: - if (base == NULL) - base = tmp_default; - if (*base == '\0') - base = tmp_default; + saved_errno = errno; - tmpdir_len = xstrxlen(base, maxlen); + if (ps->buf != NULL) { + free(ps->buf); + ps->buf = NULL; } - tmpname_len = xstrxlen(tmpname, maxlen); + if (ps->dirfd >= 0) { + (void) close_on_eintr(ps->dirfd); + ps->dirfd = -1; + } - tmppath_len = tmpdir_len + tmpname_len; - ++tmppath_len; /* for '/' or '.' */ + errno = saved_errno; - /* max length -1 of maxlen - * for termination - */ - if (tmpdir_len > maxlen - tmpname_len - 1) - goto err_new_tmpfile; + if (errno == saved_errno) + errno = EIO; /* likely open/check_dirfd */ - /* +1 for NULL */ - dest = malloc(tmppath_len + 1); - if (dest == NULL) - goto err_new_tmpfile; + return -1; +} - if (local) { +/* 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; - *dest = '.'; /* hidden file */ + fd = open(path, O_RDONLY | O_DIRECTORY | + O_CLOEXEC | O_NOCTTY +#ifdef O_NOFOLLOW + | O_NOFOLLOW +#endif + ); - memcpy(dest + (unsigned long)1, tmpname, tmpname_len); + if (fd < 0) + return -1; - memcpy(dest + (unsigned long)1 + tmpname_len, - default_tmpname, tmpdir_len); - } else { + errno = saved_errno; - memcpy(dest, base, tmpdir_len); + if (check_dirfd(fd, path) < 0) { - dest[tmpdir_len] = '/'; + saved_errno = errno; + (void) close_on_eintr(fd); - memcpy(dest + tmpdir_len + 1, tmpname, tmpname_len); + errno = saved_errno; + return -1; } - dest[tmppath_len] = '\0'; + 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; - fd_tmp = mkstemp_n(dest); - if (fd_tmp == -1) - goto err_new_tmpfile; + int saved_errno = errno; - if (fchmod(fd_tmp, 0600) == -1) - goto err_new_tmpfile; + if (dirfd < 0) { + errno = EBADF; + goto err_check_dirfd; + } - flags = fcntl(fd_tmp, F_GETFL); + if (path == NULL) { + errno = EFAULT; + goto err_check_dirfd; + } - if (flags == -1) - goto err_new_tmpfile; + if (fstat(dirfd, &st_fd) < 0) + goto err_check_dirfd; +#if !(defined(DISABLE_OPENAT) && \ + ((DISABLE_OPENAT) > 0)) /* - * O_APPEND would permit offsets - * to be ignored, which breaks - * positional read/write + * 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 (flags & O_APPEND) - goto err_new_tmpfile; + 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 (lock_file(fd_tmp, flags) == -1) - goto err_new_tmpfile; + if (st_fd.st_dev != st_path.st_dev || + st_fd.st_ino != st_path.st_ino) { - if (fstat(fd_tmp, &st) == -1) - goto err_new_tmpfile; + errno = ENOENT; + goto err_check_dirfd; + } - /* - * Extremely defensive - * likely pointless checks - */ + errno = saved_errno; - /* check if it's a file */ - if (!S_ISREG(st.st_mode)) - goto err_new_tmpfile; + return 0; - /* check if it's seekable */ - if (lseek(fd_tmp, 0, SEEK_CUR) == (off_t)-1) - goto err_new_tmpfile; +err_check_dirfd: - /* tmpfile has >1 hardlinks */ - if (st.st_nlink > 1) - goto err_new_tmpfile; + if (errno == saved_errno) + errno = EPERM; /* context: symlink attack */ - /* tmpfile unlinked while opened */ - if (st.st_nlink == 0) - goto err_new_tmpfile; + return -1; +} - *fd = fd_tmp; +/* 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; - return dest; + if (fd == NULL) { + errno = EFAULT; + goto err_secure_file; + } + if (*fd < 0) { + errno = EBADF; + goto err_secure_file; + } -err_new_tmpfile: + if (st == NULL) { + errno = EFAULT; + goto err_secure_file; + } - if (dest != NULL) - free(dest); + flags = fcntl(*fd, F_GETFL); - if (fd_tmp > -1) - close_on_eintr(fd_tmp); + if (flags == -1) + goto err_secure_file; - return NULL; -} + if (bad_flags > 0) { -int -lock_file(int fd, int flags) -{ - struct flock fl; + /* + * 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; + } + } - memset(&fl, 0, sizeof(fl)); + if (fstat(*fd, st) == -1) + goto err_secure_file; - if ((flags & O_ACCMODE) == O_RDONLY) - fl.l_type = F_RDLCK; - else - fl.l_type = F_WRLCK; + /* + * Extremely defensive + * likely pointless checks + */ - fl.l_whence = SEEK_SET; + /* check if it's a file */ + if (!S_ISREG(st->st_mode)) { + errno = EBADF; + goto err_secure_file; + } - if (fcntl(fd, F_SETLK, &fl) == -1) - return -1; + if (check_seek) { - return 0; -} + /* check if it's seekable + */ + if (lseek(*fd, 0, SEEK_CUR) == (off_t)-1) + goto err_secure_file; + } -/* return TMPDIR, or fall back - * to portable defaults - */ + /* tmpfile re-linked, or + unlinked, while opened + */ + if (st->st_nlink != 1) { + errno = ELOOP; + goto err_secure_file; + } -char * -get_tmpdir(void) -{ - char *t; - struct stat st; + /* 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 */ - t = getenv("TMPDIR"); + errno = EPERM; + goto err_secure_file; + } + if (is_owner(st) < 0) + goto err_secure_file; - if (t && *t) { + /* 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 (stat(t, &st) == 0 && S_ISDIR(st.st_mode)) { + if (do_lock) { + if (lock_file(*fd, flags) == -1) + goto err_secure_file; + } - if ((st.st_mode & S_IWOTH) && !(st.st_mode & S_ISVTX)) - return NULL; + if (fchmod(*fd, mode) == -1) + goto err_secure_file; - return t; - } - } + errno = saved_errno; + return 0; - if (stat("/tmp", &st) == 0 && S_ISDIR(st.st_mode)) - return "/tmp"; +err_secure_file: - if (stat("/var/tmp", &st) == 0 && S_ISDIR(st.st_mode)) - return "/var/tmp"; + if (errno == saved_errno) + errno = EIO; - return "."; + return -1; } -/* portable mkstemp - */ - int -mkstemp_n(char *template) +is_owner(struct stat *st) { - int fd; - unsigned long i, j; - unsigned long len; - char *p; - - unsigned long xc = 0; + if (st == NULL) { - static char ch[] = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + errno = EFAULT; + return -1; + } - unsigned long r; -#if defined(PATH_LEN) && \ - (PATH_LEN) >= 256 - unsigned long max_len = PATH_LEN; +#if ALLOW_ROOT_OVERRIDE + if (st->st_uid != geteuid() && /* someone else's file */ + geteuid() != 0) { /* override for root */ #else - unsigned long max_len = 4096; -#endif - - len = xstrxlen(template, max_len); - - if (len < 6) { - errno = EINVAL; + if (st->st_uid != geteuid()) { /* someone else's file */ +#endif /* and no root override */ + errno = EPERM; return -1; } - p = template + len; + return 0; +} - while (p > template && p[-1] == 'X') { - --p; - ++xc; +int +lock_file(int fd, int flags) +{ + struct flock fl; + int saved_errno = errno; + + if (fd < 0) { + errno = EBADF; + goto err_lock_file; } - if (xc < 6) { + if (flags < 0) { errno = EINVAL; - return -1; + goto err_lock_file; } - for (i = 0; i < 200; i++) { + memset(&fl, 0, sizeof(fl)); - for (j = 0; j < xc; j++) { + if ((flags & O_ACCMODE) == O_RDONLY) + fl.l_type = F_RDLCK; + else + fl.l_type = F_WRLCK; - r = rlong(); + fl.l_whence = SEEK_SET; - p[j] = ch[(unsigned long)(r >> 1) % (sizeof(ch) - 1)]; - } + if (fcntl(fd, F_SETLK, &fl) == -1) + goto err_lock_file; - fd = open(template, - O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + saved_errno = errno; + return 0; - if (fd >= 0) - return fd; +err_lock_file: - if (errno != EEXIST) - return -1; - } + if (errno == saved_errno) + errno = EIO; - errno = EEXIST; return -1; } @@ -493,21 +1638,23 @@ mkstemp_n(char *template) * otherwise it will return an error. */ -long -rw_file_exact(int fd, unsigned char *mem, unsigned long nrw, +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, unsigned long max_retries, + int loop_eintr, size_t max_retries, int off_reset) { - long rval; - long rc; + ssize_t rval; + ssize_t rc; - unsigned long nrw_cur; + size_t nrw_cur; off_t off_cur; void *mem_cur; - unsigned long retries_on_zero; + size_t retries_on_zero; + + int saved_errno = errno; rval = 0; @@ -515,22 +1662,28 @@ rw_file_exact(int fd, unsigned char *mem, unsigned long nrw, retries_on_zero = 0; if (io_args(fd, mem, nrw, off, rw_type) == -1) - return -1; + goto err_rw_file_exact; while (1) { /* Prevent theoretical overflow */ - if (rval >= 0 && (unsigned long)rval > (nrw - rc)) + if (rval >= 0 && (size_t)rval > (nrw - rc)) { + errno = EOVERFLOW; goto err_rw_file_exact; + } rc += rval; - if ((unsigned long)rc >= nrw) + if ((size_t)rc >= nrw) break; - mem_cur = (void *)(mem + (unsigned long)rc); - nrw_cur = (unsigned long)(nrw - (unsigned long)rc); - if (off < 0) + 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, @@ -538,24 +1691,38 @@ rw_file_exact(int fd, unsigned char *mem, unsigned long nrw, off_reset); if (rval < 0) - return -1; + 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 ((unsigned long)rc != nrw) + 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; - return rw_over_nrw(rc, nrw); + errno = saved_errno; + + return rval; err_rw_file_exact: - errno = EIO; + + if (errno == saved_errno) + errno = EIO; + return -1; } @@ -569,7 +1736,7 @@ err_rw_file_exact: * mitigate race conditions on file descriptors * * If you need real pwrite/pread, just compile - * with flag: HAVE_REAL_PREAD_PWRITE=1 + * with flag: REAL_POS_IO=1 * * A fallback is provided for regular read/write. * rw_type can be IO_READ (read), IO_WRITE (write), @@ -587,27 +1754,27 @@ err_rw_file_exact: * off_reset 0: never reset if changed */ -long -prw(int fd, void *mem, unsigned long nrw, +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) { - long r; + ssize_t rval; + ssize_t r; int positional_rw; struct stat st; -#if !defined(HAVE_REAL_PREAD_PWRITE) || \ - HAVE_REAL_PREAD_PWRITE < 1 - int saved_errno; +#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) { - return -1; - } + == -1) + goto err_prw; r = -1; @@ -621,7 +1788,7 @@ prw(int fd, void *mem, unsigned long nrw, */ if (check_file(fd, &st) == -1) - return -1; + goto err_prw; } if (rw_type >= IO_PREAD) @@ -632,16 +1799,16 @@ prw(int fd, void *mem, unsigned long nrw, try_rw_again: if (!positional_rw) { -#if defined(HAVE_REAL_PREAD_PWRITE) && \ - HAVE_REAL_PREAD_PWRITE > 0 +#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(HAVE_REAL_PREAD_PWRITE) && \ - HAVE_REAL_PREAD_PWRITE > 0 +#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) @@ -652,11 +1819,17 @@ real_pread_pwrite: || errno == try_err(loop_eagain, EAGAIN))) goto try_rw_again; - return rw_over_nrw(r, nrw); + rval = rw_over_nrw(r, nrw); + if (rval < 0) + goto err_prw; + + errno = saved_errno; + + return rval; } -#if defined(HAVE_REAL_PREAD_PWRITE) && \ - HAVE_REAL_PREAD_PWRITE > 0 +#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, @@ -687,18 +1860,19 @@ real_pread_pwrite: verified = lseek_on_eintr(fd, (off_t)0, SEEK_CUR, loop_eagain, loop_eintr); - if (off != verified) + 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) { - errno = EIO; + if (rw_over_nrw(r, nrw) == -1) break; - } } while (r == -1 && (errno == try_err(loop_eintr, EINTR) || @@ -717,65 +1891,110 @@ real_pread_pwrite: errno = saved_errno; - return rw_over_nrw(r, nrw); + rval = rw_over_nrw(r, nrw); + if (rval < 0) + goto err_prw; + + errno = saved_errno; + + return rval; + #endif err_prw: - errno = EIO; + + if (errno == saved_errno) + errno = EIO; + return -1; } int -io_args(int fd, void *mem, unsigned long nrw, +io_args(int fd, void *mem, size_t nrw, off_t off, int rw_type) { + int saved_errno = errno; + /* obviously */ - if (mem == NULL) + if (mem == NULL) { + + errno = EFAULT; goto err_io_args; + } /* uninitialised fd */ - if (fd < 0) + if (fd < 0) { + + errno = EBADF; goto err_io_args; + } /* negative offset */ - if (off < 0) + if (off < 0) { + + errno = ERANGE; goto err_io_args; + } /* prevent zero-byte rw */ if (!nrw) goto err_io_args; /* prevent overflow */ - if (nrw > (unsigned long)X_LONG_MAX) + if (nrw > (size_t)SSIZE_MAX) { + + errno = ERANGE; goto err_io_args; + } /* prevent overflow */ - if (((unsigned long)off + nrw) < (unsigned long)off) + if (((size_t)off + nrw) < (size_t)off) { + + errno = ERANGE; goto err_io_args; + } + + if (rw_type > IO_PWRITE) { - if (rw_type > IO_PWRITE) + errno = EINVAL; goto err_io_args; + } + + errno = saved_errno; return 0; err_io_args: - errno = EIO; + + 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)) + if (!S_ISREG(st->st_mode)) { + + errno = EBADF; goto err_is_file; + } + + errno = saved_errno; return 0; err_is_file: - errno = EIO; + + if (errno == saved_errno) + errno = EINVAL; + return -1; } @@ -783,9 +2002,11 @@ err_is_file: * specification != implementation */ -long -rw_over_nrw(long r, unsigned long nrw) +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 */ @@ -795,8 +2016,8 @@ rw_over_nrw(long r, unsigned long nrw) if (r == -1) return r; - if ((unsigned long) - r > X_LONG_MAX) { + if ((size_t) + r > SSIZE_MAX) { /* Theoretical buggy libc * check. Extremely academic. @@ -810,10 +2031,11 @@ rw_over_nrw(long r, unsigned long nrw) * [p]read() or [p]write() * * NOTE: here, we assume - * long integers are the + * ssize_t integers are the * same size as SSIZE_T */ + errno = ERANGE; goto err_rw_over_nrw; } @@ -821,19 +2043,26 @@ rw_over_nrw(long r, unsigned long nrw) * Should never return a number of * bytes above the requested length. */ - if ((unsigned long)r > nrw) + if ((size_t)r > nrw) { + + errno = ERANGE; goto err_rw_over_nrw; + } + + errno = saved_errno; return r; err_rw_over_nrw: - errno = EIO; + if (errno == saved_errno) + errno = EIO; + return -1; } -#if !defined(HAVE_REAL_PREAD_PWRITE) || \ - HAVE_REAL_PREAD_PWRITE < 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) @@ -869,9 +2098,9 @@ close_on_eintr(int fd) do { r = close(fd); - } while (r == -1 && errno == EINTR); + } while (r == -1 && (errno == EINTR || errno == EAGAIN)); - if (r > -1) + if (r >= 0) errno = saved_errno; return r; @@ -881,10 +2110,14 @@ int fsync_on_eintr(int fd) { int r; + int saved_errno = errno; do { r = fsync(fd); - } while (r == -1 && errno == EINTR); + } while (r == -1 && (errno == EINTR || errno == EAGAIN)); + + if (r >= 0) + errno = saved_errno; return r; } -- cgit v1.2.1