]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
lib: (fileutils) new fopen_at_no_link routine
authorChristian Goeschel Ndjomouo <cgoesc2@wgu.edu>
Mon, 27 Apr 2026 00:50:53 +0000 (20:50 -0400)
committerChristian Goeschel Ndjomouo <cgoesc2@wgu.edu>
Fri, 1 May 2026 17:23:04 +0000 (13:23 -0400)
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 <cgoesc2@wgu.edu>
include/fileutils.h
lib/fileutils.c

index d9f0ff7a813e66f14b6cd29f64a6bfc4f80635aa..197e8a0f2e9a06ac64789034ac4e7d2f905ececc 100644 (file)
@@ -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)
 {
index c4e180617725397a291f948c566b799155361d55..dfce9c5d7046f70a9f8f0af22ecba28ee9a9c4f6 100644 (file)
@@ -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 */