From fe087927046cdbc7365bef4650d2d5451ce1ea8a Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 1 Apr 2026 11:28:02 +1100 Subject: [PATCH 01/12] selftests: move openat2 tests to selftests/filesystems/ These tests really should've always belonged there, doubly so now that they include a lot of other generic filesystem-related tests. Suggested-by: Christian Brauner Signed-off-by: Aleksa Sarai Link: https://patch.msgid.link/20260401-openat2-selftests-kunit-v2-1-ad153a07da0c@amutable.com Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/{ => filesystems}/openat2/.gitignore | 0 tools/testing/selftests/{ => filesystems}/openat2/Makefile | 3 ++- tools/testing/selftests/{ => filesystems}/openat2/helpers.c | 0 tools/testing/selftests/{ => filesystems}/openat2/helpers.h | 0 .../testing/selftests/{ => filesystems}/openat2/openat2_test.c | 0 .../selftests/{ => filesystems}/openat2/rename_attack_test.c | 0 .../testing/selftests/{ => filesystems}/openat2/resolve_test.c | 0 7 files changed, 2 insertions(+), 1 deletion(-) rename tools/testing/selftests/{ => filesystems}/openat2/.gitignore (100%) rename tools/testing/selftests/{ => filesystems}/openat2/Makefile (91%) rename tools/testing/selftests/{ => filesystems}/openat2/helpers.c (100%) rename tools/testing/selftests/{ => filesystems}/openat2/helpers.h (100%) rename tools/testing/selftests/{ => filesystems}/openat2/openat2_test.c (100%) rename tools/testing/selftests/{ => filesystems}/openat2/rename_attack_test.c (100%) rename tools/testing/selftests/{ => filesystems}/openat2/resolve_test.c (100%) diff --git a/tools/testing/selftests/openat2/.gitignore b/tools/testing/selftests/filesystems/openat2/.gitignore similarity index 100% rename from tools/testing/selftests/openat2/.gitignore rename to tools/testing/selftests/filesystems/openat2/.gitignore diff --git a/tools/testing/selftests/openat2/Makefile b/tools/testing/selftests/filesystems/openat2/Makefile similarity index 91% rename from tools/testing/selftests/openat2/Makefile rename to tools/testing/selftests/filesystems/openat2/Makefile index 185dc76ebb5f..f36dedccedb6 100644 --- a/tools/testing/selftests/openat2/Makefile +++ b/tools/testing/selftests/filesystems/openat2/Makefile @@ -1,5 +1,6 @@ # 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 @@ -13,6 +14,6 @@ endif LOCAL_HDRS += helpers.h -include ../lib.mk +include ../../lib.mk $(TEST_GEN_PROGS): helpers.c diff --git a/tools/testing/selftests/openat2/helpers.c b/tools/testing/selftests/filesystems/openat2/helpers.c similarity index 100% rename from tools/testing/selftests/openat2/helpers.c rename to tools/testing/selftests/filesystems/openat2/helpers.c diff --git a/tools/testing/selftests/openat2/helpers.h b/tools/testing/selftests/filesystems/openat2/helpers.h similarity index 100% rename from tools/testing/selftests/openat2/helpers.h rename to tools/testing/selftests/filesystems/openat2/helpers.h diff --git a/tools/testing/selftests/openat2/openat2_test.c b/tools/testing/selftests/filesystems/openat2/openat2_test.c similarity index 100% rename from tools/testing/selftests/openat2/openat2_test.c rename to tools/testing/selftests/filesystems/openat2/openat2_test.c diff --git a/tools/testing/selftests/openat2/rename_attack_test.c b/tools/testing/selftests/filesystems/openat2/rename_attack_test.c similarity index 100% rename from tools/testing/selftests/openat2/rename_attack_test.c rename to tools/testing/selftests/filesystems/openat2/rename_attack_test.c diff --git a/tools/testing/selftests/openat2/resolve_test.c b/tools/testing/selftests/filesystems/openat2/resolve_test.c similarity index 100% rename from tools/testing/selftests/openat2/resolve_test.c rename to tools/testing/selftests/filesystems/openat2/resolve_test.c From d2fcf57ffc3b85b816550b3ee404ffcc83ace16c Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 1 Apr 2026 11:28:03 +1100 Subject: [PATCH 02/12] selftests: openat2: move helpers to header This is a bit ugly, but in the next patch we will move to using kselftest_harness.h -- which doesn't play well with being included in multiple compilation units due to duplicate function definitions. Not including kselftest_harness.h would let us avoid this patch, but the helpers will need include kselftest_harness.h in order to switch to TH_LOG. Signed-off-by: Aleksa Sarai Link: https://patch.msgid.link/20260401-openat2-selftests-kunit-v2-2-ad153a07da0c@amutable.com Signed-off-by: Christian Brauner (Amutable) --- .../selftests/filesystems/openat2/Makefile | 2 - .../selftests/filesystems/openat2/helpers.c | 109 --------------- .../selftests/filesystems/openat2/helpers.h | 125 +++++++++++++++--- 3 files changed, 107 insertions(+), 129 deletions(-) delete mode 100644 tools/testing/selftests/filesystems/openat2/helpers.c diff --git a/tools/testing/selftests/filesystems/openat2/Makefile b/tools/testing/selftests/filesystems/openat2/Makefile index f36dedccedb6..7736e37b7986 100644 --- a/tools/testing/selftests/filesystems/openat2/Makefile +++ b/tools/testing/selftests/filesystems/openat2/Makefile @@ -15,5 +15,3 @@ endif LOCAL_HDRS += helpers.h include ../../lib.mk - -$(TEST_GEN_PROGS): helpers.c diff --git a/tools/testing/selftests/filesystems/openat2/helpers.c b/tools/testing/selftests/filesystems/openat2/helpers.c deleted file mode 100644 index 5074681ffdc9..000000000000 --- a/tools/testing/selftests/filesystems/openat2/helpers.c +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Author: Aleksa Sarai - * Copyright (C) 2018-2019 SUSE LLC. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include - -#include "helpers.h" - -bool needs_openat2(const struct open_how *how) -{ - return how->resolve != 0; -} - -int raw_openat2(int dfd, const char *path, void *how, size_t size) -{ - int ret = syscall(__NR_openat2, dfd, path, how, size); - return ret >= 0 ? ret : -errno; -} - -int sys_openat2(int dfd, const char *path, struct open_how *how) -{ - return raw_openat2(dfd, path, how, sizeof(*how)); -} - -int sys_openat(int dfd, const char *path, struct open_how *how) -{ - int ret = openat(dfd, path, how->flags, how->mode); - return ret >= 0 ? ret : -errno; -} - -int sys_renameat2(int olddirfd, const char *oldpath, - int newdirfd, const char *newpath, unsigned int flags) -{ - int ret = syscall(__NR_renameat2, olddirfd, oldpath, - newdirfd, newpath, flags); - return ret >= 0 ? ret : -errno; -} - -int touchat(int dfd, const char *path) -{ - int fd = openat(dfd, path, O_CREAT, 0700); - if (fd >= 0) - close(fd); - return fd; -} - -char *fdreadlink(int fd) -{ - char *target, *tmp; - - E_asprintf(&tmp, "/proc/self/fd/%d", fd); - - target = malloc(PATH_MAX); - if (!target) - ksft_exit_fail_msg("fdreadlink: malloc failed\n"); - memset(target, 0, PATH_MAX); - - E_readlink(tmp, target, PATH_MAX); - free(tmp); - return target; -} - -bool fdequal(int fd, int dfd, const char *path) -{ - char *fdpath, *dfdpath, *other; - bool cmp; - - fdpath = fdreadlink(fd); - dfdpath = fdreadlink(dfd); - - if (!path) - E_asprintf(&other, "%s", dfdpath); - else if (*path == '/') - E_asprintf(&other, "%s", path); - else - E_asprintf(&other, "%s/%s", dfdpath, path); - - cmp = !strcmp(fdpath, other); - - free(fdpath); - free(dfdpath); - free(other); - return cmp; -} - -bool openat2_supported = false; - -void __attribute__((constructor)) init(void) -{ - struct open_how how = {}; - int fd; - - BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0); - - /* Check openat2(2) support. */ - fd = sys_openat2(AT_FDCWD, ".", &how); - openat2_supported = (fd >= 0); - - if (fd >= 0) - close(fd); -} diff --git a/tools/testing/selftests/filesystems/openat2/helpers.h b/tools/testing/selftests/filesystems/openat2/helpers.h index 510e60602511..975de513af86 100644 --- a/tools/testing/selftests/filesystems/openat2/helpers.h +++ b/tools/testing/selftests/filesystems/openat2/helpers.h @@ -2,6 +2,7 @@ /* * Author: Aleksa Sarai * Copyright (C) 2018-2019 SUSE LLC. + * Copyright (C) 2026 Amutable GmbH */ #ifndef __RESOLVEAT_H__ @@ -11,19 +12,14 @@ #include #include #include +#include #include +#include #include "kselftest.h" #define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X))) #define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); }))) -#ifndef SYS_openat2 -#ifndef __NR_openat2 -#define __NR_openat2 437 -#endif /* __NR_openat2 */ -#define SYS_openat2 __NR_openat2 -#endif /* SYS_openat2 */ - /* * Arguments for how openat2(2) should open the target path. If @resolve is * zero, then openat2(2) operates very similarly to openat(2). @@ -45,8 +41,6 @@ struct open_how { #define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */ #define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0 -bool needs_openat2(const struct open_how *how); - #ifndef RESOLVE_IN_ROOT /* how->resolve flags for openat2(2). */ #define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings @@ -93,16 +87,111 @@ bool needs_openat2(const struct open_how *how); __FILE__, __LINE__, #expr, ##__VA_ARGS__); \ } while (0) -int raw_openat2(int dfd, const char *path, void *how, size_t size); -int sys_openat2(int dfd, const char *path, struct open_how *how); -int sys_openat(int dfd, const char *path, struct open_how *how); -int sys_renameat2(int olddirfd, const char *oldpath, - int newdirfd, const char *newpath, unsigned int flags); +__maybe_unused +static bool needs_openat2(const struct open_how *how) +{ + return how->resolve != 0; +} -int touchat(int dfd, const char *path); -char *fdreadlink(int fd); -bool fdequal(int fd, int dfd, const char *path); +__maybe_unused +static int raw_openat2(int dfd, const char *path, void *how, size_t size) +{ + int ret = syscall(__NR_openat2, dfd, path, how, size); -extern bool openat2_supported; + return ret >= 0 ? ret : -errno; +} + +__maybe_unused +static int sys_openat2(int dfd, const char *path, struct open_how *how) +{ + return raw_openat2(dfd, path, how, sizeof(*how)); +} + +__maybe_unused +static int sys_openat(int dfd, const char *path, struct open_how *how) +{ + int ret = openat(dfd, path, how->flags, how->mode); + + return ret >= 0 ? ret : -errno; +} + +__maybe_unused +static int sys_renameat2(int olddirfd, const char *oldpath, + int newdirfd, const char *newpath, unsigned int flags) +{ + int ret = syscall(__NR_renameat2, olddirfd, oldpath, + newdirfd, newpath, flags); + + return ret >= 0 ? ret : -errno; +} + +__maybe_unused +static int touchat(int dfd, const char *path) +{ + int fd = openat(dfd, path, O_CREAT, 0700); + + if (fd >= 0) + close(fd); + return fd; +} + +__maybe_unused +static char *fdreadlink(int fd) +{ + char *target, *tmp; + + E_asprintf(&tmp, "/proc/self/fd/%d", fd); + + target = malloc(PATH_MAX); + if (!target) + ksft_exit_fail_msg("fdreadlink: malloc failed\n"); + memset(target, 0, PATH_MAX); + + E_readlink(tmp, target, PATH_MAX); + free(tmp); + return target; +} + +__maybe_unused +static bool fdequal(int fd, int dfd, const char *path) +{ + char *fdpath, *dfdpath, *other; + bool cmp; + + fdpath = fdreadlink(fd); + dfdpath = fdreadlink(dfd); + + if (!path) + E_asprintf(&other, "%s", dfdpath); + else if (*path == '/') + E_asprintf(&other, "%s", path); + else + E_asprintf(&other, "%s/%s", dfdpath, path); + + cmp = !strcmp(fdpath, other); + + free(fdpath); + free(dfdpath); + free(other); + return cmp; +} + +static bool openat2_supported = false; + +__attribute__((constructor)) +static void __detect_openat2_supported(void) +{ + struct open_how how = {}; + int fd; + + BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0); + + /* Check openat2(2) support. */ + fd = sys_openat2(AT_FDCWD, ".", &how); + openat2_supported = (fd >= 0); + + if (fd >= 0) + close(fd); +} #endif /* __RESOLVEAT_H__ */ From 582c904573a63eac7ecbac1a285899ffbf0d43ac Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 1 Apr 2026 11:28:04 +1100 Subject: [PATCH 03/12] selftests: openat2: switch from custom ARRAY_LEN to ARRAY_SIZE For whatever reason, the original version of the tests used a custom version of ARRAY_SIZE, but ARRAY_SIZE works just as well. Signed-off-by: Aleksa Sarai Link: https://patch.msgid.link/20260401-openat2-selftests-kunit-v2-3-ad153a07da0c@amutable.com Signed-off-by: Christian Brauner (Amutable) --- .../testing/selftests/filesystems/openat2/helpers.h | 1 - .../selftests/filesystems/openat2/openat2_test.c | 12 ++++++------ .../selftests/filesystems/openat2/resolve_test.c | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tools/testing/selftests/filesystems/openat2/helpers.h b/tools/testing/selftests/filesystems/openat2/helpers.h index 975de513af86..f56c0c6e3ad1 100644 --- a/tools/testing/selftests/filesystems/openat2/helpers.h +++ b/tools/testing/selftests/filesystems/openat2/helpers.h @@ -17,7 +17,6 @@ #include #include "kselftest.h" -#define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X))) #define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); }))) /* diff --git a/tools/testing/selftests/filesystems/openat2/openat2_test.c b/tools/testing/selftests/filesystems/openat2/openat2_test.c index 0e161ef9e9e4..c6c26652ac1b 100644 --- a/tools/testing/selftests/filesystems/openat2/openat2_test.c +++ b/tools/testing/selftests/filesystems/openat2/openat2_test.c @@ -83,14 +83,14 @@ void test_openat2_struct(void) .size = sizeof(struct open_how_ext), .err = -E2BIG }, }; - BUILD_BUG_ON(ARRAY_LEN(misalignments) != NUM_OPENAT2_STRUCT_VARIATIONS); - BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_STRUCT_TESTS); + BUILD_BUG_ON(ARRAY_SIZE(misalignments) != NUM_OPENAT2_STRUCT_VARIATIONS); + BUILD_BUG_ON(ARRAY_SIZE(tests) != NUM_OPENAT2_STRUCT_TESTS); - for (int i = 0; i < ARRAY_LEN(tests); i++) { + for (int i = 0; i < ARRAY_SIZE(tests); i++) { struct struct_test *test = &tests[i]; struct open_how_ext how_ext = test->arg; - for (int j = 0; j < ARRAY_LEN(misalignments); j++) { + for (int j = 0; j < ARRAY_SIZE(misalignments); j++) { int fd, misalign = misalignments[j]; char *fdpath = NULL; bool failed; @@ -241,9 +241,9 @@ void test_openat2_flags(void) .how.resolve = 0, .err = -EINVAL }, }; - BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_FLAG_TESTS); + BUILD_BUG_ON(ARRAY_SIZE(tests) != NUM_OPENAT2_FLAG_TESTS); - for (int i = 0; i < ARRAY_LEN(tests); i++) { + for (int i = 0; i < ARRAY_SIZE(tests); i++) { int fd, fdflags = -1; char *path, *fdpath = NULL; bool failed = false; diff --git a/tools/testing/selftests/filesystems/openat2/resolve_test.c b/tools/testing/selftests/filesystems/openat2/resolve_test.c index a76ef15ceb90..f7acb4300641 100644 --- a/tools/testing/selftests/filesystems/openat2/resolve_test.c +++ b/tools/testing/selftests/filesystems/openat2/resolve_test.c @@ -436,9 +436,9 @@ void test_openat2_opath_tests(void) .out.err = -ELOOP, .pass = false }, }; - BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS); + BUILD_BUG_ON(ARRAY_SIZE(tests) != NUM_OPENAT2_OPATH_TESTS); - for (int i = 0; i < ARRAY_LEN(tests); i++) { + for (int i = 0; i < ARRAY_SIZE(tests); i++) { int dfd, fd; char *fdpath = NULL; bool failed; From 215be76e025d1311acd6eafc5cfd444974f0f30e Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 1 Apr 2026 11:28:05 +1100 Subject: [PATCH 04/12] selftests: openat2: migrate to kselftest harness These tests were written in the early days of selftests' TAP support, the more modern kselftest harness is much easier to follow and maintain. The actual contents of the tests are unchanged by this change. Most of the diff involves switching from the E_* syscall wrappers we previously used to ASSERT_EQ(fn(...), 0) in tests and helper functions. The first pass of the migration was done using Claude, followed by a manual rework and review. Assisted-by: Claude:claude-4.6-opus Signed-off-by: Aleksa Sarai Link: https://patch.msgid.link/20260401-openat2-selftests-kunit-v2-4-ad153a07da0c@amutable.com Signed-off-by: Christian Brauner (Amutable) --- .../selftests/filesystems/openat2/helpers.h | 62 +-- .../filesystems/openat2/openat2_test.c | 215 +++++------ .../filesystems/openat2/rename_attack_test.c | 161 ++++---- .../filesystems/openat2/resolve_test.c | 354 ++++++++++-------- 4 files changed, 388 insertions(+), 404 deletions(-) diff --git a/tools/testing/selftests/filesystems/openat2/helpers.h b/tools/testing/selftests/filesystems/openat2/helpers.h index f56c0c6e3ad1..7ca54c718c45 100644 --- a/tools/testing/selftests/filesystems/openat2/helpers.h +++ b/tools/testing/selftests/filesystems/openat2/helpers.h @@ -15,7 +15,7 @@ #include #include #include -#include "kselftest.h" +#include "kselftest_harness.h" #define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); }))) @@ -56,36 +56,6 @@ struct open_how { (similar to chroot(2)). */ #endif /* RESOLVE_IN_ROOT */ -#define E_func(func, ...) \ - do { \ - errno = 0; \ - if (func(__VA_ARGS__) < 0) \ - ksft_exit_fail_msg("%s:%d %s failed - errno:%d\n", \ - __FILE__, __LINE__, #func, errno); \ - } while (0) - -#define E_asprintf(...) E_func(asprintf, __VA_ARGS__) -#define E_chmod(...) E_func(chmod, __VA_ARGS__) -#define E_dup2(...) E_func(dup2, __VA_ARGS__) -#define E_fchdir(...) E_func(fchdir, __VA_ARGS__) -#define E_fstatat(...) E_func(fstatat, __VA_ARGS__) -#define E_kill(...) E_func(kill, __VA_ARGS__) -#define E_mkdirat(...) E_func(mkdirat, __VA_ARGS__) -#define E_mount(...) E_func(mount, __VA_ARGS__) -#define E_prctl(...) E_func(prctl, __VA_ARGS__) -#define E_readlink(...) E_func(readlink, __VA_ARGS__) -#define E_setresuid(...) E_func(setresuid, __VA_ARGS__) -#define E_symlinkat(...) E_func(symlinkat, __VA_ARGS__) -#define E_touchat(...) E_func(touchat, __VA_ARGS__) -#define E_unshare(...) E_func(unshare, __VA_ARGS__) - -#define E_assert(expr, msg, ...) \ - do { \ - if (!(expr)) \ - ksft_exit_fail_msg("ASSERT(%s:%d) failed (%s): " msg "\n", \ - __FILE__, __LINE__, #expr, ##__VA_ARGS__); \ - } while (0) - __maybe_unused static bool needs_openat2(const struct open_how *how) { @@ -135,37 +105,39 @@ static int touchat(int dfd, const char *path) } __maybe_unused -static char *fdreadlink(int fd) +static char *fdreadlink(struct __test_metadata *_metadata, int fd) { char *target, *tmp; - E_asprintf(&tmp, "/proc/self/fd/%d", fd); + ASSERT_GT(asprintf(&tmp, "/proc/self/fd/%d", fd), 0); target = malloc(PATH_MAX); - if (!target) - ksft_exit_fail_msg("fdreadlink: malloc failed\n"); + ASSERT_NE(target, NULL); memset(target, 0, PATH_MAX); - E_readlink(tmp, target, PATH_MAX); + ASSERT_GT(readlink(tmp, target, PATH_MAX), 0); + free(tmp); return target; } __maybe_unused -static bool fdequal(int fd, int dfd, const char *path) +static bool fdequal(struct __test_metadata *_metadata, int fd, + int dfd, const char *path) { char *fdpath, *dfdpath, *other; bool cmp; - fdpath = fdreadlink(fd); - dfdpath = fdreadlink(dfd); + fdpath = fdreadlink(_metadata, fd); + dfdpath = fdreadlink(_metadata, dfd); - if (!path) - E_asprintf(&other, "%s", dfdpath); - else if (*path == '/') - E_asprintf(&other, "%s", path); - else - E_asprintf(&other, "%s/%s", dfdpath, path); + if (!path) { + ASSERT_GT(asprintf(&other, "%s", dfdpath), 0); + } else if (*path == '/') { + ASSERT_GT(asprintf(&other, "%s", path), 0); + } else { + ASSERT_GT(asprintf(&other, "%s/%s", dfdpath, path), 0); + } cmp = !strcmp(fdpath, other); diff --git a/tools/testing/selftests/filesystems/openat2/openat2_test.c b/tools/testing/selftests/filesystems/openat2/openat2_test.c index c6c26652ac1b..5ea3eebb7b59 100644 --- a/tools/testing/selftests/filesystems/openat2/openat2_test.c +++ b/tools/testing/selftests/filesystems/openat2/openat2_test.c @@ -15,8 +15,8 @@ #include #include -#include "kselftest.h" #include "helpers.h" +#include "kselftest_harness.h" /* * O_LARGEFILE is set to 0 by glibc. @@ -45,13 +45,29 @@ struct struct_test { int err; }; -#define NUM_OPENAT2_STRUCT_TESTS 7 -#define NUM_OPENAT2_STRUCT_VARIATIONS 13 +struct flag_test { + const char *name; + struct open_how how; + int err; +}; -void test_openat2_struct(void) +FIXTURE(openat2) {}; + +FIXTURE_SETUP(openat2) +{ + if (!openat2_supported) + SKIP(return, "openat2(2) not supported"); +} + +FIXTURE_TEARDOWN(openat2) {} + +/* + * Verify that the struct size and misalignment handling for openat2(2) is + * correct, including that is_zeroed_user() works. + */ +TEST_F(openat2, struct_argument_sizes) { int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 }; - struct struct_test tests[] = { /* Normal struct. */ { .name = "normal struct", @@ -83,26 +99,14 @@ void test_openat2_struct(void) .size = sizeof(struct open_how_ext), .err = -E2BIG }, }; - BUILD_BUG_ON(ARRAY_SIZE(misalignments) != NUM_OPENAT2_STRUCT_VARIATIONS); - BUILD_BUG_ON(ARRAY_SIZE(tests) != NUM_OPENAT2_STRUCT_TESTS); - for (int i = 0; i < ARRAY_SIZE(tests); i++) { struct struct_test *test = &tests[i]; struct open_how_ext how_ext = test->arg; for (int j = 0; j < ARRAY_SIZE(misalignments); j++) { int fd, misalign = misalignments[j]; - char *fdpath = NULL; - bool failed; - void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; - void *copy = NULL, *how_copy = &how_ext; - - if (!openat2_supported) { - ksft_print_msg("openat2(2) unsupported\n"); - resultfn = ksft_test_result_skip; - goto skip; - } + char *fdpath = NULL; if (misalign) { /* @@ -119,50 +123,42 @@ void test_openat2_struct(void) } fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size); - if (test->err >= 0) - failed = (fd < 0); - else - failed = (fd != test->err); if (fd >= 0) { - fdpath = fdreadlink(fd); + fdpath = fdreadlink(_metadata, fd); close(fd); } - if (failed) { - resultfn = ksft_test_result_fail; - - ksft_print_msg("openat2 unexpectedly returned "); - if (fdpath) - ksft_print_msg("%d['%s']\n", fd, fdpath); - else - ksft_print_msg("%d (%s)\n", fd, strerror(-fd)); + if (test->err >= 0) { + EXPECT_GE(fd, 0) { + TH_LOG("openat2 with %s [misalign=%d] should succeed, got %d (%s)", + test->name, misalign, + fd, strerror(-fd)); + } + } else { + EXPECT_EQ(test->err, fd) { + if (fdpath) + TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d['%s']", + test->name, misalign, + test->err, + strerror(-test->err), + fd, fdpath); + else + TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d (%s)", + test->name, misalign, + test->err, + strerror(-test->err), + fd, strerror(-fd)); + } } -skip: - if (test->err >= 0) - resultfn("openat2 with %s argument [misalign=%d] succeeds\n", - test->name, misalign); - else - resultfn("openat2 with %s argument [misalign=%d] fails with %d (%s)\n", - test->name, misalign, test->err, - strerror(-test->err)); - free(copy); free(fdpath); - fflush(stdout); } } } -struct flag_test { - const char *name; - struct open_how how; - int err; -}; - -#define NUM_OPENAT2_FLAG_TESTS 25 - -void test_openat2_flags(void) +/* Verify openat2(2) flag and mode validation. */ +TEST_F(openat2, flag_validation) { struct flag_test tests[] = { /* O_TMPFILE is incompatible with O_PATH and O_CREAT. */ @@ -241,20 +237,10 @@ void test_openat2_flags(void) .how.resolve = 0, .err = -EINVAL }, }; - BUILD_BUG_ON(ARRAY_SIZE(tests) != NUM_OPENAT2_FLAG_TESTS); - for (int i = 0; i < ARRAY_SIZE(tests); i++) { int fd, fdflags = -1; char *path, *fdpath = NULL; - bool failed = false; struct flag_test *test = &tests[i]; - void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; - - if (!openat2_supported) { - ksft_print_msg("openat2(2) unsupported\n"); - resultfn = ksft_test_result_skip; - goto skip; - } path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : "."; unlink(path); @@ -265,74 +251,63 @@ void test_openat2_flags(void) * Skip the testcase if it failed because not supported * by FS. (e.g. a valid O_TMPFILE combination on NFS) */ - ksft_test_result_skip("openat2 with %s fails with %d (%s)\n", - test->name, fd, strerror(-fd)); - goto next; + TH_LOG("openat2 with %s not supported by FS -- skipping", + test->name); + continue; } - if (test->err >= 0) - failed = (fd < 0); - else - failed = (fd != test->err); - if (fd >= 0) { - int otherflags; + if (test->err >= 0) { + EXPECT_GE(fd, 0) { + TH_LOG("openat2 with %s should succeed, got %d (%s)", + test->name, fd, strerror(-fd)); + } + if (fd >= 0) { + int otherflags; - fdpath = fdreadlink(fd); - fdflags = fcntl(fd, F_GETFL); - otherflags = fcntl(fd, F_GETFD); - close(fd); + fdpath = fdreadlink(_metadata, fd); + fdflags = fcntl(fd, F_GETFL); + otherflags = fcntl(fd, F_GETFD); + close(fd); - E_assert(fdflags >= 0, "fcntl F_GETFL of new fd"); - E_assert(otherflags >= 0, "fcntl F_GETFD of new fd"); + ASSERT_GE(fdflags, 0); + ASSERT_GE(otherflags, 0); - /* O_CLOEXEC isn't shown in F_GETFL. */ - if (otherflags & FD_CLOEXEC) - fdflags |= O_CLOEXEC; - /* O_CREAT is hidden from F_GETFL. */ - if (test->how.flags & O_CREAT) - fdflags |= O_CREAT; - if (!(test->how.flags & O_LARGEFILE)) - fdflags &= ~O_LARGEFILE; - failed |= (fdflags != test->how.flags); + /* O_CLOEXEC isn't shown in F_GETFL. */ + if (otherflags & FD_CLOEXEC) + fdflags |= O_CLOEXEC; + /* O_CREAT is hidden from F_GETFL. */ + if (test->how.flags & O_CREAT) + fdflags |= O_CREAT; + if (!(test->how.flags & O_LARGEFILE)) + fdflags &= ~O_LARGEFILE; + + EXPECT_EQ(fdflags, (int)test->how.flags) { + TH_LOG("openat2 with %s: flags mismatch %X != %llX", + test->name, fdflags, + (unsigned long long)test->how.flags); + } + } + } else { + EXPECT_EQ(test->err, fd) { + if (fd >= 0) { + fdpath = fdreadlink(_metadata, fd); + TH_LOG("openat2 with %s should fail with %d (%s), got %d['%s']", + test->name, test->err, + strerror(-test->err), + fd, fdpath); + } else { + TH_LOG("openat2 with %s should fail with %d (%s), got %d (%s)", + test->name, test->err, + strerror(-test->err), + fd, strerror(-fd)); + } + } + if (fd >= 0) + close(fd); } - if (failed) { - resultfn = ksft_test_result_fail; - - ksft_print_msg("openat2 unexpectedly returned "); - if (fdpath) - ksft_print_msg("%d['%s'] with %X (!= %llX)\n", - fd, fdpath, fdflags, - test->how.flags); - else - ksft_print_msg("%d (%s)\n", fd, strerror(-fd)); - } - -skip: - if (test->err >= 0) - resultfn("openat2 with %s succeeds\n", test->name); - else - resultfn("openat2 with %s fails with %d (%s)\n", - test->name, test->err, strerror(-test->err)); -next: free(fdpath); - fflush(stdout); } } -#define NUM_TESTS (NUM_OPENAT2_STRUCT_VARIATIONS * NUM_OPENAT2_STRUCT_TESTS + \ - NUM_OPENAT2_FLAG_TESTS) - -int main(int argc, char **argv) -{ - ksft_print_header(); - ksft_set_plan(NUM_TESTS); - - test_openat2_struct(); - test_openat2_flags(); - - if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) - ksft_exit_fail(); - else - ksft_exit_pass(); -} +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/openat2/rename_attack_test.c b/tools/testing/selftests/filesystems/openat2/rename_attack_test.c index aa5699e45729..1f33c34f56be 100644 --- a/tools/testing/selftests/filesystems/openat2/rename_attack_test.c +++ b/tools/testing/selftests/filesystems/openat2/rename_attack_test.c @@ -22,44 +22,21 @@ #include #include -#include "kselftest.h" #include "helpers.h" +#include "kselftest_harness.h" -/* Construct a test directory with the following structure: - * - * root/ - * |-- a/ - * | `-- c/ - * `-- b/ - */ -int setup_testdir(void) -{ - int dfd; - char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX"; - - /* Make the top-level directory. */ - if (!mkdtemp(dirname)) - ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n"); - dfd = open(dirname, O_PATH | O_DIRECTORY); - if (dfd < 0) - ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n"); - - E_mkdirat(dfd, "a", 0755); - E_mkdirat(dfd, "b", 0755); - E_mkdirat(dfd, "a/c", 0755); - - return dfd; -} +#define ROUNDS 400000 /* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */ -pid_t spawn_attack(int dirfd, char *a, char *b) +pid_t spawn_attack(struct __test_metadata *_metadata, + int dirfd, char *a, char *b) { pid_t child = fork(); if (child != 0) return child; /* If the parent (the test process) dies, kill ourselves too. */ - E_prctl(PR_SET_PDEATHSIG, SIGKILL); + ASSERT_EQ(prctl(PR_SET_PDEATHSIG, SIGKILL), 0); /* Swap @a and @b. */ for (;;) @@ -67,52 +44,90 @@ pid_t spawn_attack(int dirfd, char *a, char *b) exit(1); } -#define NUM_RENAME_TESTS 2 -#define ROUNDS 400000 +/* + * Construct a test directory with the following structure: + * + * root/ + * |-- a/ + * | `-- c/ + * `-- b/ + */ +FIXTURE(rename_attack) { + int dfd; + int afd; + pid_t child; +}; -const char *flagname(int resolve) +FIXTURE_SETUP(rename_attack) { - switch (resolve) { - case RESOLVE_IN_ROOT: - return "RESOLVE_IN_ROOT"; - case RESOLVE_BENEATH: - return "RESOLVE_BENEATH"; - } - return "(unknown)"; + char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX"; + + self->dfd = -1; + self->afd = -1; + self->child = 0; + + /* Make the top-level directory. */ + ASSERT_NE(mkdtemp(dirname), NULL); + self->dfd = open(dirname, O_PATH | O_DIRECTORY); + ASSERT_GE(self->dfd, 0); + + ASSERT_EQ(mkdirat(self->dfd, "a", 0755), 0); + ASSERT_EQ(mkdirat(self->dfd, "b", 0755), 0); + ASSERT_EQ(mkdirat(self->dfd, "a/c", 0755), 0); + + self->afd = openat(self->dfd, "a", O_PATH); + ASSERT_GE(self->afd, 0); + + self->child = spawn_attack(_metadata, self->dfd, "a/c", "b"); + ASSERT_GT(self->child, 0); } -void test_rename_attack(int resolve) +FIXTURE_TEARDOWN(rename_attack) { - int dfd, afd; - pid_t child; - void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; - int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0; + if (self->child > 0) + kill(self->child, SIGKILL); + if (self->afd >= 0) + close(self->afd); + if (self->dfd >= 0) + close(self->dfd); +} +FIXTURE_VARIANT(rename_attack) { + int resolve; + const char *name; +}; + +FIXTURE_VARIANT_ADD(rename_attack, resolve_beneath) { + .resolve = RESOLVE_BENEATH, + .name = "RESOLVE_BENEATH", +}; + +FIXTURE_VARIANT_ADD(rename_attack, resolve_in_root) { + .resolve = RESOLVE_IN_ROOT, + .name = "RESOLVE_IN_ROOT", +}; + +TEST_F_TIMEOUT(rename_attack, test, 120) +{ + int escapes = 0, successes = 0, other_errs = 0, exdevs = 0, eagains = 0; + char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../.."; struct open_how how = { .flags = O_PATH, - .resolve = resolve, + .resolve = variant->resolve, }; if (!openat2_supported) { how.resolve = 0; - ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n"); + TH_LOG("openat2(2) unsupported -- using openat(2) instead"); } - dfd = setup_testdir(); - afd = openat(dfd, "a", O_PATH); - if (afd < 0) - ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n"); - - child = spawn_attack(dfd, "a/c", "b"); - for (int i = 0; i < ROUNDS; i++) { int fd; - char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../.."; if (openat2_supported) - fd = sys_openat2(afd, victim_path, &how); + fd = sys_openat2(self->afd, victim_path, &how); else - fd = sys_openat(afd, victim_path, &how); + fd = sys_openat(self->afd, victim_path, &how); if (fd < 0) { if (fd == -EAGAIN) @@ -124,37 +139,21 @@ void test_rename_attack(int resolve) else other_errs++; /* unexpected error */ } else { - if (fdequal(fd, afd, NULL)) + if (fdequal(_metadata, fd, self->afd, NULL)) successes++; else escapes++; /* we got an unexpected fd */ } - close(fd); + if (fd >= 0) + close(fd); } - if (escapes > 0) - resultfn = ksft_test_result_fail; - ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E=%d success=%d\n", - eagains, exdevs, other_errs, successes); - resultfn("rename attack with %s (%d runs, got %d escapes)\n", - flagname(resolve), ROUNDS, escapes); - - /* Should be killed anyway, but might as well make sure. */ - E_kill(child, SIGKILL); + TH_LOG("non-escapes: EAGAIN=%d EXDEV=%d E=%d success=%d", + eagains, exdevs, other_errs, successes); + ASSERT_EQ(escapes, 0) { + TH_LOG("rename attack with %s (%d runs, got %d escapes)", + variant->name, ROUNDS, escapes); + } } -#define NUM_TESTS NUM_RENAME_TESTS - -int main(int argc, char **argv) -{ - ksft_print_header(); - ksft_set_plan(NUM_TESTS); - - test_rename_attack(RESOLVE_BENEATH); - test_rename_attack(RESOLVE_IN_ROOT); - - if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) - ksft_exit_fail(); - else - ksft_exit_pass(); -} +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/openat2/resolve_test.c b/tools/testing/selftests/filesystems/openat2/resolve_test.c index f7acb4300641..eacde59ce158 100644 --- a/tools/testing/selftests/filesystems/openat2/resolve_test.c +++ b/tools/testing/selftests/filesystems/openat2/resolve_test.c @@ -14,8 +14,81 @@ #include #include -#include "kselftest.h" #include "helpers.h" +#include "kselftest_harness.h" + +struct resolve_test { + const char *name; + const char *dir; + const char *path; + struct open_how how; + bool pass; + union { + int err; + const char *path; + } out; +}; + +/* + * Verify a single resolve test case. This must be called from within a TEST_F + * function with _metadata in scope. + */ +static void verify_resolve_test(struct __test_metadata *_metadata, + int rootfd, int hardcoded_fd, + const struct resolve_test *test) +{ + struct open_how how = test->how; + int dfd, fd; + char *fdpath = NULL; + + /* Auto-set O_PATH. */ + if (!(how.flags & O_CREAT)) + how.flags |= O_PATH; + + if (test->dir) + dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY); + else + dfd = dup(rootfd); + ASSERT_GE(dfd, 0) TH_LOG("failed to open dir '%s': %m", test->dir ?: "."); + ASSERT_EQ(dup2(dfd, hardcoded_fd), hardcoded_fd); + + fd = sys_openat2(dfd, test->path, &how); + + if (test->pass) { + EXPECT_GE(fd, 0) { + TH_LOG("%s: expected success, got %d (%s)", + test->name, fd, strerror(-fd)); + } + if (fd >= 0) { + EXPECT_TRUE(fdequal(_metadata, fd, rootfd, test->out.path)) { + fdpath = fdreadlink(_metadata, fd); + TH_LOG("%s: wrong path '%s', expected '%s'", + test->name, fdpath, + test->out.path ?: "."); + free(fdpath); + } + } + } else { + EXPECT_EQ(test->out.err, fd) { + if (fd >= 0) { + fdpath = fdreadlink(_metadata, fd); + TH_LOG("%s: expected %d (%s), got %d['%s']", + test->name, test->out.err, + strerror(-test->out.err), fd, fdpath); + free(fdpath); + } else { + TH_LOG("%s: expected %d (%s), got %d (%s)", + test->name, test->out.err, + strerror(-test->out.err), + fd, strerror(-fd)); + } + } + } + + if (fd >= 0) + close(fd); + close(dfd); +} /* * Construct a test directory with the following structure: @@ -39,101 +112,110 @@ * |-- absself -> / * |-- self -> ../../root/ * |-- garbageself -> /../../root/ - * |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd - * |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd + * |-- passwd -> ../cheeky/../etc/../etc/passwd + * |-- abspasswd -> /../cheeky/../etc/../etc/passwd * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd */ -int setup_testdir(void) +FIXTURE(openat2_resolve) { + int rootfd; + int hardcoded_fd; + char *hardcoded_fdpath; + char *procselfexe; +}; + +FIXTURE_SETUP(openat2_resolve) { - int dfd, tmpfd; char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX"; + int dfd, tmpfd; + + self->rootfd = -1; + self->hardcoded_fd = -1; + self->hardcoded_fdpath = NULL; + self->procselfexe = NULL; + + /* NOTE: We should be checking for CAP_SYS_ADMIN here... */ + if (geteuid() != 0) + SKIP(return, "all tests require euid == 0"); + if (!openat2_supported) + SKIP(return, "openat2(2) not supported"); /* Unshare and make /tmp a new directory. */ - E_unshare(CLONE_NEWNS); - E_mount("", "/tmp", "", MS_PRIVATE, ""); + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/tmp", "", MS_PRIVATE, ""), 0); /* Make the top-level directory. */ - if (!mkdtemp(dirname)) - ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n"); + ASSERT_NE(mkdtemp(dirname), NULL); dfd = open(dirname, O_PATH | O_DIRECTORY); - if (dfd < 0) - ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n"); + ASSERT_GE(dfd, 0); /* A sub-directory which is actually used for tests. */ - E_mkdirat(dfd, "root", 0755); + ASSERT_EQ(mkdirat(dfd, "root", 0755), 0); tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY); - if (tmpfd < 0) - ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n"); + ASSERT_GE(tmpfd, 0); close(dfd); dfd = tmpfd; - E_symlinkat("/proc/self/exe", dfd, "procexe"); - E_symlinkat("/proc/self/root", dfd, "procroot"); - E_mkdirat(dfd, "root", 0755); + ASSERT_EQ(symlinkat("/proc/self/exe", dfd, "procexe"), 0); + ASSERT_EQ(symlinkat("/proc/self/root", dfd, "procroot"), 0); + ASSERT_EQ(mkdirat(dfd, "root", 0755), 0); /* There is no mountat(2), so use chdir. */ - E_mkdirat(dfd, "mnt", 0755); - E_fchdir(dfd); - E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""); - E_symlinkat("../mnt/", dfd, "mnt/self"); - E_symlinkat("/mnt/", dfd, "mnt/absself"); + ASSERT_EQ(mkdirat(dfd, "mnt", 0755), 0); + ASSERT_EQ(fchdir(dfd), 0); + ASSERT_EQ(mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""), 0); + ASSERT_EQ(symlinkat("../mnt/", dfd, "mnt/self"), 0); + ASSERT_EQ(symlinkat("/mnt/", dfd, "mnt/absself"), 0); - E_mkdirat(dfd, "etc", 0755); - E_touchat(dfd, "etc/passwd"); + ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0); + ASSERT_GE(touchat(dfd, "etc/passwd"), 0); - E_symlinkat("/newfile3", dfd, "creatlink"); - E_symlinkat("etc/", dfd, "reletc"); - E_symlinkat("etc/passwd", dfd, "relsym"); - E_symlinkat("/etc/", dfd, "absetc"); - E_symlinkat("/etc/passwd", dfd, "abssym"); - E_symlinkat("/cheeky", dfd, "abscheeky"); + ASSERT_EQ(symlinkat("/newfile3", dfd, "creatlink"), 0); + ASSERT_EQ(symlinkat("etc/", dfd, "reletc"), 0); + ASSERT_EQ(symlinkat("etc/passwd", dfd, "relsym"), 0); + ASSERT_EQ(symlinkat("/etc/", dfd, "absetc"), 0); + ASSERT_EQ(symlinkat("/etc/passwd", dfd, "abssym"), 0); + ASSERT_EQ(symlinkat("/cheeky", dfd, "abscheeky"), 0); - E_mkdirat(dfd, "cheeky", 0755); + ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0); - E_symlinkat("/", dfd, "cheeky/absself"); - E_symlinkat("../../root/", dfd, "cheeky/self"); - E_symlinkat("/../../root/", dfd, "cheeky/garbageself"); + ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0); + ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0); + ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0); - E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd"); - E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd"); + ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd", + dfd, "cheeky/passwd"), 0); + ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd", + dfd, "cheeky/abspasswd"), 0); - E_symlinkat("../../../../../../../../../../../../../../etc/passwd", - dfd, "cheeky/dotdotlink"); - E_symlinkat("/../../../../../../../../../../../../../../etc/passwd", - dfd, "cheeky/garbagelink"); + ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd", + dfd, "cheeky/dotdotlink"), 0); + ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd", + dfd, "cheeky/garbagelink"), 0); - return dfd; + self->rootfd = dfd; + + self->hardcoded_fd = open("/dev/null", O_RDONLY); + ASSERT_GE(self->hardcoded_fd, 0); + ASSERT_GE(asprintf(&self->hardcoded_fdpath, "self/fd/%d", + self->hardcoded_fd), 0); + ASSERT_GE(asprintf(&self->procselfexe, "/proc/%d/exe", getpid()), 0); } -struct basic_test { - const char *name; - const char *dir; - const char *path; - struct open_how how; - bool pass; - union { - int err; - const char *path; - } out; -}; - -#define NUM_OPENAT2_OPATH_TESTS 88 - -void test_openat2_opath_tests(void) +FIXTURE_TEARDOWN(openat2_resolve) { - int rootfd, hardcoded_fd; - char *procselfexe, *hardcoded_fdpath; + free(self->procselfexe); + free(self->hardcoded_fdpath); + if (self->hardcoded_fd >= 0) + close(self->hardcoded_fd); + if (self->rootfd >= 0) + close(self->rootfd); +} - E_asprintf(&procselfexe, "/proc/%d/exe", getpid()); - rootfd = setup_testdir(); - - hardcoded_fd = open("/dev/null", O_RDONLY); - E_assert(hardcoded_fd >= 0, "open fd to hardcode"); - E_asprintf(&hardcoded_fdpath, "self/fd/%d", hardcoded_fd); - - struct basic_test tests[] = { - /** RESOLVE_BENEATH **/ +/* Attempts to cross the dirfd should be blocked with -EXDEV. */ +TEST_F(openat2_resolve, resolve_beneath) +{ + struct resolve_test tests[] = { /* Attempts to cross dirfd should be blocked. */ { .name = "[beneath] jump to /", .path = "/", .how.resolve = RESOLVE_BENEATH, @@ -206,9 +288,17 @@ void test_openat2_opath_tests(void) { .name = "[beneath] tricky absolute + garbage link outside $root", .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH, .out.err = -EXDEV, .pass = false }, + }; - /** RESOLVE_IN_ROOT **/ - /* All attempts to cross the dirfd will be scoped-to-root. */ + for (int i = 0; i < ARRAY_SIZE(tests); i++) + verify_resolve_test(_metadata, self->rootfd, + self->hardcoded_fd, &tests[i]); +} + +/* All attempts to cross the dirfd will be scoped-to-root. */ +TEST_F(openat2_resolve, resolve_in_root) +{ + struct resolve_test tests[] = { { .name = "[in_root] jump to /", .path = "/", .how.resolve = RESOLVE_IN_ROOT, .out.path = NULL, .pass = true }, @@ -297,8 +387,17 @@ void test_openat2_opath_tests(void) .how.mode = 0700, .how.resolve = RESOLVE_IN_ROOT, .out.path = "newfile3", .pass = true }, + }; - /** RESOLVE_NO_XDEV **/ + for (int i = 0; i < ARRAY_SIZE(tests); i++) + verify_resolve_test(_metadata, self->rootfd, + self->hardcoded_fd, &tests[i]); +} + +/* Crossing mount boundaries should be blocked. */ +TEST_F(openat2_resolve, resolve_no_xdev) +{ + struct resolve_test tests[] = { /* Crossing *down* into a mountpoint is disallowed. */ { .name = "[no_xdev] cross into $mnt", .path = "mnt", .how.resolve = RESOLVE_NO_XDEV, @@ -347,10 +446,19 @@ void test_openat2_opath_tests(void) .out.err = -EXDEV, .pass = false }, /* Except magic-link jumps inside the same vfsmount. */ { .name = "[no_xdev] jump through magic-link to same procfs", - .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV, - .out.path = "/proc", .pass = true, }, + .dir = "/proc", .path = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV, + .out.path = "/proc", .pass = true, }, + }; - /** RESOLVE_NO_MAGICLINKS **/ + for (int i = 0; i < ARRAY_SIZE(tests); i++) + verify_resolve_test(_metadata, self->rootfd, + self->hardcoded_fd, &tests[i]); +} + +/* Procfs-style magic-link resolution should be blocked. */ +TEST_F(openat2_resolve, resolve_no_magiclinks) +{ + struct resolve_test tests[] = { /* Regular symlinks should work. */ { .name = "[no_magiclinks] ordinary relative symlink", .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS, @@ -365,7 +473,7 @@ void test_openat2_opath_tests(void) { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW", .path = "/proc/self/exe", .how.flags = O_NOFOLLOW, .how.resolve = RESOLVE_NO_MAGICLINKS, - .out.path = procselfexe, .pass = true }, + .out.path = self->procselfexe, .pass = true }, { .name = "[no_magiclinks] symlink to magic-link path component", .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS, .out.err = -ELOOP, .pass = false }, @@ -376,8 +484,17 @@ void test_openat2_opath_tests(void) .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW, .how.resolve = RESOLVE_NO_MAGICLINKS, .out.err = -ELOOP, .pass = false }, + }; - /** RESOLVE_NO_SYMLINKS **/ + for (int i = 0; i < ARRAY_SIZE(tests); i++) + verify_resolve_test(_metadata, self->rootfd, + self->hardcoded_fd, &tests[i]); +} + +/* All symlink resolution should be blocked. */ +TEST_F(openat2_resolve, resolve_no_symlinks) +{ + struct resolve_test tests[] = { /* Normal paths should work. */ { .name = "[no_symlinks] ordinary path to '.'", .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS, @@ -436,88 +553,9 @@ void test_openat2_opath_tests(void) .out.err = -ELOOP, .pass = false }, }; - BUILD_BUG_ON(ARRAY_SIZE(tests) != NUM_OPENAT2_OPATH_TESTS); - - for (int i = 0; i < ARRAY_SIZE(tests); i++) { - int dfd, fd; - char *fdpath = NULL; - bool failed; - void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; - struct basic_test *test = &tests[i]; - - if (!openat2_supported) { - ksft_print_msg("openat2(2) unsupported\n"); - resultfn = ksft_test_result_skip; - goto skip; - } - - /* Auto-set O_PATH. */ - if (!(test->how.flags & O_CREAT)) - test->how.flags |= O_PATH; - - if (test->dir) - dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY); - else - dfd = dup(rootfd); - E_assert(dfd, "failed to openat root '%s': %m", test->dir); - - E_dup2(dfd, hardcoded_fd); - - fd = sys_openat2(dfd, test->path, &test->how); - if (test->pass) - failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path)); - else - failed = (fd != test->out.err); - if (fd >= 0) { - fdpath = fdreadlink(fd); - close(fd); - } - close(dfd); - - if (failed) { - resultfn = ksft_test_result_fail; - - ksft_print_msg("openat2 unexpectedly returned "); - if (fdpath) - ksft_print_msg("%d['%s']\n", fd, fdpath); - else - ksft_print_msg("%d (%s)\n", fd, strerror(-fd)); - } - -skip: - if (test->pass) - resultfn("%s gives path '%s'\n", test->name, - test->out.path ?: "."); - else - resultfn("%s fails with %d (%s)\n", test->name, - test->out.err, strerror(-test->out.err)); - - fflush(stdout); - free(fdpath); - } - - free(procselfexe); - close(rootfd); - - free(hardcoded_fdpath); - close(hardcoded_fd); + for (int i = 0; i < ARRAY_SIZE(tests); i++) + verify_resolve_test(_metadata, self->rootfd, + self->hardcoded_fd, &tests[i]); } -#define NUM_TESTS NUM_OPENAT2_OPATH_TESTS - -int main(int argc, char **argv) -{ - ksft_print_header(); - ksft_set_plan(NUM_TESTS); - - /* NOTE: We should be checking for CAP_SYS_ADMIN here... */ - if (geteuid() != 0) - ksft_exit_skip("all tests require euid == 0\n"); - - test_openat2_opath_tests(); - - if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) - ksft_exit_fail(); - else - ksft_exit_pass(); -} +TEST_HARNESS_MAIN From 31cf44efa6df72a524b40adefb80539f3a4e13ba Mon Sep 17 00:00:00 2001 From: Jori Koolstra Date: Fri, 24 Apr 2026 13:46:02 +0200 Subject: [PATCH 05/12] vfs: add O_EMPTYPATH to openat(2)/openat2(2) 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//fd/"), 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. Note: This implies that you cannot rely anymore on disabling procfs from being mounted (e.g. inside a container without procfs mounted and with CAP_SYS_ADMIN dropped) to prevent O_PATH fds from being re-opened read-write. Signed-off-by: Jori Koolstra Link: https://patch.msgid.link/20260424114611.1678641-2-jkoolstra@xs4all.nl Signed-off-by: Christian Brauner (Amutable) --- fs/fcntl.c | 2 +- fs/open.c | 6 ++++-- include/linux/fcntl.h | 2 +- include/uapi/asm-generic/fcntl.h | 4 ++++ 4 files changed, 10 insertions(+), 4 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) From c0329020da211b41afce4d1c8a1c2494c6d97883 Mon Sep 17 00:00:00 2001 From: Jori Koolstra Date: Fri, 24 Apr 2026 13:46:03 +0200 Subject: [PATCH 06/12] selftest: add tests for O_EMPTYPATH Add tests for the new O_EMPTYPATH flag of openat(2)/openat2(2). Also, the current openat2 tests include a helper header file that defines the necessary structs and constants to use openat2(2), such as struct open_how. This may result in conflicting definitions when the system header openat2.h is present as well. So add openat2.h generated by 'make headers' to the uapi header files in ./tools/include and remove the helper file definitions of the current openat2 selftests. Signed-off-by: Jori Koolstra Link: https://patch.msgid.link/20260424114611.1678641-3-jkoolstra@xs4all.nl Signed-off-by: Christian Brauner (Amutable) --- tools/include/uapi/linux/openat2.h | 43 +++++++++++++++ .../selftests/filesystems/openat2/Makefile | 4 +- .../filesystems/openat2/emptypath_test.c | 55 +++++++++++++++++++ .../selftests/filesystems/openat2/helpers.h | 35 +----------- 4 files changed, 101 insertions(+), 36 deletions(-) create mode 100644 tools/include/uapi/linux/openat2.h create mode 100644 tools/testing/selftests/filesystems/openat2/emptypath_test.c 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 + +/* + * 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 +#include +#include + +#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 #include #include +#include #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) { From cf1b04aaef8b83f668fac10bfc4d4d76ba6e6fa1 Mon Sep 17 00:00:00 2001 From: Dorjoy Chowdhury Date: Sat, 28 Mar 2026 23:22:22 +0600 Subject: [PATCH 07/12] openat2: introduce EFTYPE error code Introduce a new error code EFTYPE for wrong file type operations. EFTYPE is already used in BSD systems like FreeBSD and macOS. This will be used by the upcoming OPENAT2_REGULAR flag support to return a specific error when a path doesn't refer to a regular file. Signed-off-by: Dorjoy Chowdhury Link: https://patch.msgid.link/20260328172314.45807-2-dorjoychy111@gmail.com Reviewed-by: Jeff Layton Reviewed-by: Aleksa Sarai Signed-off-by: Christian Brauner (Amutable) --- arch/alpha/include/uapi/asm/errno.h | 2 ++ arch/mips/include/uapi/asm/errno.h | 2 ++ arch/parisc/include/uapi/asm/errno.h | 2 ++ arch/sparc/include/uapi/asm/errno.h | 2 ++ include/uapi/asm-generic/errno.h | 2 ++ tools/arch/alpha/include/uapi/asm/errno.h | 2 ++ tools/arch/mips/include/uapi/asm/errno.h | 2 ++ tools/arch/parisc/include/uapi/asm/errno.h | 2 ++ tools/arch/sparc/include/uapi/asm/errno.h | 2 ++ tools/include/uapi/asm-generic/errno.h | 2 ++ 10 files changed, 20 insertions(+) diff --git a/arch/alpha/include/uapi/asm/errno.h b/arch/alpha/include/uapi/asm/errno.h index 6791f6508632..1a99f38813c7 100644 --- a/arch/alpha/include/uapi/asm/errno.h +++ b/arch/alpha/include/uapi/asm/errno.h @@ -127,4 +127,6 @@ #define EHWPOISON 139 /* Memory page has hardware error */ +#define EFTYPE 140 /* Wrong file type for the intended operation */ + #endif diff --git a/arch/mips/include/uapi/asm/errno.h b/arch/mips/include/uapi/asm/errno.h index c01ed91b1ef4..1835a50b69ce 100644 --- a/arch/mips/include/uapi/asm/errno.h +++ b/arch/mips/include/uapi/asm/errno.h @@ -126,6 +126,8 @@ #define EHWPOISON 168 /* Memory page has hardware error */ +#define EFTYPE 169 /* Wrong file type for the intended operation */ + #define EDQUOT 1133 /* Quota exceeded */ diff --git a/arch/parisc/include/uapi/asm/errno.h b/arch/parisc/include/uapi/asm/errno.h index 8cbc07c1903e..93194fbb0a80 100644 --- a/arch/parisc/include/uapi/asm/errno.h +++ b/arch/parisc/include/uapi/asm/errno.h @@ -124,4 +124,6 @@ #define EHWPOISON 257 /* Memory page has hardware error */ +#define EFTYPE 258 /* Wrong file type for the intended operation */ + #endif diff --git a/arch/sparc/include/uapi/asm/errno.h b/arch/sparc/include/uapi/asm/errno.h index 4a41e7835fd5..71940ec9130b 100644 --- a/arch/sparc/include/uapi/asm/errno.h +++ b/arch/sparc/include/uapi/asm/errno.h @@ -117,4 +117,6 @@ #define EHWPOISON 135 /* Memory page has hardware error */ +#define EFTYPE 136 /* Wrong file type for the intended operation */ + #endif diff --git a/include/uapi/asm-generic/errno.h b/include/uapi/asm-generic/errno.h index 92e7ae493ee3..bd78e69e0a43 100644 --- a/include/uapi/asm-generic/errno.h +++ b/include/uapi/asm-generic/errno.h @@ -122,4 +122,6 @@ #define EHWPOISON 133 /* Memory page has hardware error */ +#define EFTYPE 134 /* Wrong file type for the intended operation */ + #endif diff --git a/tools/arch/alpha/include/uapi/asm/errno.h b/tools/arch/alpha/include/uapi/asm/errno.h index 6791f6508632..1a99f38813c7 100644 --- a/tools/arch/alpha/include/uapi/asm/errno.h +++ b/tools/arch/alpha/include/uapi/asm/errno.h @@ -127,4 +127,6 @@ #define EHWPOISON 139 /* Memory page has hardware error */ +#define EFTYPE 140 /* Wrong file type for the intended operation */ + #endif diff --git a/tools/arch/mips/include/uapi/asm/errno.h b/tools/arch/mips/include/uapi/asm/errno.h index c01ed91b1ef4..1835a50b69ce 100644 --- a/tools/arch/mips/include/uapi/asm/errno.h +++ b/tools/arch/mips/include/uapi/asm/errno.h @@ -126,6 +126,8 @@ #define EHWPOISON 168 /* Memory page has hardware error */ +#define EFTYPE 169 /* Wrong file type for the intended operation */ + #define EDQUOT 1133 /* Quota exceeded */ diff --git a/tools/arch/parisc/include/uapi/asm/errno.h b/tools/arch/parisc/include/uapi/asm/errno.h index 8cbc07c1903e..93194fbb0a80 100644 --- a/tools/arch/parisc/include/uapi/asm/errno.h +++ b/tools/arch/parisc/include/uapi/asm/errno.h @@ -124,4 +124,6 @@ #define EHWPOISON 257 /* Memory page has hardware error */ +#define EFTYPE 258 /* Wrong file type for the intended operation */ + #endif diff --git a/tools/arch/sparc/include/uapi/asm/errno.h b/tools/arch/sparc/include/uapi/asm/errno.h index 4a41e7835fd5..71940ec9130b 100644 --- a/tools/arch/sparc/include/uapi/asm/errno.h +++ b/tools/arch/sparc/include/uapi/asm/errno.h @@ -117,4 +117,6 @@ #define EHWPOISON 135 /* Memory page has hardware error */ +#define EFTYPE 136 /* Wrong file type for the intended operation */ + #endif diff --git a/tools/include/uapi/asm-generic/errno.h b/tools/include/uapi/asm-generic/errno.h index 92e7ae493ee3..bd78e69e0a43 100644 --- a/tools/include/uapi/asm-generic/errno.h +++ b/tools/include/uapi/asm-generic/errno.h @@ -122,4 +122,6 @@ #define EHWPOISON 133 /* Memory page has hardware error */ +#define EFTYPE 134 /* Wrong file type for the intended operation */ + #endif From 8b82cacad92ebae9619872a5a69c570eba30140b Mon Sep 17 00:00:00 2001 From: Dorjoy Chowdhury Date: Sat, 16 May 2026 16:42:39 +0200 Subject: [PATCH 08/12] openat2: new OPENAT2_REGULAR flag support This flag indicates the path should be opened if it's a regular file. This is useful to write secure programs that want to avoid being tricked into opening device nodes with special semantics while thinking they operate on regular files. This is a requested feature from the uapi-group[1]. The previously introduced EFTYPE error code is returned when the path doesn't refer to a regular file. For example, if openat2 is called on path /dev/null with OPENAT2_REGULAR in the flag param, it will return -EFTYPE. When used in combination with O_CREAT, either the regular file is created, or if the path already exists, it is opened if it's a regular file. Otherwise, -EFTYPE is returned. When OPENAT2_REGULAR is combined with O_DIRECTORY, -EINVAL is returned as it doesn't make sense to open a path that is both a directory and a regular file. The UAPI bit lives in the upper 32 bits of open_how::flags (((__u64)1 << 32)) so that open(2) and openat(2) -- whose @flags argument is a C int -- cannot physically express it. This is a structural guarantee, not a runtime mask: the bit is unrepresentable in 32 bits. Because the rest of the VFS open path narrows to 32 bits in several places (op->open_flag, f->f_flags, the unsigned open_flag argument of i_op->atomic_open()), build_open_flags() translates OPENAT2_REGULAR into a kernel-internal lower-32-bit carrier __O_REGULAR (bit 4, unused as an O_* on every architecture) before the assignment to op->open_flag. __O_REGULAR then rides through the existing channels exactly like __FMODE_EXEC. do_dentry_open() strips it so it cannot leak back to userspace via fcntl(F_GETFL). Four BUILD_BUG_ON_MSG() invariants in build_open_flags() prevent any future bit collision or accidental low-32 redefinition: - VALID_OPEN_FLAGS fits in 32 bits. - OPENAT2_REGULAR lives in the upper 32 bits. - OPENAT2_REGULAR does not alias any open()/openat() flag. - __O_REGULAR does not alias any user-visible flag. [1]: https://uapi-group.org/kernel-features/#ability-to-only-open-regular-files Christian Brauner says: Move OPENAT2_REGULAR to the upper 32 bits of open_how::flags with a kernel-internal __O_REGULAR carrier so that open(2)/openat(2) cannot encode the flag; add BUILD_BUG_ON_MSG() invariants and register __O_REGULAR in the fcntl_init() allocation-uniqueness BUILD_BUG_ON() (bit count 21 -> 22). Signed-off-by: Dorjoy Chowdhury Link: https://patch.msgid.link/20260328172314.45807-2-dorjoychy111@gmail.com Reviewed-by: Jeff Layton Reviewed-by: Aleksa Sarai Signed-off-by: Christian Brauner (Amutable) --- fs/ceph/file.c | 4 ++++ fs/fcntl.c | 4 ++-- fs/gfs2/inode.c | 7 +++++++ fs/namei.c | 4 ++++ fs/nfs/dir.c | 4 ++++ fs/open.c | 35 ++++++++++++++++++++++++++++++++--- fs/smb/client/dir.c | 18 +++++++++++++++--- include/linux/fcntl.h | 18 ++++++++++++++++++ include/uapi/linux/openat2.h | 7 +++++++ 9 files changed, 93 insertions(+), 8 deletions(-) diff --git a/fs/ceph/file.c b/fs/ceph/file.c index d54d71669176..0ad42e1cc305 100644 --- a/fs/ceph/file.c +++ b/fs/ceph/file.c @@ -996,6 +996,10 @@ int ceph_atomic_open(struct inode *dir, struct dentry *dentry, ceph_init_inode_acls(newino, &as_ctx); file->f_mode |= FMODE_CREATED; } + if ((flags & __O_REGULAR) && !d_is_reg(dentry)) { + err = -EFTYPE; + goto out_req; + } err = finish_open(file, dentry, ceph_open); } out_req: diff --git a/fs/fcntl.c b/fs/fcntl.c index 7d2165855a9c..b3ea135b74d8 100644 --- a/fs/fcntl.c +++ b/fs/fcntl.c @@ -1169,10 +1169,10 @@ 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(21 - 1 /* for O_RDONLY being 0 */ != + BUILD_BUG_ON(22 - 1 /* for O_RDONLY being 0 */ != HWEIGHT32( (VALID_OPEN_FLAGS & ~(O_NONBLOCK | O_NDELAY)) | - __FMODE_EXEC)); + __FMODE_EXEC | __O_REGULAR)); fasync_cache = kmem_cache_create("fasync_cache", sizeof(struct fasync_struct), 0, diff --git a/fs/gfs2/inode.c b/fs/gfs2/inode.c index e9bf4879c07f..e9895dea0da4 100644 --- a/fs/gfs2/inode.c +++ b/fs/gfs2/inode.c @@ -738,6 +738,13 @@ static int gfs2_create_inode(struct inode *dir, struct dentry *dentry, inode = gfs2_dir_search(dir, &dentry->d_name, !S_ISREG(mode) || excl); error = PTR_ERR(inode); if (!IS_ERR(inode)) { + if (file && (file->f_flags & __O_REGULAR) && + !S_ISREG(inode->i_mode)) { + iput(inode); + inode = NULL; + error = -EFTYPE; + goto fail_gunlock; + } if (S_ISDIR(inode->i_mode)) { iput(inode); inode = NULL; diff --git a/fs/namei.c b/fs/namei.c index c7fac83c9a85..e1fe0f28b923 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -4679,6 +4679,10 @@ static int do_open(struct nameidata *nd, if (unlikely(error)) return error; } + + if ((open_flag & __O_REGULAR) && !d_is_reg(nd->path.dentry)) + return -EFTYPE; + if ((nd->flags & LOOKUP_DIRECTORY) && !d_can_lookup(nd->path.dentry)) return -ENOTDIR; diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c index e9ce1883288c..1b9c368fb133 100644 --- a/fs/nfs/dir.c +++ b/fs/nfs/dir.c @@ -2194,6 +2194,10 @@ int nfs_atomic_open(struct inode *dir, struct dentry *dentry, break; case -EISDIR: case -ENOTDIR: + if (open_flags & __O_REGULAR) { + err = -EFTYPE; + break; + } goto no_open; case -ELOOP: if (!(open_flags & O_NOFOLLOW)) diff --git a/fs/open.c b/fs/open.c index 9e0164a8c1fb..5458668a68e1 100644 --- a/fs/open.c +++ b/fs/open.c @@ -960,7 +960,7 @@ static int do_dentry_open(struct file *f, if (f->f_mapping->a_ops && f->f_mapping->a_ops->direct_IO) f->f_mode |= FMODE_CAN_ODIRECT; - f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC); + f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC | __O_REGULAR); f->f_iocb_flags = iocb_flags(f); file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping); @@ -1184,7 +1184,15 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op) int acc_mode = ACC_MODE(flags); BUILD_BUG_ON_MSG(upper_32_bits(VALID_OPEN_FLAGS), - "struct open_flags doesn't yet handle flags > 32 bits"); + "VALID_OPEN_FLAGS must fit in 32 bits"); + /* The whole point: OPENAT2_REGULAR must be unrepresentable in int. */ + BUILD_BUG_ON_MSG(!upper_32_bits(OPENAT2_REGULAR), + "OPENAT2_REGULAR must live in the upper 32 bits of open_how::flags"); + /* Prevent a future bit collision between UAPI and internal carrier. */ + BUILD_BUG_ON_MSG(OPENAT2_REGULAR & VALID_OPEN_FLAGS, + "OPENAT2_REGULAR must not alias any open()/openat() flag"); + BUILD_BUG_ON_MSG(__O_REGULAR & VALID_OPENAT2_FLAGS, + "__O_REGULAR must not alias any user-visible flag"); /* * Strip flags that aren't relevant in determining struct open_flags. @@ -1196,7 +1204,7 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op) * values before calling build_open_flags(), but openat2(2) checks all * of its arguments. */ - if (flags & ~VALID_OPEN_FLAGS) + if (flags & ~VALID_OPENAT2_FLAGS) return -EINVAL; if (how->resolve & ~VALID_RESOLVE_FLAGS) return -EINVAL; @@ -1236,6 +1244,14 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op) if (!(acc_mode & MAY_WRITE)) return -EINVAL; } + /* + * Asking to open a directory and a regular file at the same time is + * contradictory. + */ + if ((flags & (O_DIRECTORY | OPENAT2_REGULAR)) == + (O_DIRECTORY | OPENAT2_REGULAR)) + return -EINVAL; + if (flags & O_PATH) { /* O_PATH only permits certain other flags to be set. */ if (flags & ~O_PATH_FLAGS) @@ -1252,6 +1268,19 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op) if (flags & __O_SYNC) flags |= O_DSYNC; + /* + * Translate the upper-32-bit UAPI bit OPENAT2_REGULAR into the + * kernel-internal lower-32-bit __O_REGULAR carrier so the bit + * survives the assignment to op->open_flag (an int) below and the + * subsequent flow through f->f_flags (unsigned int) and the + * i_op->atomic_open() callback (unsigned). do_dentry_open() strips + * __O_REGULAR before the file becomes visible to userspace. + */ + if (flags & OPENAT2_REGULAR) { + flags &= ~OPENAT2_REGULAR; + flags |= __O_REGULAR; + } + op->open_flag = flags; /* O_TRUNC implies we need access checks for write permissions */ diff --git a/fs/smb/client/dir.c b/fs/smb/client/dir.c index e4295a5b55b3..88a4a1787ff0 100644 --- a/fs/smb/client/dir.c +++ b/fs/smb/client/dir.c @@ -241,6 +241,12 @@ static int __cifs_do_create(struct inode *dir, struct dentry *direntry, goto cifs_create_get_file_info; } + if ((oflags & __O_REGULAR) && !S_ISREG(newinode->i_mode)) { + CIFSSMBClose(xid, tcon, fid->netfid); + iput(newinode); + return -EFTYPE; + } + if (S_ISDIR(newinode->i_mode)) { CIFSSMBClose(xid, tcon, fid->netfid); iput(newinode); @@ -458,9 +464,15 @@ static int __cifs_do_create(struct inode *dir, struct dentry *direntry, goto out_err; } - if (newinode && S_ISDIR(newinode->i_mode)) { - rc = -EISDIR; - goto out_err; + if (newinode) { + if ((oflags & __O_REGULAR) && !S_ISREG(newinode->i_mode)) { + rc = -EFTYPE; + goto out_err; + } + if (S_ISDIR(newinode->i_mode)) { + rc = -EISDIR; + goto out_err; + } } *inode = newinode; diff --git a/include/linux/fcntl.h b/include/linux/fcntl.h index c65c5c73d362..6ad6b9e7a226 100644 --- a/include/linux/fcntl.h +++ b/include/linux/fcntl.h @@ -4,6 +4,7 @@ #include #include +#include /* List of all valid flags for the open/openat flags argument: */ #define VALID_OPEN_FLAGS \ @@ -12,6 +13,23 @@ FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | \ O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH) +/* List of all valid flags for openat2(2)'s how->flags argument. */ +#define VALID_OPENAT2_FLAGS (VALID_OPEN_FLAGS | OPENAT2_REGULAR) + +/* + * Kernel-internal carrier for OPENAT2_REGULAR. The UAPI bit lives in the + * upper 32 bits of open_how::flags so open()/openat() cannot encode it. + * build_open_flags() translates it to this internal flag, which then + * propagates through op->open_flag and f->f_flags exactly like __FMODE_EXEC. + * do_dentry_open() strips it so userspace cannot observe it via + * fcntl(F_GETFL). + * + * Bit 30 is not claimed by any O_* flag on any architecture and stays clear + * of the sign bit of the int op->open_flag. fcntl_init() enforces that it + * never aliases an open-flag bit. + */ +#define __O_REGULAR (1 << 30) + /* List of all valid flags for the how->resolve argument: */ #define VALID_RESOLVE_FLAGS \ (RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS | \ diff --git a/include/uapi/linux/openat2.h b/include/uapi/linux/openat2.h index a5feb7604948..575c2c59d14a 100644 --- a/include/uapi/linux/openat2.h +++ b/include/uapi/linux/openat2.h @@ -22,6 +22,13 @@ struct open_how { __u64 resolve; }; +/* + * how->flags bits exclusive to openat2(2). These live in the upper 32 bits + * of @flags so that they cannot be expressed by open(2) / openat(2), whose + * @flags argument is a C int. + */ +#define OPENAT2_REGULAR ((__u64)1 << 32) /* Only open regular files. */ + /* how->resolve flags for openat2(2). */ #define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings (includes bind-mounts). */ From 6045a75399b45f6805f07a03020abf384b9f53c3 Mon Sep 17 00:00:00 2001 From: Dorjoy Chowdhury Date: Sat, 16 May 2026 16:43:11 +0200 Subject: [PATCH 09/12] kselftest/openat2: test for OPENAT2_REGULAR flag Just a happy path test. Christian Brauner says: Update OPENAT2_REGULAR fallback define to match upper-32-bit UAPI value. Port the test to the kselftest_harness TEST*/FIXTURE framework to match the migrated openat2_test.c, and add a regression test ensuring open()/openat() keep ignoring the internal __O_REGULAR carrier bit. Signed-off-by: Dorjoy Chowdhury Link: https://patch.msgid.link/20260328172314.45807-3-dorjoychy111@gmail.com Reviewed-by: Aleksa Sarai Signed-off-by: Christian Brauner (Amutable) --- .../filesystems/openat2/openat2_test.c | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tools/testing/selftests/filesystems/openat2/openat2_test.c b/tools/testing/selftests/filesystems/openat2/openat2_test.c index 5ea3eebb7b59..6f5afbe2d8d3 100644 --- a/tools/testing/selftests/filesystems/openat2/openat2_test.c +++ b/tools/testing/selftests/filesystems/openat2/openat2_test.c @@ -310,4 +310,53 @@ TEST_F(openat2, flag_validation) } } +#ifndef OPENAT2_REGULAR +#define OPENAT2_REGULAR ((__u64)1 << 32) +#endif + +#ifndef EFTYPE +#define EFTYPE 134 +#endif + +/* Kernel-internal carrier for OPENAT2_REGULAR (see __O_REGULAR in fcntl.h). */ +#ifndef __O_REGULAR +#define __O_REGULAR (1 << 30) +#endif + +/* Verify that OPENAT2_REGULAR rejects non-regular files with EFTYPE. */ +TEST_F(openat2, regular_flag) +{ + struct open_how how = { + .flags = OPENAT2_REGULAR | O_RDONLY, + }; + int fd; + + fd = sys_openat2(AT_FDCWD, "/dev/null", &how); + if (fd == -ENOENT) + SKIP(return, "/dev/null does not exist"); + + EXPECT_EQ(-EFTYPE, fd) { + TH_LOG("openat2 with OPENAT2_REGULAR should fail with %d (%s), got %d (%s)", + -EFTYPE, strerror(EFTYPE), fd, strerror(-fd)); + } + if (fd >= 0) + close(fd); +} + +/* open()/openat() must keep ignoring the internal __O_REGULAR bit. */ +TEST(legacy_openat_ignores_o_regular) +{ + int fd; + + fd = openat(AT_FDCWD, "/dev/null", O_RDONLY | __O_REGULAR); + if (fd < 0 && errno == ENOENT) + SKIP(return, "/dev/null does not exist"); + + ASSERT_GE(fd, 0) { + TH_LOG("legacy openat() must ignore the __O_REGULAR carrier bit, got errno %d (%s)", + errno, strerror(errno)); + } + close(fd); +} + TEST_HARNESS_MAIN From 8c47b5e9b9f4a861a87e40d5294de7fda548fa37 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 21 May 2026 12:55:39 +0200 Subject: [PATCH 10/12] selftests: openat2: port emptypath_test to kselftest harness Convert the emptypath selftests to the FIXTURE_SETUP()/FIXTURE_TEARDOWN() and the two checks become TEST_F()s. No change in coverage. Link: https://patch.msgid.link/20260521-wettstreit-meinen-46271dede480@brauner Signed-off-by: Christian Brauner (Amutable) --- .../filesystems/openat2/emptypath_test.c | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/tools/testing/selftests/filesystems/openat2/emptypath_test.c b/tools/testing/selftests/filesystems/openat2/emptypath_test.c index d75b5e998ff9..be37ccba57ec 100644 --- a/tools/testing/selftests/filesystems/openat2/emptypath_test.c +++ b/tools/testing/selftests/filesystems/openat2/emptypath_test.c @@ -1,55 +1,77 @@ // SPDX-License-Identifier: GPL-2.0-or-later +#define _GNU_SOURCE #define __SANE_USERSPACE_TYPES__ #include #include #include +#include +#include -#include "kselftest.h" +#include "kselftest_harness.h" #ifndef O_EMPTYPATH #define O_EMPTYPATH (1 << 26) #endif -int main(void) +#define EMPTYPATH_TEST_FILE "/tmp/emptypath_test" + +FIXTURE(emptypath) { + int opath_fd; +}; + +FIXTURE_SETUP(emptypath) { - int opath_fd, reopen_fd; - const char *path = "/tmp/emptypath_test"; + int fd; - ksft_print_header(); - ksft_set_plan(2); + self->opath_fd = -1; - 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"); + fd = open(EMPTYPATH_TEST_FILE, O_CREAT | O_WRONLY, S_IRWXU); + ASSERT_GE(fd, 0) { + TH_LOG("create %s: %s", EMPTYPATH_TEST_FILE, strerror(errno)); } + close(fd); - 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"); + self->opath_fd = open(EMPTYPATH_TEST_FILE, O_PATH); + ASSERT_GE(self->opath_fd, 0) { + TH_LOG("open %s O_PATH: %s", EMPTYPATH_TEST_FILE, strerror(errno)); } - - unlink(path); - ksft_finished(); } + +FIXTURE_TEARDOWN(emptypath) +{ + if (self->opath_fd >= 0) + close(self->opath_fd); + unlink(EMPTYPATH_TEST_FILE); +} + +/* An empty path is rejected with ENOENT unless O_EMPTYPATH is set. */ +TEST_F(emptypath, without_flag_returns_enoent) +{ + int fd = openat(self->opath_fd, "", O_RDONLY); + + if (fd >= 0) + close(fd); + ASSERT_LT(fd, 0) { + TH_LOG("empty path without O_EMPTYPATH unexpectedly succeeded"); + } + EXPECT_EQ(errno, ENOENT) { + TH_LOG("expected ENOENT, got %s", strerror(errno)); + } +} + +/* O_EMPTYPATH reopens the O_PATH fd through an empty path. */ +TEST_F(emptypath, reopens_opath_fd) +{ + int fd = openat(self->opath_fd, "", O_RDONLY | O_EMPTYPATH); + + if (fd < 0 && errno == EINVAL) + SKIP(return, "O_EMPTYPATH not supported"); + + ASSERT_GE(fd, 0) { + TH_LOG("O_EMPTYPATH failed: %s", strerror(errno)); + } + close(fd); +} + +TEST_HARNESS_MAIN From f2f1dddccae50f7a1d088285c53c376e26cedf67 Mon Sep 17 00:00:00 2001 From: Jori Koolstra Date: Thu, 28 May 2026 17:58:46 +0000 Subject: [PATCH 11/12] vfs: make LAST_XXX private to fs/namei.c The only user of LAST_XXX outside of fs/namei.c is fs/smb/server/vfs.c; ksmbd_vfs_path_lookup() calls vfs_path_parent_lookup() and expects a LAST_NORM last type (or it will be ENOENT). ksmbd_vfs_rename() also calls vfs_path_parent_lookup() but forgets the LAST_NORM check. It does not really make sense to have vfs_path_parent_lookup() expose the last_type because it is only needed to ensure it is LAST_NORM. So let's do this check in vfs_path_parent_lookup() instead and keep the LAST_XXX internal to fs/namei.c. This changes the ENOENT errno in ksmbd_vfs_path_lookup() to EINVAL, which matches better with how this is handled by callers of filename_parentat(). Signed-off-by: Jori Koolstra Link: https://patch.msgid.link/20260528175854.57626-1-jkoolstra@xs4all.nl Reviewed-by: Amir Goldstein Reviewed-by: NeilBrown Reviewed-by: Namjae Jeon Signed-off-by: Christian Brauner (Amutable) --- fs/namei.c | 20 ++++++++++++++++---- fs/smb/server/vfs.c | 15 +++------------ include/linux/namei.h | 7 +------ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/fs/namei.c b/fs/namei.c index e1fe0f28b923..05291e6f4bba 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -129,6 +129,11 @@ static struct kmem_cache *__names_cache __ro_after_init; #define names_cache runtime_const_ptr(__names_cache) +/* + * Type of the last component on LOOKUP_PARENT + */ +enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT}; + void __init filename_init(void) { __names_cache = kmem_cache_create_usercopy("names_cache", sizeof(struct filename), 0, @@ -3051,15 +3056,22 @@ EXPORT_SYMBOL(kern_path); * @flags: lookup flags * @parent: pointer to struct path to fill * @last: last component - * @type: type of the last component * @root: pointer to struct path of the base directory */ int vfs_path_parent_lookup(struct filename *filename, unsigned int flags, - struct path *parent, struct qstr *last, int *type, + struct path *parent, struct qstr *last, const struct path *root) { - return __filename_parentat(AT_FDCWD, filename, flags, parent, last, - type, root); + int type; + int err = __filename_parentat(AT_FDCWD, filename, flags, parent, last, + &type, root); + if (err) + return err; + if (unlikely(type != LAST_NORM)) { + path_put(parent); + return -EINVAL; + } + return 0; } EXPORT_SYMBOL(vfs_path_parent_lookup); diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index d08973b288e5..cd1dbca0cffb 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -19,7 +19,6 @@ #include #include #include -#include #include #include "glob.h" @@ -56,7 +55,7 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf, { struct qstr last; const struct path *root_share_path = &share_conf->vfs_path; - int err, type; + int err; struct dentry *d; if (pathname[0] == '\0') { @@ -67,17 +66,11 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf, } CLASS(filename_kernel, filename)(pathname); - err = vfs_path_parent_lookup(filename, flags, - path, &last, &type, + err = vfs_path_parent_lookup(filename, flags, path, &last, root_share_path); if (err) return err; - if (unlikely(type != LAST_NORM)) { - path_put(path); - return -ENOENT; - } - if (for_remove) { err = mnt_want_write(path->mnt); if (err) { @@ -668,7 +661,6 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, struct renamedata rd; struct ksmbd_share_config *share_conf = work->tcon->share_conf; struct ksmbd_file *parent_fp; - int new_type; int err, lookup_flags = LOOKUP_NO_SYMLINKS; if (ksmbd_override_fsids(work)) @@ -678,8 +670,7 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, retry: err = vfs_path_parent_lookup(to, lookup_flags | LOOKUP_BENEATH, - &new_path, &new_last, &new_type, - &share_conf->vfs_path); + &new_path, &new_last, &share_conf->vfs_path); if (err) goto out1; diff --git a/include/linux/namei.h b/include/linux/namei.h index 2ad6dd9987b9..3941b9f1dec7 100644 --- a/include/linux/namei.h +++ b/include/linux/namei.h @@ -13,11 +13,6 @@ enum { MAX_NESTED_LINKS = 8 }; #define MAXSYMLINKS 40 -/* - * Type of the last component on LOOKUP_PARENT - */ -enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT}; - /* pathwalk mode */ #define LOOKUP_FOLLOW BIT(0) /* follow links at the end */ #define LOOKUP_DIRECTORY BIT(1) /* require a directory */ @@ -67,7 +62,7 @@ static inline void end_removing_path(const struct path *path , struct dentry *de end_creating_path(path, dentry); } int vfs_path_parent_lookup(struct filename *filename, unsigned int flags, - struct path *parent, struct qstr *last, int *type, + struct path *parent, struct qstr *last, const struct path *root); int vfs_path_lookup(struct dentry *, struct vfsmount *, const char *, unsigned int, struct path *); From 318643721de396012da102723f337f35ba7ec1e9 Mon Sep 17 00:00:00 2001 From: Jori Koolstra Date: Thu, 28 May 2026 17:58:47 +0000 Subject: [PATCH 12/12] vfs: replace ints with enum last_type for LAST_XXX Several functions in namei.c take an "int *type" parameter, such as filename_parentat(). To know what values this can take you have to find the anonymous struct that defines the LAST_XXX values. Define an enum last_type to make this type explicit. Signed-off-by: Jori Koolstra Link: https://patch.msgid.link/20260528175854.57626-2-jkoolstra@xs4all.nl Signed-off-by: Christian Brauner (Amutable) --- fs/namei.c | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/fs/namei.c b/fs/namei.c index 05291e6f4bba..026165f6fcb6 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -132,7 +132,7 @@ static struct kmem_cache *__names_cache __ro_after_init; /* * Type of the last component on LOOKUP_PARENT */ -enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT}; +enum last_type {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT}; void __init filename_init(void) { @@ -732,7 +732,7 @@ struct nameidata { struct inode *inode; /* path.dentry.d_inode */ unsigned int flags, state; unsigned seq, next_seq, m_seq, r_seq; - int last_type; + enum last_type last_type; unsigned depth; int total_link_count; struct saved { @@ -2225,7 +2225,7 @@ static struct dentry *follow_dotdot(struct nameidata *nd) return dget(nd->path.dentry); } -static const char *handle_dots(struct nameidata *nd, int type) +static const char *handle_dots(struct nameidata *nd, enum last_type type) { if (type == LAST_DOTDOT) { const char *error = NULL; @@ -2873,7 +2873,7 @@ static int path_parentat(struct nameidata *nd, unsigned flags, /* Note: this does not consume "name" */ static int __filename_parentat(int dfd, struct filename *name, unsigned int flags, struct path *parent, - struct qstr *last, int *type, + struct qstr *last, enum last_type *type, const struct path *root) { int retval; @@ -2898,7 +2898,7 @@ static int __filename_parentat(int dfd, struct filename *name, static int filename_parentat(int dfd, struct filename *name, unsigned int flags, struct path *parent, - struct qstr *last, int *type) + struct qstr *last, enum last_type *type) { return __filename_parentat(dfd, name, flags, parent, last, type, NULL); } @@ -2966,7 +2966,8 @@ static struct dentry *__start_removing_path(int dfd, struct filename *name, struct path parent_path __free(path_put) = {}; struct dentry *d; struct qstr last; - int type, error; + enum last_type type; + int error; error = filename_parentat(dfd, name, 0, &parent_path, &last, &type); if (error) @@ -3012,7 +3013,8 @@ struct dentry *kern_path_parent(const char *name, struct path *path) CLASS(filename_kernel, filename)(name); struct dentry *d; struct qstr last; - int type, error; + enum last_type type; + int error; error = filename_parentat(AT_FDCWD, filename, 0, &parent_path, &last, &type); if (error) @@ -3062,7 +3064,7 @@ int vfs_path_parent_lookup(struct filename *filename, unsigned int flags, struct path *parent, struct qstr *last, const struct path *root) { - int type; + enum last_type type; int err = __filename_parentat(AT_FDCWD, filename, flags, parent, last, &type, root); if (err) @@ -4941,7 +4943,7 @@ static struct dentry *filename_create(int dfd, struct filename *name, bool want_dir = lookup_flags & LOOKUP_DIRECTORY; unsigned int reval_flag = lookup_flags & LOOKUP_REVAL; unsigned int create_flags = LOOKUP_CREATE | LOOKUP_EXCL; - int type; + enum last_type type; int error; error = filename_parentat(dfd, name, reval_flag, path, &last, &type); @@ -5403,7 +5405,7 @@ int filename_rmdir(int dfd, struct filename *name) struct dentry *dentry; struct path path; struct qstr last; - int type; + enum last_type type; unsigned int lookup_flags = 0; struct delegated_inode delegated_inode = { }; retry: @@ -5412,6 +5414,8 @@ int filename_rmdir(int dfd, struct filename *name) return error; switch (type) { + case LAST_NORM: + break; case LAST_DOTDOT: error = -ENOTEMPTY; goto exit2; @@ -5545,7 +5549,7 @@ int filename_unlinkat(int dfd, struct filename *name) struct dentry *dentry; struct path path; struct qstr last; - int type; + enum last_type type; struct inode *inode; struct delegated_inode delegated_inode = { }; unsigned int lookup_flags = 0; @@ -6112,7 +6116,7 @@ int filename_renameat2(int olddfd, struct filename *from, struct renamedata rd; struct path old_path, new_path; struct qstr old_last, new_last; - int old_type, new_type; + enum last_type old_type, new_type; struct delegated_inode delegated_inode = { }; unsigned int lookup_flags = 0; bool should_retry = false;