From: Christian Goeschel Ndjomouo Date: Mon, 27 Apr 2026 00:50:53 +0000 (-0400) Subject: lib: (fileutils) new fopen_at_no_link routine X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=24fca0b8f70800d59547a9c02471517c576254de;p=thirdparty%2Futil-linux.git lib: (fileutils) new fopen_at_no_link routine This function wraps around openat(2), fstat(2), ftruncate(2) and fdopen(3) to create a file stream that is not a symbolic or hard link in a race-free manner. It achieves this by implictly setting the O_NOFOLLOW bit to openat(2) opening flags and later checks if any hardlinks are registered for the file pointing to the acquired file descriptor. If all checks pass a pointer to the file stream handler is returned. The relevance for this function becomes apparent in script(2) where the default logfile is not allowed to be a link unless the --force option is passed and where the current method is vulnerable to a TOCTOU attack. It also helps with the code clarity as it moves the complexity of opening link free file streams to an internal helper and prevents scope creep as well as keeping the code base focused on its most relevant concern, which is recording the input/output. Signed-off-by: Christian Goeschel Ndjomouo --- diff --git a/include/fileutils.h b/include/fileutils.h index d9f0ff7a8..197e8a0f2 100644 --- a/include/fileutils.h +++ b/include/fileutils.h @@ -48,7 +48,10 @@ static inline FILE *fopen_at(int dir, const char *filename, close(fd); return ret; } -#endif + +extern FILE *fopen_at_no_link(int dir, const char *filename, + int flags, mode_t perm, const char *mode); +#endif /* HAVE_OPENAT */ static inline int is_same_inode(const int fd, const struct stat *st) { diff --git a/lib/fileutils.c b/lib/fileutils.c index c4e180617..dfce9c5d7 100644 --- a/lib/fileutils.c +++ b/lib/fileutils.c @@ -432,3 +432,62 @@ char *ul_basename(char *path) return p; } + +#ifdef HAVE_OPENAT +/* + * fopen_at_no_link() - Open a file stream that is not a symbolic/hard link. + * + * This function wraps around openat(2), fstat(2), ftruncate(2) and fdopen(3) + * to create a file stream that is not a symbolic or hard link in a race-free + * manner. + * + * @dir: dirfd as passed to openat(2), e.g. AT_FDCWD for the calling process + current working directory + * @filename: name of the target file + * @flags: open(2) file creation/status flags, O_NOFOLLOW is implicitly set + * @perm: open(2) file mode, can be bitwise ORed, these are only relevant + when O_CREAT is set in @flags, otherwise pass as 0. + * @mode: fopen(3) mode + * + * Return: On success, a valid pointer to a file stream is returned. + * On failure, NULL is returned and errno is set to indicate the issue. + */ +FILE *fopen_at_no_link(int dir, const char *filename, + int flags, mode_t perm, const char *mode) +{ + FILE *fp; + int fd; + struct stat st; + + /* We temporarily clear the O_TRUNC bit because we do not want + * to accidentally truncate the target file if it is a hard link + * instead of a symbolic one, where the latter is what we are + * guarding against here. The test for the hard link is done below + * with fstat()... + */ + fd = openat(dir, filename, ((flags & ~O_TRUNC) | O_NOFOLLOW), perm); + if (fd < 0) + return NULL; + + if (fstat(fd, &st)) { + close(fd); + return NULL; + } + + if (st.st_nlink > 1) { + close(fd); + errno = EMLINK; + return NULL; + } + + if ((flags & O_TRUNC) && ftruncate(fd, 0)) { + close(fd); + return NULL; + } + + fp = fdopen(fd, mode); + if (!fp) + close(fd); + return fp; +} +#endif /* HAVE_OPENAT */