aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Brauner <brauner@kernel.org>2026-05-11 12:27:46 +0200
committerChristian Brauner <brauner@kernel.org>2026-05-21 10:53:41 +0200
commit09e8b7a428b3f52b7625870edb4cd42e621fac07 (patch)
tree57f42aec27054b1bc92e8ce455763fdba821cb67
parent3e0be8ccff090cde4e10410f8e6a3fefeeff4124 (diff)
parentc0329020da211b41afce4d1c8a1c2494c6d97883 (diff)
Merge patch series "vfs: add O_EMPTYPATH to openat(2)/openat2(2)"
Jori Koolstra <jkoolstra@xs4all.nl> says: To get an operable version of an O_PATH file descriptor, it is possible to use openat(fd, ".", O_DIRECTORY) for directories, but other files currently require going through open("/proc/<pid>/fd/<nr>"), which depends on a functioning procfs. This patch adds the O_EMPTYPATH flag to openat(2)/openat2(2). If passed, LOOKUP_EMPTY is set at path resolution time. * patches from https://patch.msgid.link/20260424114611.1678641-1-jkoolstra@xs4all.nl: selftest: add tests for O_EMPTYPATH vfs: add O_EMPTYPATH to openat(2)/openat2(2) Link: https://patch.msgid.link/20260424114611.1678641-1-jkoolstra@xs4all.nl Signed-off-by: Christian Brauner <brauner@kernel.org>
-rw-r--r--fs/fcntl.c2
-rw-r--r--fs/open.c6
-rw-r--r--include/linux/fcntl.h2
-rw-r--r--include/uapi/asm-generic/fcntl.h4
-rw-r--r--tools/include/uapi/linux/openat2.h43
-rw-r--r--tools/testing/selftests/filesystems/openat2/Makefile4
-rw-r--r--tools/testing/selftests/filesystems/openat2/emptypath_test.c55
-rw-r--r--tools/testing/selftests/filesystems/openat2/helpers.h35
8 files changed, 111 insertions, 40 deletions
diff --git a/fs/fcntl.c b/fs/fcntl.c
index beab8080badf..7d2165855a9c 100644
--- a/fs/fcntl.c
+++ b/fs/fcntl.c
@@ -1169,7 +1169,7 @@ static int __init fcntl_init(void)
* Exceptions: O_NONBLOCK is a two bit define on parisc; O_NDELAY
* is defined as O_NONBLOCK on some platforms and not on others.
*/
- BUILD_BUG_ON(20 - 1 /* for O_RDONLY being 0 */ !=
+ BUILD_BUG_ON(21 - 1 /* for O_RDONLY being 0 */ !=
HWEIGHT32(
(VALID_OPEN_FLAGS & ~(O_NONBLOCK | O_NDELAY)) |
__FMODE_EXEC));
diff --git a/fs/open.c b/fs/open.c
index 681d405bc61e..9e0164a8c1fb 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1158,7 +1158,7 @@ struct file *kernel_file_open(const struct path *path, int flags,
EXPORT_SYMBOL_GPL(kernel_file_open);
#define WILL_CREATE(flags) (flags & (O_CREAT | __O_TMPFILE))
-#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC)
+#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC | O_EMPTYPATH)
inline struct open_how build_open_how(int flags, umode_t mode)
{
@@ -1279,6 +1279,8 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
lookup_flags |= LOOKUP_DIRECTORY;
if (!(flags & O_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
+ if (flags & O_EMPTYPATH)
+ lookup_flags |= LOOKUP_EMPTY;
if (how->resolve & RESOLVE_NO_XDEV)
lookup_flags |= LOOKUP_NO_XDEV;
@@ -1360,7 +1362,7 @@ static int do_sys_openat2(int dfd, const char __user *filename,
if (unlikely(err))
return err;
- CLASS(filename, name)(filename);
+ CLASS(filename_flags, name)(filename, op.lookup_flags);
return FD_ADD(how->flags, do_file_open(dfd, name, &op));
}
diff --git a/include/linux/fcntl.h b/include/linux/fcntl.h
index a332e79b3207..c65c5c73d362 100644
--- a/include/linux/fcntl.h
+++ b/include/linux/fcntl.h
@@ -10,7 +10,7 @@
(O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC | \
O_APPEND | O_NDELAY | O_NONBLOCK | __O_SYNC | O_DSYNC | \
FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | \
- O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE)
+ O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH)
/* List of all valid flags for the how->resolve argument: */
#define VALID_RESOLVE_FLAGS \
diff --git a/include/uapi/asm-generic/fcntl.h b/include/uapi/asm-generic/fcntl.h
index 613475285643..bfc68156b45a 100644
--- a/include/uapi/asm-generic/fcntl.h
+++ b/include/uapi/asm-generic/fcntl.h
@@ -88,6 +88,10 @@
#define __O_TMPFILE 020000000
#endif
+#ifndef O_EMPTYPATH
+#define O_EMPTYPATH (1 << 26) /* allow empty path */
+#endif
+
/* a horrid kludge trying to make sure that this will fail on old kernels */
#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
diff --git a/tools/include/uapi/linux/openat2.h b/tools/include/uapi/linux/openat2.h
new file mode 100644
index 000000000000..4759c471676c
--- /dev/null
+++ b/tools/include/uapi/linux/openat2.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _LINUX_OPENAT2_H
+#define _LINUX_OPENAT2_H
+
+#include <linux/types.h>
+
+/*
+ * Arguments for how openat2(2) should open the target path. If only @flags and
+ * @mode are non-zero, then openat2(2) operates very similarly to openat(2).
+ *
+ * However, unlike openat(2), unknown or invalid bits in @flags result in
+ * -EINVAL rather than being silently ignored. @mode must be zero unless one of
+ * {O_CREAT, O_TMPFILE} are set.
+ *
+ * @flags: O_* flags.
+ * @mode: O_CREAT/O_TMPFILE file mode.
+ * @resolve: RESOLVE_* flags.
+ */
+struct open_how {
+ __u64 flags;
+ __u64 mode;
+ __u64 resolve;
+};
+
+/* how->resolve flags for openat2(2). */
+#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings
+ (includes bind-mounts). */
+#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style
+ "magic-links". */
+#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks
+ (implies OEXT_NO_MAGICLINKS) */
+#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like
+ "..", symlinks, and absolute
+ paths which escape the dirfd. */
+#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".."
+ be scoped inside the dirfd
+ (similar to chroot(2)). */
+#define RESOLVE_CACHED 0x20 /* Only complete if resolution can be
+ completed through cached lookup. May
+ return -EAGAIN if that's not
+ possible. */
+
+#endif /* _LINUX_OPENAT2_H */
diff --git a/tools/testing/selftests/filesystems/openat2/Makefile b/tools/testing/selftests/filesystems/openat2/Makefile
index 7736e37b7986..d848aac96bde 100644
--- a/tools/testing/selftests/filesystems/openat2/Makefile
+++ b/tools/testing/selftests/filesystems/openat2/Makefile
@@ -1,8 +1,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += $(KHDR_INCLUDES)
-CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined
-TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test
+CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined $(TOOLS_INCLUDES)
+TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test emptypath_test
# gcc requires -static-libasan in order to ensure that Address Sanitizer's
# library is the first one loaded. However, clang already statically links the
diff --git a/tools/testing/selftests/filesystems/openat2/emptypath_test.c b/tools/testing/selftests/filesystems/openat2/emptypath_test.c
new file mode 100644
index 000000000000..d75b5e998ff9
--- /dev/null
+++ b/tools/testing/selftests/filesystems/openat2/emptypath_test.c
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#define __SANE_USERSPACE_TYPES__
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "kselftest.h"
+
+#ifndef O_EMPTYPATH
+#define O_EMPTYPATH (1 << 26)
+#endif
+
+int main(void)
+{
+ int opath_fd, reopen_fd;
+ const char *path = "/tmp/emptypath_test";
+
+ ksft_print_header();
+ ksft_set_plan(2);
+
+ opath_fd = open(path, O_CREAT | O_WRONLY, S_IRWXU);
+ if (opath_fd < 0)
+ ksft_exit_fail_msg("create %s: %m\n", path);
+ close(opath_fd);
+
+ opath_fd = open(path, O_PATH);
+ if (opath_fd < 0)
+ ksft_exit_fail_msg("open %s O_PATH: %m\n", path);
+
+ reopen_fd = openat(opath_fd, "", O_RDONLY);
+ if (reopen_fd < 0 && errno == ENOENT)
+ ksft_test_result_pass("empty path without O_EMPTYPATH returns ENOENT\n");
+ else if (reopen_fd >= 0) {
+ ksft_test_result_fail("empty path without O_EMPTYPATH unexpectedly succeeded\n");
+ close(reopen_fd);
+ } else {
+ ksft_test_result_fail("empty path without O_EMPTYPATH: expected ENOENT, got %m\n");
+ }
+
+ reopen_fd = openat(opath_fd, "", O_RDONLY | O_EMPTYPATH);
+
+ if (reopen_fd < 0 && errno == EINVAL)
+ ksft_exit_skip("O_EMPTYPATH not supported\n");
+
+ if (reopen_fd >= 0) {
+ ksft_test_result_pass("O_EMPTYPATH reopens O_PATH fd\n");
+ close(reopen_fd);
+ } else {
+ ksft_test_result_fail("O_EMPTYPATH failed: %m\n");
+ }
+
+ unlink(path);
+ ksft_finished();
+}
diff --git a/tools/testing/selftests/filesystems/openat2/helpers.h b/tools/testing/selftests/filesystems/openat2/helpers.h
index 7ca54c718c45..3f01fb68c5a6 100644
--- a/tools/testing/selftests/filesystems/openat2/helpers.h
+++ b/tools/testing/selftests/filesystems/openat2/helpers.h
@@ -15,47 +15,14 @@
#include <limits.h>
#include <linux/types.h>
#include <linux/unistd.h>
+#include <linux/openat2.h>
#include "kselftest_harness.h"
#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
-/*
- * Arguments for how openat2(2) should open the target path. If @resolve is
- * zero, then openat2(2) operates very similarly to openat(2).
- *
- * However, unlike openat(2), unknown bits in @flags result in -EINVAL rather
- * than being silently ignored. @mode must be zero unless one of {O_CREAT,
- * O_TMPFILE} are set.
- *
- * @flags: O_* flags.
- * @mode: O_CREAT/O_TMPFILE file mode.
- * @resolve: RESOLVE_* flags.
- */
-struct open_how {
- __u64 flags;
- __u64 mode;
- __u64 resolve;
-};
-
#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */
#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0
-#ifndef RESOLVE_IN_ROOT
-/* how->resolve flags for openat2(2). */
-#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings
- (includes bind-mounts). */
-#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style
- "magic-links". */
-#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks
- (implies OEXT_NO_MAGICLINKS) */
-#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like
- "..", symlinks, and absolute
- paths which escape the dirfd. */
-#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".."
- be scoped inside the dirfd
- (similar to chroot(2)). */
-#endif /* RESOLVE_IN_ROOT */
-
__maybe_unused
static bool needs_openat2(const struct open_how *how)
{