+2025-05-28 Paul Eggert <eggert@cs.ucla.edu>
+
+ open, openat: handle O_DIRECTORY on special files
+ On deficient platforms where we must check for directories
+ ourselves when opening files, check before opening as well as after.
+ This prevents a hang when trying to open a special file like a
+ fifo in a context where a directory is required. Although there
+ is still a race so we could still hang in a perverse situation,
+ it’s the best we can do and it is better than hanging in the
+ more-common case.
+ * lib/open.c (lstatif): New static function.
+ (open) [REPLACE_FCHDIR]: Also inspect O_CREAT.
+ * lib/open.c (open), lib/openat.c (rpl_openat):
+ When checking for directories, also do this before opening.
+ Also, respect O_NOFOLLOW when checking for directories.
+ * lib/openat.c: Remove a few more unnecessary differences from open.c.
+ * modules/open (Depends-on): Depend on lstat.
+ * modules/openat (Depends-on): Add fstatat.
+ * modules/open-tests, modules/openat-tests:
+ (configure.ac) Check for alarm decl.
+ * tests/test-open.c, tests/test-openat.c:
+ Include sys/stat.h, for mkfifo.
+ [HAVE_DECL_ALARM]: Include signal.h, for alarm.
+ * tests/test-open.h (test_open): Fail if test takes too long
+ because we tried to open a fifo. Test opening /dev/null,
+ /dev/tty and a fifo, with a trailing "/" and with O_DIRECTORY.
+
2025-05-28 Collin Funk <collin.funk1@gmail.com>
vasnprintf: Fix uninitialized values.
# define REPLACE_OPEN_DIRECTORY false
#endif
+static int
+lstatif (char const *filename, struct stat *st, int flags)
+{
+ return flags & O_NOFOLLOW ? lstat (filename, st) : stat (filename, st);
+}
+
int
open (const char *filename, int flags, ...)
{
directories,
- if O_WRONLY or O_RDWR is specified, open() must fail because the
file does not contain a '.' directory. */
- if (OPEN_TRAILING_SLASH_BUG
+ bool check_for_slash_bug;
+ if (OPEN_TRAILING_SLASH_BUG)
+ {
+ size_t len = strlen (filename);
+ check_for_slash_bug = len && filename[len - 1] == '/';
+ }
+ else
+ check_for_slash_bug = false;
+
+ if (check_for_slash_bug
&& (flags & O_CREAT
|| (flags & O_ACCMODE) == O_RDWR
|| (flags & O_ACCMODE) == O_WRONLY))
{
- size_t len = strlen (filename);
- if (len > 0 && filename[len - 1] == '/')
+ errno = EISDIR;
+ return -1;
+ }
+
+ /* With the trailing slash bug or without working O_DIRECTORY, check with
+ stat first lest we hang trying to open a fifo. Although there is
+ a race between this and opening the file, we can do no better.
+ After opening the file we will check again with fstat. */
+ bool check_directory =
+ (check_for_slash_bug
+ || (!HAVE_WORKING_O_DIRECTORY && flags & O_DIRECTORY));
+ if (check_directory)
+ {
+ struct stat statbuf;
+ if (lstatif (filename, &statbuf, flags) < 0)
+ {
+ if (! (flags & O_CREAT && errno == ENOENT))
+ return -1;
+ }
+ else if (!S_ISDIR (statbuf.st_mode))
{
- errno = EISDIR;
+ errno = ENOTDIR;
return -1;
}
}
#if REPLACE_FCHDIR
/* Implementing fchdir and fdopendir requires the ability to open a
directory file descriptor. If open doesn't support that (as on
- mingw), we use a dummy file that behaves the same as directories
+ mingw), use a dummy file that behaves the same as directories
on Linux (ie. always reports EOF on attempts to read()), and
- override fstat() in fchdir.c to hide the fact that we have a
- dummy. */
+ override fstat in fchdir.c to hide the dummy. */
if (REPLACE_OPEN_DIRECTORY && fd < 0 && errno == EACCES
- && ((flags & O_ACCMODE) == O_RDONLY
- || (O_SEARCH != O_RDONLY && (flags & O_ACCMODE) == O_SEARCH)))
+ && ((flags & (O_ACCMODE | O_CREAT)) == O_RDONLY
+ || (O_SEARCH != O_RDONLY
+ && (flags & (O_ACCMODE | O_CREAT)) == O_SEARCH))
{
struct stat statbuf;
- if (stat (filename, &statbuf) == 0 && S_ISDIR (statbuf.st_mode))
+ if (check_directory
+ || (lstatif (filename, &statbuf, flags) == 0
+ && S_ISDIR (statbuf.st_mode)))
{
/* Maximum recursion depth of 1. */
fd = open ("/dev/null", flags, mode);
}
#endif
- /* If the filename ends in a slash or O_DIRECTORY is given,
- then fail if fd does not refer to a directory.
+ /* If checking for directories, fail if fd does not refer to a directory.
Rationale: A filename ending in slash cannot name a non-directory
<https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13>:
"A pathname that contains at least one non-<slash> character and that
<slash> characters names an existing directory"
If the named file without the slash is not a directory, open() must fail
with ENOTDIR. */
- if (((!HAVE_WORKING_O_DIRECTORY && flags & O_DIRECTORY)
- || OPEN_TRAILING_SLASH_BUG)
- && 0 <= fd)
+ if (check_directory && 0 <= fd)
{
- /* FILENAME must be nonempty, as open did not fail with ENOENT. */
- if ((!HAVE_WORKING_O_DIRECTORY && flags & O_DIRECTORY)
- || filename[strlen (filename) - 1] == '/')
+ struct stat statbuf;
+ int r = fstat (fd, &statbuf);
+ if (r < 0 || !S_ISDIR (statbuf.st_mode))
{
- struct stat statbuf;
- int r = fstat (fd, &statbuf);
- if (r < 0 || !S_ISDIR (statbuf.st_mode))
- {
- int err = r < 0 ? errno : ENOTDIR;
- close (fd);
- errno = err;
- return -1;
- }
+ int err = r < 0 ? errno : ENOTDIR;
+ close (fd);
+ errno = err;
+ return -1;
}
}
}
#endif
+/* Specification. */
#ifdef __osf__
/* Write "fcntl.h" here, not <fcntl.h>, otherwise OSF/1 5.1 DTK cc eliminates
this include because of the preliminary #include <fcntl.h> above. */
#include "cloexec.h"
+#include <errno.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
-#include <errno.h>
#ifndef OPEN_TRAILING_SLASH_BUG
# define OPEN_TRAILING_SLASH_BUG false
directories,
- if O_WRONLY or O_RDWR is specified, open() must fail because the
file does not contain a '.' directory. */
- if (OPEN_TRAILING_SLASH_BUG
+ bool check_for_slash_bug;
+ if (OPEN_TRAILING_SLASH_BUG)
+ {
+ size_t len = strlen (filename);
+ check_for_slash_bug = len && filename[len - 1] == '/';
+ }
+ else
+ check_for_slash_bug = false;
+
+ if (check_for_slash_bug
&& (flags & O_CREAT
|| (flags & O_ACCMODE) == O_RDWR
|| (flags & O_ACCMODE) == O_WRONLY))
{
- size_t len = strlen (filename);
- if (len > 0 && filename[len - 1] == '/')
+ errno = EISDIR;
+ return -1;
+ }
+
+ /* With the trailing slash bug or without working O_DIRECTORY, check with
+ stat first lest we hang trying to open a fifo. Although there is
+ a race between this and opening the file, we can do no better.
+ After opening the file we will check again with fstat. */
+ bool check_directory =
+ (check_for_slash_bug
+ || (!HAVE_WORKING_O_DIRECTORY && flags & O_DIRECTORY));
+ if (check_directory)
+ {
+ struct stat statbuf;
+ int fstatat_flags = flags & O_NOFOLLOW ? AT_SYMLINK_NOFOLLOW : 0;
+ if (fstatat (dfd, filename, &statbuf, fstatat_flags) < 0)
+ {
+ if (! (flags & O_CREAT && errno == ENOENT))
+ return -1;
+ }
+ else if (!S_ISDIR (statbuf.st_mode))
{
- errno = EISDIR;
+ errno = ENOTDIR;
return -1;
}
}
}
- /* If the filename ends in a slash or O_DIRECTORY is given,
- then fail if fd does not refer to a directory.
+ /* If checking for directories, fail if fd does not refer to a directory.
Rationale: A filename ending in slash cannot name a non-directory
<https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13>:
"A pathname that contains at least one non-<slash> character and that
<slash> characters names an existing directory"
If the named file without the slash is not a directory, open() must fail
with ENOTDIR. */
- if (((!HAVE_WORKING_O_DIRECTORY && flags & O_DIRECTORY)
- || OPEN_TRAILING_SLASH_BUG)
- && 0 <= fd)
+ if (check_directory && 0 <= fd)
{
- /* FILENAME must be nonempty, as open did not fail with ENOENT. */
- if ((!HAVE_WORKING_O_DIRECTORY && flags & O_DIRECTORY)
- || filename[strlen (filename) - 1] == '/')
+ struct stat statbuf;
+ int r = fstat (fd, &statbuf);
+ if (r < 0 || !S_ISDIR (statbuf.st_mode))
{
- struct stat statbuf;
- int r = fstat (fd, &statbuf);
- if (r < 0 || !S_ISDIR (statbuf.st_mode))
- {
- int err = r < 0 ? errno : ENOTDIR;
- close (fd);
- errno = err;
- return -1;
- }
+ int err = r < 0 ? errno : ENOTDIR;
+ close (fd);
+ errno = err;
+ return -1;
}
}
largefile
cloexec [test $REPLACE_OPEN = 1]
fstat [test $REPLACE_OPEN = 1]
+lstat [test $REPLACE_OPEN = 1]
stat [test $REPLACE_OPEN = 1]
configure.ac:
Depends-on:
bool
fcntl
+mkfifo
symlink
configure.ac:
+AC_CHECK_DECLS_ONCE([alarm])
Makefile.am:
TESTS += test-open
sys_stat-h [test $HAVE_OPENAT = 0 || test $REPLACE_OPENAT = 1]
cloexec [test $REPLACE_OPENAT = 1]
fstat [test $REPLACE_OPENAT = 1]
+fstatat [test $REPLACE_OPENAT = 1]
at-internal [test $HAVE_OPENAT = 0]
errno-h [test $HAVE_OPENAT = 0]
fchdir [test $HAVE_OPENAT = 0]
Depends-on:
fcntl
+mkfifo
symlink
configure.ac:
+AC_CHECK_DECLS_ONCE([alarm])
Makefile.am:
TESTS += test-openat
#include <errno.h>
#include <stdio.h>
+#include <sys/stat.h>
#include <unistd.h>
+#if HAVE_DECL_ALARM
+# include <signal.h>
+#endif
+
#include "macros.h"
#define BASE "test-open.t"
static ALWAYS_INLINE int
test_open (int (*func) (char const *, int, ...), bool print)
{
+#if HAVE_DECL_ALARM
+ /* Declare failure if test takes too long, by using default abort
+ caused by SIGALRM. */
+ int alarm_value = 5;
+ signal (SIGALRM, SIG_DFL);
+ alarm (alarm_value);
+#endif
+
int fd;
/* Remove anything from prior partial run. */
+ unlink (BASE "fifo");
unlink (BASE "file");
unlink (BASE "e.exe");
unlink (BASE "link");
ASSERT (func (BASE "file/", O_RDONLY) == -1);
ASSERT (errno == ENOTDIR || errno == EISDIR || errno == EINVAL);
- /* Cannot open non-directory with O_DIRECTORY. */
+ /* Cannot open regular file with O_DIRECTORY. */
errno = 0;
ASSERT (func (BASE "file", O_RDONLY | O_DIRECTORY) == -1);
ASSERT (errno == ENOTDIR);
+ /* Cannot open /dev/null with trailing slash or O_DIRECTORY. */
+ errno = 0;
+ ASSERT (func ("/dev/null/", O_RDONLY) == -1);
+ ASSERT (errno == ENOTDIR || errno == EISDIR || errno == EINVAL);
+
+ errno = 0;
+ ASSERT (func ("/dev/null", O_RDONLY | O_DIRECTORY) == -1);
+ ASSERT (errno == ENOTDIR);
+
+ /* Cannot open /dev/tty with trailing slash or O_DIRECTORY,
+ though errno may differ as there may not be a controlling tty. */
+ ASSERT (func ("/dev/tty/", O_RDONLY) == -1);
+ ASSERT (func ("/dev/tty", O_RDONLY | O_DIRECTORY) == -1);
+
+ /* Cannot open fifo with trailing slash or O_DIRECTORY. */
+ if (mkfifo (BASE "fifo", 0666) == 0)
+ {
+ errno = 0;
+ ASSERT (func (BASE "fifo/", O_RDONLY) == -1);
+ ASSERT (errno == ENOTDIR || errno == EISDIR || errno == EINVAL);
+
+ errno = 0;
+ ASSERT (func (BASE "fifo", O_RDONLY | O_DIRECTORY) == -1);
+ ASSERT (errno == ENOTDIR);
+
+ ASSERT (unlink (BASE "fifo") == 0);
+ }
+
/* Directories cannot be opened for writing. */
errno = 0;
ASSERT (func (".", O_WRONLY) == -1);
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
+#include <sys/stat.h>
#include <unistd.h>
+#if HAVE_DECL_ALARM
+# include <signal.h>
+#endif
+
#include "macros.h"
#define BASE "test-openat.t"