[RFC PATCH v2 3/3] selftest: add tests for OPENAT2_EMPTY_PATH and allowed_upgrades

Jori Koolstra posted 3 patches 1 week ago
[RFC PATCH v2 3/3] selftest: add tests for OPENAT2_EMPTY_PATH and allowed_upgrades
Posted by Jori Koolstra 1 week ago
Add tests for new openat2 flag OPENAT2_EMPTY_PATH and new open_how
field: allowed_upgrades.

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 also 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 <jkoolstra@xs4all.nl>
---
 tools/include/uapi/linux/openat2.h            |  53 ++++
 tools/testing/selftests/openat2/Makefile      |   4 +-
 tools/testing/selftests/openat2/helpers.c     |   2 +-
 tools/testing/selftests/openat2/helpers.h     |  40 +--
 .../testing/selftests/openat2/upgrade_test.c  | 242 ++++++++++++++++++
 5 files changed, 301 insertions(+), 40 deletions(-)
 create mode 100644 tools/include/uapi/linux/openat2.h
 create mode 100644 tools/testing/selftests/openat2/upgrade_test.c

diff --git a/tools/include/uapi/linux/openat2.h b/tools/include/uapi/linux/openat2.h
new file mode 100644
index 000000000000..fbbf5483dc9d
--- /dev/null
+++ b/tools/include/uapi/linux/openat2.h
@@ -0,0 +1,53 @@
+/* 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;
+	__u64 allowed_upgrades;
+};
+
+/* how->allowed_upgrades flags for openat2(2). */
+#define DENY_UPGRADES		0x01
+#define READ_UPGRADABLE		(0x02 | DENY_UPGRADES)
+#define WRITE_UPGRADABLE	(0x04 | DENY_UPGRADES)
+
+/* 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. */
+
+/* openat2(2) exclusive flags are defined in the upper 32 bits of
+   open_how->flags  */
+#define OPENAT2_EMPTY_PATH	0x100000000 /* (1ULL << 32) */
+
+#endif /* _LINUX_OPENAT2_H */
diff --git a/tools/testing/selftests/openat2/Makefile b/tools/testing/selftests/openat2/Makefile
index 185dc76ebb5f..cc6d4fad999c 100644
--- a/tools/testing/selftests/openat2/Makefile
+++ b/tools/testing/selftests/openat2/Makefile
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
-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 upgrade_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/openat2/helpers.c b/tools/testing/selftests/openat2/helpers.c
index 5074681ffdc9..b6533f0b1124 100644
--- a/tools/testing/selftests/openat2/helpers.c
+++ b/tools/testing/selftests/openat2/helpers.c
@@ -98,7 +98,7 @@ void __attribute__((constructor)) init(void)
 	struct open_how how = {};
 	int fd;
 
-	BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0);
+	BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER1);
 
 	/* Check openat2(2) support. */
 	fd = sys_openat2(AT_FDCWD, ".", &how);
diff --git a/tools/testing/selftests/openat2/helpers.h b/tools/testing/selftests/openat2/helpers.h
index 510e60602511..af94d8211b9f 100644
--- a/tools/testing/selftests/openat2/helpers.h
+++ b/tools/testing/selftests/openat2/helpers.h
@@ -14,6 +14,9 @@
 #include <linux/types.h>
 #include "kselftest.h"
 
+#define OPEN_HOW_SIZE_VER0 24
+#define OPEN_HOW_SIZE_VER1 32
+
 #define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X)))
 #define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
 
@@ -24,45 +27,8 @@
 #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).
- *
- * 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
-
 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
-					(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 */
-
 #define E_func(func, ...)						      \
 	do {								      \
 		errno = 0;						      \
diff --git a/tools/testing/selftests/openat2/upgrade_test.c b/tools/testing/selftests/openat2/upgrade_test.c
new file mode 100644
index 000000000000..489d9088d7ce
--- /dev/null
+++ b/tools/testing/selftests/openat2/upgrade_test.c
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#define __SANE_USERSPACE_TYPES__
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#include "helpers.h"
+
+static int open_opath(const char *path, __u64 allowed_upgrades)
+{
+	struct open_how how = {
+		.flags = O_PATH,
+		.allowed_upgrades = allowed_upgrades,
+	};
+	int ret = raw_openat2(AT_FDCWD, path, &how, OPEN_HOW_SIZE_VER1);
+
+	if (ret < 0)
+		ksft_exit_fail_msg("open O_PATH: %s\n", strerror(errno));
+
+	return ret;
+}
+
+static int reopen_empty(int dfd, __u64 flags, bool fatal)
+{
+	struct open_how how = {
+		.flags = flags | OPENAT2_EMPTY_PATH,
+	};
+	int ret = raw_openat2(dfd, "", &how, OPEN_HOW_SIZE_VER1);
+
+	if (ret < 0 && fatal)
+		ksft_exit_fail_msg("open with OPENAT2_EMPTY_PATH: %s\n",
+				   strerror(errno));
+	return ret;
+}
+
+static int reopen_empty_opath(int dfd, __u64 allowed_upgrades)
+{
+	struct open_how how = {
+		.flags = O_PATH | OPENAT2_EMPTY_PATH,
+		.allowed_upgrades = allowed_upgrades,
+	};
+	int ret = raw_openat2(dfd, "", &how, OPEN_HOW_SIZE_VER1);
+
+	if (ret < 0)
+		ksft_exit_fail_msg("open O_PATH with OPENAT2_EMPTY_PATH: %s\n",
+				   strerror(errno));
+	return ret;
+}
+
+static int reopen_proc(int dfd, int flags, bool fatal)
+{
+	char path[64];
+	snprintf(path, sizeof(path), "/proc/self/fd/%d", dfd);
+	int ret = open(path, flags);
+
+	if (ret < 0 && fatal)
+		ksft_exit_fail_msg("open via procfs: %s\n", strerror(errno));
+
+	return ret;
+}
+
+static void check_success(const char *desc, int ret)
+{
+	if (ret >= 0) {
+		ksft_test_result_pass("%s\n", desc);
+		close(ret);
+	} else {
+		ksft_test_result_fail("%s: expected success, got %s\n",
+				      desc, strerror(errno));
+	}
+}
+
+static void check_failure(const char *desc, int ret, int expected_errno)
+{
+	if (ret < 0 && errno == expected_errno) {
+		ksft_test_result_pass("%s\n", desc);
+	} else if (ret >= 0) {
+		ksft_test_result_fail("%s: expected %s, got success\n",
+				      desc, strerror(expected_errno));
+		close(ret);
+	} else {
+		ksft_test_result_fail("%s: expected %s, got %s\n",
+				      desc, strerror(expected_errno),
+				      strerror(errno));
+	}
+}
+
+static void check(const char *desc, int ret, int expected_errno)
+{
+	if (!expected_errno) {
+		check_success(desc, ret);
+	} else {
+		check_failure(desc, ret, expected_errno);
+	}
+}
+
+#define NUM_TESTS 42
+
+int main(void)
+{
+	const char *path = "/tmp/upgrade_mask_test";
+	int fd, src;
+
+	ksft_print_header();
+
+	if (!openat2_supported)
+		ksft_exit_skip("openat2(2) not supported\n");
+
+	/* Check allowed_upgrades support */
+	{
+		struct open_how how = { .flags = O_PATH,
+					.allowed_upgrades = DENY_UPGRADES };
+		fd = raw_openat2(AT_FDCWD, "/", &how, sizeof(how));
+		if (fd < 0 && -fd == EINVAL)
+			ksft_exit_skip("allowed_upgrades not supported by kernel\n");
+		if (fd >= 0)
+			close(fd);
+	}
+
+	ksft_set_plan(NUM_TESTS);
+
+	fd = open(path, O_CREAT | O_WRONLY, 0644);
+	if (fd < 0)
+		ksft_exit_fail_msg("failed to create test file: %s\n",
+				   strerror(errno));
+	close(fd);
+
+	/* test 1: DENY_UPGRADES (deny all) */
+	src = open_opath(path, DENY_UPGRADES);
+	check("deny_all: use empty_path to reopen O_RDONLY", reopen_empty(src, O_RDONLY, false), EACCES);
+	check("deny_all: use empty_path to reopen O_WRONLY", reopen_empty(src, O_WRONLY, false), EACCES);
+	check("deny_all: use empty_path to reopen O_RDWR",   reopen_empty(src, O_RDWR,   false), EACCES);
+	check("deny_all: use procfs to reopen O_RDONLY",     reopen_proc(src, O_RDONLY,  false), EACCES);
+	check("deny_all: use procfs to reopen O_WRONLY",     reopen_proc(src, O_WRONLY,  false), EACCES);
+	check("deny_all: use procfs to reopen O_RDWR",       reopen_proc(src, O_RDWR,    false), EACCES);
+	close(src);
+
+	/* test 2: READ_UPGRADABLE */
+	src = open_opath(path, READ_UPGRADABLE);
+	check("read_only: use empty_path to reopen O_RDONLY", reopen_empty(src, O_RDONLY, false), 0);
+	check("read_only: use empty_path to reopen O_WRONLY", reopen_empty(src, O_WRONLY, false), EACCES);
+	check("read_only: use empty_path to reopen O_RDWR",   reopen_empty(src, O_RDWR,   false), EACCES);
+	check("read_only: use procfs to reopen O_RDONLY",     reopen_proc(src, O_RDONLY,  false), 0);
+	check("read_only: use procfs to reopen O_WRONLY",     reopen_proc(src, O_WRONLY,  false), EACCES);
+	check("read_only: use procfs to reopen O_RDWR",       reopen_proc(src, O_RDWR,    false), EACCES);
+	close(src);
+
+	/* test 3: WRITE_UPGRADABLE */
+	src = open_opath(path, WRITE_UPGRADABLE);
+	check("write_only: use empty_path to reopen O_RDONLY", reopen_empty(src, O_RDONLY, false), EACCES);
+	check("write_only: use empty_path to reopen O_WRONLY", reopen_empty(src, O_WRONLY, false), 0);
+	check("write_only: use empty_path to reopen O_RDWR",   reopen_empty(src, O_RDWR,   false), EACCES);
+	check("write_only: use procfs to reopen O_RDONLY",     reopen_proc(src, O_RDONLY,  false), EACCES);
+	check("write_only: use procfs to reopen O_WRONLY",     reopen_proc(src, O_WRONLY,  false), 0);
+	check("write_only: use procfs to reopen O_RDWR",       reopen_proc(src, O_RDWR,    false), EACCES);
+	close(src);
+
+	/* test 4: READ_UPGRADABLE | WRITE_UPGRADABLE */
+	src = open_opath(path, READ_UPGRADABLE | WRITE_UPGRADABLE);
+	check("allow_all: use empty_path to reopen O_RDONLY", reopen_empty(src, O_RDONLY, false), 0);
+	check("allow_all: use empty_path to reopen O_WRONLY", reopen_empty(src, O_WRONLY, false), 0);
+	check("allow_all: use empty_path to reopen O_RDWR",   reopen_empty(src, O_RDWR,   false), 0);
+	check("allow_all: use procfs to reopen O_RDONLY",     reopen_proc(src, O_RDONLY,  false), 0);
+	check("allow_all: use procfs to reopen O_WRONLY",     reopen_proc(src, O_WRONLY,  false), 0);
+	check("allow_all: use procfs to reopen O_RDWR",       reopen_proc(src, O_RDWR,    false), 0);
+	close(src);
+
+	/* test 5: VER0 open_how (allowed_upgrades absent, defaults to unrestricted) */
+	{
+		struct open_how how = { .flags = O_PATH };
+		src = raw_openat2(AT_FDCWD, path, &how, OPEN_HOW_SIZE_VER0);
+
+		check("ver0: use empty_path to reopen O_RDONLY", reopen_empty(src, O_RDONLY, false), 0);
+		check("ver0: use empty_path to reopen O_WRONLY", reopen_empty(src, O_WRONLY, false), 0);
+		check("ver0: use empty_path to reopen O_RDWR",   reopen_empty(src, O_RDWR,   false), 0);
+		check("ver0: use procfs to reopen O_RDONLY",     reopen_proc(src, O_RDONLY,  false), 0);
+		check("ver0: use procfs to reopen O_WRONLY",     reopen_proc(src, O_WRONLY,  false), 0);
+		check("ver0: use procfs to reopen O_RDWR",       reopen_proc(src, O_RDWR,    false), 0);
+		close(src);
+	}
+
+	/* test 6: invalid allowed_upgrades bit rejected */
+	{
+		struct open_how how = { .flags = O_PATH, .allowed_upgrades = (1ULL << 63) };
+		fd = raw_openat2(AT_FDCWD, path, &how, sizeof(how));
+		check("invalid: unknown bit in allowed_upgrades rejected with EINVAL", fd, EINVAL);
+	}
+
+	/* test 7: transitivity through OPENAT2_EMPTY_PATH reopen */
+	src = open_opath(path, READ_UPGRADABLE);
+	{
+		int mid = reopen_empty(src, O_RDONLY, true);
+		close(src);
+		check("transitive_empty: use procfs to reopen O_RDONLY", reopen_proc(mid, O_RDONLY, false), 0);
+		check("transitive_empty: use procfs to reopen O_WRONLY", reopen_proc(mid, O_WRONLY, false), EACCES);
+		check("transitive_empty: use procfs to reopen O_RDWR",   reopen_proc(mid, O_RDWR,   false), EACCES);
+		close(mid);
+	}
+
+	/* test 8: transitivity through procfs reopen */
+	src = open_opath(path, READ_UPGRADABLE);
+	{
+		int mid = reopen_proc(src, O_RDONLY, true);
+		close(src);
+		check("transitive_proc: use procfs to reopen O_RDONLY", reopen_empty(mid, O_RDONLY, false), 0);
+		check("transitive_proc: use procfs to reopen O_WRONLY", reopen_empty(mid, O_WRONLY, false), EACCES);
+		check("transitive_proc: use procfs to reopen O_RDWR",   reopen_empty(mid, O_RDWR,   false), EACCES);
+		close(mid);
+	}
+
+	/* test 9: narrowing via intermediate O_PATH reopen */
+	src = open_opath(path, READ_UPGRADABLE | WRITE_UPGRADABLE);
+	{
+		int mid = reopen_empty_opath(src, READ_UPGRADABLE);
+		close(src);
+		check("narrowing: use procfs to reopen O_RDONLY", reopen_proc(mid, O_RDONLY, false), 0);
+		check("narrowing: use procfs to reopen O_WRONLY", reopen_proc(mid, O_WRONLY, false), EACCES);
+		check("narrowing: use procfs to reopen O_RDWR",   reopen_proc(mid, O_RDWR,   false), EACCES);
+		close(mid);
+	}
+
+	/* test 10: three-level chain */
+	src = open_opath(path, READ_UPGRADABLE);
+	{
+		int mid = reopen_proc(src, O_RDONLY, true);
+		close(src);
+		int dst = reopen_proc(mid, O_RDONLY, true);
+		close(mid);
+		check("three_level: use procfs to reopen O_RDONLY", reopen_empty(dst, O_RDONLY, false), 0);
+		check("three_level: use procfs to reopen O_WRONLY", reopen_empty(dst, O_WRONLY, false), EACCES);
+		close(dst);
+	}
+
+	unlink(path);
+
+	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
+		ksft_exit_fail();
+	else
+		ksft_exit_pass();
+}
-- 
2.53.0