diff options
| author | Breno Leitao <leitao@debian.org> | 2026-08-05 07:52:36 -0700 |
|---|---|---|
| committer | Tejun Heo <tj@kernel.org> | 2026-08-10 11:47:20 -1000 |
| commit | a5bde5d8fde8a8cb28e59a672d5ddc5b9c1e7656 (patch) | |
| tree | 39f5b258386e1127d253d293194e9a2991bf3c34 /tools/testing/selftests/filesystems | |
| download | linux-stable-a5bde5d8fde8a8cb28e59a672d5ddc5b9c1e7656.tar.gz linux-stable-a5bde5d8fde8a8cb28e59a672d5ddc5b9c1e7656.zip | |
workqueue: skip the node_nr_active update for non-unbound workqueuesgrafted
apply_wqattrs_commit() updates node_nr_active->max unconditionally.
wq->node_nr_active[] is only allocated for unbound workqueues, so guard
the call before per-cpu workqueues start using this path.
No functional change: only unbound workqueues reach apply_wqattrs_*()
today.
Signed-off-by: Breno Leitao <leitao@debian.org>
Signed-off-by: Tejun Heo <tj@kernel.org>
Diffstat (limited to 'tools/testing/selftests/filesystems')
78 files changed, 17205 insertions, 0 deletions
diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore new file mode 100644 index 000000000..64ac0dfa4 --- /dev/null +++ b/tools/testing/selftests/filesystems/.gitignore @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +dnotify_test +devpts_pts +fclog +file_stressor +anon_inode_test +kernfs_test diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile new file mode 100644 index 000000000..85427d7f1 --- /dev/null +++ b/tools/testing/selftests/filesystems/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += $(KHDR_INCLUDES) +TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog +TEST_GEN_PROGS_EXTENDED := dnotify_test + +include ../lib.mk diff --git a/tools/testing/selftests/filesystems/anon_inode_test.c b/tools/testing/selftests/filesystems/anon_inode_test.c new file mode 100644 index 000000000..2c4c50500 --- /dev/null +++ b/tools/testing/selftests/filesystems/anon_inode_test.c @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ + +#include <fcntl.h> +#include <stdio.h> +#include <sys/stat.h> + +#include "kselftest_harness.h" +#include "wrappers.h" + +TEST(anon_inode_no_chown) +{ + int fd_context; + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_LT(fchown(fd_context, 1234, 5678), 0); + ASSERT_EQ(errno, EOPNOTSUPP); + + EXPECT_EQ(close(fd_context), 0); +} + +TEST(anon_inode_no_chmod) +{ + int fd_context; + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_LT(fchmod(fd_context, 0777), 0); + ASSERT_EQ(errno, EOPNOTSUPP); + + EXPECT_EQ(close(fd_context), 0); +} + +TEST(anon_inode_no_exec) +{ + int fd_context; + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + char *const empty_argv[] = {NULL}; + char *const empty_envp[] = {NULL}; + + ASSERT_LT(execveat(fd_context, "", empty_argv, empty_envp, AT_EMPTY_PATH), 0); + ASSERT_EQ(errno, EACCES); + + EXPECT_EQ(close(fd_context), 0); +} + +TEST(anon_inode_no_open) +{ + int fd_context; + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_GE(dup2(fd_context, 500), 0); + ASSERT_EQ(close(fd_context), 0); + fd_context = 500; + + ASSERT_LT(open("/proc/self/fd/500", 0), 0); + ASSERT_EQ(errno, ENXIO); + + EXPECT_EQ(close(fd_context), 0); +} + +TEST_HARNESS_MAIN + diff --git a/tools/testing/selftests/filesystems/binderfs/.gitignore b/tools/testing/selftests/filesystems/binderfs/.gitignore new file mode 100644 index 000000000..8e5cf9084 --- /dev/null +++ b/tools/testing/selftests/filesystems/binderfs/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +binderfs_test diff --git a/tools/testing/selftests/filesystems/binderfs/Makefile b/tools/testing/selftests/filesystems/binderfs/Makefile new file mode 100644 index 000000000..eb4c3b411 --- /dev/null +++ b/tools/testing/selftests/filesystems/binderfs/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += $(KHDR_INCLUDES) -pthread +TEST_GEN_PROGS := binderfs_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c new file mode 100644 index 000000000..a1a79a6fe --- /dev/null +++ b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: GPL-2.0 + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <sched.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/fsuid.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysinfo.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <linux/android/binder.h> +#include <linux/android/binderfs.h> + +#include "kselftest_harness.h" + +#define DEFAULT_THREADS 4 + +#define PTR_TO_INT(p) ((int)((intptr_t)(p))) +#define INT_TO_PTR(u) ((void *)((intptr_t)(u))) + +#define close_prot_errno_disarm(fd) \ + if (fd >= 0) { \ + int _e_ = errno; \ + close(fd); \ + errno = _e_; \ + fd = -EBADF; \ + } + +static void change_mountns(struct __test_metadata *_metadata) +{ + int ret; + + ret = unshare(CLONE_NEWNS); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to unshare mount namespace", + strerror(errno)); + } + + ret = mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to mount / as private", + strerror(errno)); + } +} + +static int __do_binderfs_test(struct __test_metadata *_metadata) +{ + int fd, ret, saved_errno, result = 1; + size_t len; + struct binderfs_device device = { 0 }; + struct binder_version version = { 0 }; + char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX", + device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME]; + static const char * const binder_features[] = { + "oneway_spam_detection", + "extended_error", + "freeze_notification", + "transaction_report", + }; + + change_mountns(_metadata); + + EXPECT_NE(mkdtemp(binderfs_mntpt), NULL) { + TH_LOG("%s - Failed to create binderfs mountpoint", + strerror(errno)); + goto out; + } + + ret = mount(NULL, binderfs_mntpt, "binder", 0, 0); + EXPECT_EQ(ret, 0) { + if (errno == ENODEV) + SKIP(goto out, "binderfs missing"); + TH_LOG("%s - Failed to mount binderfs", strerror(errno)); + goto rmdir; + } + + /* success: binderfs mounted */ + + memcpy(device.name, "my-binder", strlen("my-binder")); + + snprintf(device_path, sizeof(device_path), "%s/binder-control", binderfs_mntpt); + fd = open(device_path, O_RDONLY | O_CLOEXEC); + EXPECT_GE(fd, 0) { + TH_LOG("%s - Failed to open binder-control device", + strerror(errno)); + goto umount; + } + + ret = ioctl(fd, BINDER_CTL_ADD, &device); + saved_errno = errno; + close(fd); + errno = saved_errno; + EXPECT_GE(ret, 0) { + TH_LOG("%s - Failed to allocate new binder device", + strerror(errno)); + goto umount; + } + + TH_LOG("Allocated new binder device with major %d, minor %d, and name %s", + device.major, device.minor, device.name); + + /* success: binder device allocation */ + + snprintf(device_path, sizeof(device_path), "%s/my-binder", binderfs_mntpt); + fd = open(device_path, O_CLOEXEC | O_RDONLY); + EXPECT_GE(fd, 0) { + TH_LOG("%s - Failed to open my-binder device", + strerror(errno)); + goto umount; + } + + ret = ioctl(fd, BINDER_VERSION, &version); + saved_errno = errno; + close(fd); + errno = saved_errno; + EXPECT_GE(ret, 0) { + TH_LOG("%s - Failed to open perform BINDER_VERSION request", + strerror(errno)); + goto umount; + } + + TH_LOG("Detected binder version: %d", version.protocol_version); + + /* success: binder transaction with binderfs binder device */ + + ret = unlink(device_path); + EXPECT_EQ(ret, 0) { + TH_LOG("%s - Failed to delete binder device", + strerror(errno)); + goto umount; + } + + /* success: binder device removal */ + + snprintf(device_path, sizeof(device_path), "%s/binder-control", binderfs_mntpt); + ret = unlink(device_path); + EXPECT_NE(ret, 0) { + TH_LOG("Managed to delete binder-control device"); + goto umount; + } + EXPECT_EQ(errno, EPERM) { + TH_LOG("%s - Failed to delete binder-control device but exited with unexpected error code", + strerror(errno)); + goto umount; + } + + /* success: binder-control device removal failed as expected */ + + for (int i = 0; i < ARRAY_SIZE(binder_features); i++) { + snprintf(device_path, sizeof(device_path), "%s/features/%s", + binderfs_mntpt, binder_features[i]); + fd = open(device_path, O_CLOEXEC | O_RDONLY); + EXPECT_GE(fd, 0) { + TH_LOG("%s - Failed to open binder feature: %s", + strerror(errno), binder_features[i]); + goto umount; + } + close(fd); + } + + /* success: binder feature files found */ + result = 0; + +umount: + ret = umount2(binderfs_mntpt, MNT_DETACH); + EXPECT_EQ(ret, 0) { + TH_LOG("%s - Failed to unmount binderfs", strerror(errno)); + } +rmdir: + ret = rmdir(binderfs_mntpt); + EXPECT_EQ(ret, 0) { + TH_LOG("%s - Failed to rmdir binderfs mount", strerror(errno)); + } +out: + return result; +} + +static int wait_for_pid(pid_t pid) +{ + int status, ret; + +again: + ret = waitpid(pid, &status, 0); + if (ret == -1) { + if (errno == EINTR) + goto again; + + return -1; + } + + if (!WIFEXITED(status)) + return -1; + + return WEXITSTATUS(status); +} + +static int setid_userns_root(void) +{ + if (setuid(0)) + return -1; + if (setgid(0)) + return -1; + + setfsuid(0); + setfsgid(0); + + return 0; +} + +enum idmap_type { + UID_MAP, + GID_MAP, +}; + +static ssize_t read_nointr(int fd, void *buf, size_t count) +{ + ssize_t ret; +again: + ret = read(fd, buf, count); + if (ret < 0 && errno == EINTR) + goto again; + + return ret; +} + +static ssize_t write_nointr(int fd, const void *buf, size_t count) +{ + ssize_t ret; +again: + ret = write(fd, buf, count); + if (ret < 0 && errno == EINTR) + goto again; + + return ret; +} + +static int write_id_mapping(enum idmap_type type, pid_t pid, const char *buf, + size_t buf_size) +{ + int fd; + int ret; + char path[4096]; + + if (type == GID_MAP) { + int setgroups_fd; + + snprintf(path, sizeof(path), "/proc/%d/setgroups", pid); + setgroups_fd = open(path, O_WRONLY | O_CLOEXEC | O_NOFOLLOW); + if (setgroups_fd < 0 && errno != ENOENT) + return -1; + + if (setgroups_fd >= 0) { + ret = write_nointr(setgroups_fd, "deny", sizeof("deny") - 1); + close_prot_errno_disarm(setgroups_fd); + if (ret != sizeof("deny") - 1) + return -1; + } + } + + switch (type) { + case UID_MAP: + ret = snprintf(path, sizeof(path), "/proc/%d/uid_map", pid); + break; + case GID_MAP: + ret = snprintf(path, sizeof(path), "/proc/%d/gid_map", pid); + break; + default: + return -1; + } + if (ret < 0 || ret >= sizeof(path)) + return -E2BIG; + + fd = open(path, O_WRONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) + return -1; + + ret = write_nointr(fd, buf, buf_size); + close_prot_errno_disarm(fd); + if (ret != buf_size) + return -1; + + return 0; +} + +static void change_userns(struct __test_metadata *_metadata, int syncfds[2]) +{ + int ret; + char buf; + + close_prot_errno_disarm(syncfds[1]); + + ret = unshare(CLONE_NEWUSER); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to unshare user namespace", + strerror(errno)); + } + + ret = write_nointr(syncfds[0], "1", 1); + ASSERT_EQ(ret, 1) { + TH_LOG("write_nointr() failed"); + } + + ret = read_nointr(syncfds[0], &buf, 1); + ASSERT_EQ(ret, 1) { + TH_LOG("read_nointr() failed"); + } + + close_prot_errno_disarm(syncfds[0]); + + ASSERT_EQ(setid_userns_root(), 0) { + TH_LOG("setid_userns_root() failed"); + } +} + +static void change_idmaps(struct __test_metadata *_metadata, int syncfds[2], pid_t pid) +{ + int ret; + char buf; + char id_map[4096]; + + close_prot_errno_disarm(syncfds[0]); + + ret = read_nointr(syncfds[1], &buf, 1); + ASSERT_EQ(ret, 1) { + TH_LOG("read_nointr() failed"); + } + + snprintf(id_map, sizeof(id_map), "0 %d 1\n", getuid()); + ret = write_id_mapping(UID_MAP, pid, id_map, strlen(id_map)); + ASSERT_EQ(ret, 0) { + TH_LOG("write_id_mapping(UID_MAP) failed"); + } + + snprintf(id_map, sizeof(id_map), "0 %d 1\n", getgid()); + ret = write_id_mapping(GID_MAP, pid, id_map, strlen(id_map)); + ASSERT_EQ(ret, 0) { + TH_LOG("write_id_mapping(GID_MAP) failed"); + } + + ret = write_nointr(syncfds[1], "1", 1); + ASSERT_EQ(ret, 1) { + TH_LOG("write_nointr() failed"); + } + + close_prot_errno_disarm(syncfds[1]); +} + +struct __test_metadata *_thread_metadata; +static void *binder_version_thread(void *data) +{ + struct __test_metadata *_metadata = _thread_metadata; + int fd = PTR_TO_INT(data); + struct binder_version version = { 0 }; + int ret; + + ret = ioctl(fd, BINDER_VERSION, &version); + if (ret < 0) + TH_LOG("%s - Failed to open perform BINDER_VERSION request\n", + strerror(errno)); + + pthread_exit(data); +} + +/* + * Regression test: + * 2669b8b0c798 ("binder: prevent UAF for binderfs devices") + * f0fe2c0f050d ("binder: prevent UAF for binderfs devices II") + * 211b64e4b5b6 ("binderfs: use refcount for binder control devices too") + */ +TEST(binderfs_stress) +{ + int fds[1000]; + int syncfds[2]; + pid_t pid; + int fd, ret; + size_t len; + struct binderfs_device device = { 0 }; + char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX", + device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME]; + + ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to create socket pair", strerror(errno)); + } + + pid = fork(); + ASSERT_GE(pid, 0) { + TH_LOG("%s - Failed to fork", strerror(errno)); + close_prot_errno_disarm(syncfds[0]); + close_prot_errno_disarm(syncfds[1]); + } + + if (pid == 0) { + int i, j, k, nthreads; + pthread_attr_t attr; + pthread_t threads[DEFAULT_THREADS]; + change_userns(_metadata, syncfds); + change_mountns(_metadata); + + ASSERT_NE(mkdtemp(binderfs_mntpt), NULL) { + TH_LOG("%s - Failed to create binderfs mountpoint", + strerror(errno)); + } + + ret = mount(NULL, binderfs_mntpt, "binder", 0, 0); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to mount binderfs, check if CONFIG_ANDROID_BINDERFS is enabled in the running kernel", + strerror(errno)); + } + + for (int i = 0; i < ARRAY_SIZE(fds); i++) { + + snprintf(device_path, sizeof(device_path), + "%s/binder-control", binderfs_mntpt); + fd = open(device_path, O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0) { + TH_LOG("%s - Failed to open binder-control device", + strerror(errno)); + } + + memset(&device, 0, sizeof(device)); + snprintf(device.name, sizeof(device.name), "%d", i); + ret = ioctl(fd, BINDER_CTL_ADD, &device); + close_prot_errno_disarm(fd); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to allocate new binder device", + strerror(errno)); + } + + snprintf(device_path, sizeof(device_path), "%s/%d", + binderfs_mntpt, i); + fds[i] = open(device_path, O_RDONLY | O_CLOEXEC); + ASSERT_GE(fds[i], 0) { + TH_LOG("%s - Failed to open binder device", strerror(errno)); + } + } + + ret = umount2(binderfs_mntpt, MNT_DETACH); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to unmount binderfs", strerror(errno)); + rmdir(binderfs_mntpt); + } + + nthreads = get_nprocs_conf(); + if (nthreads > DEFAULT_THREADS) + nthreads = DEFAULT_THREADS; + + _thread_metadata = _metadata; + pthread_attr_init(&attr); + for (k = 0; k < ARRAY_SIZE(fds); k++) { + for (i = 0; i < nthreads; i++) { + ret = pthread_create(&threads[i], &attr, binder_version_thread, INT_TO_PTR(fds[k])); + if (ret) { + TH_LOG("%s - Failed to create thread %d", + strerror(errno), i); + break; + } + } + + for (j = 0; j < i; j++) { + void *fdptr = NULL; + + ret = pthread_join(threads[j], &fdptr); + if (ret) + TH_LOG("%s - Failed to join thread %d for fd %d", + strerror(errno), j, PTR_TO_INT(fdptr)); + } + } + pthread_attr_destroy(&attr); + + for (k = 0; k < ARRAY_SIZE(fds); k++) + close(fds[k]); + + exit(EXIT_SUCCESS); + } + + change_idmaps(_metadata, syncfds, pid); + + ret = wait_for_pid(pid); + ASSERT_EQ(ret, 0) { + TH_LOG("wait_for_pid() failed"); + } +} + +TEST(binderfs_test_privileged) +{ + if (geteuid() != 0) + SKIP(return, "Tests are not run as root. Skipping privileged tests"); + + if (__do_binderfs_test(_metadata)) + SKIP(return, "The Android binderfs filesystem is not available"); +} + +TEST(binderfs_test_unprivileged) +{ + int ret; + int syncfds[2]; + pid_t pid; + + ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds); + ASSERT_EQ(ret, 0) { + TH_LOG("%s - Failed to create socket pair", strerror(errno)); + } + + pid = fork(); + ASSERT_GE(pid, 0) { + close_prot_errno_disarm(syncfds[0]); + close_prot_errno_disarm(syncfds[1]); + TH_LOG("%s - Failed to fork", strerror(errno)); + } + + if (pid == 0) { + change_userns(_metadata, syncfds); + if (__do_binderfs_test(_metadata)) + exit(2); + exit(EXIT_SUCCESS); + } + + change_idmaps(_metadata, syncfds, pid); + + ret = wait_for_pid(pid); + if (ret) { + if (ret == 2) + SKIP(return, "The Android binderfs filesystem is not available"); + ASSERT_EQ(ret, 0) { + TH_LOG("wait_for_pid() failed"); + } + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/binderfs/config b/tools/testing/selftests/filesystems/binderfs/config new file mode 100644 index 000000000..7b4fc6ee6 --- /dev/null +++ b/tools/testing/selftests/filesystems/binderfs/config @@ -0,0 +1,2 @@ +CONFIG_ANDROID_BINDERFS=y +CONFIG_ANDROID_BINDER_IPC=y diff --git a/tools/testing/selftests/filesystems/devpts_pts.c b/tools/testing/selftests/filesystems/devpts_pts.c new file mode 100644 index 000000000..aa8d5324f --- /dev/null +++ b/tools/testing/selftests/filesystems/devpts_pts.c @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <asm/ioctls.h> +#include <sys/mount.h> +#include <sys/wait.h> +#include "kselftest.h" + +static bool terminal_dup2(int duplicate, int original) +{ + int ret; + + ret = dup2(duplicate, original); + if (ret < 0) + return false; + + return true; +} + +static int terminal_set_stdfds(int fd) +{ + int i; + + if (fd < 0) + return 0; + + for (i = 0; i < 3; i++) + if (!terminal_dup2(fd, (int[]){STDIN_FILENO, STDOUT_FILENO, + STDERR_FILENO}[i])) + return -1; + + return 0; +} + +static int login_pty(int fd) +{ + int ret; + + setsid(); + + ret = ioctl(fd, TIOCSCTTY, NULL); + if (ret < 0) + return -1; + + ret = terminal_set_stdfds(fd); + if (ret < 0) + return -1; + + if (fd > STDERR_FILENO) + close(fd); + + return 0; +} + +static int wait_for_pid(pid_t pid) +{ + int status, ret; + +again: + ret = waitpid(pid, &status, 0); + if (ret == -1) { + if (errno == EINTR) + goto again; + return -1; + } + if (ret != pid) + goto again; + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) + return -1; + + return 0; +} + +static int resolve_procfd_symlink(int fd, char *buf, size_t buflen) +{ + int ret; + char procfd[4096]; + + ret = snprintf(procfd, 4096, "/proc/self/fd/%d", fd); + if (ret < 0 || ret >= 4096) + return -1; + + ret = readlink(procfd, buf, buflen); + if (ret < 0 || (size_t)ret >= buflen) + return -1; + + buf[ret] = '\0'; + + return 0; +} + +static int do_tiocgptpeer(char *ptmx, char *expected_procfd_contents) +{ + int ret; + int master = -1, slave = -1, fret = -1; + + master = open(ptmx, O_RDWR | O_NOCTTY | O_CLOEXEC); + if (master < 0) { + fprintf(stderr, "Failed to open \"%s\": %s\n", ptmx, + strerror(errno)); + return -1; + } + + /* + * grantpt() makes assumptions about /dev/pts/ so ignore it. It's also + * not really needed. + */ + ret = unlockpt(master); + if (ret < 0) { + fprintf(stderr, "Failed to unlock terminal\n"); + goto do_cleanup; + } + + slave = ioctl(master, TIOCGPTPEER, O_RDWR | O_NOCTTY | O_CLOEXEC); + if (slave < 0) { + if (errno == EINVAL) { + fprintf(stderr, "TIOCGPTPEER is not supported. " + "Skipping test.\n"); + fret = KSFT_SKIP; + } else { + fprintf(stderr, + "Failed to perform TIOCGPTPEER ioctl\n"); + fret = EXIT_FAILURE; + } + goto do_cleanup; + } + + pid_t pid = fork(); + if (pid < 0) + goto do_cleanup; + + if (pid == 0) { + char buf[4096]; + + ret = login_pty(slave); + if (ret < 0) { + fprintf(stderr, "Failed to setup terminal\n"); + _exit(EXIT_FAILURE); + } + + ret = resolve_procfd_symlink(STDIN_FILENO, buf, sizeof(buf)); + if (ret < 0) { + fprintf(stderr, "Failed to retrieve pathname of pts " + "slave file descriptor\n"); + _exit(EXIT_FAILURE); + } + + if (strncmp(expected_procfd_contents, buf, + strlen(expected_procfd_contents)) != 0) { + fprintf(stderr, "Received invalid contents for " + "\"/proc/<pid>/fd/%d\" symlink: %s\n", + STDIN_FILENO, buf); + _exit(-1); + } + + fprintf(stderr, "Contents of \"/proc/<pid>/fd/%d\" " + "symlink are valid: %s\n", STDIN_FILENO, buf); + + _exit(EXIT_SUCCESS); + } + + ret = wait_for_pid(pid); + if (ret < 0) + goto do_cleanup; + + fret = EXIT_SUCCESS; + +do_cleanup: + if (master >= 0) + close(master); + if (slave >= 0) + close(slave); + + return fret; +} + +static int verify_non_standard_devpts_mount(void) +{ + char *mntpoint; + int ret = -1; + char devpts[] = P_tmpdir "/devpts_fs_XXXXXX"; + char ptmx[] = P_tmpdir "/devpts_fs_XXXXXX/ptmx"; + + ret = umount("/dev/pts"); + if (ret < 0) { + fprintf(stderr, "Failed to unmount \"/dev/pts\": %s\n", + strerror(errno)); + return -1; + } + + (void)umount("/dev/ptmx"); + + mntpoint = mkdtemp(devpts); + if (!mntpoint) { + fprintf(stderr, "Failed to create temporary mountpoint: %s\n", + strerror(errno)); + return -1; + } + + ret = mount("devpts", mntpoint, "devpts", MS_NOSUID | MS_NOEXEC, + "newinstance,ptmxmode=0666,mode=0620,gid=5"); + if (ret < 0) { + fprintf(stderr, "Failed to mount devpts fs to \"%s\" in new " + "mount namespace: %s\n", mntpoint, + strerror(errno)); + unlink(mntpoint); + return -1; + } + + ret = snprintf(ptmx, sizeof(ptmx), "%s/ptmx", devpts); + if (ret < 0 || (size_t)ret >= sizeof(ptmx)) { + unlink(mntpoint); + return -1; + } + + ret = do_tiocgptpeer(ptmx, mntpoint); + unlink(mntpoint); + if (ret < 0) + return -1; + + return 0; +} + +static int verify_ptmx_bind_mount(void) +{ + int ret; + + ret = mount("/dev/pts/ptmx", "/dev/ptmx", NULL, MS_BIND, NULL); + if (ret < 0) { + fprintf(stderr, "Failed to bind mount \"/dev/pts/ptmx\" to " + "\"/dev/ptmx\" mount namespace\n"); + return -1; + } + + ret = do_tiocgptpeer("/dev/ptmx", "/dev/pts/"); + if (ret < 0) + return -1; + + return 0; +} + +static int verify_invalid_ptmx_bind_mount(void) +{ + int ret; + char mntpoint_fd; + char ptmx[] = P_tmpdir "/devpts_ptmx_XXXXXX"; + + mntpoint_fd = mkstemp(ptmx); + if (mntpoint_fd < 0) { + fprintf(stderr, "Failed to create temporary directory: %s\n", + strerror(errno)); + return -1; + } + + ret = mount("/dev/pts/ptmx", ptmx, NULL, MS_BIND, NULL); + close(mntpoint_fd); + if (ret < 0) { + fprintf(stderr, "Failed to bind mount \"/dev/pts/ptmx\" to " + "\"%s\" mount namespace\n", ptmx); + return -1; + } + + ret = do_tiocgptpeer(ptmx, "/dev/pts/"); + if (ret == 0) + return -1; + + return 0; +} + +int main(int argc, char *argv[]) +{ + int ret; + + if (!isatty(STDIN_FILENO)) { + fprintf(stderr, "Standard input file descriptor is not attached " + "to a terminal. Skipping test\n"); + exit(KSFT_SKIP); + } + + ret = unshare(CLONE_NEWNS); + if (ret < 0) { + fprintf(stderr, "Failed to unshare mount namespace\n"); + exit(EXIT_FAILURE); + } + + ret = mount("", "/", NULL, MS_PRIVATE | MS_REC, 0); + if (ret < 0) { + fprintf(stderr, "Failed to make \"/\" MS_PRIVATE in new mount " + "namespace\n"); + exit(EXIT_FAILURE); + } + + ret = verify_ptmx_bind_mount(); + if (ret < 0) + exit(EXIT_FAILURE); + + ret = verify_invalid_ptmx_bind_mount(); + if (ret < 0) + exit(EXIT_FAILURE); + + ret = verify_non_standard_devpts_mount(); + if (ret < 0) + exit(EXIT_FAILURE); + + exit(EXIT_SUCCESS); +} diff --git a/tools/testing/selftests/filesystems/dnotify_test.c b/tools/testing/selftests/filesystems/dnotify_test.c new file mode 100644 index 000000000..c0a9b2d33 --- /dev/null +++ b/tools/testing/selftests/filesystems/dnotify_test.c @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE /* needed to get the defines */ +#include <fcntl.h> /* in glibc 2.2 this has the needed + values defined */ +#include <signal.h> +#include <stdio.h> +#include <unistd.h> + +static volatile int event_fd; + +static void handler(int sig, siginfo_t *si, void *data) +{ + event_fd = si->si_fd; +} + +int main(void) +{ + struct sigaction act; + int fd; + + act.sa_sigaction = handler; + sigemptyset(&act.sa_mask); + act.sa_flags = SA_SIGINFO; + sigaction(SIGRTMIN + 1, &act, NULL); + + fd = open(".", O_RDONLY); + fcntl(fd, F_SETSIG, SIGRTMIN + 1); + fcntl(fd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_MULTISHOT); + /* we will now be notified if any of the files + in "." is modified or new files are created */ + while (1) { + pause(); + printf("Got event on fd=%d\n", event_fd); + } +} diff --git a/tools/testing/selftests/filesystems/empty_mntns/.gitignore b/tools/testing/selftests/filesystems/empty_mntns/.gitignore new file mode 100644 index 000000000..99f89d329 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/.gitignore @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only +clone3_empty_mntns_test +empty_mntns_test +overmount_chroot_test diff --git a/tools/testing/selftests/filesystems/empty_mntns/Makefile b/tools/testing/selftests/filesystems/empty_mntns/Makefile new file mode 100644 index 000000000..22e3fb915 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +LDLIBS += -lcap + +TEST_GEN_PROGS := empty_mntns_test overmount_chroot_test clone3_empty_mntns_test + +include ../../lib.mk + +$(OUTPUT)/empty_mntns_test: ../utils.c +$(OUTPUT)/overmount_chroot_test: ../utils.c +$(OUTPUT)/clone3_empty_mntns_test: ../utils.c diff --git a/tools/testing/selftests/filesystems/empty_mntns/clone3_empty_mntns_test.c b/tools/testing/selftests/filesystems/empty_mntns/clone3_empty_mntns_test.c new file mode 100644 index 000000000..6370086f8 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/clone3_empty_mntns_test.c @@ -0,0 +1,938 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Tests for empty mount namespace creation via clone3() CLONE_EMPTY_MNTNS + * + * These tests exercise the clone3() code path for creating empty mount + * namespaces, which is distinct from the unshare() path tested in + * empty_mntns_test.c. With clone3(), CLONE_EMPTY_MNTNS (0x2000000000ULL) + * is a 64-bit flag that implies CLONE_NEWNS. The implication happens in + * kernel_clone() before copy_process(), unlike unshare() where it goes + * through UNSHARE_EMPTY_MNTNS -> CLONE_EMPTY_MNTNS conversion in + * unshare_nsproxy_namespaces(). + * + * Copyright (c) 2024 Christian Brauner <brauner@kernel.org> + */ + +#define _GNU_SOURCE +#include <fcntl.h> +#include <linux/mount.h> +#include <linux/stat.h> +#include <stdio.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "../utils.h" +#include "../wrappers.h" +#include "clone3/clone3_selftests.h" +#include "empty_mntns.h" +#include "kselftest_harness.h" + +static pid_t clone3_empty_mntns(uint64_t extra_flags) +{ + struct __clone_args args = { + .flags = CLONE_EMPTY_MNTNS | extra_flags, + .exit_signal = SIGCHLD, + }; + + return sys_clone3(&args, sizeof(args)); +} + +static bool clone3_empty_mntns_supported(void) +{ + pid_t pid; + int status; + + pid = fork(); + if (pid < 0) + return false; + + if (pid == 0) { + if (enter_userns()) + _exit(1); + + pid = clone3_empty_mntns(0); + if (pid < 0) + _exit(1); + + if (pid == 0) + _exit(0); + + _exit(wait_for_pid(pid) != 0); + } + + if (waitpid(pid, &status, 0) != pid) + return false; + + if (!WIFEXITED(status)) + return false; + + return WEXITSTATUS(status) == 0; +} + +FIXTURE(clone3_empty_mntns) {}; + +FIXTURE_SETUP(clone3_empty_mntns) +{ + if (!clone3_empty_mntns_supported()) + SKIP(return, "CLONE_EMPTY_MNTNS via clone3 not supported"); +} + +FIXTURE_TEARDOWN(clone3_empty_mntns) {} + +/* + * Basic clone3() with CLONE_EMPTY_MNTNS: child gets empty mount namespace + * with exactly 1 mount and root == cwd. + */ +TEST_F(clone3_empty_mntns, basic) +{ + pid_t pid, inner; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + if (enter_userns()) + _exit(1); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(2); + + if (inner == 0) { + uint64_t root_id, cwd_id; + + if (count_mounts() != 1) + _exit(3); + + root_id = get_unique_mnt_id("/"); + cwd_id = get_unique_mnt_id("."); + if (root_id == 0 || cwd_id == 0) + _exit(4); + + if (root_id != cwd_id) + _exit(5); + + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * CLONE_EMPTY_MNTNS implies CLONE_NEWNS. Verify that it works without + * explicitly setting CLONE_NEWNS (tests fork.c:2627-2630). + */ +TEST_F(clone3_empty_mntns, implies_newns) +{ + pid_t pid, inner; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + ssize_t parent_mounts; + + if (enter_userns()) + _exit(1); + + /* Verify we have mounts in our current namespace. */ + parent_mounts = count_mounts(); + if (parent_mounts < 1) + _exit(2); + + /* Only CLONE_EMPTY_MNTNS, no explicit CLONE_NEWNS. */ + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(3); + + if (inner == 0) { + if (count_mounts() != 1) + _exit(4); + + _exit(0); + } + + /* Parent still has its mounts. */ + if (count_mounts() != parent_mounts) + _exit(5); + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Helper macro: generate a test that clones with CLONE_EMPTY_MNTNS | + * @extra_flags and verifies the child has exactly one mount. + */ +#define TEST_CLONE3_FLAGS(test_name, extra_flags) \ +TEST_F(clone3_empty_mntns, test_name) \ +{ \ + pid_t pid, inner; \ + \ + pid = fork(); \ + ASSERT_GE(pid, 0); \ + \ + if (pid == 0) { \ + if (enter_userns()) \ + _exit(1); \ + \ + inner = clone3_empty_mntns(extra_flags); \ + if (inner < 0) \ + _exit(2); \ + \ + if (inner == 0) { \ + if (count_mounts() != 1) \ + _exit(3); \ + _exit(0); \ + } \ + \ + _exit(wait_for_pid(inner)); \ + } \ + \ + ASSERT_EQ(wait_for_pid(pid), 0); \ +} + +/* Redundant CLONE_NEWNS | CLONE_EMPTY_MNTNS should succeed. */ +TEST_CLONE3_FLAGS(with_explicit_newns, CLONE_NEWNS) + +/* CLONE_EMPTY_MNTNS combined with CLONE_NEWUSER. */ +TEST_CLONE3_FLAGS(with_newuser, CLONE_NEWUSER) + +/* CLONE_EMPTY_MNTNS combined with other namespace flags. */ +TEST_CLONE3_FLAGS(with_other_ns_flags, CLONE_NEWUTS | CLONE_NEWIPC) + +/* + * CLONE_EMPTY_MNTNS combined with CLONE_NEWPID. + */ +TEST_F(clone3_empty_mntns, with_newpid) +{ + pid_t pid, inner; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + if (enter_userns()) + _exit(1); + + inner = clone3_empty_mntns(CLONE_NEWPID); + if (inner < 0) + _exit(2); + + if (inner == 0) { + if (count_mounts() != 1) + _exit(3); + + /* In a new PID namespace, getpid() returns 1. */ + if (getpid() != 1) + _exit(4); + + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * CLONE_EMPTY_MNTNS | CLONE_FS must fail because the implied CLONE_NEWNS + * and CLONE_FS are mutually exclusive (fork.c:1981). + */ +TEST_F(clone3_empty_mntns, with_clone_fs_fails) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct __clone_args args = { + .flags = CLONE_EMPTY_MNTNS | CLONE_FS, + .exit_signal = SIGCHLD, + }; + pid_t ret; + + if (enter_userns()) + _exit(1); + + ret = sys_clone3(&args, sizeof(args)); + if (ret >= 0) { + if (ret == 0) + _exit(0); + wait_for_pid(ret); + _exit(2); + } + + if (errno != EINVAL) + _exit(3); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * CLONE_EMPTY_MNTNS combined with CLONE_PIDFD returns a valid pidfd. + */ +TEST_F(clone3_empty_mntns, with_pidfd) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct __clone_args args = { + .flags = CLONE_EMPTY_MNTNS | CLONE_PIDFD, + .exit_signal = SIGCHLD, + }; + int pidfd = -1; + pid_t inner; + + if (enter_userns()) + _exit(1); + + args.pidfd = (uintptr_t)&pidfd; + + inner = sys_clone3(&args, sizeof(args)); + if (inner < 0) + _exit(2); + + if (inner == 0) { + if (count_mounts() != 1) + _exit(3); + + _exit(0); + } + + /* Verify we got a valid pidfd. */ + if (pidfd < 0) + _exit(4); + + close(pidfd); + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * clone3 without CAP_SYS_ADMIN must fail with EPERM. + */ +TEST_F(clone3_empty_mntns, eperm_without_caps) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + pid_t ret; + + /* Skip if already root. */ + if (getuid() == 0) + _exit(0); + + ret = clone3_empty_mntns(0); + if (ret >= 0) { + if (ret == 0) + _exit(0); + wait_for_pid(ret); + _exit(1); + } + + if (errno != EPERM) + _exit(2); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Parent's mount namespace is unaffected after clone3 with CLONE_EMPTY_MNTNS. + */ +TEST_F(clone3_empty_mntns, parent_unchanged) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + ssize_t nr_before, nr_after; + pid_t inner; + + if (enter_userns()) + _exit(1); + + nr_before = count_mounts(); + if (nr_before < 1) + _exit(2); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(3); + + if (inner == 0) + _exit(0); + + if (wait_for_pid(inner) != 0) + _exit(4); + + nr_after = count_mounts(); + if (nr_after != nr_before) + _exit(5); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Parent with many mounts: child still gets exactly 1 mount. + */ +TEST_F(clone3_empty_mntns, many_parent_mounts) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + char tmpdir[] = "/tmp/clone3_mntns_test.XXXXXX"; + pid_t inner; + int i; + + if (enter_userns()) + _exit(1); + + if (unshare(CLONE_NEWNS)) + _exit(2); + + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) + _exit(3); + + if (!mkdtemp(tmpdir)) + _exit(4); + + if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M")) + _exit(5); + + for (i = 0; i < 5; i++) { + char subdir[256]; + + snprintf(subdir, sizeof(subdir), "%s/sub%d", tmpdir, i); + if (mkdir(subdir, 0755) && errno != EEXIST) + _exit(6); + if (mount(subdir, subdir, NULL, MS_BIND, NULL)) + _exit(7); + } + + if (count_mounts() < 5) + _exit(8); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(9); + + if (inner == 0) { + if (count_mounts() != 1) + _exit(10); + + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Verify the child's root mount is nullfs with expected statmount properties. + */ +TEST_F(clone3_empty_mntns, mount_properties) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + pid_t inner; + + if (enter_userns()) + _exit(1); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(2); + + if (inner == 0) { + struct statmount *sm; + uint64_t root_id; + + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(3); + + sm = statmount_alloc(root_id, 0, + STATMOUNT_MNT_BASIC | + STATMOUNT_MNT_POINT | + STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(4); + + /* Root mount point is "/". */ + if (!(sm->mask & STATMOUNT_MNT_POINT)) + _exit(5); + if (strcmp(sm->str + sm->mnt_point, "/") != 0) + _exit(6); + + /* Filesystem type is nullfs. */ + if (!(sm->mask & STATMOUNT_FS_TYPE)) + _exit(7); + if (strcmp(sm->str + sm->fs_type, "nullfs") != 0) + _exit(8); + + /* Root mount is its own parent. */ + if (!(sm->mask & STATMOUNT_MNT_BASIC)) + _exit(9); + if (sm->mnt_parent_id != sm->mnt_id) + _exit(10); + + free(sm); + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Listmount returns only the root mount in the child's empty namespace. + */ +TEST_F(clone3_empty_mntns, listmount_single_entry) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + pid_t inner; + + if (enter_userns()) + _exit(1); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(2); + + if (inner == 0) { + uint64_t list[16]; + ssize_t nr_mounts; + uint64_t root_id; + + nr_mounts = listmount(LSMT_ROOT, 0, 0, list, 16, 0); + if (nr_mounts != 1) + _exit(3); + + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(4); + + if (list[0] != root_id) + _exit(5); + + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Child can mount tmpfs over nullfs root (the primary container use case). + * + * Uses the new mount API (fsopen/fsmount/move_mount) because resolving + * "/" returns the process root directly without following overmounts. + * The mount fd from fsmount lets us fchdir + chroot into the new tmpfs. + */ +TEST_F(clone3_empty_mntns, child_overmount_tmpfs) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + pid_t inner; + + if (enter_userns()) + _exit(1); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(2); + + if (inner == 0) { + struct statmount *sm; + uint64_t root_id; + int fd, fsfd, mntfd; + + if (count_mounts() != 1) + _exit(3); + + /* Verify root is nullfs. */ + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(4); + + sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(5); + if (!(sm->mask & STATMOUNT_FS_TYPE)) + _exit(6); + if (strcmp(sm->str + sm->fs_type, "nullfs") != 0) + _exit(7); + free(sm); + + /* Create tmpfs via the new mount API. */ + fsfd = sys_fsopen("tmpfs", 0); + if (fsfd < 0) + _exit(8); + + if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, + "size", "1M", 0)) { + close(fsfd); + _exit(9); + } + + if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, + NULL, NULL, 0)) { + close(fsfd); + _exit(10); + } + + mntfd = sys_fsmount(fsfd, 0, 0); + close(fsfd); + if (mntfd < 0) + _exit(11); + + /* Attach tmpfs to "/". */ + if (sys_move_mount(mntfd, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH)) { + close(mntfd); + _exit(12); + } + + if (count_mounts() != 2) { + close(mntfd); + _exit(13); + } + + /* Enter the tmpfs. */ + if (fchdir(mntfd)) { + close(mntfd); + _exit(14); + } + + if (chroot(".")) { + close(mntfd); + _exit(15); + } + + close(mntfd); + + /* Verify "/" is now tmpfs. */ + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(16); + + sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(17); + if (!(sm->mask & STATMOUNT_FS_TYPE)) + _exit(18); + if (strcmp(sm->str + sm->fs_type, "tmpfs") != 0) + _exit(19); + free(sm); + + /* Verify tmpfs is writable. */ + fd = open("/testfile", O_CREAT | O_RDWR, 0644); + if (fd < 0) + _exit(20); + + if (write(fd, "test", 4) != 4) { + close(fd); + _exit(21); + } + close(fd); + + if (access("/testfile", F_OK)) + _exit(22); + + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Multiple clone3 calls with CLONE_EMPTY_MNTNS produce children with + * distinct mount namespace root mount IDs. + */ +TEST_F(clone3_empty_mntns, repeated) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + int pipe1[2], pipe2[2]; + uint64_t id1 = 0, id2 = 0; + pid_t inner1, inner2; + + if (enter_userns()) + _exit(1); + + if (pipe(pipe1) || pipe(pipe2)) + _exit(2); + + inner1 = clone3_empty_mntns(0); + if (inner1 < 0) + _exit(3); + + if (inner1 == 0) { + uint64_t root_id; + + close(pipe1[0]); + root_id = get_unique_mnt_id("/"); + if (write(pipe1[1], &root_id, sizeof(root_id)) != sizeof(root_id)) + _exit(1); + close(pipe1[1]); + _exit(0); + } + + inner2 = clone3_empty_mntns(0); + if (inner2 < 0) + _exit(4); + + if (inner2 == 0) { + uint64_t root_id; + + close(pipe2[0]); + root_id = get_unique_mnt_id("/"); + if (write(pipe2[1], &root_id, sizeof(root_id)) != sizeof(root_id)) + _exit(1); + close(pipe2[1]); + _exit(0); + } + + close(pipe1[1]); + close(pipe2[1]); + + if (read(pipe1[0], &id1, sizeof(id1)) != sizeof(id1)) + _exit(5); + if (read(pipe2[0], &id2, sizeof(id2)) != sizeof(id2)) + _exit(6); + + close(pipe1[0]); + close(pipe2[0]); + + if (wait_for_pid(inner1) || wait_for_pid(inner2)) + _exit(7); + + /* Each child must have a distinct root mount ID. */ + if (id1 == 0 || id2 == 0) + _exit(8); + if (id1 == id2) + _exit(9); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Verify setns() into a child's empty mount namespace works. + */ +TEST_F(clone3_empty_mntns, setns_into_child_mntns) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + int pipe_fd[2]; + pid_t inner; + char c; + + if (enter_userns()) + _exit(1); + + if (pipe(pipe_fd)) + _exit(2); + + inner = clone3_empty_mntns(0); + if (inner < 0) + _exit(3); + + if (inner == 0) { + /* Signal parent we're ready. */ + close(pipe_fd[0]); + if (write(pipe_fd[1], "r", 1) != 1) + _exit(1); + + /* + * Wait for parent to finish. Reading from our + * write end will block until the parent closes + * its read end, giving us an implicit barrier. + */ + if (read(pipe_fd[1], &c, 1) < 0) + ; + close(pipe_fd[1]); + _exit(0); + } + + close(pipe_fd[1]); + + /* Wait for child to be ready. */ + if (read(pipe_fd[0], &c, 1) != 1) + _exit(4); + + /* Open child's mount namespace. */ + { + char path[64]; + int mntns_fd; + + snprintf(path, sizeof(path), "/proc/%d/ns/mnt", inner); + mntns_fd = open(path, O_RDONLY); + if (mntns_fd < 0) + _exit(5); + + if (setns(mntns_fd, CLONE_NEWNS)) + _exit(6); + + close(mntns_fd); + } + + /* Now we should be in the child's empty mntns. */ + if (count_mounts() != 1) + _exit(7); + + close(pipe_fd[0]); + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Tests below do not require CLONE_EMPTY_MNTNS support. + */ + +/* + * Unknown 64-bit flags beyond the known set are rejected. + */ +TEST(unknown_flags_rejected) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct __clone_args args = { + .flags = 0x800000000ULL, + .exit_signal = SIGCHLD, + }; + pid_t ret; + + ret = sys_clone3(&args, sizeof(args)); + if (ret >= 0) { + if (ret == 0) + _exit(0); + wait_for_pid(ret); + _exit(1); + } + + if (errno != EINVAL) + _exit(2); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Regular clone3 with CLONE_NEWNS (without CLONE_EMPTY_MNTNS) still + * copies the full mount tree. + */ +TEST(clone3_newns_full_copy) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct __clone_args args = { + .flags = CLONE_NEWNS, + .exit_signal = SIGCHLD, + }; + ssize_t parent_mounts; + pid_t inner; + + if (enter_userns()) + _exit(1); + + parent_mounts = count_mounts(); + if (parent_mounts < 1) + _exit(2); + + inner = sys_clone3(&args, sizeof(args)); + if (inner < 0) + _exit(3); + + if (inner == 0) { + /* Full copy should have at least as many mounts. */ + if (count_mounts() < parent_mounts) + _exit(1); + + _exit(0); + } + + _exit(wait_for_pid(inner)); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/empty_mntns/empty_mntns.h b/tools/testing/selftests/filesystems/empty_mntns/empty_mntns.h new file mode 100644 index 000000000..3d9c6b14b --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/empty_mntns.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef EMPTY_MNTNS_H +#define EMPTY_MNTNS_H + +#include <errno.h> +#include <stdlib.h> + +#include "../statmount/statmount.h" + +#ifndef UNSHARE_EMPTY_MNTNS +#define UNSHARE_EMPTY_MNTNS 0x00100000 +#endif + +#ifndef CLONE_EMPTY_MNTNS +#define CLONE_EMPTY_MNTNS (1ULL << 37) +#endif + +static inline ssize_t count_mounts(void) +{ + uint64_t list[4096]; + + return listmount(LSMT_ROOT, 0, 0, list, sizeof(list) / sizeof(list[0]), 0); +} + +#endif /* EMPTY_MNTNS_H */ diff --git a/tools/testing/selftests/filesystems/empty_mntns/empty_mntns_test.c b/tools/testing/selftests/filesystems/empty_mntns/empty_mntns_test.c new file mode 100644 index 000000000..43e296b97 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/empty_mntns_test.c @@ -0,0 +1,725 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Tests for empty mount namespace creation via UNSHARE_EMPTY_MNTNS + * + * Copyright (c) 2024 Christian Brauner <brauner@kernel.org> + */ + +#define _GNU_SOURCE +#include <fcntl.h> +#include <linux/mount.h> +#include <linux/stat.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../utils.h" +#include "../wrappers.h" +#include "empty_mntns.h" +#include "kselftest_harness.h" + +static bool unshare_empty_mntns_supported(void) +{ + pid_t pid; + int status; + + pid = fork(); + if (pid < 0) + return false; + + if (pid == 0) { + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS) && errno == EINVAL) + _exit(1); + _exit(0); + } + + if (waitpid(pid, &status, 0) != pid) + return false; + + if (!WIFEXITED(status)) + return false; + + return WEXITSTATUS(status) == 0; +} + + +FIXTURE(empty_mntns) {}; + +FIXTURE_SETUP(empty_mntns) +{ + if (!unshare_empty_mntns_supported()) + SKIP(return, "UNSHARE_EMPTY_MNTNS not supported"); +} + +FIXTURE_TEARDOWN(empty_mntns) {} + +/* Verify unshare succeeds, produces exactly 1 mount, and root == cwd */ +TEST_F(empty_mntns, basic) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t root_id, cwd_id; + + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(2); + + if (count_mounts() != 1) + _exit(3); + + root_id = get_unique_mnt_id("/"); + cwd_id = get_unique_mnt_id("."); + if (root_id == 0 || cwd_id == 0) + _exit(4); + + if (root_id != cwd_id) + _exit(5); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * UNSHARE_EMPTY_MNTNS combined with CLONE_NEWUSER. + * + * The user namespace must be created first so /proc is still accessible + * for writing uid_map/gid_map. The empty mount namespace is created + * afterwards. + */ +TEST_F(empty_mntns, with_clone_newuser) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uid_t uid = getuid(); + gid_t gid = getgid(); + char map[100]; + + if (unshare(CLONE_NEWUSER)) + _exit(1); + + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + _exit(2); + + if (write_file("/proc/self/setgroups", "deny")) + _exit(3); + + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + _exit(4); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(5); + + if (count_mounts() != 1) + _exit(6); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* UNSHARE_EMPTY_MNTNS combined with other namespace flags */ +TEST_F(empty_mntns, with_other_ns_flags) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS | CLONE_NEWUTS | CLONE_NEWIPC)) + _exit(2); + + if (count_mounts() != 1) + _exit(3); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* EPERM without proper capabilities */ +TEST_F(empty_mntns, eperm_without_caps) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + /* Skip if already root */ + if (getuid() == 0) + _exit(0); + + if (unshare(UNSHARE_EMPTY_MNTNS) == 0) + _exit(1); + + if (errno != EPERM) + _exit(2); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Many source mounts still result in exactly 1 mount */ +TEST_F(empty_mntns, many_source_mounts) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + char tmpdir[] = "/tmp/empty_mntns_test.XXXXXX"; + int i; + + if (enter_userns()) + _exit(1); + + if (unshare(CLONE_NEWNS)) + _exit(2); + + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) + _exit(3); + + if (!mkdtemp(tmpdir)) + _exit(4); + + if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M")) + _exit(5); + + for (i = 0; i < 5; i++) { + char subdir[256]; + + snprintf(subdir, sizeof(subdir), "%s/sub%d", tmpdir, i); + if (mkdir(subdir, 0755) && errno != EEXIST) + _exit(6); + if (mount(subdir, subdir, NULL, MS_BIND, NULL)) + _exit(7); + } + + if (count_mounts() < 5) + _exit(8); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(9); + + if (count_mounts() != 1) + _exit(10); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* CWD on a different mount gets reset to root */ +TEST_F(empty_mntns, cwd_reset) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + char tmpdir[] = "/tmp/empty_mntns_cwd.XXXXXX"; + uint64_t root_id, cwd_id; + struct statmount *sm; + + if (enter_userns()) + _exit(1); + + if (unshare(CLONE_NEWNS)) + _exit(2); + + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) + _exit(3); + + if (!mkdtemp(tmpdir)) + _exit(4); + + if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M")) + _exit(5); + + if (chdir(tmpdir)) + _exit(6); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(7); + + root_id = get_unique_mnt_id("/"); + cwd_id = get_unique_mnt_id("."); + if (root_id == 0 || cwd_id == 0) + _exit(8); + + if (root_id != cwd_id) + _exit(9); + + sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_ROOT | STATMOUNT_MNT_POINT, 0); + if (!sm) + _exit(10); + + if (strcmp(sm->str + sm->mnt_point, "/") != 0) + _exit(11); + + free(sm); + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Verify statmount properties of the root mount */ +TEST_F(empty_mntns, mount_properties) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct statmount *sm; + uint64_t root_id; + + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(2); + + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(3); + + sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_BASIC | STATMOUNT_MNT_ROOT | + STATMOUNT_MNT_POINT | STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(4); + + if (!(sm->mask & STATMOUNT_MNT_POINT)) + _exit(5); + + if (strcmp(sm->str + sm->mnt_point, "/") != 0) + _exit(6); + + if (!(sm->mask & STATMOUNT_MNT_BASIC)) + _exit(7); + + if (sm->mnt_id != root_id) + _exit(8); + + free(sm); + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Consecutive UNSHARE_EMPTY_MNTNS calls produce new namespaces */ +TEST_F(empty_mntns, repeated_unshare) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t first_root_id, second_root_id; + + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(2); + + if (count_mounts() != 1) + _exit(3); + + first_root_id = get_unique_mnt_id("/"); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(4); + + if (count_mounts() != 1) + _exit(5); + + second_root_id = get_unique_mnt_id("/"); + + if (first_root_id == second_root_id) + _exit(6); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Root mount's parent is itself */ +TEST_F(empty_mntns, root_is_own_parent) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct statmount sm; + uint64_t root_id; + + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(2); + + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(3); + + if (statmount(root_id, 0, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0) < 0) + _exit(4); + + if (!(sm.mask & STATMOUNT_MNT_BASIC)) + _exit(5); + + if (sm.mnt_parent_id != sm.mnt_id) + _exit(6); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Listmount returns only the root mount */ +TEST_F(empty_mntns, listmount_single_entry) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t list[16]; + ssize_t nr_mounts; + uint64_t root_id; + + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(2); + + nr_mounts = listmount(LSMT_ROOT, 0, 0, list, 16, 0); + if (nr_mounts != 1) + _exit(3); + + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(4); + + if (list[0] != root_id) + _exit(5); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Mount tmpfs over nullfs root to build a writable filesystem from scratch. + * This exercises the intended usage pattern: create an empty mount namespace + * (which has a nullfs root), then mount a real filesystem over it. + * + * Because resolving "/" returns the process root directly (via nd_jump_root) + * without following overmounts, we use the new mount API (fsopen/fsmount) + * to obtain a mount fd, then fchdir + chroot to enter the new filesystem. + */ +TEST_F(empty_mntns, overmount_tmpfs) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + struct statmount *sm; + uint64_t root_id, cwd_id; + int fd, fsfd, mntfd; + + if (enter_userns()) + _exit(1); + + if (unshare(UNSHARE_EMPTY_MNTNS)) + _exit(2); + + if (count_mounts() != 1) + _exit(3); + + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(4); + + /* Verify root is nullfs */ + sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(5); + + if (!(sm->mask & STATMOUNT_FS_TYPE)) + _exit(6); + + if (strcmp(sm->str + sm->fs_type, "nullfs") != 0) + _exit(7); + + free(sm); + + cwd_id = get_unique_mnt_id("."); + if (!cwd_id || root_id != cwd_id) + _exit(8); + + /* + * nullfs root is immutable. open(O_CREAT) returns ENOENT + * because empty_dir_lookup() returns -ENOENT before the + * IS_IMMUTABLE permission check in may_o_create() is reached. + */ + fd = open("/test", O_CREAT | O_RDWR, 0644); + if (fd >= 0) { + close(fd); + _exit(9); + } + if (errno != ENOENT) + _exit(10); + + /* + * Use the new mount API to create tmpfs and get a mount fd. + * We need the fd because after attaching the tmpfs on top of + * "/", path resolution of "/" still returns the process root + * (nullfs) without following the overmount. The mount fd + * lets us fchdir + chroot into the tmpfs. + */ + fsfd = sys_fsopen("tmpfs", 0); + if (fsfd < 0) + _exit(11); + + if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "size", "1M", 0)) { + close(fsfd); + _exit(12); + } + + if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) { + close(fsfd); + _exit(13); + } + + mntfd = sys_fsmount(fsfd, 0, 0); + close(fsfd); + if (mntfd < 0) + _exit(14); + + if (sys_move_mount(mntfd, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH)) { + close(mntfd); + _exit(15); + } + + if (count_mounts() != 2) { + close(mntfd); + _exit(16); + } + + /* Enter the tmpfs via the mount fd */ + if (fchdir(mntfd)) { + close(mntfd); + _exit(17); + } + + if (chroot(".")) { + close(mntfd); + _exit(18); + } + + close(mntfd); + + /* Verify "/" now resolves to tmpfs */ + root_id = get_unique_mnt_id("/"); + if (!root_id) + _exit(19); + + sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(20); + + if (!(sm->mask & STATMOUNT_FS_TYPE)) + _exit(21); + + if (strcmp(sm->str + sm->fs_type, "tmpfs") != 0) + _exit(22); + + free(sm); + + /* Verify tmpfs is writable */ + fd = open("/testfile", O_CREAT | O_RDWR, 0644); + if (fd < 0) + _exit(23); + + if (write(fd, "test", 4) != 4) { + close(fd); + _exit(24); + } + + close(fd); + + if (access("/testfile", F_OK)) + _exit(25); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* + * Tests below do not require UNSHARE_EMPTY_MNTNS support. + */ + +/* Invalid unshare flags return EINVAL */ +TEST(invalid_flags) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + if (enter_userns()) + _exit(1); + + if (unshare(0x80000000) == 0) + _exit(2); + + if (errno != EINVAL) + _exit(3); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Regular CLONE_NEWNS still copies the full mount tree */ +TEST(clone_newns_full_copy) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + ssize_t nr_mounts_before, nr_mounts_after; + char tmpdir[] = "/tmp/empty_mntns_regr.XXXXXX"; + int i; + + if (enter_userns()) + _exit(1); + + if (unshare(CLONE_NEWNS)) + _exit(2); + + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) + _exit(3); + + if (!mkdtemp(tmpdir)) + _exit(4); + + if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M")) + _exit(5); + + for (i = 0; i < 3; i++) { + char subdir[256]; + + snprintf(subdir, sizeof(subdir), "%s/sub%d", tmpdir, i); + if (mkdir(subdir, 0755) && errno != EEXIST) + _exit(6); + if (mount(subdir, subdir, NULL, MS_BIND, NULL)) + _exit(7); + } + + nr_mounts_before = count_mounts(); + if (nr_mounts_before < 3) + _exit(8); + + if (unshare(CLONE_NEWNS)) + _exit(9); + + nr_mounts_after = count_mounts(); + if (nr_mounts_after < nr_mounts_before) + _exit(10); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +/* Other namespace unshares are unaffected */ +TEST(other_ns_unaffected) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + char hostname[256]; + + if (enter_userns()) + _exit(1); + + if (unshare(CLONE_NEWUTS)) + _exit(2); + + if (sethostname("test-empty-mntns", 16)) + _exit(3); + + if (gethostname(hostname, sizeof(hostname))) + _exit(4); + + if (strcmp(hostname, "test-empty-mntns") != 0) + _exit(5); + + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/empty_mntns/overmount_chroot_test.c b/tools/testing/selftests/filesystems/empty_mntns/overmount_chroot_test.c new file mode 100644 index 000000000..6e21c5825 --- /dev/null +++ b/tools/testing/selftests/filesystems/empty_mntns/overmount_chroot_test.c @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Test: rootfs overmounted multiple times with chroot into topmost + * + * This test creates a scenario where: + * 1. A new mount namespace is created with a tmpfs root (via pivot_root) + * 2. A mountpoint is created and overmounted multiple times + * 3. The caller chroots into the topmost mount layer + * + * The test verifies that: + * - Multiple overmounts create separate mount layers + * - Each layer's files are isolated + * - chroot correctly sets the process's root to the topmost layer + * - After chroot, only the topmost layer's files are visible + * + * Copyright (c) 2024 Christian Brauner <brauner@kernel.org> + */ + +#define _GNU_SOURCE +#include <fcntl.h> +#include <linux/mount.h> +#include <linux/stat.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../utils.h" +#include "empty_mntns.h" +#include "kselftest_harness.h" + +#define NR_OVERMOUNTS 5 + +/* + * Setup a proper root filesystem using pivot_root. + * This ensures we own the root directory in our user namespace. + */ +static int setup_root(void) +{ + char tmpdir[] = "/tmp/overmount_test.XXXXXX"; + char oldroot[256]; + + if (!mkdtemp(tmpdir)) + return -1; + + /* Mount tmpfs at the temporary directory */ + if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=10M")) + return -1; + + /* Create directory for old root */ + snprintf(oldroot, sizeof(oldroot), "%s/oldroot", tmpdir); + if (mkdir(oldroot, 0755)) + return -1; + + /* pivot_root to use the tmpfs as new root */ + if (syscall(SYS_pivot_root, tmpdir, oldroot)) + return -1; + + if (chdir("/")) + return -1; + + /* Unmount old root */ + if (umount2("/oldroot", MNT_DETACH)) + return -1; + + /* Remove oldroot directory */ + if (rmdir("/oldroot")) + return -1; + + return 0; +} + +/* + * Test scenario: + * 1. Enter a user namespace to gain CAP_SYS_ADMIN + * 2. Create a new mount namespace + * 3. Setup a tmpfs root via pivot_root + * 4. Create a mountpoint /newroot and overmount it multiple times + * 5. Create a marker file in each layer + * 6. Chroot into /newroot (the topmost overmount) + * 7. Verify we're in the topmost layer (only topmost marker visible) + */ +TEST(overmount_chroot) +{ + pid_t pid; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + ssize_t nr_mounts; + uint64_t mnt_ids[NR_OVERMOUNTS + 1]; + uint64_t root_id_before, root_id_after; + struct statmount *sm; + char marker[64]; + int fd, i; + + /* Step 1: Enter user namespace for privileges */ + if (enter_userns()) + _exit(1); + + /* Step 2: Create a new mount namespace */ + if (unshare(CLONE_NEWNS)) + _exit(2); + + /* Step 3: Make the mount tree private */ + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) + _exit(3); + + /* Step 4: Setup a proper tmpfs root via pivot_root */ + if (setup_root()) + _exit(4); + + /* Create the base mount point for overmounting */ + if (mkdir("/newroot", 0755)) + _exit(5); + + /* Mount base tmpfs on /newroot */ + if (mount("tmpfs", "/newroot", "tmpfs", 0, "size=1M")) + _exit(6); + + /* Record base mount ID */ + mnt_ids[0] = get_unique_mnt_id("/newroot"); + if (!mnt_ids[0]) + _exit(7); + + /* Create marker in base layer */ + fd = open("/newroot/layer_0", O_CREAT | O_RDWR, 0644); + if (fd < 0) + _exit(8); + if (write(fd, "layer_0", 7) != 7) { + close(fd); + _exit(9); + } + close(fd); + + /* Step 5: Overmount /newroot multiple times with tmpfs */ + for (i = 0; i < NR_OVERMOUNTS; i++) { + if (mount("tmpfs", "/newroot", "tmpfs", 0, "size=1M")) + _exit(10); + + /* Record mount ID for this layer */ + mnt_ids[i + 1] = get_unique_mnt_id("/newroot"); + if (!mnt_ids[i + 1]) + _exit(11); + + /* Create a marker file in each layer */ + snprintf(marker, sizeof(marker), "/newroot/layer_%d", i + 1); + fd = open(marker, O_CREAT | O_RDWR, 0644); + if (fd < 0) + _exit(12); + + if (write(fd, marker, strlen(marker)) != (ssize_t)strlen(marker)) { + close(fd); + _exit(13); + } + close(fd); + } + + /* Verify mount count increased */ + nr_mounts = count_mounts(); + if (nr_mounts < NR_OVERMOUNTS + 2) + _exit(14); + + /* Record root mount ID before chroot */ + root_id_before = get_unique_mnt_id("/newroot"); + + /* Verify this is the topmost layer's mount */ + if (root_id_before != mnt_ids[NR_OVERMOUNTS]) + _exit(15); + + /* Step 6: Chroot into /newroot (the topmost overmount) */ + if (chroot("/newroot")) + _exit(16); + + /* Change to root directory within the chroot */ + if (chdir("/")) + _exit(17); + + /* Step 7: Verify we're in the topmost layer */ + root_id_after = get_unique_mnt_id("/"); + + /* The mount ID should be the same as the topmost layer */ + if (root_id_after != mnt_ids[NR_OVERMOUNTS]) + _exit(18); + + /* Verify the topmost layer's marker file exists */ + snprintf(marker, sizeof(marker), "/layer_%d", NR_OVERMOUNTS); + if (access(marker, F_OK)) + _exit(19); + + /* Verify we cannot see markers from lower layers (they're hidden) */ + for (i = 0; i < NR_OVERMOUNTS; i++) { + snprintf(marker, sizeof(marker), "/layer_%d", i); + if (access(marker, F_OK) == 0) + _exit(20); + } + + /* Verify the root mount is tmpfs */ + sm = statmount_alloc(root_id_after, 0, + STATMOUNT_MNT_BASIC | STATMOUNT_MNT_ROOT | + STATMOUNT_MNT_POINT | STATMOUNT_FS_TYPE, 0); + if (!sm) + _exit(21); + + if (sm->mask & STATMOUNT_FS_TYPE) { + if (strcmp(sm->str + sm->fs_type, "tmpfs") != 0) { + free(sm); + _exit(22); + } + } + + free(sm); + _exit(0); + } + + ASSERT_EQ(wait_for_pid(pid), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/epoll/.gitignore b/tools/testing/selftests/filesystems/epoll/.gitignore new file mode 100644 index 000000000..909015725 --- /dev/null +++ b/tools/testing/selftests/filesystems/epoll/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +epoll_wakeup_test diff --git a/tools/testing/selftests/filesystems/epoll/Makefile b/tools/testing/selftests/filesystems/epoll/Makefile new file mode 100644 index 000000000..0788a7dc8 --- /dev/null +++ b/tools/testing/selftests/filesystems/epoll/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += $(KHDR_INCLUDES) +LDLIBS += -lpthread +TEST_GEN_PROGS := epoll_wakeup_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/epoll/epoll_wakeup_test.c b/tools/testing/selftests/filesystems/epoll/epoll_wakeup_test.c new file mode 100644 index 000000000..f6f1a7ff0 --- /dev/null +++ b/tools/testing/selftests/filesystems/epoll/epoll_wakeup_test.c @@ -0,0 +1,3541 @@ +// SPDX-License-Identifier: GPL-2.0 + +#define _GNU_SOURCE +#include <asm/unistd.h> +#include <linux/time_types.h> +#include <poll.h> +#include <unistd.h> +#include <assert.h> +#include <signal.h> +#include <pthread.h> +#include <sys/epoll.h> +#include <sys/socket.h> +#include <sys/eventfd.h> +#include "kselftest_harness.h" + +struct epoll_mtcontext +{ + int efd[3]; + int sfd[4]; + volatile int count; + + pthread_t main; + pthread_t waiter; +}; + +#ifndef __NR_epoll_pwait2 +#define __NR_epoll_pwait2 -1 +#endif + +static inline int sys_epoll_pwait2(int fd, struct epoll_event *events, + int maxevents, + const struct __kernel_timespec *timeout, + const sigset_t *sigset, size_t sigsetsize) +{ + return syscall(__NR_epoll_pwait2, fd, events, maxevents, timeout, + sigset, sigsetsize); +} + +static void signal_handler(int signum) +{ +} + +static void kill_timeout(struct epoll_mtcontext *ctx) +{ + usleep(1000000); + pthread_kill(ctx->main, SIGUSR1); + pthread_kill(ctx->waiter, SIGUSR1); +} + +static void *waiter_entry1a(void *data) +{ + struct epoll_event e; + struct epoll_mtcontext *ctx = data; + + if (epoll_wait(ctx->efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx->count, 1); + + return NULL; +} + +static void *waiter_entry1ap(void *data) +{ + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext *ctx = data; + + pfd.fd = ctx->efd[0]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx->efd[0], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx->count, 1); + } + + return NULL; +} + +static void *waiter_entry1o(void *data) +{ + struct epoll_event e; + struct epoll_mtcontext *ctx = data; + + if (epoll_wait(ctx->efd[0], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx->count, 1); + + return NULL; +} + +static void *waiter_entry1op(void *data) +{ + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext *ctx = data; + + pfd.fd = ctx->efd[0]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx->efd[0], &e, 1, 0) > 0) + __sync_fetch_and_or(&ctx->count, 1); + } + + return NULL; +} + +static void *waiter_entry2a(void *data) +{ + struct epoll_event events[2]; + struct epoll_mtcontext *ctx = data; + + if (epoll_wait(ctx->efd[0], events, 2, -1) > 0) + __sync_fetch_and_add(&ctx->count, 1); + + return NULL; +} + +static void *waiter_entry2ap(void *data) +{ + struct pollfd pfd; + struct epoll_event events[2]; + struct epoll_mtcontext *ctx = data; + + pfd.fd = ctx->efd[0]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx->efd[0], events, 2, 0) > 0) + __sync_fetch_and_add(&ctx->count, 1); + } + + return NULL; +} + +static void *emitter_entry1(void *data) +{ + struct epoll_mtcontext *ctx = data; + + usleep(100000); + write(ctx->sfd[1], "w", 1); + + kill_timeout(ctx); + + return NULL; +} + +static void *emitter_entry2(void *data) +{ + struct epoll_mtcontext *ctx = data; + + usleep(100000); + write(ctx->sfd[1], "w", 1); + write(ctx->sfd[3], "w", 1); + + kill_timeout(ctx); + + return NULL; +} + +/* + * t0 + * | (ew) + * e0 + * | (lt) + * s0 + */ +TEST(epoll1) +{ + int efd; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd, &e, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd, &e, 1, 0), 1); + + close(efd); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (ew) + * e0 + * | (et) + * s0 + */ +TEST(epoll2) +{ + int efd; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd, &e, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd, &e, 1, 0), 0); + + close(efd); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (ew) + * e0 + * (lt) / \ (lt) + * s0 s2 + */ +TEST(epoll3) +{ + int efd; + int sfd[4]; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 2); + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 2); + + close(efd); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 + * | (ew) + * e0 + * (et) / \ (et) + * s0 s2 + */ +TEST(epoll4) +{ + int efd; + int sfd[4]; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 2); + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 0); + + close(efd); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 + * | (p) + * e0 + * | (lt) + * s0 + */ +TEST(epoll5) +{ + int efd; + int sfd[2]; + struct pollfd pfd; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + pfd.fd = efd; + pfd.events = POLLIN; + ASSERT_EQ(poll(&pfd, 1, 0), 1); + ASSERT_EQ(epoll_wait(efd, &e, 1, 0), 1); + + pfd.fd = efd; + pfd.events = POLLIN; + ASSERT_EQ(poll(&pfd, 1, 0), 1); + ASSERT_EQ(epoll_wait(efd, &e, 1, 0), 1); + + close(efd); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (p) + * e0 + * | (et) + * s0 + */ +TEST(epoll6) +{ + int efd; + int sfd[2]; + struct pollfd pfd; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + pfd.fd = efd; + pfd.events = POLLIN; + ASSERT_EQ(poll(&pfd, 1, 0), 1); + ASSERT_EQ(epoll_wait(efd, &e, 1, 0), 1); + + pfd.fd = efd; + pfd.events = POLLIN; + ASSERT_EQ(poll(&pfd, 1, 0), 0); + ASSERT_EQ(epoll_wait(efd, &e, 1, 0), 0); + + close(efd); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (p) + * e0 + * (lt) / \ (lt) + * s0 s2 + */ + +TEST(epoll7) +{ + int efd; + int sfd[4]; + struct pollfd pfd; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + pfd.fd = efd; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 2); + + pfd.fd = efd; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 2); + + close(efd); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 + * | (p) + * e0 + * (et) / \ (et) + * s0 s2 + */ +TEST(epoll8) +{ + int efd; + int sfd[4]; + struct pollfd pfd; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + pfd.fd = efd; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 2); + + pfd.fd = efd; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 0); + EXPECT_EQ(epoll_wait(efd, events, 2, 0), 0); + + close(efd); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (lt) + * s0 + */ +TEST(epoll9) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (et) + * s0 + */ +TEST(epoll10) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 1); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * (lt) / \ (lt) + * s0 s2 + */ +TEST(epoll11) +{ + pthread_t emitter; + struct epoll_event events[2]; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[2], events), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry2a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], events, 2, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * (et) / \ (et) + * s0 s2 + */ +TEST(epoll12) +{ + pthread_t emitter; + struct epoll_event events[2]; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[2], events), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], events, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * | (lt) + * s0 + */ +TEST(epoll13) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * | (et) + * s0 + */ +TEST(epoll14) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 1); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * (lt) / \ (lt) + * s0 s2 + */ +TEST(epoll15) +{ + pthread_t emitter; + struct epoll_event events[2]; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[2], events), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry2ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], events, 2, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * (et) / \ (et) + * s0 s2 + */ +TEST(epoll16) +{ + pthread_t emitter; + struct epoll_event events[2]; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[2], events), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], events, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 + * | (ew) + * e0 + * | (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll17) +{ + int efd[2]; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (ew) + * e0 + * | (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll18) +{ + int efd[2]; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (ew) + * e0 + * | (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll19) +{ + int efd[2]; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 0); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (ew) + * e0 + * | (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll20) +{ + int efd[2]; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 0); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (p) + * e0 + * | (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll21) +{ + int efd[2]; + int sfd[2]; + struct pollfd pfd; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (p) + * e0 + * | (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll22) +{ + int efd[2]; + int sfd[2]; + struct pollfd pfd; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (p) + * e0 + * | (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll23) +{ + int efd[2]; + int sfd[2]; + struct pollfd pfd; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 0); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 0); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 + * | (p) + * e0 + * | (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll24) +{ + int efd[2]; + int sfd[2]; + struct pollfd pfd; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 0); + EXPECT_EQ(epoll_wait(efd[0], &e, 1, 0), 0); + + close(efd[0]); + close(efd[1]); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll25) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll26) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll27) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 1); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll28) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 1); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * | (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll29) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * | (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll30) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * | (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll31) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 1); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * | (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll32) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 1); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (ew) + * | e0 + * \ / (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll33) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (ew) + * | e0 + * \ / (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll34) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1o, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx.count, 2); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (ew) + * | e0 + * \ / (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll35) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (ew) + * | e0 + * \ / (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll36) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1o, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx.count, 2); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (ew) + * | e0 + * \ / (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll37) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + pfd.fd = ctx.efd[1]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[1], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx.count, 1); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (ew) + * | e0 + * \ / (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll38) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1o, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + pfd.fd = ctx.efd[1]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[1], &e, 1, 0) > 0) + __sync_fetch_and_or(&ctx.count, 2); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (ew) + * | e0 + * \ / (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll39) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + pfd.fd = ctx.efd[1]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[1], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx.count, 1); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (ew) + * | e0 + * \ / (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll40) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1o, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + pfd.fd = ctx.efd[1]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[1], &e, 1, 0) > 0) + __sync_fetch_and_or(&ctx.count, 2); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (p) + * | e0 + * \ / (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll41) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (p) + * | e0 + * \ / (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll42) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1op, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx.count, 2); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (p) + * | e0 + * \ / (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll43) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (ew) | | (p) + * | e0 + * \ / (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll44) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1op, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx.count, 2); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (p) + * | e0 + * \ / (lt) + * e1 + * | (lt) + * s0 + */ +TEST(epoll45) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + pfd.fd = ctx.efd[1]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[1], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx.count, 1); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (p) + * | e0 + * \ / (lt) + * e1 + * | (et) + * s0 + */ +TEST(epoll46) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1op, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx.count, 2); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (p) + * | e0 + * \ / (et) + * e1 + * | (lt) + * s0 + */ +TEST(epoll47) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + pfd.fd = ctx.efd[1]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[1], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx.count, 1); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 t1 + * (p) | | (p) + * | e0 + * \ / (et) + * e1 + * | (et) + * s0 + */ +TEST(epoll48) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1op, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry1, &ctx), 0); + + if (epoll_wait(ctx.efd[1], &e, 1, -1) > 0) + __sync_fetch_and_or(&ctx.count, 2); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_TRUE((ctx.count == 2) || (ctx.count == 3)); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +/* + * t0 + * | (ew) + * e0 + * (lt) / \ (lt) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll49) +{ + int efd[3]; + int sfd[4]; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + efd[2] = epoll_create(1); + ASSERT_GE(efd[2], 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[2], EPOLL_CTL_ADD, sfd[2], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 2); + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 2); + + close(efd[0]); + close(efd[1]); + close(efd[2]); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 + * | (ew) + * e0 + * (et) / \ (et) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll50) +{ + int efd[3]; + int sfd[4]; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + efd[2] = epoll_create(1); + ASSERT_GE(efd[2], 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[2], EPOLL_CTL_ADD, sfd[2], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 2); + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 0); + + close(efd[0]); + close(efd[1]); + close(efd[2]); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 + * | (p) + * e0 + * (lt) / \ (lt) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll51) +{ + int efd[3]; + int sfd[4]; + struct pollfd pfd; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + efd[2] = epoll_create(1); + ASSERT_GE(efd[2], 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[2], EPOLL_CTL_ADD, sfd[2], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 2); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 2); + + close(efd[0]); + close(efd[1]); + close(efd[2]); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 + * | (p) + * e0 + * (et) / \ (et) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll52) +{ + int efd[3]; + int sfd[4]; + struct pollfd pfd; + struct epoll_event events[2]; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &sfd[2]), 0); + + efd[0] = epoll_create(1); + ASSERT_GE(efd[0], 0); + + efd[1] = epoll_create(1); + ASSERT_GE(efd[1], 0); + + efd[2] = epoll_create(1); + ASSERT_GE(efd[2], 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[1], EPOLL_CTL_ADD, sfd[0], events), 0); + + events[0].events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd[2], EPOLL_CTL_ADD, sfd[2], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[1], events), 0); + + events[0].events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(efd[0], EPOLL_CTL_ADD, efd[2], events), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + ASSERT_EQ(write(sfd[3], "w", 1), 1); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 1); + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 2); + + pfd.fd = efd[0]; + pfd.events = POLLIN; + EXPECT_EQ(poll(&pfd, 1, 0), 0); + EXPECT_EQ(epoll_wait(efd[0], events, 2, 0), 0); + + close(efd[0]); + close(efd[1]); + close(efd[2]); + close(sfd[0]); + close(sfd[1]); + close(sfd[2]); + close(sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * (lt) / \ (lt) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll53) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + ctx.efd[2] = epoll_create(1); + ASSERT_GE(ctx.efd[2], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[2], EPOLL_CTL_ADD, ctx.sfd[2], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[2], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.efd[2]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * (et) / \ (et) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll54) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + ctx.efd[2] = epoll_create(1); + ASSERT_GE(ctx.efd[2], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[2], EPOLL_CTL_ADD, ctx.sfd[2], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[2], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.efd[2]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * (lt) / \ (lt) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll55) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + ctx.efd[2] = epoll_create(1); + ASSERT_GE(ctx.efd[2], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[2], EPOLL_CTL_ADD, ctx.sfd[2], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[2], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.efd[2]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (ew) \ / (p) + * e0 + * (et) / \ (et) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll56) +{ + pthread_t emitter; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + ctx.efd[2] = epoll_create(1); + ASSERT_GE(ctx.efd[2], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[2], EPOLL_CTL_ADD, ctx.sfd[2], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[2], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + if (epoll_wait(ctx.efd[0], &e, 1, -1) > 0) + __sync_fetch_and_add(&ctx.count, 1); + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.efd[2]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (p) \ / (p) + * e0 + * (lt) / \ (lt) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll57) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + ctx.efd[2] = epoll_create(1); + ASSERT_GE(ctx.efd[2], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[2], EPOLL_CTL_ADD, ctx.sfd[2], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[2], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + pfd.fd = ctx.efd[0]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[0], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx.count, 1); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.efd[2]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +/* + * t0 t1 + * (p) \ / (p) + * e0 + * (et) / \ (et) + * e1 e2 + * (lt) | | (lt) + * s0 s2 + */ +TEST(epoll58) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[0]), 0); + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, &ctx.sfd[2]), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + ctx.efd[1] = epoll_create(1); + ASSERT_GE(ctx.efd[1], 0); + + ctx.efd[2] = epoll_create(1); + ASSERT_GE(ctx.efd[2], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[1], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[2], EPOLL_CTL_ADD, ctx.sfd[2], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &e), 0); + + e.events = EPOLLIN | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[2], &e), 0); + + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&ctx.waiter, NULL, waiter_entry1ap, &ctx), 0); + ASSERT_EQ(pthread_create(&emitter, NULL, emitter_entry2, &ctx), 0); + + pfd.fd = ctx.efd[0]; + pfd.events = POLLIN; + if (poll(&pfd, 1, -1) > 0) { + if (epoll_wait(ctx.efd[0], &e, 1, 0) > 0) + __sync_fetch_and_add(&ctx.count, 1); + } + + ASSERT_EQ(pthread_join(ctx.waiter, NULL), 0); + EXPECT_EQ(ctx.count, 2); + + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); + close(ctx.efd[2]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); + close(ctx.sfd[2]); + close(ctx.sfd[3]); +} + +static void *epoll59_thread(void *ctx_) +{ + struct epoll_mtcontext *ctx = ctx_; + struct epoll_event e; + int i; + + for (i = 0; i < 100000; i++) { + while (ctx->count == 0) + ; + + e.events = EPOLLIN | EPOLLERR | EPOLLET; + epoll_ctl(ctx->efd[0], EPOLL_CTL_MOD, ctx->sfd[0], &e); + ctx->count = 0; + } + + return NULL; +} + +/* + * t0 + * (p) \ + * e0 + * (et) / + * e0 + * + * Based on https://bugzilla.kernel.org/show_bug.cgi?id=205933 + */ +TEST(epoll59) +{ + pthread_t emitter; + struct pollfd pfd; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + int i, ret; + + signal(SIGUSR1, signal_handler); + + ctx.efd[0] = epoll_create1(0); + ASSERT_GE(ctx.efd[0], 0); + + ctx.sfd[0] = eventfd(1, 0); + ASSERT_GE(ctx.sfd[0], 0); + + e.events = EPOLLIN | EPOLLERR | EPOLLET; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + ASSERT_EQ(pthread_create(&emitter, NULL, epoll59_thread, &ctx), 0); + + for (i = 0; i < 100000; i++) { + ret = epoll_wait(ctx.efd[0], &e, 1, 1000); + ASSERT_GT(ret, 0); + + while (ctx.count != 0) + ; + ctx.count = 1; + } + if (pthread_tryjoin_np(emitter, NULL) < 0) { + pthread_kill(emitter, SIGUSR1); + pthread_join(emitter, NULL); + } + close(ctx.efd[0]); + close(ctx.sfd[0]); +} + +enum { + EPOLL60_EVENTS_NR = 10, +}; + +struct epoll60_ctx { + volatile int stopped; + int ready; + int waiters; + int epfd; + int evfd[EPOLL60_EVENTS_NR]; +}; + +static void *epoll60_wait_thread(void *ctx_) +{ + struct epoll60_ctx *ctx = ctx_; + struct epoll_event e; + sigset_t sigmask; + uint64_t v; + int ret; + + /* Block SIGUSR1 */ + sigemptyset(&sigmask); + sigaddset(&sigmask, SIGUSR1); + sigprocmask(SIG_SETMASK, &sigmask, NULL); + + /* Prepare empty mask for epoll_pwait() */ + sigemptyset(&sigmask); + + while (!ctx->stopped) { + /* Mark we are ready */ + __atomic_fetch_add(&ctx->ready, 1, __ATOMIC_ACQUIRE); + + /* Start when all are ready */ + while (__atomic_load_n(&ctx->ready, __ATOMIC_ACQUIRE) && + !ctx->stopped); + + /* Account this waiter */ + __atomic_fetch_add(&ctx->waiters, 1, __ATOMIC_ACQUIRE); + + ret = epoll_pwait(ctx->epfd, &e, 1, 2000, &sigmask); + if (ret != 1) { + /* We expect only signal delivery on stop */ + assert(ret < 0 && errno == EINTR && "Lost wakeup!\n"); + assert(ctx->stopped); + break; + } + + ret = read(e.data.fd, &v, sizeof(v)); + /* Since we are on ET mode, thus each thread gets its own fd. */ + assert(ret == sizeof(v)); + + __atomic_fetch_sub(&ctx->waiters, 1, __ATOMIC_RELEASE); + } + + return NULL; +} + +static inline unsigned long long msecs(void) +{ + struct timespec ts; + unsigned long long msecs; + + clock_gettime(CLOCK_REALTIME, &ts); + msecs = ts.tv_sec * 1000ull; + msecs += ts.tv_nsec / 1000000ull; + + return msecs; +} + +static inline int count_waiters(struct epoll60_ctx *ctx) +{ + return __atomic_load_n(&ctx->waiters, __ATOMIC_ACQUIRE); +} + +TEST(epoll60) +{ + struct epoll60_ctx ctx = { 0 }; + pthread_t waiters[ARRAY_SIZE(ctx.evfd)]; + struct epoll_event e; + int i, n, ret; + + signal(SIGUSR1, signal_handler); + + ctx.epfd = epoll_create1(0); + ASSERT_GE(ctx.epfd, 0); + + /* Create event fds */ + for (i = 0; i < ARRAY_SIZE(ctx.evfd); i++) { + ctx.evfd[i] = eventfd(0, EFD_NONBLOCK); + ASSERT_GE(ctx.evfd[i], 0); + + e.events = EPOLLIN | EPOLLET; + e.data.fd = ctx.evfd[i]; + ASSERT_EQ(epoll_ctl(ctx.epfd, EPOLL_CTL_ADD, ctx.evfd[i], &e), 0); + } + + /* Create waiter threads */ + for (i = 0; i < ARRAY_SIZE(waiters); i++) + ASSERT_EQ(pthread_create(&waiters[i], NULL, + epoll60_wait_thread, &ctx), 0); + + for (i = 0; i < 300; i++) { + uint64_t v = 1, ms; + + /* Wait for all to be ready */ + while (__atomic_load_n(&ctx.ready, __ATOMIC_ACQUIRE) != + ARRAY_SIZE(ctx.evfd)) + ; + + /* Steady, go */ + __atomic_fetch_sub(&ctx.ready, ARRAY_SIZE(ctx.evfd), + __ATOMIC_ACQUIRE); + + /* Wait all have gone to kernel */ + while (count_waiters(&ctx) != ARRAY_SIZE(ctx.evfd)) + ; + + /* 1ms should be enough to schedule away */ + usleep(1000); + + /* Quickly signal all handles at once */ + for (n = 0; n < ARRAY_SIZE(ctx.evfd); n++) { + ret = write(ctx.evfd[n], &v, sizeof(v)); + ASSERT_EQ(ret, sizeof(v)); + } + + /* Busy loop for 1s and wait for all waiters to wake up */ + ms = msecs(); + while (count_waiters(&ctx) && msecs() < ms + 1000) + ; + + ASSERT_EQ(count_waiters(&ctx), 0); + } + ctx.stopped = 1; + /* Stop waiters */ + for (i = 0; i < ARRAY_SIZE(waiters); i++) + ret = pthread_kill(waiters[i], SIGUSR1); + for (i = 0; i < ARRAY_SIZE(waiters); i++) + pthread_join(waiters[i], NULL); + + for (i = 0; i < ARRAY_SIZE(waiters); i++) + close(ctx.evfd[i]); + close(ctx.epfd); +} + +struct epoll61_ctx { + int epfd; + int evfd; +}; + +static void *epoll61_write_eventfd(void *ctx_) +{ + struct epoll61_ctx *ctx = ctx_; + int64_t l = 1; + + usleep(10950); + write(ctx->evfd, &l, sizeof(l)); + return NULL; +} + +static void *epoll61_epoll_with_timeout(void *ctx_) +{ + struct epoll61_ctx *ctx = ctx_; + struct epoll_event events[1]; + int n; + + n = epoll_wait(ctx->epfd, events, 1, 11); + /* + * If epoll returned the eventfd, write on the eventfd to wake up the + * blocking poller. + */ + if (n == 1) { + int64_t l = 1; + + write(ctx->evfd, &l, sizeof(l)); + } + return NULL; +} + +static void *epoll61_blocking_epoll(void *ctx_) +{ + struct epoll61_ctx *ctx = ctx_; + struct epoll_event events[1]; + + epoll_wait(ctx->epfd, events, 1, -1); + return NULL; +} + +TEST(epoll61) +{ + struct epoll61_ctx ctx; + struct epoll_event ev; + int i, r; + + ctx.epfd = epoll_create1(0); + ASSERT_GE(ctx.epfd, 0); + ctx.evfd = eventfd(0, EFD_NONBLOCK); + ASSERT_GE(ctx.evfd, 0); + + ev.events = EPOLLIN | EPOLLET | EPOLLERR | EPOLLHUP; + ev.data.ptr = NULL; + r = epoll_ctl(ctx.epfd, EPOLL_CTL_ADD, ctx.evfd, &ev); + ASSERT_EQ(r, 0); + + /* + * We are testing a race. Repeat the test case 1000 times to make it + * more likely to fail in case of a bug. + */ + for (i = 0; i < 1000; i++) { + pthread_t threads[3]; + int n; + + /* + * Start 3 threads: + * Thread 1 sleeps for 10.9ms and writes to the evenfd. + * Thread 2 calls epoll with a timeout of 11ms. + * Thread 3 calls epoll with a timeout of -1. + * + * The eventfd write by Thread 1 should either wakeup Thread 2 + * or Thread 3. If it wakes up Thread 2, Thread 2 writes on the + * eventfd to wake up Thread 3. + * + * If no events are missed, all three threads should eventually + * be joinable. + */ + ASSERT_EQ(pthread_create(&threads[0], NULL, + epoll61_write_eventfd, &ctx), 0); + ASSERT_EQ(pthread_create(&threads[1], NULL, + epoll61_epoll_with_timeout, &ctx), 0); + ASSERT_EQ(pthread_create(&threads[2], NULL, + epoll61_blocking_epoll, &ctx), 0); + + for (n = 0; n < ARRAY_SIZE(threads); ++n) + ASSERT_EQ(pthread_join(threads[n], NULL), 0); + } + + close(ctx.epfd); + close(ctx.evfd); +} + +/* Equivalent to basic test epoll1, but exercising epoll_pwait2. */ +TEST(epoll62) +{ + int efd; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], &e), 0); + + ASSERT_EQ(write(sfd[1], "w", 1), 1); + + EXPECT_EQ(sys_epoll_pwait2(efd, &e, 1, NULL, NULL, 0), 1); + EXPECT_EQ(sys_epoll_pwait2(efd, &e, 1, NULL, NULL, 0), 1); + + close(efd); + close(sfd[0]); + close(sfd[1]); +} + +/* Epoll_pwait2 basic timeout test. */ +TEST(epoll63) +{ + const int cfg_delay_ms = 10; + unsigned long long tdiff; + struct __kernel_timespec ts; + int efd; + int sfd[2]; + struct epoll_event e; + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sfd), 0); + + efd = epoll_create(1); + ASSERT_GE(efd, 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(efd, EPOLL_CTL_ADD, sfd[0], &e), 0); + + ts.tv_sec = 0; + ts.tv_nsec = cfg_delay_ms * 1000 * 1000; + + tdiff = msecs(); + EXPECT_EQ(sys_epoll_pwait2(efd, &e, 1, &ts, NULL, 0), 0); + tdiff = msecs() - tdiff; + + EXPECT_GE(tdiff, cfg_delay_ms); + + close(efd); + close(sfd[0]); + close(sfd[1]); +} + +/* + * t0 t1 + * (ew) \ / (ew) + * e0 + * | (lt) + * s0 + */ +TEST(epoll64) +{ + pthread_t waiter[2]; + struct epoll_event e; + struct epoll_mtcontext ctx = { 0 }; + + signal(SIGUSR1, signal_handler); + + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, ctx.sfd), 0); + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + + e.events = EPOLLIN; + ASSERT_EQ(epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.sfd[0], &e), 0); + + /* + * main will act as the emitter once both waiter threads are + * blocked and expects to both be awoken upon the ready event. + */ + ctx.main = pthread_self(); + ASSERT_EQ(pthread_create(&waiter[0], NULL, waiter_entry1a, &ctx), 0); + ASSERT_EQ(pthread_create(&waiter[1], NULL, waiter_entry1a, &ctx), 0); + + usleep(100000); + ASSERT_EQ(write(ctx.sfd[1], "w", 1), 1); + + ASSERT_EQ(pthread_join(waiter[0], NULL), 0); + ASSERT_EQ(pthread_join(waiter[1], NULL), 0); + + EXPECT_EQ(ctx.count, 2); + + close(ctx.efd[0]); + close(ctx.sfd[0]); + close(ctx.sfd[1]); +} + +static void *epoll65_wait(void *ctx_) +{ + struct epoll_mtcontext *ctx = ctx_; + struct epoll_event event; + + for (int i = 0; i < 100000; ++i) { + if (!epoll_wait(ctx->efd[0], &event, 1, 0)) + return (void *)ENODATA; + } + + return (void *)0; +} + +TEST(epoll65) +{ + struct epoll_mtcontext ctx; + struct epoll_event event; + int64_t dummy_data = 99; + pthread_t threads[64]; + uintptr_t ret; + int i, err; + + ctx.efd[0] = epoll_create(1); + ASSERT_GE(ctx.efd[0], 0); + ctx.efd[1] = eventfd(0, 0); + ASSERT_GE(ctx.efd[1], 0); + + event.events = EPOLLIN; + err = epoll_ctl(ctx.efd[0], EPOLL_CTL_ADD, ctx.efd[1], &event); + ASSERT_EQ(err, 0); + + write(ctx.efd[1], &dummy_data, sizeof(dummy_data)); + + for (i = 0; i < ARRAY_SIZE(threads); ++i) + ASSERT_EQ(pthread_create(&threads[i], NULL, epoll65_wait, &ctx), 0); + + for (i = 0; i < ARRAY_SIZE(threads); ++i) { + ASSERT_EQ(pthread_join(threads[i], (void **)&ret), 0); + ASSERT_EQ(ret, 0); + } + + close(ctx.efd[0]); + close(ctx.efd[1]); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/eventfd/.gitignore b/tools/testing/selftests/filesystems/eventfd/.gitignore new file mode 100644 index 000000000..483faf59f --- /dev/null +++ b/tools/testing/selftests/filesystems/eventfd/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +eventfd_test diff --git a/tools/testing/selftests/filesystems/eventfd/Makefile b/tools/testing/selftests/filesystems/eventfd/Makefile new file mode 100644 index 000000000..0a8e3910d --- /dev/null +++ b/tools/testing/selftests/filesystems/eventfd/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += $(KHDR_INCLUDES) +LDLIBS += -lpthread +TEST_GEN_PROGS := eventfd_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/eventfd/eventfd_test.c b/tools/testing/selftests/filesystems/eventfd/eventfd_test.c new file mode 100644 index 000000000..1b48f2671 --- /dev/null +++ b/tools/testing/selftests/filesystems/eventfd/eventfd_test.c @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-2.0 + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <asm/unistd.h> +#include <linux/time_types.h> +#include <unistd.h> +#include <assert.h> +#include <signal.h> +#include <pthread.h> +#include <sys/epoll.h> +#include <sys/eventfd.h> +#include "kselftest_harness.h" + +#define EVENTFD_TEST_ITERATIONS 100000UL + +struct error { + int code; + char msg[512]; +}; + +static int error_set(struct error *err, int code, const char *fmt, ...) +{ + va_list args; + int r; + + if (code == 0 || !err || err->code != 0) + return code; + + err->code = code; + va_start(args, fmt); + r = vsnprintf(err->msg, sizeof(err->msg), fmt, args); + assert((size_t)r < sizeof(err->msg)); + va_end(args); + + return code; +} + +static inline int sys_eventfd2(unsigned int count, int flags) +{ + return syscall(__NR_eventfd2, count, flags); +} + +TEST(eventfd_check_flag_rdwr) +{ + int fd, flags; + + fd = sys_eventfd2(0, 0); + ASSERT_GE(fd, 0); + + flags = fcntl(fd, F_GETFL); + // The kernel automatically adds the O_RDWR flag. + EXPECT_EQ(flags, O_RDWR); + + close(fd); +} + +TEST(eventfd_check_flag_cloexec) +{ + int fd, flags; + + fd = sys_eventfd2(0, EFD_CLOEXEC); + ASSERT_GE(fd, 0); + + flags = fcntl(fd, F_GETFD); + ASSERT_GT(flags, -1); + EXPECT_EQ(flags, FD_CLOEXEC); + + close(fd); +} + +TEST(eventfd_check_flag_nonblock) +{ + int fd, flags; + + fd = sys_eventfd2(0, EFD_NONBLOCK); + ASSERT_GE(fd, 0); + + flags = fcntl(fd, F_GETFL); + ASSERT_GT(flags, -1); + EXPECT_EQ(flags & EFD_NONBLOCK, EFD_NONBLOCK); + EXPECT_EQ(flags & O_RDWR, O_RDWR); + + close(fd); +} + +TEST(eventfd_check_flag_cloexec_and_nonblock) +{ + int fd, flags; + + fd = sys_eventfd2(0, EFD_CLOEXEC|EFD_NONBLOCK); + ASSERT_GE(fd, 0); + + flags = fcntl(fd, F_GETFL); + ASSERT_GT(flags, -1); + EXPECT_EQ(flags & EFD_NONBLOCK, EFD_NONBLOCK); + EXPECT_EQ(flags & O_RDWR, O_RDWR); + + flags = fcntl(fd, F_GETFD); + ASSERT_GT(flags, -1); + EXPECT_EQ(flags, FD_CLOEXEC); + + close(fd); +} + +static inline void trim_newline(char *str) +{ + char *pos = strrchr(str, '\n'); + + if (pos) + *pos = '\0'; +} + +static int verify_fdinfo(int fd, struct error *err, const char *prefix, + size_t prefix_len, const char *expect, ...) +{ + char buffer[512] = {0, }; + char path[512] = {0, }; + va_list args; + FILE *f; + char *line = NULL; + size_t n = 0; + int found = 0; + int r; + + va_start(args, expect); + r = vsnprintf(buffer, sizeof(buffer), expect, args); + assert((size_t)r < sizeof(buffer)); + va_end(args); + + snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd); + f = fopen(path, "re"); + if (!f) + return error_set(err, -1, "fdinfo open failed for %d", fd); + + while (getline(&line, &n, f) != -1) { + char *val; + + if (strncmp(line, prefix, prefix_len)) + continue; + + found = 1; + + val = line + prefix_len; + r = strcmp(val, buffer); + if (r != 0) { + trim_newline(line); + trim_newline(buffer); + error_set(err, -1, "%s '%s' != '%s'", + prefix, val, buffer); + } + break; + } + + free(line); + fclose(f); + + if (found == 0) + return error_set(err, -1, "%s not found for fd %d", + prefix, fd); + + return 0; +} + +TEST(eventfd_check_flag_semaphore) +{ + struct error err = {0}; + int fd, ret; + + fd = sys_eventfd2(0, EFD_SEMAPHORE); + ASSERT_GE(fd, 0); + + ret = fcntl(fd, F_GETFL); + ASSERT_GT(ret, -1); + EXPECT_EQ(ret & O_RDWR, O_RDWR); + + // The semaphore could only be obtained from fdinfo. + ret = verify_fdinfo(fd, &err, "eventfd-semaphore: ", 19, "1\n"); + if (ret != 0) + ksft_print_msg("eventfd semaphore flag check failed: %s\n", err.msg); + EXPECT_EQ(ret, 0); + + close(fd); +} + +/* + * A write(2) fails with the error EINVAL if the size of the supplied buffer + * is less than 8 bytes, or if an attempt is made to write the value + * 0xffffffffffffffff. + */ +TEST(eventfd_check_write) +{ + uint64_t value = 1; + ssize_t size; + int fd; + + fd = sys_eventfd2(0, 0); + ASSERT_GE(fd, 0); + + size = write(fd, &value, sizeof(int)); + EXPECT_EQ(size, -1); + EXPECT_EQ(errno, EINVAL); + + size = write(fd, &value, sizeof(value)); + EXPECT_EQ(size, sizeof(value)); + + value = (uint64_t)-1; + size = write(fd, &value, sizeof(value)); + EXPECT_EQ(size, -1); + EXPECT_EQ(errno, EINVAL); + + close(fd); +} + +/* + * A read(2) fails with the error EINVAL if the size of the supplied buffer is + * less than 8 bytes. + */ +TEST(eventfd_check_read) +{ + uint64_t value; + ssize_t size; + int fd; + + fd = sys_eventfd2(1, 0); + ASSERT_GE(fd, 0); + + size = read(fd, &value, sizeof(int)); + EXPECT_EQ(size, -1); + EXPECT_EQ(errno, EINVAL); + + size = read(fd, &value, sizeof(value)); + EXPECT_EQ(size, sizeof(value)); + EXPECT_EQ(value, 1); + + close(fd); +} + + +/* + * If EFD_SEMAPHORE was not specified and the eventfd counter has a nonzero + * value, then a read(2) returns 8 bytes containing that value, and the + * counter's value is reset to zero. + * If the eventfd counter is zero at the time of the call to read(2), then the + * call fails with the error EAGAIN if the file descriptor has been made nonblocking. + */ +TEST(eventfd_check_read_with_nonsemaphore) +{ + uint64_t value; + ssize_t size; + int fd; + int i; + + fd = sys_eventfd2(0, EFD_NONBLOCK); + ASSERT_GE(fd, 0); + + value = 1; + for (i = 0; i < EVENTFD_TEST_ITERATIONS; i++) { + size = write(fd, &value, sizeof(value)); + EXPECT_EQ(size, sizeof(value)); + } + + size = read(fd, &value, sizeof(value)); + EXPECT_EQ(size, sizeof(uint64_t)); + EXPECT_EQ(value, EVENTFD_TEST_ITERATIONS); + + size = read(fd, &value, sizeof(value)); + EXPECT_EQ(size, -1); + EXPECT_EQ(errno, EAGAIN); + + close(fd); +} + +/* + * If EFD_SEMAPHORE was specified and the eventfd counter has a nonzero value, + * then a read(2) returns 8 bytes containing the value 1, and the counter's + * value is decremented by 1. + * If the eventfd counter is zero at the time of the call to read(2), then the + * call fails with the error EAGAIN if the file descriptor has been made nonblocking. + */ +TEST(eventfd_check_read_with_semaphore) +{ + uint64_t value; + ssize_t size; + int fd; + int i; + + fd = sys_eventfd2(0, EFD_SEMAPHORE|EFD_NONBLOCK); + ASSERT_GE(fd, 0); + + value = 1; + for (i = 0; i < EVENTFD_TEST_ITERATIONS; i++) { + size = write(fd, &value, sizeof(value)); + EXPECT_EQ(size, sizeof(value)); + } + + for (i = 0; i < EVENTFD_TEST_ITERATIONS; i++) { + size = read(fd, &value, sizeof(value)); + EXPECT_EQ(size, sizeof(value)); + EXPECT_EQ(value, 1); + } + + size = read(fd, &value, sizeof(value)); + EXPECT_EQ(size, -1); + EXPECT_EQ(errno, EAGAIN); + + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/fat/.gitignore b/tools/testing/selftests/filesystems/fat/.gitignore new file mode 100644 index 000000000..b89920ed8 --- /dev/null +++ b/tools/testing/selftests/filesystems/fat/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +rename_exchange diff --git a/tools/testing/selftests/filesystems/fat/Makefile b/tools/testing/selftests/filesystems/fat/Makefile new file mode 100644 index 000000000..902033f6e --- /dev/null +++ b/tools/testing/selftests/filesystems/fat/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 + +TEST_PROGS := run_fat_tests.sh +TEST_GEN_PROGS_EXTENDED := rename_exchange +CFLAGS += -O2 -g -Wall $(KHDR_INCLUDES) + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/fat/config b/tools/testing/selftests/filesystems/fat/config new file mode 100644 index 000000000..6cf95e787 --- /dev/null +++ b/tools/testing/selftests/filesystems/fat/config @@ -0,0 +1,2 @@ +CONFIG_BLK_DEV_LOOP=y +CONFIG_VFAT_FS=y diff --git a/tools/testing/selftests/filesystems/fat/rename_exchange.c b/tools/testing/selftests/filesystems/fat/rename_exchange.c new file mode 100644 index 000000000..e488ad354 --- /dev/null +++ b/tools/testing/selftests/filesystems/fat/rename_exchange.c @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Program that atomically exchanges two paths using + * the renameat2() system call RENAME_EXCHANGE flag. + * + * Copyright 2022 Red Hat Inc. + * Author: Javier Martinez Canillas <javierm@redhat.com> + */ + +#define _GNU_SOURCE +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> + +void print_usage(const char *program) +{ + printf("Usage: %s [oldpath] [newpath]\n", program); + printf("Atomically exchange oldpath and newpath\n"); +} + +int main(int argc, char *argv[]) +{ + int ret; + + if (argc != 3) { + print_usage(argv[0]); + exit(EXIT_FAILURE); + } + + ret = renameat2(AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE); + if (ret) { + perror("rename exchange failed"); + exit(EXIT_FAILURE); + } + + exit(EXIT_SUCCESS); +} diff --git a/tools/testing/selftests/filesystems/fat/run_fat_tests.sh b/tools/testing/selftests/filesystems/fat/run_fat_tests.sh new file mode 100755 index 000000000..d61264d47 --- /dev/null +++ b/tools/testing/selftests/filesystems/fat/run_fat_tests.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Run filesystem operations tests on an 1 MiB disk image that is formatted with +# a vfat filesystem and mounted in a temporary directory using a loop device. +# +# Copyright 2022 Red Hat Inc. +# Author: Javier Martinez Canillas <javierm@redhat.com> + +set -e +set -u +set -o pipefail + +BASE_DIR="$(dirname $0)" +TMP_DIR="$(mktemp -d /tmp/fat_tests_tmp.XXXXXX)" +IMG_PATH="${TMP_DIR}/fat.img" +MNT_PATH="${TMP_DIR}/mnt" + +cleanup() +{ + mountpoint -q "${MNT_PATH}" && unmount_image + rm -rf "${TMP_DIR}" +} +trap cleanup SIGINT SIGTERM EXIT + +create_loopback() +{ + touch "${IMG_PATH}" + chattr +C "${IMG_PATH}" >/dev/null 2>&1 || true + + truncate -s 1M "${IMG_PATH}" + mkfs.vfat "${IMG_PATH}" >/dev/null 2>&1 +} + +mount_image() +{ + mkdir -p "${MNT_PATH}" + sudo mount -o loop "${IMG_PATH}" "${MNT_PATH}" +} + +rename_exchange_test() +{ + local rename_exchange="${BASE_DIR}/rename_exchange" + local old_path="${MNT_PATH}/old_file" + local new_path="${MNT_PATH}/new_file" + + echo old | sudo tee "${old_path}" >/dev/null 2>&1 + echo new | sudo tee "${new_path}" >/dev/null 2>&1 + sudo "${rename_exchange}" "${old_path}" "${new_path}" >/dev/null 2>&1 + sudo sync -f "${MNT_PATH}" + grep new "${old_path}" >/dev/null 2>&1 + grep old "${new_path}" >/dev/null 2>&1 +} + +rename_exchange_subdir_test() +{ + local rename_exchange="${BASE_DIR}/rename_exchange" + local dir_path="${MNT_PATH}/subdir" + local old_path="${MNT_PATH}/old_file" + local new_path="${dir_path}/new_file" + + sudo mkdir -p "${dir_path}" + echo old | sudo tee "${old_path}" >/dev/null 2>&1 + echo new | sudo tee "${new_path}" >/dev/null 2>&1 + sudo "${rename_exchange}" "${old_path}" "${new_path}" >/dev/null 2>&1 + sudo sync -f "${MNT_PATH}" + grep new "${old_path}" >/dev/null 2>&1 + grep old "${new_path}" >/dev/null 2>&1 +} + +unmount_image() +{ + sudo umount "${MNT_PATH}" &> /dev/null +} + +create_loopback +mount_image +rename_exchange_test +rename_exchange_subdir_test +unmount_image + +exit 0 diff --git a/tools/testing/selftests/filesystems/fclog.c b/tools/testing/selftests/filesystems/fclog.c new file mode 100644 index 000000000..551c4a0f3 --- /dev/null +++ b/tools/testing/selftests/filesystems/fclog.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: Aleksa Sarai <cyphar@cyphar.com> + * Copyright (C) 2025 SUSE LLC. + */ + +#include <assert.h> +#include <errno.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> + +#include "kselftest_harness.h" + +#define ASSERT_ERRNO(expected, _t, seen) \ + __EXPECT(expected, #expected, \ + ({__typeof__(seen) _tmp_seen = (seen); \ + _tmp_seen >= 0 ? _tmp_seen : -errno; }), #seen, _t, 1) + +#define ASSERT_ERRNO_EQ(expected, seen) \ + ASSERT_ERRNO(expected, ==, seen) + +#define ASSERT_SUCCESS(seen) \ + ASSERT_ERRNO(0, <=, seen) + +FIXTURE(ns) +{ + int host_mntns; +}; + +FIXTURE_SETUP(ns) +{ + /* Stash the old mntns. */ + self->host_mntns = open("/proc/self/ns/mnt", O_RDONLY|O_CLOEXEC); + ASSERT_SUCCESS(self->host_mntns); + + /* Create a new mount namespace and make it private. */ + ASSERT_SUCCESS(unshare(CLONE_NEWNS)); + ASSERT_SUCCESS(mount(NULL, "/", NULL, MS_PRIVATE|MS_REC, NULL)); +} + +FIXTURE_TEARDOWN(ns) +{ + ASSERT_SUCCESS(setns(self->host_mntns, CLONE_NEWNS)); + ASSERT_SUCCESS(close(self->host_mntns)); +} + +TEST_F(ns, fscontext_log_enodata) +{ + int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC); + ASSERT_SUCCESS(fsfd); + + /* A brand new fscontext has no log entries. */ + char buf[128] = {}; + for (int i = 0; i < 16; i++) + ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf))); + + ASSERT_SUCCESS(close(fsfd)); +} + +TEST_F(ns, fscontext_log_errorfc) +{ + int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC); + ASSERT_SUCCESS(fsfd); + + ASSERT_ERRNO_EQ(-EINVAL, fsconfig(fsfd, FSCONFIG_SET_STRING, "invalid-arg", "123", 0)); + + char buf[128] = {}; + ASSERT_SUCCESS(read(fsfd, buf, sizeof(buf))); + EXPECT_STREQ("e tmpfs: Unknown parameter 'invalid-arg'\n", buf); + + /* The message has been consumed. */ + ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf))); + ASSERT_SUCCESS(close(fsfd)); +} + +TEST_F(ns, fscontext_log_errorfc_after_fsmount) +{ + int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC); + ASSERT_SUCCESS(fsfd); + + ASSERT_ERRNO_EQ(-EINVAL, fsconfig(fsfd, FSCONFIG_SET_STRING, "invalid-arg", "123", 0)); + + ASSERT_SUCCESS(fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)); + int mfd = fsmount(fsfd, FSMOUNT_CLOEXEC, MOUNT_ATTR_NOEXEC | MOUNT_ATTR_NOSUID); + ASSERT_SUCCESS(mfd); + ASSERT_SUCCESS(move_mount(mfd, "", AT_FDCWD, "/tmp", MOVE_MOUNT_F_EMPTY_PATH)); + + /* + * The fscontext log should still contain data even after + * FSCONFIG_CMD_CREATE and fsmount(). + */ + char buf[128] = {}; + ASSERT_SUCCESS(read(fsfd, buf, sizeof(buf))); + EXPECT_STREQ("e tmpfs: Unknown parameter 'invalid-arg'\n", buf); + + /* The message has been consumed. */ + ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf))); + ASSERT_SUCCESS(close(fsfd)); +} + +TEST_F(ns, fscontext_log_emsgsize) +{ + int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC); + ASSERT_SUCCESS(fsfd); + + ASSERT_ERRNO_EQ(-EINVAL, fsconfig(fsfd, FSCONFIG_SET_STRING, "invalid-arg", "123", 0)); + + char buf[128] = {}; + /* + * Attempting to read a message with too small a buffer should not + * result in the message getting consumed. + */ + ASSERT_ERRNO_EQ(-EMSGSIZE, read(fsfd, buf, 0)); + ASSERT_ERRNO_EQ(-EMSGSIZE, read(fsfd, buf, 1)); + for (int i = 0; i < 16; i++) + ASSERT_ERRNO_EQ(-EMSGSIZE, read(fsfd, buf, 16)); + + ASSERT_SUCCESS(read(fsfd, buf, sizeof(buf))); + EXPECT_STREQ("e tmpfs: Unknown parameter 'invalid-arg'\n", buf); + + /* The message has been consumed. */ + ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf))); + ASSERT_SUCCESS(close(fsfd)); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/file_stressor.c b/tools/testing/selftests/filesystems/file_stressor.c new file mode 100644 index 000000000..141badd67 --- /dev/null +++ b/tools/testing/selftests/filesystems/file_stressor.c @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ + +#include <fcntl.h> +#include <limits.h> +#include <pthread.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <unistd.h> + +#include "kselftest_harness.h" + +#include <linux/types.h> +#include <linux/mount.h> +#include <sys/syscall.h> + +static inline int sys_fsopen(const char *fsname, unsigned int flags) +{ + return syscall(__NR_fsopen, fsname, flags); +} + +static inline int sys_fsconfig(int fd, unsigned int cmd, const char *key, + const char *value, int aux) +{ + return syscall(__NR_fsconfig, fd, cmd, key, value, aux); +} + +static inline int sys_fsmount(int fd, unsigned int flags, + unsigned int attr_flags) +{ + return syscall(__NR_fsmount, fd, flags, attr_flags); +} + +#ifndef MOVE_MOUNT_F_EMPTY_PATH +#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 /* Empty from path permitted */ +#endif + +static inline int sys_move_mount(int from_dfd, const char *from_pathname, + int to_dfd, const char *to_pathname, + unsigned int flags) +{ + return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd, + to_pathname, flags); +} + +FIXTURE(file_stressor) { + int fd_tmpfs; + int nr_procs; + int max_fds; + pid_t *pids_openers; + pid_t *pids_getdents; + int *fd_proc_pid; +}; + +FIXTURE_SETUP(file_stressor) +{ + int fd_context; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + ASSERT_EQ(mkdir("/slab_typesafe_by_rcu", 0755), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + self->fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(self->fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + ASSERT_EQ(sys_move_mount(self->fd_tmpfs, "", -EBADF, "/slab_typesafe_by_rcu", MOVE_MOUNT_F_EMPTY_PATH), 0); + + self->nr_procs = sysconf(_SC_NPROCESSORS_ONLN); + self->pids_openers = malloc(sizeof(pid_t) * self->nr_procs); + ASSERT_NE(self->pids_openers, NULL); + self->pids_getdents = malloc(sizeof(pid_t) * self->nr_procs); + ASSERT_NE(self->pids_getdents, NULL); + self->fd_proc_pid = malloc(sizeof(int) * self->nr_procs); + ASSERT_NE(self->fd_proc_pid, NULL); + self->max_fds = 500; +} + +FIXTURE_TEARDOWN(file_stressor) +{ + for (int i = 0; i < self->nr_procs; i++) { + int wstatus; + pid_t pid; + + pid = waitpid(self->pids_openers[i], &wstatus, 0); + ASSERT_EQ(pid, self->pids_openers[i]); + ASSERT_TRUE(!WIFEXITED(wstatus) || !WIFSIGNALED(wstatus)); + + pid = waitpid(self->pids_getdents[i], &wstatus, 0); + ASSERT_EQ(pid, self->pids_getdents[i]); + ASSERT_TRUE(!WIFEXITED(wstatus) || !WIFSIGNALED(wstatus)); + } + free(self->pids_openers); + free(self->pids_getdents); + ASSERT_EQ(close(self->fd_tmpfs), 0); + + umount2("/slab_typesafe_by_rcu", 0); + ASSERT_EQ(rmdir("/slab_typesafe_by_rcu"), 0); +} + +TEST_F_TIMEOUT(file_stressor, slab_typesafe_by_rcu, 900 * 2) +{ + for (int i = 0; i < self->nr_procs; i++) { + pid_t pid_self; + + self->pids_openers[i] = fork(); + ASSERT_GE(self->pids_openers[i], 0); + + if (self->pids_openers[i] != 0) + continue; + + self->pids_openers[i] = getpid(); + for (;;) { + for (int i = 0; i < self->max_fds; i++) { + char path[PATH_MAX]; + int fd; + + sprintf(path, "/slab_typesafe_by_rcu/file-%d-%d", self->pids_openers[i], i); + fd = open(path, O_CREAT | O_RDONLY | O_CLOEXEC, 0644); + if (fd < 0) + continue; + } + + close_range(3, ~0U, 0); + } + + exit(0); + } + + for (int i = 0; i < self->nr_procs; i++) { + char path[PATH_MAX]; + + sprintf(path, "/proc/%d/fd/", self->pids_openers[i]); + self->fd_proc_pid[i] = open(path, O_DIRECTORY | O_RDONLY | O_CLOEXEC); + ASSERT_GE(self->fd_proc_pid[i], 0); + } + + for (int i = 0; i < self->nr_procs; i++) { + self->pids_getdents[i] = fork(); + ASSERT_GE(self->pids_getdents[i], 0); + + if (self->pids_getdents[i] != 0) + continue; + + self->pids_getdents[i] = getpid(); + for (;;) { + char ents[1024]; + ssize_t nr_read; + + /* + * Concurrently read /proc/<pid>/fd/ which roughly does: + * + * f = fget_task_next(p, &fd); + * if (!f) + * break; + * data.mode = f->f_mode; + * fput(f); + * + * Which means that it'll try to get a reference to a + * file in another task's file descriptor table. + * + * Under heavy file load it is increasingly likely that + * the other task will manage to close @file and @file + * is being recycled due to SLAB_TYPEAFE_BY_RCU + * concurrently. This will trigger various warnings in + * the file reference counting code. + */ + do { + nr_read = syscall(SYS_getdents64, self->fd_proc_pid[i], ents, sizeof(ents)); + } while (nr_read >= 0); + + lseek(self->fd_proc_pid[i], 0, SEEK_SET); + } + + exit(0); + } + + ASSERT_EQ(clock_nanosleep(CLOCK_MONOTONIC, 0, &(struct timespec){ .tv_sec = 900 /* 15 min */ }, NULL), 0); + + for (int i = 0; i < self->nr_procs; i++) { + kill(self->pids_openers[i], SIGKILL); + kill(self->pids_getdents[i], SIGKILL); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/fsmount_ns/.gitignore b/tools/testing/selftests/filesystems/fsmount_ns/.gitignore new file mode 100644 index 000000000..f1ecf6c6e --- /dev/null +++ b/tools/testing/selftests/filesystems/fsmount_ns/.gitignore @@ -0,0 +1 @@ +fsmount_ns_test diff --git a/tools/testing/selftests/filesystems/fsmount_ns/Makefile b/tools/testing/selftests/filesystems/fsmount_ns/Makefile new file mode 100644 index 000000000..d9647efc0 --- /dev/null +++ b/tools/testing/selftests/filesystems/fsmount_ns/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0 +TEST_GEN_PROGS := fsmount_ns_test + +CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +LDLIBS := -lcap + +include ../../lib.mk + +$(OUTPUT)/fsmount_ns_test: fsmount_ns_test.c ../utils.c + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) diff --git a/tools/testing/selftests/filesystems/fsmount_ns/fsmount_ns_test.c b/tools/testing/selftests/filesystems/fsmount_ns/fsmount_ns_test.c new file mode 100644 index 000000000..b70b3051e --- /dev/null +++ b/tools/testing/selftests/filesystems/fsmount_ns/fsmount_ns_test.c @@ -0,0 +1,1135 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Christian Brauner <brauner@kernel.org> + * + * Test for FSMOUNT_NAMESPACE flag. + * + * Test that fsmount() with FSMOUNT_NAMESPACE creates a new mount + * namespace containing the specified mount. + */ +#define _GNU_SOURCE + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <linux/nsfs.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../wrappers.h" +#include "../statmount/statmount.h" +#include "../utils.h" +#include "../../kselftest_harness.h" + +#ifndef FSMOUNT_NAMESPACE +#define FSMOUNT_NAMESPACE 0x00000002 +#endif + +#ifndef FSMOUNT_CLOEXEC +#define FSMOUNT_CLOEXEC 0x00000001 +#endif + +#ifndef FSCONFIG_CMD_CREATE +#define FSCONFIG_CMD_CREATE 6 +#endif + +static int get_mnt_ns_id(int fd, uint64_t *mnt_ns_id) +{ + if (ioctl(fd, NS_GET_MNTNS_ID, mnt_ns_id) < 0) + return -errno; + return 0; +} + +static int get_mnt_ns_id_from_path(const char *path, uint64_t *mnt_ns_id) +{ + int fd, ret; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -errno; + + ret = get_mnt_ns_id(fd, mnt_ns_id); + close(fd); + return ret; +} + +static void log_mount(struct __test_metadata *_metadata, struct statmount *sm) +{ + const char *fs_type = ""; + const char *mnt_root = ""; + const char *mnt_point = ""; + + if (sm->mask & STATMOUNT_FS_TYPE) + fs_type = sm->str + sm->fs_type; + if (sm->mask & STATMOUNT_MNT_ROOT) + mnt_root = sm->str + sm->mnt_root; + if (sm->mask & STATMOUNT_MNT_POINT) + mnt_point = sm->str + sm->mnt_point; + + TH_LOG(" mnt_id: %llu, parent_id: %llu, fs_type: %s, root: %s, point: %s", + (unsigned long long)sm->mnt_id, + (unsigned long long)sm->mnt_parent_id, + fs_type, mnt_root, mnt_point); +} + +static void dump_mounts(struct __test_metadata *_metadata, uint64_t mnt_ns_id) +{ + uint64_t list[256]; + ssize_t nr_mounts; + + nr_mounts = listmount(LSMT_ROOT, mnt_ns_id, 0, list, 256, 0); + if (nr_mounts < 0) { + TH_LOG("listmount failed: %s", strerror(errno)); + return; + } + + TH_LOG("Mount namespace %llu contains %zd mount(s):", + (unsigned long long)mnt_ns_id, nr_mounts); + + for (ssize_t i = 0; i < nr_mounts; i++) { + struct statmount *sm; + + sm = statmount_alloc(list[i], mnt_ns_id, + STATMOUNT_MNT_BASIC | + STATMOUNT_FS_TYPE | + STATMOUNT_MNT_ROOT | + STATMOUNT_MNT_POINT, 0); + if (!sm) { + TH_LOG(" [%zd] mnt_id %llu: statmount failed: %s", + i, (unsigned long long)list[i], strerror(errno)); + continue; + } + + log_mount(_metadata, sm); + free(sm); + } +} + +static int create_tmpfs_fd(void) +{ + int fs_fd, ret; + + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs_fd < 0) + return -errno; + + ret = sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0); + if (ret < 0) { + close(fs_fd); + return -errno; + } + + return fs_fd; +} + +FIXTURE(fsmount_ns) +{ + int fd; + int fs_fd; + uint64_t current_ns_id; +}; + +FIXTURE_VARIANT(fsmount_ns) +{ + const char *fstype; + unsigned int flags; + bool expect_success; + bool expect_different_ns; + int min_mounts; +}; + +FIXTURE_VARIANT_ADD(fsmount_ns, basic_tmpfs) +{ + .fstype = "tmpfs", + .flags = FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(fsmount_ns, cloexec_only) +{ + .fstype = "tmpfs", + .flags = FSMOUNT_CLOEXEC, + .expect_success = true, + .expect_different_ns = false, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(fsmount_ns, namespace_only) +{ + .fstype = "tmpfs", + .flags = FSMOUNT_NAMESPACE, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_SETUP(fsmount_ns) +{ + int ret; + + self->fd = -1; + self->fs_fd = -1; + + /* Check if fsopen syscall is supported */ + ret = sys_fsopen("tmpfs", 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "fsopen() syscall not supported"); + if (ret >= 0) + close(ret); + + /* Check if statmount/listmount are supported */ + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "statmount() syscall not supported"); + + /* Get current mount namespace ID for comparison */ + ret = get_mnt_ns_id_from_path("/proc/self/ns/mnt", &self->current_ns_id); + if (ret < 0) + SKIP(return, "Failed to get current mount namespace ID"); +} + +FIXTURE_TEARDOWN(fsmount_ns) +{ + if (self->fd >= 0) + close(self->fd); + if (self->fs_fd >= 0) + close(self->fs_fd); +} + +TEST_F(fsmount_ns, create_namespace) +{ + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, variant->flags, 0); + + if (!variant->expect_success) { + ASSERT_LT(self->fd, 0); + return; + } + + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + if (variant->expect_different_ns) { + /* Verify we can get the namespace ID from the fd */ + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + /* Verify it's a different namespace */ + ASSERT_NE(new_ns_id, self->current_ns_id); + + /* List mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 0) { + TH_LOG("%m - listmount failed"); + } + + /* Verify minimum expected mounts */ + ASSERT_GE(nr_mounts, variant->min_mounts); + TH_LOG("Namespace contains %zd mounts", nr_mounts); + } +} + +TEST_F(fsmount_ns, setns_into_namespace) +{ + uint64_t new_ns_id; + pid_t pid; + int status; + int ret; + + /* Only test with FSMOUNT_NAMESPACE flag */ + if (!(variant->flags & FSMOUNT_NAMESPACE)) + SKIP(return, "setns test only for FSMOUNT_NAMESPACE case"); + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, variant->flags, 0); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + /* Get namespace ID and dump all mounts */ + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + dump_mounts(_metadata, new_ns_id); + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + /* Child: try to enter the namespace */ + if (setns(self->fd, CLONE_NEWNS) < 0) + _exit(1); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(fsmount_ns, verify_mount_properties) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + /* Only test with basic FSMOUNT_NAMESPACE flags */ + if (variant->flags != (FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC)) + SKIP(return, "mount properties test only for basic case"); + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + /* Get info about the root mount */ + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + TH_LOG("Root mount id: %llu, parent: %llu", + (unsigned long long)sm.mnt_id, + (unsigned long long)sm.mnt_parent_id); +} + +TEST_F(fsmount_ns, verify_tmpfs_type) +{ + struct statmount *sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + const char *fs_type; + int ret; + + /* Only test with basic FSMOUNT_NAMESPACE flags */ + if (variant->flags != (FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC)) + SKIP(return, "fs type test only for basic case"); + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + sm = statmount_alloc(list[0], new_ns_id, STATMOUNT_FS_TYPE, 0); + ASSERT_NE(sm, NULL); + + fs_type = sm->str + sm->fs_type; + ASSERT_STREQ(fs_type, "tmpfs"); + + free(sm); +} + +FIXTURE(fsmount_ns_caps) +{ + bool has_caps; +}; + +FIXTURE_SETUP(fsmount_ns_caps) +{ + int ret; + + /* Check if fsopen syscall is supported */ + ret = sys_fsopen("tmpfs", 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "fsopen() syscall not supported"); + if (ret >= 0) + close(ret); + + self->has_caps = (geteuid() == 0); +} + +FIXTURE_TEARDOWN(fsmount_ns_caps) +{ +} + +TEST_F(fsmount_ns_caps, requires_cap_sys_admin) +{ + pid_t pid; + int status; + int fs_fd; + + /* + * Prepare the configured filesystem fd as root before forking. + * fsopen() requires CAP_SYS_ADMIN in the mount namespace's + * user_ns, which won't be available after enter_userns(). + */ + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + ASSERT_GE(fs_fd, 0); + + ASSERT_EQ(sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + int fd; + + /* Child: drop privileges using utils.h helper */ + if (enter_userns() != 0) + _exit(2); + + /* Drop all caps using utils.h helper */ + if (caps_down() == 0) + _exit(3); + + fd = sys_fsmount(fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + close(fs_fd); + + if (fd >= 0) { + close(fd); + /* Should have failed without caps */ + _exit(1); + } + + if (errno == EPERM) + _exit(0); + + /* EINVAL means FSMOUNT_NAMESPACE not supported */ + if (errno == EINVAL) + _exit(6); + + /* Unexpected error */ + _exit(7); + } + + close(fs_fd); + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Expected: EPERM without caps */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("FSMOUNT_NAMESPACE succeeded without caps"); + break; + case 2: + SKIP(return, "enter_userns failed"); + break; + case 3: + SKIP(return, "caps_down failed"); + break; + case 6: + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +FIXTURE(fsmount_ns_userns) +{ + int fd; + int fs_fd; +}; + +FIXTURE_SETUP(fsmount_ns_userns) +{ + int ret; + + self->fd = -1; + self->fs_fd = -1; + + /* Check if fsopen syscall is supported */ + ret = sys_fsopen("tmpfs", 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "fsopen() syscall not supported"); + if (ret >= 0) + close(ret); + + /* Check if statmount/listmount are supported */ + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "statmount() syscall not supported"); +} + +FIXTURE_TEARDOWN(fsmount_ns_userns) +{ + if (self->fd >= 0) + close(self->fd); + if (self->fs_fd >= 0) + close(self->fs_fd); +} + +TEST_F(fsmount_ns_userns, create_in_userns) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fs_fd, fd; + + /* Create new user namespace (also creates mount namespace) */ + if (setup_userns() != 0) + _exit(2); + + /* Now we have CAP_SYS_ADMIN in the user namespace */ + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs_fd < 0) + _exit(3); + + if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) { + close(fs_fd); + _exit(4); + } + + fd = sys_fsmount(fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + close(fs_fd); + + if (fd < 0) { + if (errno == EINVAL) + _exit(6); /* FSMOUNT_NAMESPACE not supported */ + _exit(1); + } + + /* Verify we can get the namespace ID */ + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(7); + + /* Verify we can list mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + if (nr_mounts < 0) + _exit(8); + + /* Should have at least 1 mount (the tmpfs) */ + if (nr_mounts < 1) + _exit(9); + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Success */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("fsmount(FSMOUNT_NAMESPACE) failed in userns"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 3: + SKIP(return, "fsopen failed in userns"); + break; + case 4: + SKIP(return, "fsconfig CMD_CREATE failed in userns"); + break; + case 6: + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 8: + ASSERT_FALSE(true) TH_LOG("listmount failed in new namespace"); + break; + case 9: + ASSERT_FALSE(true) TH_LOG("New namespace has no mounts"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(fsmount_ns_userns, setns_in_userns) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + int fs_fd, fd; + pid_t inner_pid; + int inner_status; + + /* Create new user namespace */ + if (setup_userns() != 0) + _exit(2); + + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs_fd < 0) + _exit(3); + + if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) { + close(fs_fd); + _exit(4); + } + + fd = sys_fsmount(fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + close(fs_fd); + + if (fd < 0) { + if (errno == EINVAL) + _exit(6); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(7); + + /* Fork again to test setns into the new namespace */ + inner_pid = fork(); + if (inner_pid < 0) + _exit(10); + + if (inner_pid == 0) { + /* Inner child: enter the new namespace */ + if (setns(fd, CLONE_NEWNS) < 0) + _exit(1); + _exit(0); + } + + if (waitpid(inner_pid, &inner_status, 0) != inner_pid) + _exit(11); + + if (!WIFEXITED(inner_status) || WEXITSTATUS(inner_status) != 0) + _exit(12); + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Success */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("fsmount or setns failed in userns"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 3: + SKIP(return, "fsopen failed in userns"); + break; + case 4: + SKIP(return, "fsconfig CMD_CREATE failed in userns"); + break; + case 6: + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 10: + ASSERT_FALSE(true) TH_LOG("Inner fork failed"); + break; + case 11: + ASSERT_FALSE(true) TH_LOG("Inner waitpid failed"); + break; + case 12: + ASSERT_FALSE(true) TH_LOG("setns into new namespace failed"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(fsmount_ns_userns, umount_fails_einval) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fs_fd, fd; + ssize_t i; + + /* Create new user namespace */ + if (setup_userns() != 0) + _exit(2); + + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs_fd < 0) + _exit(3); + + if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) { + close(fs_fd); + _exit(4); + } + + fd = sys_fsmount(fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + close(fs_fd); + + if (fd < 0) { + if (errno == EINVAL) + _exit(6); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(7); + + /* Get all mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, LISTMOUNT_REVERSE); + if (nr_mounts < 0) + _exit(13); + + if (nr_mounts < 1) + _exit(14); + + /* Enter the new namespace */ + if (setns(fd, CLONE_NEWNS) < 0) + _exit(8); + + for (i = 0; i < nr_mounts; i++) { + struct statmount *sm; + const char *mnt_point; + + sm = statmount_alloc(list[i], new_ns_id, + STATMOUNT_MNT_POINT, 0); + if (!sm) + _exit(15); + + mnt_point = sm->str + sm->mnt_point; + + if (umount2(mnt_point, MNT_DETACH) == 0) { + free(sm); + _exit(9); + } + + if (errno != EINVAL) { + /* Wrong error */ + free(sm); + _exit(10); + } + + free(sm); + } + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + break; + case 1: + ASSERT_FALSE(true) TH_LOG("fsmount(FSMOUNT_NAMESPACE) failed"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 3: + SKIP(return, "fsopen failed in userns"); + break; + case 4: + SKIP(return, "fsconfig CMD_CREATE failed in userns"); + break; + case 6: + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 8: + ASSERT_FALSE(true) TH_LOG("setns into new namespace failed"); + break; + case 9: + ASSERT_FALSE(true) TH_LOG("umount succeeded but should have failed with EINVAL"); + break; + case 10: + ASSERT_FALSE(true) TH_LOG("umount failed with wrong error (expected EINVAL)"); + break; + case 13: + ASSERT_FALSE(true) TH_LOG("listmount failed"); + break; + case 14: + ASSERT_FALSE(true) TH_LOG("No mounts in new namespace"); + break; + case 15: + ASSERT_FALSE(true) TH_LOG("statmount_alloc failed"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(fsmount_ns_userns, umount_succeeds) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fs_fd, fd; + ssize_t i; + + if (unshare(CLONE_NEWNS)) + _exit(1); + + if (sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) != 0) + _exit(1); + + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs_fd < 0) + _exit(3); + + if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) { + close(fs_fd); + _exit(4); + } + + fd = sys_fsmount(fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, 0); + close(fs_fd); + + if (fd < 0) { + if (errno == EINVAL) + _exit(6); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(7); + + /* Get all mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, LISTMOUNT_REVERSE); + if (nr_mounts < 0) + _exit(13); + + if (nr_mounts < 1) + _exit(14); + + /* Enter the new namespace */ + if (setns(fd, CLONE_NEWNS) < 0) + _exit(8); + + for (i = 0; i < nr_mounts; i++) { + struct statmount *sm; + const char *mnt_point; + + sm = statmount_alloc(list[i], new_ns_id, + STATMOUNT_MNT_POINT, 0); + if (!sm) + _exit(15); + + mnt_point = sm->str + sm->mnt_point; + + if (umount2(mnt_point, MNT_DETACH) != 0) { + free(sm); + _exit(9); + } + + free(sm); + } + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + break; + case 1: + ASSERT_FALSE(true) TH_LOG("fsmount(FSMOUNT_NAMESPACE) failed or unshare failed"); + break; + case 3: + SKIP(return, "fsopen failed"); + break; + case 4: + SKIP(return, "fsconfig CMD_CREATE failed"); + break; + case 6: + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 8: + ASSERT_FALSE(true) TH_LOG("setns into new namespace failed"); + break; + case 9: + ASSERT_FALSE(true) TH_LOG("umount failed but should have succeeded"); + break; + case 13: + ASSERT_FALSE(true) TH_LOG("listmount failed"); + break; + case 14: + ASSERT_FALSE(true) TH_LOG("No mounts in new namespace"); + break; + case 15: + ASSERT_FALSE(true) TH_LOG("statmount_alloc failed"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +FIXTURE(fsmount_ns_mount_attrs) +{ + int fd; + int fs_fd; +}; + +FIXTURE_SETUP(fsmount_ns_mount_attrs) +{ + int ret; + + self->fd = -1; + self->fs_fd = -1; + + /* Check if fsopen syscall is supported */ + ret = sys_fsopen("tmpfs", 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "fsopen() syscall not supported"); + if (ret >= 0) + close(ret); + + /* Check if statmount/listmount are supported */ + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "statmount() syscall not supported"); +} + +FIXTURE_TEARDOWN(fsmount_ns_mount_attrs) +{ + if (self->fd >= 0) + close(self->fd); + if (self->fs_fd >= 0) + close(self->fs_fd); +} + +TEST_F(fsmount_ns_mount_attrs, readonly) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, + MOUNT_ATTR_RDONLY); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + /* Verify the mount is read-only */ + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_RDONLY); +} + +TEST_F(fsmount_ns_mount_attrs, noexec) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, + MOUNT_ATTR_NOEXEC); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + /* Verify the mount is noexec */ + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_NOEXEC); +} + +TEST_F(fsmount_ns_mount_attrs, nosuid) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, + MOUNT_ATTR_NOSUID); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + /* Verify the mount is nosuid */ + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_NOSUID); +} + +TEST_F(fsmount_ns_mount_attrs, noatime) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, + MOUNT_ATTR_NOATIME); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + /* Verify the mount is noatime */ + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_NOATIME); +} + +TEST_F(fsmount_ns_mount_attrs, combined) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fs_fd = create_tmpfs_fd(); + ASSERT_GE(self->fs_fd, 0); + + self->fd = sys_fsmount(self->fs_fd, FSMOUNT_NAMESPACE | FSMOUNT_CLOEXEC, + MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOEXEC | + MOUNT_ATTR_NOSUID | MOUNT_ATTR_NOATIME); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "FSMOUNT_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + /* Verify all attributes are set */ + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_RDONLY); + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_NOEXEC); + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_NOSUID); + ASSERT_TRUE(sm.mnt_attr & MOUNT_ATTR_NOATIME); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore new file mode 100644 index 000000000..3e72e742d --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +fuse_mnt +fusectl_test diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile new file mode 100644 index 000000000..612aad69a --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) + +TEST_GEN_PROGS := fusectl_test +TEST_GEN_FILES := fuse_mnt + +include ../../lib.mk + +VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null) +ifeq ($(VAR_CFLAGS),) +VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse +endif + +VAR_LDLIBS := $(shell pkg-config fuse --libs 2>/dev/null) +ifeq ($(VAR_LDLIBS),) +VAR_LDLIBS := -lfuse -pthread +endif + +$(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS) +$(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS) diff --git a/tools/testing/selftests/filesystems/fuse/fuse_mnt.c b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c new file mode 100644 index 000000000..d12b17f30 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_mnt.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * fusectl test file-system + * Creates a simple FUSE filesystem with a single read-write file (/test) + */ + +#define FUSE_USE_VERSION 26 + +#include <fuse.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <unistd.h> + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +static char *content; +static size_t content_size = 0; +static const char test_path[] = "/test"; + +static int test_getattr(const char *path, struct stat *st) +{ + memset(st, 0, sizeof(*st)); + + if (!strcmp(path, "/")) { + st->st_mode = S_IFDIR | 0755; + st->st_nlink = 2; + return 0; + } + + if (!strcmp(path, test_path)) { + st->st_mode = S_IFREG | 0664; + st->st_nlink = 1; + st->st_size = content_size; + return 0; + } + + return -ENOENT; +} + +static int test_readdir(const char *path, void *buf, fuse_fill_dir_t filler, + off_t offset, struct fuse_file_info *fi) +{ + if (strcmp(path, "/")) + return -ENOENT; + + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + filler(buf, test_path + 1, NULL, 0); + + return 0; +} + +static int test_open(const char *path, struct fuse_file_info *fi) +{ + if (strcmp(path, test_path)) + return -ENOENT; + + return 0; +} + +static int test_read(const char *path, char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) +{ + if (strcmp(path, test_path) != 0) + return -ENOENT; + + if (!content || content_size == 0) + return 0; + + if (offset >= content_size) + return 0; + + if (offset + size > content_size) + size = content_size - offset; + + memcpy(buf, content + offset, size); + + return size; +} + +static int test_write(const char *path, const char *buf, size_t size, + off_t offset, struct fuse_file_info *fi) +{ + size_t new_size; + + if (strcmp(path, test_path) != 0) + return -ENOENT; + + if(offset > content_size) + return -EINVAL; + + new_size = MAX(offset + size, content_size); + + if (new_size > content_size) + content = realloc(content, new_size); + + content_size = new_size; + + if (!content) + return -ENOMEM; + + memcpy(content + offset, buf, size); + + return size; +} + +static int test_truncate(const char *path, off_t size) +{ + if (strcmp(path, test_path) != 0) + return -ENOENT; + + if (size == 0) { + free(content); + content = NULL; + content_size = 0; + return 0; + } + + content = realloc(content, size); + + if (!content) + return -ENOMEM; + + if (size > content_size) + memset(content + content_size, 0, size - content_size); + + content_size = size; + return 0; +} + +static struct fuse_operations memfd_ops = { + .getattr = test_getattr, + .readdir = test_readdir, + .open = test_open, + .read = test_read, + .write = test_write, + .truncate = test_truncate, +}; + +int main(int argc, char *argv[]) +{ + return fuse_main(argc, argv, &memfd_ops, NULL); +} diff --git a/tools/testing/selftests/filesystems/fuse/fusectl_test.c b/tools/testing/selftests/filesystems/fuse/fusectl_test.c new file mode 100644 index 000000000..0d1d012c3 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fusectl_test.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2025 Chen Linxuan <chenlinxuan@uniontech.com> + +#define _GNU_SOURCE + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <dirent.h> +#include <sched.h> +#include <linux/limits.h> + +#include "kselftest_harness.h" + +#define FUSECTL_MOUNTPOINT "/sys/fs/fuse/connections" +#define FUSE_MOUNTPOINT "/tmp/fuse_mnt_XXXXXX" +#define FUSE_DEVICE "/dev/fuse" +#define FUSECTL_TEST_VALUE "1" + +static void write_file(struct __test_metadata *const _metadata, + const char *path, const char *val) +{ + int fd = open(path, O_WRONLY); + size_t len = strlen(val); + + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, val, len), len); + ASSERT_EQ(close(fd), 0); +} + +FIXTURE(fusectl){ + char fuse_mountpoint[sizeof(FUSE_MOUNTPOINT)]; + int connection; +}; + +FIXTURE_SETUP(fusectl) +{ + const char *fuse_mnt_prog = "./fuse_mnt"; + int status, pid; + struct stat statbuf; + uid_t uid = getuid(); + gid_t gid = getgid(); + char buf[32]; + + /* Setup userns */ + ASSERT_EQ(unshare(CLONE_NEWNS|CLONE_NEWUSER), 0); + sprintf(buf, "0 %d 1", uid); + write_file(_metadata, "/proc/self/uid_map", buf); + write_file(_metadata, "/proc/self/setgroups", "deny"); + sprintf(buf, "0 %d 1", gid); + write_file(_metadata, "/proc/self/gid_map", buf); + ASSERT_EQ(mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL), 0); + + strcpy(self->fuse_mountpoint, FUSE_MOUNTPOINT); + + if (!mkdtemp(self->fuse_mountpoint)) + SKIP(return, + "Failed to create FUSE mountpoint %s", + strerror(errno)); + + if (access(FUSECTL_MOUNTPOINT, F_OK)) + SKIP(return, + "FUSE control filesystem not mounted"); + + pid = fork(); + if (pid < 0) + SKIP(return, + "Failed to fork FUSE daemon process: %s", + strerror(errno)); + + if (pid == 0) { + execlp(fuse_mnt_prog, fuse_mnt_prog, self->fuse_mountpoint, NULL); + exit(errno); + } + + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + SKIP(return, + "Failed to start FUSE daemon %s", + strerror(WEXITSTATUS(status))); + } + + if (stat(self->fuse_mountpoint, &statbuf)) + SKIP(return, + "Failed to stat FUSE mountpoint %s", + strerror(errno)); + + self->connection = statbuf.st_dev; +} + +FIXTURE_TEARDOWN(fusectl) +{ + umount2(self->fuse_mountpoint, MNT_DETACH); + rmdir(self->fuse_mountpoint); +} + +TEST_F(fusectl, abort) +{ + char path_buf[PATH_MAX]; + int abort_fd, test_fd, ret; + + sprintf(path_buf, "/sys/fs/fuse/connections/%d/abort", self->connection); + + ASSERT_EQ(0, access(path_buf, F_OK)); + + abort_fd = open(path_buf, O_WRONLY); + ASSERT_GE(abort_fd, 0); + + sprintf(path_buf, "%s/test", self->fuse_mountpoint); + + test_fd = open(path_buf, O_RDWR); + ASSERT_GE(test_fd, 0); + + ret = read(test_fd, path_buf, sizeof(path_buf)); + ASSERT_EQ(ret, 0); + + ret = write(test_fd, "test", sizeof("test")); + ASSERT_EQ(ret, sizeof("test")); + + ret = lseek(test_fd, 0, SEEK_SET); + ASSERT_GE(ret, 0); + + ret = write(abort_fd, FUSECTL_TEST_VALUE, sizeof(FUSECTL_TEST_VALUE)); + ASSERT_GT(ret, 0); + + close(abort_fd); + + ret = read(test_fd, path_buf, sizeof(path_buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOTCONN); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/kernfs_test.c b/tools/testing/selftests/filesystems/kernfs_test.c new file mode 100644 index 000000000..84c2b910a --- /dev/null +++ b/tools/testing/selftests/filesystems/kernfs_test.c @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ + +#include <fcntl.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/xattr.h> + +#include "kselftest_harness.h" +#include "wrappers.h" + +TEST(kernfs_listxattr) +{ + int fd; + + /* Read-only file that can never have any extended attributes set. */ + fd = open("/sys/kernel/warn_count", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(flistxattr(fd, NULL, 0), 0); + EXPECT_EQ(close(fd), 0); +} + +TEST(kernfs_getxattr) +{ + int fd; + char buf[1]; + + /* Read-only file that can never have any extended attributes set. */ + fd = open("/sys/kernel/warn_count", O_RDONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_LT(fgetxattr(fd, "user.foo", buf, sizeof(buf)), 0); + ASSERT_EQ(errno, ENODATA); + EXPECT_EQ(close(fd), 0); +} + +TEST_HARNESS_MAIN + diff --git a/tools/testing/selftests/filesystems/mount-notify/.gitignore b/tools/testing/selftests/filesystems/mount-notify/.gitignore new file mode 100644 index 000000000..124339ea7 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount-notify/.gitignore @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +/*_test +/*_test_ns diff --git a/tools/testing/selftests/filesystems/mount-notify/Makefile b/tools/testing/selftests/filesystems/mount-notify/Makefile new file mode 100644 index 000000000..836a4eb7b --- /dev/null +++ b/tools/testing/selftests/filesystems/mount-notify/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +LDLIBS += -lcap + +TEST_GEN_PROGS := mount-notify_test mount-notify_test_ns + +include ../../lib.mk + +$(OUTPUT)/mount-notify_test: ../utils.c +$(OUTPUT)/mount-notify_test_ns: ../utils.c diff --git a/tools/testing/selftests/filesystems/mount-notify/mount-notify_test.c b/tools/testing/selftests/filesystems/mount-notify/mount-notify_test.c new file mode 100644 index 000000000..6381af6a4 --- /dev/null +++ b/tools/testing/selftests/filesystems/mount-notify/mount-notify_test.c @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2025 Miklos Szeredi <miklos@szeredi.hu> + +#define _GNU_SOURCE + +// Needed for linux/fanotify.h +typedef struct { + int val[2]; +} __kernel_fsid_t; +#define __kernel_fsid_t __kernel_fsid_t + +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <unistd.h> +#include <sys/syscall.h> +#include <sys/fanotify.h> + +#include "kselftest_harness.h" +#include "../statmount/statmount.h" +#include "../utils.h" + +static const char root_mntpoint_templ[] = "/tmp/mount-notify_test_root.XXXXXX"; + +static const int mark_cmds[] = { + FAN_MARK_ADD, + FAN_MARK_REMOVE, + FAN_MARK_FLUSH +}; + +#define NUM_FAN_FDS ARRAY_SIZE(mark_cmds) + +FIXTURE(fanotify) { + int fan_fd[NUM_FAN_FDS]; + char buf[256]; + unsigned int rem; + void *next; + char root_mntpoint[sizeof(root_mntpoint_templ)]; + int orig_root; + int ns_fd; + uint64_t root_id; +}; + +FIXTURE_SETUP(fanotify) +{ + int i, ret; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + + self->ns_fd = open("/proc/self/ns/mnt", O_RDONLY); + ASSERT_GE(self->ns_fd, 0); + + ASSERT_EQ(mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL), 0); + + strcpy(self->root_mntpoint, root_mntpoint_templ); + ASSERT_NE(mkdtemp(self->root_mntpoint), NULL); + + self->orig_root = open("/", O_PATH | O_CLOEXEC); + ASSERT_GE(self->orig_root, 0); + + ASSERT_EQ(mount("tmpfs", self->root_mntpoint, "tmpfs", 0, NULL), 0); + + ASSERT_EQ(chroot(self->root_mntpoint), 0); + + ASSERT_EQ(chdir("/"), 0); + + ASSERT_EQ(mkdir("a", 0700), 0); + + ASSERT_EQ(mkdir("b", 0700), 0); + + self->root_id = get_unique_mnt_id("/"); + ASSERT_NE(self->root_id, 0); + + for (i = 0; i < NUM_FAN_FDS; i++) { + self->fan_fd[i] = fanotify_init(FAN_REPORT_MNT | FAN_NONBLOCK, + 0); + ASSERT_GE(self->fan_fd[i], 0); + ret = fanotify_mark(self->fan_fd[i], FAN_MARK_ADD | + FAN_MARK_MNTNS, + FAN_MNT_ATTACH | FAN_MNT_DETACH, + self->ns_fd, NULL); + ASSERT_EQ(ret, 0); + // On fd[0] we do an extra ADD that changes nothing. + // On fd[1]/fd[2] we REMOVE/FLUSH which removes the mark. + ret = fanotify_mark(self->fan_fd[i], mark_cmds[i] | + FAN_MARK_MNTNS, + FAN_MNT_ATTACH | FAN_MNT_DETACH, + self->ns_fd, NULL); + ASSERT_EQ(ret, 0); + } + + self->rem = 0; +} + +FIXTURE_TEARDOWN(fanotify) +{ + int i; + + ASSERT_EQ(self->rem, 0); + for (i = 0; i < NUM_FAN_FDS; i++) + close(self->fan_fd[i]); + + ASSERT_EQ(fchdir(self->orig_root), 0); + + ASSERT_EQ(chroot("."), 0); + + EXPECT_EQ(umount2(self->root_mntpoint, MNT_DETACH), 0); + EXPECT_EQ(chdir(self->root_mntpoint), 0); + EXPECT_EQ(chdir("/"), 0); + EXPECT_EQ(rmdir(self->root_mntpoint), 0); +} + +static uint64_t expect_notify(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + uint64_t *mask) +{ + struct fanotify_event_metadata *meta; + struct fanotify_event_info_mnt *mnt; + unsigned int thislen; + + if (!self->rem) { + ssize_t len; + int i; + + for (i = NUM_FAN_FDS - 1; i >= 0; i--) { + len = read(self->fan_fd[i], self->buf, + sizeof(self->buf)); + if (i > 0) { + // Groups 1,2 should get EAGAIN + ASSERT_EQ(len, -1); + ASSERT_EQ(errno, EAGAIN); + } else { + // Group 0 should get events + ASSERT_GT(len, 0); + } + } + + self->rem = len; + self->next = (void *) self->buf; + } + + meta = self->next; + ASSERT_TRUE(FAN_EVENT_OK(meta, self->rem)); + + thislen = meta->event_len; + self->rem -= thislen; + self->next += thislen; + + *mask = meta->mask; + thislen -= sizeof(*meta); + + mnt = ((void *) meta) + meta->event_len - thislen; + + ASSERT_EQ(thislen, sizeof(*mnt)); + + return mnt->mnt_id; +} + +static void expect_notify_n(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + unsigned int n, uint64_t mask[], uint64_t mnts[]) +{ + unsigned int i; + + for (i = 0; i < n; i++) + mnts[i] = expect_notify(_metadata, self, &mask[i]); +} + +static uint64_t expect_notify_mask(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + uint64_t expect_mask) +{ + uint64_t mntid, mask; + + mntid = expect_notify(_metadata, self, &mask); + ASSERT_EQ(expect_mask, mask); + + return mntid; +} + + +static void expect_notify_mask_n(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + uint64_t mask, unsigned int n, uint64_t mnts[]) +{ + unsigned int i; + + for (i = 0; i < n; i++) + mnts[i] = expect_notify_mask(_metadata, self, mask); +} + +static void verify_mount_ids(struct __test_metadata *const _metadata, + const uint64_t list1[], const uint64_t list2[], + size_t num) +{ + unsigned int i, j; + + // Check that neither list has any duplicates + for (i = 0; i < num; i++) { + for (j = 0; j < num; j++) { + if (i != j) { + ASSERT_NE(list1[i], list1[j]); + ASSERT_NE(list2[i], list2[j]); + } + } + } + // Check that all list1 memebers can be found in list2. Together with + // the above it means that the list1 and list2 represent the same sets. + for (i = 0; i < num; i++) { + for (j = 0; j < num; j++) { + if (list1[i] == list2[j]) + break; + } + ASSERT_NE(j, num); + } +} + +static void check_mounted(struct __test_metadata *const _metadata, + const uint64_t mnts[], size_t num) +{ + ssize_t ret; + uint64_t *list; + + list = malloc((num + 1) * sizeof(list[0])); + ASSERT_NE(list, NULL); + + ret = listmount(LSMT_ROOT, 0, 0, list, num + 1, 0); + ASSERT_EQ(ret, num); + + verify_mount_ids(_metadata, mnts, list, num); + + free(list); +} + +static void setup_mount_tree(struct __test_metadata *const _metadata, + int log2_num) +{ + int ret, i; + + ret = mount("", "/", NULL, MS_SHARED, NULL); + ASSERT_EQ(ret, 0); + + for (i = 0; i < log2_num; i++) { + ret = mount("/", "/", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + } +} + +TEST_F(fanotify, bind) +{ + int ret; + uint64_t mnts[2] = { self->root_id }; + + ret = mount("/", "/", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + ASSERT_NE(mnts[0], mnts[1]); + + check_mounted(_metadata, mnts, 2); + + // Cleanup + uint64_t detach_id; + ret = umount("/"); + ASSERT_EQ(ret, 0); + + detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH); + ASSERT_EQ(detach_id, mnts[1]); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, move) +{ + int ret; + uint64_t mnts[2] = { self->root_id }; + uint64_t move_id; + + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + ASSERT_NE(mnts[0], mnts[1]); + + check_mounted(_metadata, mnts, 2); + + ret = move_mount(AT_FDCWD, "/a", AT_FDCWD, "/b", 0); + ASSERT_EQ(ret, 0); + + move_id = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH); + ASSERT_EQ(move_id, mnts[1]); + + // Cleanup + ret = umount("/b"); + ASSERT_EQ(ret, 0); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, propagate) +{ + const unsigned int log2_num = 4; + const unsigned int num = (1 << log2_num); + uint64_t mnts[num]; + + setup_mount_tree(_metadata, log2_num); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, num - 1, mnts + 1); + + mnts[0] = self->root_id; + check_mounted(_metadata, mnts, num); + + // Cleanup + int ret; + uint64_t mnts2[num]; + ret = umount2("/", MNT_DETACH); + ASSERT_EQ(ret, 0); + + ret = mount("", "/", NULL, MS_PRIVATE, NULL); + ASSERT_EQ(ret, 0); + + mnts2[0] = self->root_id; + expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, num - 1, mnts2 + 1); + verify_mount_ids(_metadata, mnts, mnts2, num); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, fsmount) +{ + int ret, fs, mnt; + uint64_t mnts[2] = { self->root_id }; + + fs = fsopen("tmpfs", 0); + ASSERT_GE(fs, 0); + + ret = fsconfig(fs, FSCONFIG_CMD_CREATE, 0, 0, 0); + ASSERT_EQ(ret, 0); + + mnt = fsmount(fs, 0, 0); + ASSERT_GE(mnt, 0); + + close(fs); + + ret = move_mount(mnt, "", AT_FDCWD, "/a", MOVE_MOUNT_F_EMPTY_PATH); + ASSERT_EQ(ret, 0); + + close(mnt); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + ASSERT_NE(mnts[0], mnts[1]); + + check_mounted(_metadata, mnts, 2); + + // Cleanup + uint64_t detach_id; + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH); + ASSERT_EQ(detach_id, mnts[1]); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, reparent) +{ + uint64_t mnts[6] = { self->root_id }; + uint64_t dmnts[3]; + uint64_t masks[3]; + unsigned int i; + int ret; + + // Create setup with a[1] -> b[2] propagation + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("", "/a", NULL, MS_SHARED, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("/a", "/b", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("", "/b", NULL, MS_SLAVE, NULL); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1); + + check_mounted(_metadata, mnts, 3); + + // Mount on a[3], which is propagated to b[4] + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 3); + + check_mounted(_metadata, mnts, 5); + + // Mount on b[5], not propagated + ret = mount("/", "/b", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[5] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + + check_mounted(_metadata, mnts, 6); + + // Umount a[3], which is propagated to b[4], but not b[5] + // This will result in b[5] "falling" on b[2] + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + expect_notify_n(_metadata, self, 3, masks, dmnts); + verify_mount_ids(_metadata, mnts + 3, dmnts, 3); + + for (i = 0; i < 3; i++) { + if (dmnts[i] == mnts[5]) { + ASSERT_EQ(masks[i], FAN_MNT_ATTACH | FAN_MNT_DETACH); + } else { + ASSERT_EQ(masks[i], FAN_MNT_DETACH); + } + } + + mnts[3] = mnts[5]; + check_mounted(_metadata, mnts, 4); + + // Cleanup + ret = umount("/b"); + ASSERT_EQ(ret, 0); + + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + ret = umount("/b"); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 3, dmnts); + verify_mount_ids(_metadata, mnts + 1, dmnts, 3); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, rmdir) +{ + uint64_t mnts[3] = { self->root_id }; + int ret; + + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("/", "/a/b", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1); + + check_mounted(_metadata, mnts, 3); + + ret = chdir("/a"); + ASSERT_EQ(ret, 0); + + ret = fork(); + ASSERT_GE(ret, 0); + + if (ret == 0) { + chdir("/"); + unshare(CLONE_NEWNS); + mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL); + umount2("/a", MNT_DETACH); + // This triggers a detach in the other namespace + rmdir("/a"); + exit(0); + } + wait(NULL); + + expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 2, mnts + 1); + check_mounted(_metadata, mnts, 1); + + // Cleanup + ret = chdir("/"); + ASSERT_EQ(ret, 0); +} + +TEST_F(fanotify, pivot_root) +{ + uint64_t mnts[3] = { self->root_id }; + uint64_t mnts2[3]; + int ret; + + ret = mount("tmpfs", "/a", "tmpfs", 0, NULL); + ASSERT_EQ(ret, 0); + + mnts[2] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + + ret = mkdir("/a/new", 0700); + ASSERT_EQ(ret, 0); + + ret = mkdir("/a/old", 0700); + ASSERT_EQ(ret, 0); + + ret = mount("/a", "/a/new", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + check_mounted(_metadata, mnts, 3); + + ret = syscall(SYS_pivot_root, "/a/new", "/a/new/old"); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH, 2, mnts2); + verify_mount_ids(_metadata, mnts, mnts2, 2); + check_mounted(_metadata, mnts, 3); + + // Cleanup + ret = syscall(SYS_pivot_root, "/old", "/old/a/new"); + ASSERT_EQ(ret, 0); + + ret = umount("/a/new"); + ASSERT_EQ(ret, 0); + + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + check_mounted(_metadata, mnts, 1); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/mount-notify/mount-notify_test_ns.c b/tools/testing/selftests/filesystems/mount-notify/mount-notify_test_ns.c new file mode 100644 index 000000000..320ee25dc --- /dev/null +++ b/tools/testing/selftests/filesystems/mount-notify/mount-notify_test_ns.c @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2025 Miklos Szeredi <miklos@szeredi.hu> + +#define _GNU_SOURCE + +// Needed for linux/fanotify.h +typedef struct { + int val[2]; +} __kernel_fsid_t; +#define __kernel_fsid_t __kernel_fsid_t + +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <unistd.h> +#include <sys/syscall.h> +#include <sys/fanotify.h> + +#include "kselftest_harness.h" +#include "../statmount/statmount.h" +#include "../utils.h" + +static const char root_mntpoint_templ[] = "/tmp/mount-notify_test_root.XXXXXX"; + +static const int mark_types[] = { + FAN_MARK_FILESYSTEM, + FAN_MARK_MOUNT, + FAN_MARK_INODE +}; + +static const int mark_cmds[] = { + FAN_MARK_ADD, + FAN_MARK_REMOVE, + FAN_MARK_FLUSH +}; + +#define NUM_FAN_FDS ARRAY_SIZE(mark_cmds) + +FIXTURE(fanotify) { + int fan_fd[NUM_FAN_FDS]; + char buf[256]; + unsigned int rem; + void *next; + char root_mntpoint[sizeof(root_mntpoint_templ)]; + int orig_root; + int orig_ns_fd; + int ns_fd; + uint64_t root_id; +}; + +FIXTURE_SETUP(fanotify) +{ + int i, ret; + + self->orig_ns_fd = open("/proc/self/ns/mnt", O_RDONLY); + ASSERT_GE(self->orig_ns_fd, 0); + + ret = setup_userns(); + ASSERT_EQ(ret, 0); + + self->ns_fd = open("/proc/self/ns/mnt", O_RDONLY); + ASSERT_GE(self->ns_fd, 0); + + strcpy(self->root_mntpoint, root_mntpoint_templ); + ASSERT_NE(mkdtemp(self->root_mntpoint), NULL); + + self->orig_root = open("/", O_PATH | O_CLOEXEC); + ASSERT_GE(self->orig_root, 0); + + ASSERT_EQ(mount("tmpfs", self->root_mntpoint, "tmpfs", 0, NULL), 0); + + ASSERT_EQ(chroot(self->root_mntpoint), 0); + + ASSERT_EQ(chdir("/"), 0); + + ASSERT_EQ(mkdir("a", 0700), 0); + + ASSERT_EQ(mkdir("b", 0700), 0); + + self->root_id = get_unique_mnt_id("/"); + ASSERT_NE(self->root_id, 0); + + for (i = 0; i < NUM_FAN_FDS; i++) { + int fan_fd = fanotify_init(FAN_REPORT_FID, 0); + // Verify that watching tmpfs mounted inside userns is allowed + ret = fanotify_mark(fan_fd, FAN_MARK_ADD | mark_types[i], + FAN_OPEN, AT_FDCWD, "/"); + ASSERT_EQ(ret, 0); + // ...but watching entire orig root filesystem is not allowed + ret = fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, + FAN_OPEN, self->orig_root, "."); + ASSERT_NE(ret, 0); + close(fan_fd); + + self->fan_fd[i] = fanotify_init(FAN_REPORT_MNT | FAN_NONBLOCK, + 0); + ASSERT_GE(self->fan_fd[i], 0); + // Verify that watching mntns where group was created is allowed + ret = fanotify_mark(self->fan_fd[i], FAN_MARK_ADD | + FAN_MARK_MNTNS, + FAN_MNT_ATTACH | FAN_MNT_DETACH, + self->ns_fd, NULL); + ASSERT_EQ(ret, 0); + // ...but watching orig mntns is not allowed + ret = fanotify_mark(self->fan_fd[i], FAN_MARK_ADD | + FAN_MARK_MNTNS, + FAN_MNT_ATTACH | FAN_MNT_DETACH, + self->orig_ns_fd, NULL); + ASSERT_NE(ret, 0); + // On fd[0] we do an extra ADD that changes nothing. + // On fd[1]/fd[2] we REMOVE/FLUSH which removes the mark. + ret = fanotify_mark(self->fan_fd[i], mark_cmds[i] | + FAN_MARK_MNTNS, + FAN_MNT_ATTACH | FAN_MNT_DETACH, + self->ns_fd, NULL); + ASSERT_EQ(ret, 0); + } + + self->rem = 0; +} + +FIXTURE_TEARDOWN(fanotify) +{ + int i; + + ASSERT_EQ(self->rem, 0); + for (i = 0; i < NUM_FAN_FDS; i++) + close(self->fan_fd[i]); + + ASSERT_EQ(fchdir(self->orig_root), 0); + + ASSERT_EQ(chroot("."), 0); + + EXPECT_EQ(umount2(self->root_mntpoint, MNT_DETACH), 0); + EXPECT_EQ(chdir(self->root_mntpoint), 0); + EXPECT_EQ(chdir("/"), 0); + EXPECT_EQ(rmdir(self->root_mntpoint), 0); +} + +static uint64_t expect_notify(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + uint64_t *mask) +{ + struct fanotify_event_metadata *meta; + struct fanotify_event_info_mnt *mnt; + unsigned int thislen; + + if (!self->rem) { + ssize_t len; + int i; + + for (i = NUM_FAN_FDS - 1; i >= 0; i--) { + len = read(self->fan_fd[i], self->buf, + sizeof(self->buf)); + if (i > 0) { + // Groups 1,2 should get EAGAIN + ASSERT_EQ(len, -1); + ASSERT_EQ(errno, EAGAIN); + } else { + // Group 0 should get events + ASSERT_GT(len, 0); + } + } + + self->rem = len; + self->next = (void *) self->buf; + } + + meta = self->next; + ASSERT_TRUE(FAN_EVENT_OK(meta, self->rem)); + + thislen = meta->event_len; + self->rem -= thislen; + self->next += thislen; + + *mask = meta->mask; + thislen -= sizeof(*meta); + + mnt = ((void *) meta) + meta->event_len - thislen; + + ASSERT_EQ(thislen, sizeof(*mnt)); + + return mnt->mnt_id; +} + +static void expect_notify_n(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + unsigned int n, uint64_t mask[], uint64_t mnts[]) +{ + unsigned int i; + + for (i = 0; i < n; i++) + mnts[i] = expect_notify(_metadata, self, &mask[i]); +} + +static uint64_t expect_notify_mask(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + uint64_t expect_mask) +{ + uint64_t mntid, mask; + + mntid = expect_notify(_metadata, self, &mask); + ASSERT_EQ(expect_mask, mask); + + return mntid; +} + + +static void expect_notify_mask_n(struct __test_metadata *const _metadata, + FIXTURE_DATA(fanotify) *self, + uint64_t mask, unsigned int n, uint64_t mnts[]) +{ + unsigned int i; + + for (i = 0; i < n; i++) + mnts[i] = expect_notify_mask(_metadata, self, mask); +} + +static void verify_mount_ids(struct __test_metadata *const _metadata, + const uint64_t list1[], const uint64_t list2[], + size_t num) +{ + unsigned int i, j; + + // Check that neither list has any duplicates + for (i = 0; i < num; i++) { + for (j = 0; j < num; j++) { + if (i != j) { + ASSERT_NE(list1[i], list1[j]); + ASSERT_NE(list2[i], list2[j]); + } + } + } + // Check that all list1 memebers can be found in list2. Together with + // the above it means that the list1 and list2 represent the same sets. + for (i = 0; i < num; i++) { + for (j = 0; j < num; j++) { + if (list1[i] == list2[j]) + break; + } + ASSERT_NE(j, num); + } +} + +static void check_mounted(struct __test_metadata *const _metadata, + const uint64_t mnts[], size_t num) +{ + ssize_t ret; + uint64_t *list; + + list = malloc((num + 1) * sizeof(list[0])); + ASSERT_NE(list, NULL); + + ret = listmount(LSMT_ROOT, 0, 0, list, num + 1, 0); + ASSERT_EQ(ret, num); + + verify_mount_ids(_metadata, mnts, list, num); + + free(list); +} + +static void setup_mount_tree(struct __test_metadata *const _metadata, + int log2_num) +{ + int ret, i; + + ret = mount("", "/", NULL, MS_SHARED, NULL); + ASSERT_EQ(ret, 0); + + for (i = 0; i < log2_num; i++) { + ret = mount("/", "/", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + } +} + +TEST_F(fanotify, bind) +{ + int ret; + uint64_t mnts[2] = { self->root_id }; + + ret = mount("/", "/", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + ASSERT_NE(mnts[0], mnts[1]); + + check_mounted(_metadata, mnts, 2); + + // Cleanup + uint64_t detach_id; + ret = umount("/"); + ASSERT_EQ(ret, 0); + + detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH); + ASSERT_EQ(detach_id, mnts[1]); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, move) +{ + int ret; + uint64_t mnts[2] = { self->root_id }; + uint64_t move_id; + + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + ASSERT_NE(mnts[0], mnts[1]); + + check_mounted(_metadata, mnts, 2); + + ret = move_mount(AT_FDCWD, "/a", AT_FDCWD, "/b", 0); + ASSERT_EQ(ret, 0); + + move_id = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH); + ASSERT_EQ(move_id, mnts[1]); + + // Cleanup + ret = umount("/b"); + ASSERT_EQ(ret, 0); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, propagate) +{ + const unsigned int log2_num = 4; + const unsigned int num = (1 << log2_num); + uint64_t mnts[num]; + + setup_mount_tree(_metadata, log2_num); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, num - 1, mnts + 1); + + mnts[0] = self->root_id; + check_mounted(_metadata, mnts, num); + + // Cleanup + int ret; + uint64_t mnts2[num]; + ret = umount2("/", MNT_DETACH); + ASSERT_EQ(ret, 0); + + ret = mount("", "/", NULL, MS_PRIVATE, NULL); + ASSERT_EQ(ret, 0); + + mnts2[0] = self->root_id; + expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, num - 1, mnts2 + 1); + verify_mount_ids(_metadata, mnts, mnts2, num); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, fsmount) +{ + int ret, fs, mnt; + uint64_t mnts[2] = { self->root_id }; + + fs = fsopen("tmpfs", 0); + ASSERT_GE(fs, 0); + + ret = fsconfig(fs, FSCONFIG_CMD_CREATE, 0, 0, 0); + ASSERT_EQ(ret, 0); + + mnt = fsmount(fs, 0, 0); + ASSERT_GE(mnt, 0); + + close(fs); + + ret = move_mount(mnt, "", AT_FDCWD, "/a", MOVE_MOUNT_F_EMPTY_PATH); + ASSERT_EQ(ret, 0); + + close(mnt); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + ASSERT_NE(mnts[0], mnts[1]); + + check_mounted(_metadata, mnts, 2); + + // Cleanup + uint64_t detach_id; + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH); + ASSERT_EQ(detach_id, mnts[1]); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, reparent) +{ + uint64_t mnts[6] = { self->root_id }; + uint64_t dmnts[3]; + uint64_t masks[3]; + unsigned int i; + int ret; + + // Create setup with a[1] -> b[2] propagation + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("", "/a", NULL, MS_SHARED, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("/a", "/b", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("", "/b", NULL, MS_SLAVE, NULL); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1); + + check_mounted(_metadata, mnts, 3); + + // Mount on a[3], which is propagated to b[4] + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 3); + + check_mounted(_metadata, mnts, 5); + + // Mount on b[5], not propagated + ret = mount("/", "/b", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[5] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + + check_mounted(_metadata, mnts, 6); + + // Umount a[3], which is propagated to b[4], but not b[5] + // This will result in b[5] "falling" on b[2] + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + expect_notify_n(_metadata, self, 3, masks, dmnts); + verify_mount_ids(_metadata, mnts + 3, dmnts, 3); + + for (i = 0; i < 3; i++) { + if (dmnts[i] == mnts[5]) { + ASSERT_EQ(masks[i], FAN_MNT_ATTACH | FAN_MNT_DETACH); + } else { + ASSERT_EQ(masks[i], FAN_MNT_DETACH); + } + } + + mnts[3] = mnts[5]; + check_mounted(_metadata, mnts, 4); + + // Cleanup + ret = umount("/b"); + ASSERT_EQ(ret, 0); + + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + ret = umount("/b"); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 3, dmnts); + verify_mount_ids(_metadata, mnts + 1, dmnts, 3); + + check_mounted(_metadata, mnts, 1); +} + +TEST_F(fanotify, rmdir) +{ + uint64_t mnts[3] = { self->root_id }; + int ret; + + ret = mount("/", "/a", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + ret = mount("/", "/a/b", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1); + + check_mounted(_metadata, mnts, 3); + + ret = chdir("/a"); + ASSERT_EQ(ret, 0); + + ret = fork(); + ASSERT_GE(ret, 0); + + if (ret == 0) { + chdir("/"); + unshare(CLONE_NEWNS); + mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL); + umount2("/a", MNT_DETACH); + // This triggers a detach in the other namespace + rmdir("/a"); + exit(0); + } + wait(NULL); + + expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 2, mnts + 1); + check_mounted(_metadata, mnts, 1); + + // Cleanup + ret = chdir("/"); + ASSERT_EQ(ret, 0); +} + +TEST_F(fanotify, pivot_root) +{ + uint64_t mnts[3] = { self->root_id }; + uint64_t mnts2[3]; + int ret; + + ret = mount("tmpfs", "/a", "tmpfs", 0, NULL); + ASSERT_EQ(ret, 0); + + mnts[2] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + + ret = mkdir("/a/new", 0700); + ASSERT_EQ(ret, 0); + + ret = mkdir("/a/old", 0700); + ASSERT_EQ(ret, 0); + + ret = mount("/a", "/a/new", NULL, MS_BIND, NULL); + ASSERT_EQ(ret, 0); + + mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH); + check_mounted(_metadata, mnts, 3); + + ret = syscall(SYS_pivot_root, "/a/new", "/a/new/old"); + ASSERT_EQ(ret, 0); + + expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH, 2, mnts2); + verify_mount_ids(_metadata, mnts, mnts2, 2); + check_mounted(_metadata, mnts, 3); + + // Cleanup + ret = syscall(SYS_pivot_root, "/old", "/old/a/new"); + ASSERT_EQ(ret, 0); + + ret = umount("/a/new"); + ASSERT_EQ(ret, 0); + + ret = umount("/a"); + ASSERT_EQ(ret, 0); + + check_mounted(_metadata, mnts, 1); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/move_mount/.gitignore b/tools/testing/selftests/filesystems/move_mount/.gitignore new file mode 100644 index 000000000..c7557db30 --- /dev/null +++ b/tools/testing/selftests/filesystems/move_mount/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +move_mount_test diff --git a/tools/testing/selftests/filesystems/move_mount/Makefile b/tools/testing/selftests/filesystems/move_mount/Makefile new file mode 100644 index 000000000..5c5b199b4 --- /dev/null +++ b/tools/testing/selftests/filesystems/move_mount/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +LDLIBS += -lcap + +TEST_GEN_PROGS := move_mount_test + +include ../../lib.mk + +$(OUTPUT)/move_mount_test: ../utils.c diff --git a/tools/testing/selftests/filesystems/move_mount/move_mount_test.c b/tools/testing/selftests/filesystems/move_mount/move_mount_test.c new file mode 100644 index 000000000..f08f94b1f --- /dev/null +++ b/tools/testing/selftests/filesystems/move_mount/move_mount_test.c @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Christian Brauner <brauner@kernel.org> + +#define _GNU_SOURCE + +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <unistd.h> +#include <sys/syscall.h> + +#include "../wrappers.h" +#include "../utils.h" +#include "../statmount/statmount.h" +#include "../../kselftest_harness.h" + +#include <linux/stat.h> + +#ifndef MOVE_MOUNT_BENEATH +#define MOVE_MOUNT_BENEATH 0x00000200 +#endif + +static uint64_t get_unique_mnt_id_fd(int fd) +{ + struct statx sx; + int ret; + + ret = statx(fd, "", AT_EMPTY_PATH, STATX_MNT_ID_UNIQUE, &sx); + if (ret) + return 0; + + if (!(sx.stx_mask & STATX_MNT_ID_UNIQUE)) + return 0; + + return sx.stx_mnt_id; +} + +/* + * Create a locked overmount stack at /mnt_dir for testing MNT_LOCKED + * transfer on non-rootfs mounts. + * + * Mounts tmpfs A at /mnt_dir, overmounts with tmpfs B, then enters a + * new user+mount namespace where both become locked. Returns the exit + * code to use on failure, or 0 on success. + */ +static int setup_locked_overmount(void) +{ + /* Isolate so mounts don't leak. */ + if (unshare(CLONE_NEWNS)) + return 1; + if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) + return 2; + + /* + * Create mounts while still in the initial user namespace so + * they become locked after the subsequent user namespace + * unshare. + */ + rmdir("/mnt_dir"); + if (mkdir("/mnt_dir", 0755)) + return 3; + + /* Mount tmpfs A */ + if (mount("tmpfs", "/mnt_dir", "tmpfs", 0, NULL)) + return 4; + + /* Overmount with tmpfs B */ + if (mount("tmpfs", "/mnt_dir", "tmpfs", 0, NULL)) + return 5; + + /* + * Create user+mount namespace. Mounts A and B become locked + * because they might be covering something that is not supposed + * to be revealed. + */ + if (setup_userns()) + return 6; + + /* Sanity check: B must be locked */ + if (!umount2("/mnt_dir", MNT_DETACH) || errno != EINVAL) + return 7; + + return 0; +} + +/* + * Create a detached tmpfs mount and return its fd, or -1 on failure. + */ +static int create_detached_tmpfs(void) +{ + int fs_fd, mnt_fd; + + fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC); + if (fs_fd < 0) + return -1; + + if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) { + close(fs_fd); + return -1; + } + + mnt_fd = sys_fsmount(fs_fd, FSMOUNT_CLOEXEC, 0); + close(fs_fd); + return mnt_fd; +} + +FIXTURE(move_mount) { + uint64_t orig_root_id; +}; + +FIXTURE_SETUP(move_mount) +{ + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + self->orig_root_id = get_unique_mnt_id("/"); + ASSERT_NE(self->orig_root_id, 0); +} + +FIXTURE_TEARDOWN(move_mount) +{ +} + +/* + * Test successful MOVE_MOUNT_BENEATH on the rootfs. + * Mount a clone beneath /, fchdir to the clone, chroot to switch root, + * then detach the old root. + */ +TEST_F(move_mount, beneath_rootfs_success) +{ + int fd_tree, ret; + uint64_t clone_id, root_id; + + fd_tree = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd_tree, 0); + + clone_id = get_unique_mnt_id_fd(fd_tree); + ASSERT_NE(clone_id, 0); + ASSERT_NE(clone_id, self->orig_root_id); + + ASSERT_EQ(fchdir(fd_tree), 0); + + ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(fd_tree); + + /* Switch root to the clone */ + ASSERT_EQ(chroot("."), 0); + + /* Verify "/" is now the clone */ + root_id = get_unique_mnt_id("/"); + ASSERT_NE(root_id, 0); + ASSERT_EQ(root_id, clone_id); + + /* Detach old root */ + ASSERT_EQ(umount2(".", MNT_DETACH), 0); +} + +/* + * Test that after MOVE_MOUNT_BENEATH on the rootfs the old root is + * stacked on top of the clone. Verify via statmount that the old + * root's parent is the clone. + */ +TEST_F(move_mount, beneath_rootfs_old_root_stacked) +{ + int fd_tree, ret; + uint64_t clone_id; + struct statmount sm; + + fd_tree = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd_tree, 0); + + clone_id = get_unique_mnt_id_fd(fd_tree); + ASSERT_NE(clone_id, 0); + ASSERT_NE(clone_id, self->orig_root_id); + + ASSERT_EQ(fchdir(fd_tree), 0); + + ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(fd_tree); + + ASSERT_EQ(chroot("."), 0); + + /* Old root's parent should now be the clone */ + ASSERT_EQ(statmount(self->orig_root_id, 0, 0, + STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0), 0); + ASSERT_EQ(sm.mnt_parent_id, clone_id); + + ASSERT_EQ(umount2(".", MNT_DETACH), 0); +} + +/* + * Test that MOVE_MOUNT_BENEATH on rootfs fails when chroot'd into a + * subdirectory of the same mount. The caller's fs->root.dentry doesn't + * match mnt->mnt_root so the kernel rejects it. + */ +TEST_F(move_mount, beneath_rootfs_in_chroot_fail) +{ + int fd_tree, ret; + uint64_t chroot_id, clone_id; + + rmdir("/chroot_dir"); + ASSERT_EQ(mkdir("/chroot_dir", 0755), 0); + + chroot_id = get_unique_mnt_id("/chroot_dir"); + ASSERT_NE(chroot_id, 0); + ASSERT_EQ(self->orig_root_id, chroot_id); + + ASSERT_EQ(chdir("/chroot_dir"), 0); + ASSERT_EQ(chroot("."), 0); + + fd_tree = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd_tree, 0); + + clone_id = get_unique_mnt_id_fd(fd_tree); + ASSERT_NE(clone_id, 0); + ASSERT_NE(clone_id, chroot_id); + + ASSERT_EQ(fchdir(fd_tree), 0); + + /* + * Should fail: fs->root.dentry (/chroot_dir) doesn't match + * the mount's mnt_root (/). + */ + ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, EINVAL); + + close(fd_tree); +} + +/* + * Test that MOVE_MOUNT_BENEATH on rootfs succeeds when chroot'd into a + * separate tmpfs mount. The caller's root dentry matches the mount's + * mnt_root since it's a dedicated mount. + */ +TEST_F(move_mount, beneath_rootfs_in_chroot_success) +{ + int fd_tree, ret; + uint64_t chroot_id, clone_id, root_id; + struct statmount sm; + + rmdir("/chroot_dir"); + ASSERT_EQ(mkdir("/chroot_dir", 0755), 0); + ASSERT_EQ(mount("tmpfs", "/chroot_dir", "tmpfs", 0, NULL), 0); + + chroot_id = get_unique_mnt_id("/chroot_dir"); + ASSERT_NE(chroot_id, 0); + + ASSERT_EQ(chdir("/chroot_dir"), 0); + ASSERT_EQ(chroot("."), 0); + + ASSERT_EQ(get_unique_mnt_id("/"), chroot_id); + + fd_tree = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd_tree, 0); + + clone_id = get_unique_mnt_id_fd(fd_tree); + ASSERT_NE(clone_id, 0); + ASSERT_NE(clone_id, chroot_id); + + ASSERT_EQ(fchdir(fd_tree), 0); + + ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(fd_tree); + + ASSERT_EQ(chroot("."), 0); + + root_id = get_unique_mnt_id("/"); + ASSERT_NE(root_id, 0); + ASSERT_EQ(root_id, clone_id); + + ASSERT_EQ(statmount(chroot_id, 0, 0, + STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0), 0); + ASSERT_EQ(sm.mnt_parent_id, clone_id); + + ASSERT_EQ(umount2(".", MNT_DETACH), 0); +} + +/* + * Test MNT_LOCKED transfer when mounting beneath rootfs in a user+mount + * namespace. After mount-beneath the new root gets MNT_LOCKED and the + * old root has MNT_LOCKED cleared so it can be unmounted. + */ +TEST_F(move_mount, beneath_rootfs_locked_transfer) +{ + int fd_tree, ret; + uint64_t clone_id, root_id; + + ASSERT_EQ(setup_userns(), 0); + + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + fd_tree = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | + AT_RECURSIVE); + ASSERT_GE(fd_tree, 0); + + clone_id = get_unique_mnt_id_fd(fd_tree); + ASSERT_NE(clone_id, 0); + + ASSERT_EQ(fchdir(fd_tree), 0); + + ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH | + MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(fd_tree); + + ASSERT_EQ(chroot("."), 0); + + root_id = get_unique_mnt_id("/"); + ASSERT_EQ(root_id, clone_id); + + /* + * The old root should be unmountable (MNT_LOCKED was + * transferred to the clone). If MNT_LOCKED wasn't + * cleared, this would fail with EINVAL. + */ + ASSERT_EQ(umount2(".", MNT_DETACH), 0); + + /* Verify "/" is still the clone after detaching old root */ + root_id = get_unique_mnt_id("/"); + ASSERT_EQ(root_id, clone_id); +} + +/* + * Test containment invariant: after mount-beneath rootfs in a user+mount + * namespace, the new root must be MNT_LOCKED. The lock transfer from the + * old root preserves containment -- the process cannot unmount the new root + * to escape the namespace. + */ +TEST_F(move_mount, beneath_rootfs_locked_containment) +{ + int fd_tree, ret; + uint64_t clone_id, root_id; + + ASSERT_EQ(setup_userns(), 0); + + ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + /* Sanity: rootfs must be locked in the new userns */ + ASSERT_EQ(umount2("/", MNT_DETACH), -1); + ASSERT_EQ(errno, EINVAL); + + fd_tree = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | + AT_RECURSIVE); + ASSERT_GE(fd_tree, 0); + + clone_id = get_unique_mnt_id_fd(fd_tree); + ASSERT_NE(clone_id, 0); + + ASSERT_EQ(fchdir(fd_tree), 0); + + ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/", + MOVE_MOUNT_F_EMPTY_PATH | + MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(fd_tree); + + ASSERT_EQ(chroot("."), 0); + + root_id = get_unique_mnt_id("/"); + ASSERT_EQ(root_id, clone_id); + + /* Detach old root (MNT_LOCKED was cleared from it) */ + ASSERT_EQ(umount2(".", MNT_DETACH), 0); + + /* Verify "/" is still the clone after detaching old root */ + root_id = get_unique_mnt_id("/"); + ASSERT_EQ(root_id, clone_id); + + /* + * The new root must be locked (MNT_LOCKED was transferred + * from the old root). Attempting to unmount it must fail + * with EINVAL, preserving the containment invariant. + */ + ASSERT_EQ(umount2("/", MNT_DETACH), -1); + ASSERT_EQ(errno, EINVAL); +} + +/* + * Test MNT_LOCKED transfer when mounting beneath a non-rootfs locked mount. + * Mounts created before unshare(CLONE_NEWUSER | CLONE_NEWNS) become locked + * in the new namespace. Mount-beneath transfers the lock from the displaced + * mount to the new mount, so the displaced mount can be unmounted. + */ +TEST_F(move_mount, beneath_non_rootfs_locked_transfer) +{ + int mnt_fd, ret; + uint64_t mnt_new_id, mnt_visible_id; + + ASSERT_EQ(setup_locked_overmount(), 0); + + mnt_fd = create_detached_tmpfs(); + ASSERT_GE(mnt_fd, 0); + + mnt_new_id = get_unique_mnt_id_fd(mnt_fd); + ASSERT_NE(mnt_new_id, 0); + + /* Move mount beneath B (which is locked) */ + ret = sys_move_mount(mnt_fd, "", AT_FDCWD, "/mnt_dir", + MOVE_MOUNT_F_EMPTY_PATH | + MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(mnt_fd); + + /* + * B should now be unmountable (MNT_LOCKED was transferred + * to the new mount beneath it). If MNT_LOCKED wasn't + * cleared from B, this would fail with EINVAL. + */ + ASSERT_EQ(umount2("/mnt_dir", MNT_DETACH), 0); + + /* Verify the new mount is now visible */ + mnt_visible_id = get_unique_mnt_id("/mnt_dir"); + ASSERT_EQ(mnt_visible_id, mnt_new_id); +} + +/* + * Test MNT_LOCKED containment when mounting beneath a non-rootfs mount + * that was locked during unshare(CLONE_NEWUSER | CLONE_NEWNS). + * Mounts created before unshare become locked in the new namespace. + * Mount-beneath transfers the lock, preserving containment: the new + * mount cannot be unmounted, but the displaced mount can. + */ +TEST_F(move_mount, beneath_non_rootfs_locked_containment) +{ + int mnt_fd, ret; + uint64_t mnt_new_id, mnt_visible_id; + + ASSERT_EQ(setup_locked_overmount(), 0); + + mnt_fd = create_detached_tmpfs(); + ASSERT_GE(mnt_fd, 0); + + mnt_new_id = get_unique_mnt_id_fd(mnt_fd); + ASSERT_NE(mnt_new_id, 0); + + /* + * Move new tmpfs beneath B at /mnt_dir. + * Stack becomes: A -> new -> B + * Lock transfers from B to new. + */ + ret = sys_move_mount(mnt_fd, "", AT_FDCWD, "/mnt_dir", + MOVE_MOUNT_F_EMPTY_PATH | + MOVE_MOUNT_BENEATH); + ASSERT_EQ(ret, 0); + + close(mnt_fd); + + /* + * B lost MNT_LOCKED -- unmounting it must succeed. + * This reveals the new mount at /mnt_dir. + */ + ASSERT_EQ(umount2("/mnt_dir", MNT_DETACH), 0); + + /* Verify the new mount is now visible */ + mnt_visible_id = get_unique_mnt_id("/mnt_dir"); + ASSERT_EQ(mnt_visible_id, mnt_new_id); + + /* + * The new mount gained MNT_LOCKED -- unmounting it must + * fail with EINVAL, preserving the containment invariant. + */ + ASSERT_EQ(umount2("/mnt_dir", MNT_DETACH), -1); + ASSERT_EQ(errno, EINVAL); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/nsfs/.gitignore b/tools/testing/selftests/filesystems/nsfs/.gitignore new file mode 100644 index 000000000..92a824900 --- /dev/null +++ b/tools/testing/selftests/filesystems/nsfs/.gitignore @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only +owner +pidns +iterate_mntns diff --git a/tools/testing/selftests/filesystems/nsfs/Makefile b/tools/testing/selftests/filesystems/nsfs/Makefile new file mode 100644 index 000000000..231aaa7df --- /dev/null +++ b/tools/testing/selftests/filesystems/nsfs/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only +TEST_GEN_PROGS := owner pidns iterate_mntns + +CFLAGS := -Wall -Werror + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/nsfs/config b/tools/testing/selftests/filesystems/nsfs/config new file mode 100644 index 000000000..598d0a225 --- /dev/null +++ b/tools/testing/selftests/filesystems/nsfs/config @@ -0,0 +1,3 @@ +CONFIG_USER_NS=y +CONFIG_UTS_NS=y +CONFIG_PID_NS=y diff --git a/tools/testing/selftests/filesystems/nsfs/iterate_mntns.c b/tools/testing/selftests/filesystems/nsfs/iterate_mntns.c new file mode 100644 index 000000000..e19ff8168 --- /dev/null +++ b/tools/testing/selftests/filesystems/nsfs/iterate_mntns.c @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2024 Christian Brauner <brauner@kernel.org> + +#define _GNU_SOURCE +#include <fcntl.h> +#include <linux/auto_dev-ioctl.h> +#include <linux/errno.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <unistd.h> + +#include "kselftest_harness.h" + +#define MNT_NS_COUNT 11 +#define MNT_NS_LAST_INDEX 10 + +struct mnt_ns_info { + __u32 size; + __u32 nr_mounts; + __u64 mnt_ns_id; +}; + +#define MNT_NS_INFO_SIZE_VER0 16 /* size of first published struct */ + +/* Get information about namespace. */ +#define NS_MNT_GET_INFO _IOR(0xb7, 10, struct mnt_ns_info) +/* Get next namespace. */ +#define NS_MNT_GET_NEXT _IOR(0xb7, 11, struct mnt_ns_info) +/* Get previous namespace. */ +#define NS_MNT_GET_PREV _IOR(0xb7, 12, struct mnt_ns_info) + +FIXTURE(iterate_mount_namespaces) { + int fd_mnt_ns[MNT_NS_COUNT]; + __u64 mnt_ns_id[MNT_NS_COUNT]; +}; + +static inline bool mntns_in_list(__u64 *mnt_ns_id, struct mnt_ns_info *info) +{ + for (int i = 0; i < MNT_NS_COUNT; i++) { + if (mnt_ns_id[i] == info->mnt_ns_id) + return true; + } + return false; +} + +FIXTURE_SETUP(iterate_mount_namespaces) +{ + for (int i = 0; i < MNT_NS_COUNT; i++) + self->fd_mnt_ns[i] = -EBADF; + + for (int i = 0; i < MNT_NS_COUNT; i++) { + struct mnt_ns_info info = {}; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + self->fd_mnt_ns[i] = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC); + ASSERT_GE(self->fd_mnt_ns[i], 0); + ASSERT_EQ(ioctl(self->fd_mnt_ns[i], NS_MNT_GET_INFO, &info), 0); + self->mnt_ns_id[i] = info.mnt_ns_id; + } +} + +FIXTURE_TEARDOWN(iterate_mount_namespaces) +{ + for (int i = 0; i < MNT_NS_COUNT; i++) { + if (self->fd_mnt_ns[i] < 0) + continue; + ASSERT_EQ(close(self->fd_mnt_ns[i]), 0); + } +} + +TEST_F(iterate_mount_namespaces, iterate_all_forward) +{ + int fd_mnt_ns_cur, count = 0; + + fd_mnt_ns_cur = fcntl(self->fd_mnt_ns[0], F_DUPFD_CLOEXEC); + ASSERT_GE(fd_mnt_ns_cur, 0); + + for (;;) { + struct mnt_ns_info info = {}; + int fd_mnt_ns_next; + + fd_mnt_ns_next = ioctl(fd_mnt_ns_cur, NS_MNT_GET_NEXT, &info); + if (fd_mnt_ns_next < 0 && errno == ENOENT) + break; + if (mntns_in_list(self->mnt_ns_id, &info)) + count++; + ASSERT_GE(fd_mnt_ns_next, 0); + ASSERT_EQ(close(fd_mnt_ns_cur), 0); + fd_mnt_ns_cur = fd_mnt_ns_next; + } + ASSERT_EQ(count, MNT_NS_LAST_INDEX); +} + +TEST_F(iterate_mount_namespaces, iterate_all_backwards) +{ + int fd_mnt_ns_cur, count = 0; + + fd_mnt_ns_cur = fcntl(self->fd_mnt_ns[MNT_NS_LAST_INDEX], F_DUPFD_CLOEXEC); + ASSERT_GE(fd_mnt_ns_cur, 0); + + for (;;) { + struct mnt_ns_info info = {}; + int fd_mnt_ns_prev; + + fd_mnt_ns_prev = ioctl(fd_mnt_ns_cur, NS_MNT_GET_PREV, &info); + if (fd_mnt_ns_prev < 0 && errno == ENOENT) + break; + if (mntns_in_list(self->mnt_ns_id, &info)) + count++; + ASSERT_GE(fd_mnt_ns_prev, 0); + ASSERT_EQ(close(fd_mnt_ns_cur), 0); + fd_mnt_ns_cur = fd_mnt_ns_prev; + } + ASSERT_EQ(count, MNT_NS_LAST_INDEX); +} + +TEST_F(iterate_mount_namespaces, iterate_forward) +{ + int fd_mnt_ns_cur; + + ASSERT_EQ(setns(self->fd_mnt_ns[0], CLONE_NEWNS), 0); + + fd_mnt_ns_cur = self->fd_mnt_ns[0]; + for (int i = 1; i < MNT_NS_COUNT; i++) { + struct mnt_ns_info info = {}; + int fd_mnt_ns_next; + + fd_mnt_ns_next = ioctl(fd_mnt_ns_cur, NS_MNT_GET_NEXT, &info); + ASSERT_GE(fd_mnt_ns_next, 0); + ASSERT_EQ(close(fd_mnt_ns_cur), 0); + fd_mnt_ns_cur = fd_mnt_ns_next; + } +} + +TEST_F(iterate_mount_namespaces, iterate_backward) +{ + int fd_mnt_ns_cur; + + ASSERT_EQ(setns(self->fd_mnt_ns[MNT_NS_LAST_INDEX], CLONE_NEWNS), 0); + + fd_mnt_ns_cur = self->fd_mnt_ns[MNT_NS_LAST_INDEX]; + for (int i = MNT_NS_LAST_INDEX - 1; i >= 0; i--) { + struct mnt_ns_info info = {}; + int fd_mnt_ns_prev; + + fd_mnt_ns_prev = ioctl(fd_mnt_ns_cur, NS_MNT_GET_PREV, &info); + ASSERT_GE(fd_mnt_ns_prev, 0); + ASSERT_EQ(close(fd_mnt_ns_cur), 0); + fd_mnt_ns_cur = fd_mnt_ns_prev; + } +} + +TEST_F(iterate_mount_namespaces, nfs_valid_ioctl) +{ + ASSERT_NE(ioctl(self->fd_mnt_ns[0], AUTOFS_DEV_IOCTL_OPENMOUNT, NULL), 0); + ASSERT_EQ(errno, ENOTTY); + + ASSERT_NE(ioctl(self->fd_mnt_ns[0], AUTOFS_DEV_IOCTL_CLOSEMOUNT, NULL), 0); + ASSERT_EQ(errno, ENOTTY); + + ASSERT_NE(ioctl(self->fd_mnt_ns[0], AUTOFS_DEV_IOCTL_READY, NULL), 0); + ASSERT_EQ(errno, ENOTTY); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/nsfs/owner.c b/tools/testing/selftests/filesystems/nsfs/owner.c new file mode 100644 index 000000000..96a976c74 --- /dev/null +++ b/tools/testing/selftests/filesystems/nsfs/owner.c @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <sched.h> +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <signal.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <sys/prctl.h> +#include <sys/wait.h> + +#define NSIO 0xb7 +#define NS_GET_USERNS _IO(NSIO, 0x1) + +#define pr_err(fmt, ...) \ + ({ \ + fprintf(stderr, "%s:%d:" fmt ": %m\n", \ + __func__, __LINE__, ##__VA_ARGS__); \ + 1; \ + }) + +int main(int argc, char *argvp[]) +{ + int pfd[2], ns, uns, init_uns; + struct stat st1, st2; + char path[128]; + pid_t pid; + char c; + + if (pipe(pfd)) + return 1; + + pid = fork(); + if (pid < 0) + return pr_err("fork"); + if (pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGKILL); + if (unshare(CLONE_NEWUTS | CLONE_NEWUSER)) + return pr_err("unshare"); + close(pfd[0]); + close(pfd[1]); + while (1) + sleep(1); + return 0; + } + close(pfd[1]); + if (read(pfd[0], &c, 1) != 0) + return pr_err("Unable to read from pipe"); + close(pfd[0]); + + snprintf(path, sizeof(path), "/proc/%d/ns/uts", pid); + ns = open(path, O_RDONLY); + if (ns < 0) + return pr_err("Unable to open %s", path); + + uns = ioctl(ns, NS_GET_USERNS); + if (uns < 0) + return pr_err("Unable to get an owning user namespace"); + + if (fstat(uns, &st1)) + return pr_err("fstat"); + + snprintf(path, sizeof(path), "/proc/%d/ns/user", pid); + if (stat(path, &st2)) + return pr_err("stat"); + + if (st1.st_ino != st2.st_ino) + return pr_err("NS_GET_USERNS returned a wrong namespace"); + + init_uns = ioctl(uns, NS_GET_USERNS); + if (uns < 0) + return pr_err("Unable to get an owning user namespace"); + + if (ioctl(init_uns, NS_GET_USERNS) >= 0 || errno != EPERM) + return pr_err("Don't get EPERM"); + + if (unshare(CLONE_NEWUSER)) + return pr_err("unshare"); + + if (ioctl(ns, NS_GET_USERNS) >= 0 || errno != EPERM) + return pr_err("Don't get EPERM"); + if (ioctl(init_uns, NS_GET_USERNS) >= 0 || errno != EPERM) + return pr_err("Don't get EPERM"); + + kill(pid, SIGKILL); + wait(NULL); + return 0; +} diff --git a/tools/testing/selftests/filesystems/nsfs/pidns.c b/tools/testing/selftests/filesystems/nsfs/pidns.c new file mode 100644 index 000000000..e3c772c6a --- /dev/null +++ b/tools/testing/selftests/filesystems/nsfs/pidns.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <sched.h> +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <signal.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <sys/prctl.h> +#include <sys/wait.h> + +#define pr_err(fmt, ...) \ + ({ \ + fprintf(stderr, "%s:%d:" fmt ": %m\n", \ + __func__, __LINE__, ##__VA_ARGS__); \ + 1; \ + }) + +#define NSIO 0xb7 +#define NS_GET_USERNS _IO(NSIO, 0x1) +#define NS_GET_PARENT _IO(NSIO, 0x2) + +#define __stack_aligned__ __attribute__((aligned(16))) +struct cr_clone_arg { + char stack[128] __stack_aligned__; + char stack_ptr[]; +}; + +static int child(void *args) +{ + prctl(PR_SET_PDEATHSIG, SIGKILL); + while (1) + sleep(1); + exit(0); +} + +int main(int argc, char *argv[]) +{ + char *ns_strs[] = {"pid", "user"}; + char path[] = "/proc/0123456789/ns/pid"; + struct cr_clone_arg ca; + struct stat st1, st2; + int ns, pns, i; + pid_t pid; + + pid = clone(child, ca.stack_ptr, CLONE_NEWUSER | CLONE_NEWPID | SIGCHLD, NULL); + if (pid < 0) + return pr_err("clone"); + + for (i = 0; i < 2; i++) { + snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, ns_strs[i]); + ns = open(path, O_RDONLY); + if (ns < 0) + return pr_err("Unable to open %s", path); + + pns = ioctl(ns, NS_GET_PARENT); + if (pns < 0) + return pr_err("Unable to get a parent pidns"); + + snprintf(path, sizeof(path), "/proc/self/ns/%s", ns_strs[i]); + if (stat(path, &st2)) + return pr_err("Unable to stat %s", path); + if (fstat(pns, &st1)) + return pr_err("Unable to stat the parent pidns"); + if (st1.st_ino != st2.st_ino) + return pr_err("NS_GET_PARENT returned a wrong namespace"); + + if (ioctl(pns, NS_GET_PARENT) >= 0 || errno != EPERM) + return pr_err("Don't get EPERM"); + } + + kill(pid, SIGKILL); + wait(NULL); + return 0; +} diff --git a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore new file mode 100644 index 000000000..fb12b93fb --- /dev/null +++ b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore @@ -0,0 +1 @@ +open_tree_ns_test diff --git a/tools/testing/selftests/filesystems/open_tree_ns/Makefile b/tools/testing/selftests/filesystems/open_tree_ns/Makefile new file mode 100644 index 000000000..4976ed1d7 --- /dev/null +++ b/tools/testing/selftests/filesystems/open_tree_ns/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0 +TEST_GEN_PROGS := open_tree_ns_test + +CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +LDLIBS := -lcap + +include ../../lib.mk + +$(OUTPUT)/open_tree_ns_test: open_tree_ns_test.c ../utils.c + $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) diff --git a/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c new file mode 100644 index 000000000..82f3c8c02 --- /dev/null +++ b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c @@ -0,0 +1,1007 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2026 Christian Brauner <brauner@kernel.org> + * + * Test for OPEN_TREE_NAMESPACE flag. + * + * Test that open_tree() with OPEN_TREE_NAMESPACE creates a new mount + * namespace containing the specified mount tree. + */ +#define _GNU_SOURCE + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <linux/nsfs.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../wrappers.h" +#include "../statmount/statmount.h" +#include "../utils.h" +#include "../../kselftest_harness.h" + +#ifndef OPEN_TREE_NAMESPACE +#define OPEN_TREE_NAMESPACE (1 << 1) +#endif + +static int get_mnt_ns_id(int fd, uint64_t *mnt_ns_id) +{ + if (ioctl(fd, NS_GET_MNTNS_ID, mnt_ns_id) < 0) + return -errno; + return 0; +} + +static int get_mnt_ns_id_from_path(const char *path, uint64_t *mnt_ns_id) +{ + int fd, ret; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -errno; + + ret = get_mnt_ns_id(fd, mnt_ns_id); + close(fd); + return ret; +} + +static void log_mount(struct __test_metadata *_metadata, struct statmount *sm) +{ + const char *fs_type = ""; + const char *mnt_root = ""; + const char *mnt_point = ""; + + if (sm->mask & STATMOUNT_FS_TYPE) + fs_type = sm->str + sm->fs_type; + if (sm->mask & STATMOUNT_MNT_ROOT) + mnt_root = sm->str + sm->mnt_root; + if (sm->mask & STATMOUNT_MNT_POINT) + mnt_point = sm->str + sm->mnt_point; + + TH_LOG(" mnt_id: %llu, parent_id: %llu, fs_type: %s, root: %s, point: %s", + (unsigned long long)sm->mnt_id, + (unsigned long long)sm->mnt_parent_id, + fs_type, mnt_root, mnt_point); +} + +static void dump_mounts(struct __test_metadata *_metadata, uint64_t mnt_ns_id) +{ + uint64_t list[256]; + ssize_t nr_mounts; + + nr_mounts = listmount(LSMT_ROOT, mnt_ns_id, 0, list, 256, 0); + if (nr_mounts < 0) { + TH_LOG("listmount failed: %s", strerror(errno)); + return; + } + + TH_LOG("Mount namespace %llu contains %zd mount(s):", + (unsigned long long)mnt_ns_id, nr_mounts); + + for (ssize_t i = 0; i < nr_mounts; i++) { + struct statmount *sm; + + sm = statmount_alloc(list[i], mnt_ns_id, + STATMOUNT_MNT_BASIC | + STATMOUNT_FS_TYPE | + STATMOUNT_MNT_ROOT | + STATMOUNT_MNT_POINT, 0); + if (!sm) { + TH_LOG(" [%zd] mnt_id %llu: statmount failed: %s", + i, (unsigned long long)list[i], strerror(errno)); + continue; + } + + log_mount(_metadata, sm); + free(sm); + } +} + +FIXTURE(open_tree_ns) +{ + int fd; + uint64_t current_ns_id; +}; + +FIXTURE_VARIANT(open_tree_ns) +{ + const char *path; + unsigned int flags; + bool expect_success; + bool expect_different_ns; + int min_mounts; +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, basic_root) +{ + .path = "/", + .flags = OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + /* + * The empty rootfs is hidden from listmount()/mountinfo, + * so we only see the bind mount on top of it. + */ + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, recursive_root) +{ + .path = "/", + .flags = OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, subdir_tmp) +{ + .path = "/tmp", + .flags = OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, subdir_proc) +{ + .path = "/proc", + .flags = OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, recursive_tmp) +{ + .path = "/tmp", + .flags = OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, recursive_run) +{ + .path = "/run", + .flags = OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC, + .expect_success = true, + .expect_different_ns = true, + .min_mounts = 1, +}; + +FIXTURE_VARIANT_ADD(open_tree_ns, invalid_recursive_alone) +{ + .path = "/", + .flags = AT_RECURSIVE | OPEN_TREE_CLOEXEC, + .expect_success = false, + .expect_different_ns = false, + .min_mounts = 0, +}; + +FIXTURE_SETUP(open_tree_ns) +{ + int ret; + + self->fd = -1; + + /* Check if open_tree syscall is supported */ + ret = sys_open_tree(-1, NULL, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "open_tree() syscall not supported"); + + /* Check if statmount/listmount are supported */ + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "statmount() syscall not supported"); + + /* Get current mount namespace ID for comparison */ + ret = get_mnt_ns_id_from_path("/proc/self/ns/mnt", &self->current_ns_id); + if (ret < 0) + SKIP(return, "Failed to get current mount namespace ID"); +} + +FIXTURE_TEARDOWN(open_tree_ns) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(open_tree_ns, create_namespace) +{ + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + self->fd = sys_open_tree(AT_FDCWD, variant->path, variant->flags); + + if (!variant->expect_success) { + ASSERT_LT(self->fd, 0); + ASSERT_EQ(errno, EINVAL); + return; + } + + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + /* Verify we can get the namespace ID */ + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + /* Verify it's a different namespace */ + if (variant->expect_different_ns) + ASSERT_NE(new_ns_id, self->current_ns_id); + + /* List mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 0) { + TH_LOG("%m - listmount failed"); + } + + /* Verify minimum expected mounts */ + ASSERT_GE(nr_mounts, variant->min_mounts); + TH_LOG("Namespace contains %zd mounts", nr_mounts); +} + +TEST_F(open_tree_ns, setns_into_namespace) +{ + uint64_t new_ns_id; + pid_t pid; + int status; + int ret; + + /* Only test with basic flags */ + if (!(variant->flags & OPEN_TREE_NAMESPACE)) + SKIP(return, "setns test only for basic / case"); + + self->fd = sys_open_tree(AT_FDCWD, variant->path, variant->flags); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + /* Get namespace ID and dump all mounts */ + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + dump_mounts(_metadata, new_ns_id); + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + /* Child: try to enter the namespace */ + if (setns(self->fd, CLONE_NEWNS) < 0) + _exit(1); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(open_tree_ns, verify_mount_properties) +{ + struct statmount sm; + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int ret; + + /* Only test with basic flags on root */ + if (variant->flags != (OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC) || + strcmp(variant->path, "/") != 0) + SKIP(return, "mount properties test only for basic / case"); + + self->fd = sys_open_tree(AT_FDCWD, "/", OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (self->fd < 0 && errno == EINVAL) + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + + ASSERT_GE(self->fd, 0); + + ret = get_mnt_ns_id(self->fd, &new_ns_id); + ASSERT_EQ(ret, 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 1); + + /* Get info about the root mount (the bind mount, rootfs is hidden) */ + ret = statmount(list[0], new_ns_id, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0); + ASSERT_EQ(ret, 0); + + ASSERT_NE(sm.mnt_id, sm.mnt_parent_id); + + TH_LOG("Root mount id: %llu, parent: %llu", + (unsigned long long)sm.mnt_id, + (unsigned long long)sm.mnt_parent_id); +} + +FIXTURE(open_tree_ns_caps) +{ + bool has_caps; +}; + +FIXTURE_SETUP(open_tree_ns_caps) +{ + int ret; + + /* Check if open_tree syscall is supported */ + ret = sys_open_tree(-1, NULL, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "open_tree() syscall not supported"); + + self->has_caps = (geteuid() == 0); +} + +FIXTURE_TEARDOWN(open_tree_ns_caps) +{ +} + +TEST_F(open_tree_ns_caps, requires_cap_sys_admin) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + int fd; + + /* Child: drop privileges using utils.h helper */ + if (enter_userns() != 0) + _exit(2); + + /* Drop all caps using utils.h helper */ + if (caps_down() == 0) + _exit(3); + + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (fd >= 0) { + close(fd); + /* Should have failed without caps */ + _exit(1); + } + + if (errno == EPERM) + _exit(0); + + /* EINVAL means OPEN_TREE_NAMESPACE not supported */ + if (errno == EINVAL) + _exit(4); + + /* Unexpected error */ + _exit(5); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Expected: EPERM without caps */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("OPEN_TREE_NAMESPACE succeeded without caps"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 3: + SKIP(return, "caps_down failed"); + break; + case 4: + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +FIXTURE(open_tree_ns_userns) +{ + int fd; +}; + +FIXTURE_SETUP(open_tree_ns_userns) +{ + int ret; + + self->fd = -1; + + /* Check if open_tree syscall is supported */ + ret = sys_open_tree(-1, NULL, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "open_tree() syscall not supported"); + + /* Check if statmount/listmount are supported */ + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "statmount() syscall not supported"); +} + +FIXTURE_TEARDOWN(open_tree_ns_userns) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(open_tree_ns_userns, create_in_userns) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fd; + + /* Create new user namespace (also creates mount namespace) */ + if (enter_userns() != 0) + _exit(2); + + /* Now we have CAP_SYS_ADMIN in the user namespace */ + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (fd < 0) { + if (errno == EINVAL) + _exit(4); /* OPEN_TREE_NAMESPACE not supported */ + _exit(1); + } + + /* Verify we can get the namespace ID */ + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(5); + + /* Verify we can list mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + if (nr_mounts < 0) + _exit(6); + + /* Should have at least 1 mount */ + if (nr_mounts < 1) + _exit(7); + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Success */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("open_tree(OPEN_TREE_NAMESPACE) failed in userns"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 4: + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + break; + case 5: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 6: + ASSERT_FALSE(true) TH_LOG("listmount failed in new namespace"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("New namespace has no mounts"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(open_tree_ns_userns, setns_in_userns) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + int fd; + pid_t inner_pid; + int inner_status; + + /* Create new user namespace */ + if (enter_userns() != 0) + _exit(2); + + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + if (fd < 0) { + if (errno == EINVAL) + _exit(4); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(5); + + /* Fork again to test setns into the new namespace */ + inner_pid = fork(); + if (inner_pid < 0) + _exit(8); + + if (inner_pid == 0) { + /* Inner child: enter the new namespace */ + if (setns(fd, CLONE_NEWNS) < 0) + _exit(1); + _exit(0); + } + + if (waitpid(inner_pid, &inner_status, 0) != inner_pid) + _exit(9); + + if (!WIFEXITED(inner_status) || WEXITSTATUS(inner_status) != 0) + _exit(10); + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Success */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("open_tree or setns failed in userns"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 4: + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + break; + case 5: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 8: + ASSERT_FALSE(true) TH_LOG("Inner fork failed"); + break; + case 9: + ASSERT_FALSE(true) TH_LOG("Inner waitpid failed"); + break; + case 10: + ASSERT_FALSE(true) TH_LOG("setns into new namespace failed"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(open_tree_ns_userns, recursive_in_userns) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fd; + + /* Create new user namespace */ + if (enter_userns() != 0) + _exit(2); + + /* Test recursive flag in userns */ + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC); + if (fd < 0) { + if (errno == EINVAL) + _exit(4); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(5); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + if (nr_mounts < 0) + _exit(6); + + /* Recursive should copy submounts too */ + if (nr_mounts < 1) + _exit(7); + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + /* Success */ + break; + case 1: + ASSERT_FALSE(true) TH_LOG("open_tree(OPEN_TREE_NAMESPACE|AT_RECURSIVE) failed in userns"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 4: + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + break; + case 5: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 6: + ASSERT_FALSE(true) TH_LOG("listmount failed in new namespace"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("New namespace has no mounts"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(open_tree_ns_userns, umount_fails_einval) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fd; + ssize_t i; + + /* Create new user namespace */ + if (enter_userns() != 0) + _exit(2); + + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC); + if (fd < 0) { + if (errno == EINVAL) + _exit(4); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(5); + + /* Get all mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, LISTMOUNT_REVERSE); + if (nr_mounts < 0) + _exit(9); + + if (nr_mounts < 1) + _exit(10); + + /* Enter the new namespace */ + if (setns(fd, CLONE_NEWNS) < 0) + _exit(6); + + for (i = 0; i < nr_mounts; i++) { + struct statmount *sm; + const char *mnt_point; + + sm = statmount_alloc(list[i], new_ns_id, + STATMOUNT_MNT_POINT, 0); + if (!sm) + _exit(11); + + mnt_point = sm->str + sm->mnt_point; + + TH_LOG("Trying to umount %s", mnt_point); + if (umount2(mnt_point, MNT_DETACH) == 0) { + free(sm); + _exit(7); + } + + if (errno != EINVAL) { + /* Wrong error */ + free(sm); + _exit(8); + } + + free(sm); + } + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + break; + case 1: + ASSERT_FALSE(true) TH_LOG("open_tree(OPEN_TREE_NAMESPACE) failed"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 4: + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + break; + case 5: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 6: + ASSERT_FALSE(true) TH_LOG("setns into new namespace failed"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("umount succeeded but should have failed with EINVAL"); + break; + case 8: + ASSERT_FALSE(true) TH_LOG("umount failed with wrong error (expected EINVAL)"); + break; + case 9: + ASSERT_FALSE(true) TH_LOG("listmount failed"); + break; + case 10: + ASSERT_FALSE(true) TH_LOG("No mounts in new namespace"); + break; + case 11: + ASSERT_FALSE(true) TH_LOG("statmount_alloc failed"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +TEST_F(open_tree_ns_userns, umount_succeeds) +{ + pid_t pid; + int status; + + pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fd; + ssize_t i; + + if (unshare(CLONE_NEWNS)) + _exit(1); + + if (sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) != 0) + _exit(1); + + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC); + if (fd < 0) { + if (errno == EINVAL) + _exit(4); + _exit(1); + } + + if (get_mnt_ns_id(fd, &new_ns_id) != 0) + _exit(5); + + /* Get all mounts in the new namespace */ + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, LISTMOUNT_REVERSE); + if (nr_mounts < 0) + _exit(9); + + if (nr_mounts < 1) + _exit(10); + + /* Enter the new namespace */ + if (setns(fd, CLONE_NEWNS) < 0) + _exit(6); + + for (i = 0; i < nr_mounts; i++) { + struct statmount *sm; + const char *mnt_point; + + sm = statmount_alloc(list[i], new_ns_id, + STATMOUNT_MNT_POINT, 0); + if (!sm) + _exit(11); + + mnt_point = sm->str + sm->mnt_point; + + TH_LOG("Trying to umount %s", mnt_point); + if (umount2(mnt_point, MNT_DETACH) != 0) { + free(sm); + _exit(7); + } + + free(sm); + } + + close(fd); + _exit(0); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + + switch (WEXITSTATUS(status)) { + case 0: + break; + case 1: + ASSERT_FALSE(true) TH_LOG("open_tree(OPEN_TREE_NAMESPACE) failed"); + break; + case 2: + SKIP(return, "setup_userns failed"); + break; + case 4: + SKIP(return, "OPEN_TREE_NAMESPACE not supported"); + break; + case 5: + ASSERT_FALSE(true) TH_LOG("Failed to get mount namespace ID"); + break; + case 6: + ASSERT_FALSE(true) TH_LOG("setns into new namespace failed"); + break; + case 7: + ASSERT_FALSE(true) TH_LOG("umount failed but should have succeeded"); + break; + case 9: + ASSERT_FALSE(true) TH_LOG("listmount failed"); + break; + case 10: + ASSERT_FALSE(true) TH_LOG("No mounts in new namespace"); + break; + case 11: + ASSERT_FALSE(true) TH_LOG("statmount_alloc failed"); + break; + default: + ASSERT_FALSE(true) TH_LOG("Unexpected error in child (exit %d)", + WEXITSTATUS(status)); + break; + } +} + +FIXTURE(open_tree_ns_unbindable) +{ + char tmpdir[PATH_MAX]; + bool mounted; +}; + +FIXTURE_SETUP(open_tree_ns_unbindable) +{ + int ret; + + self->mounted = false; + + /* Check if open_tree syscall is supported */ + ret = sys_open_tree(-1, NULL, 0); + if (ret == -1 && errno == ENOSYS) + SKIP(return, "open_tree() syscall not supported"); + + /* Create a temporary directory for the test mount */ + snprintf(self->tmpdir, sizeof(self->tmpdir), + "/tmp/open_tree_ns_test.XXXXXX"); + ASSERT_NE(mkdtemp(self->tmpdir), NULL); + + /* Mount tmpfs there */ + ret = mount("tmpfs", self->tmpdir, "tmpfs", 0, NULL); + if (ret < 0) { + rmdir(self->tmpdir); + SKIP(return, "Failed to mount tmpfs"); + } + self->mounted = true; + + ret = mount(NULL, self->tmpdir, NULL, MS_UNBINDABLE, NULL); + if (ret < 0) { + rmdir(self->tmpdir); + SKIP(return, "Failed to make tmpfs unbindable"); + } +} + +FIXTURE_TEARDOWN(open_tree_ns_unbindable) +{ + if (self->mounted) + umount2(self->tmpdir, MNT_DETACH); + rmdir(self->tmpdir); +} + +TEST_F(open_tree_ns_unbindable, fails_on_unbindable) +{ + int fd; + + fd = sys_open_tree(AT_FDCWD, self->tmpdir, + OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC); + ASSERT_LT(fd, 0); +} + +TEST_F(open_tree_ns_unbindable, recursive_skips_on_unbindable) +{ + uint64_t new_ns_id; + uint64_t list[256]; + ssize_t nr_mounts; + int fd; + ssize_t i; + bool found_unbindable = false; + + fd = sys_open_tree(AT_FDCWD, "/", + OPEN_TREE_NAMESPACE | AT_RECURSIVE | OPEN_TREE_CLOEXEC); + ASSERT_GT(fd, 0); + + ASSERT_EQ(get_mnt_ns_id(fd, &new_ns_id), 0); + + nr_mounts = listmount(LSMT_ROOT, new_ns_id, 0, list, 256, 0); + ASSERT_GE(nr_mounts, 0) { + TH_LOG("listmount failed: %m"); + } + + /* + * Iterate through all mounts in the new namespace and verify + * the unbindable tmpfs mount was silently dropped. + */ + for (i = 0; i < nr_mounts; i++) { + struct statmount *sm; + const char *mnt_point; + + sm = statmount_alloc(list[i], new_ns_id, STATMOUNT_MNT_POINT, 0); + ASSERT_NE(sm, NULL) { + TH_LOG("statmount_alloc failed for mnt_id %llu", + (unsigned long long)list[i]); + } + + mnt_point = sm->str + sm->mnt_point; + + if (strcmp(mnt_point, self->tmpdir) == 0) { + TH_LOG("Found unbindable mount at %s (should have been dropped)", + mnt_point); + found_unbindable = true; + } + + free(sm); + } + + ASSERT_FALSE(found_unbindable) { + TH_LOG("Unbindable mount at %s was not dropped", self->tmpdir); + } + + close(fd); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/openat2/.gitignore b/tools/testing/selftests/filesystems/openat2/.gitignore new file mode 100644 index 000000000..82a4846cb --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +/*_test diff --git a/tools/testing/selftests/filesystems/openat2/Makefile b/tools/testing/selftests/filesystems/openat2/Makefile new file mode 100644 index 000000000..d848aac96 --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/Makefile @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += $(KHDR_INCLUDES) +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 +# Address Sanitizer if -fsanitize is specified. Therefore, simply omit +# -static-libasan for clang builds. +ifeq ($(LLVM),) + CFLAGS += -static-libasan +endif + +LOCAL_HDRS += helpers.h + +include ../../lib.mk 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 000000000..be37ccba5 --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/emptypath_test.c @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> +#include <sys/stat.h> + +#include "kselftest_harness.h" + +#ifndef O_EMPTYPATH +#define O_EMPTYPATH (1 << 26) +#endif + +#define EMPTYPATH_TEST_FILE "/tmp/emptypath_test" + +FIXTURE(emptypath) { + int opath_fd; +}; + +FIXTURE_SETUP(emptypath) +{ + int fd; + + self->opath_fd = -1; + + 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); + + 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)); + } +} + +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 diff --git a/tools/testing/selftests/filesystems/openat2/helpers.h b/tools/testing/selftests/filesystems/openat2/helpers.h new file mode 100644 index 000000000..3f01fb68c --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/helpers.h @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: Aleksa Sarai <cyphar@cyphar.com> + * Copyright (C) 2018-2019 SUSE LLC. + * Copyright (C) 2026 Amutable GmbH + */ + +#ifndef __RESOLVEAT_H__ +#define __RESOLVEAT_H__ + +#define _GNU_SOURCE +#include <stdint.h> +#include <stdbool.h> +#include <errno.h> +#include <limits.h> +#include <linux/types.h> +#include <linux/unistd.h> +#include <linux/openat2.h> +#include "kselftest_harness.h" + +#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); }))) + +#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */ +#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0 + +__maybe_unused +static bool needs_openat2(const struct open_how *how) +{ + return how->resolve != 0; +} + +__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); + + 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(struct __test_metadata *_metadata, int fd) +{ + char *target, *tmp; + + ASSERT_GT(asprintf(&tmp, "/proc/self/fd/%d", fd), 0); + + target = malloc(PATH_MAX); + ASSERT_NE(target, NULL); + memset(target, 0, PATH_MAX); + + ASSERT_GT(readlink(tmp, target, PATH_MAX), 0); + + free(tmp); + return target; +} + +__maybe_unused +static bool fdequal(struct __test_metadata *_metadata, int fd, + int dfd, const char *path) +{ + char *fdpath, *dfdpath, *other; + bool cmp; + + fdpath = fdreadlink(_metadata, fd); + dfdpath = fdreadlink(_metadata, dfd); + + 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); + + 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__ */ diff --git a/tools/testing/selftests/filesystems/openat2/openat2_test.c b/tools/testing/selftests/filesystems/openat2/openat2_test.c new file mode 100644 index 000000000..6f5afbe2d --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/openat2_test.c @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: Aleksa Sarai <cyphar@cyphar.com> + * Copyright (C) 2018-2019 SUSE LLC. + */ + +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ // Use ll64 +#include <fcntl.h> +#include <sched.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/mount.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> + +#include "helpers.h" +#include "kselftest_harness.h" + +/* + * O_LARGEFILE is set to 0 by glibc. + * XXX: This is wrong on {mips, parisc, powerpc, sparc}. + */ +#undef O_LARGEFILE +#ifdef __aarch64__ +#define O_LARGEFILE 0x20000 +#else +#define O_LARGEFILE 0x8000 +#endif + +struct open_how_ext { + struct open_how inner; + uint32_t extra1; + char pad1[128]; + uint32_t extra2; + char pad2[128]; + uint32_t extra3; +}; + +struct struct_test { + const char *name; + struct open_how_ext arg; + size_t size; + int err; +}; + +struct flag_test { + const char *name; + struct open_how how; + int err; +}; + +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", + .arg.inner.flags = O_RDONLY, + .size = sizeof(struct open_how) }, + /* Bigger struct, with zeroed out end. */ + { .name = "bigger struct (zeroed out)", + .arg.inner.flags = O_RDONLY, + .size = sizeof(struct open_how_ext) }, + + /* TODO: Once expanded, check zero-padding. */ + + /* Smaller than version-0 struct. */ + { .name = "zero-sized 'struct'", + .arg.inner.flags = O_RDONLY, .size = 0, .err = -EINVAL }, + { .name = "smaller-than-v0 struct", + .arg.inner.flags = O_RDONLY, + .size = OPEN_HOW_SIZE_VER0 - 1, .err = -EINVAL }, + + /* Bigger struct, with non-zero trailing bytes. */ + { .name = "bigger struct (non-zero data in first 'future field')", + .arg.inner.flags = O_RDONLY, .arg.extra1 = 0xdeadbeef, + .size = sizeof(struct open_how_ext), .err = -E2BIG }, + { .name = "bigger struct (non-zero data in middle of 'future fields')", + .arg.inner.flags = O_RDONLY, .arg.extra2 = 0xfeedcafe, + .size = sizeof(struct open_how_ext), .err = -E2BIG }, + { .name = "bigger struct (non-zero data at end of 'future fields')", + .arg.inner.flags = O_RDONLY, .arg.extra3 = 0xabad1dea, + .size = sizeof(struct open_how_ext), .err = -E2BIG }, + }; + + 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]; + void *copy = NULL, *how_copy = &how_ext; + char *fdpath = NULL; + + if (misalign) { + /* + * Explicitly misalign the structure copying it with the given + * (mis)alignment offset. The other data is set to be non-zero to + * make sure that non-zero bytes outside the struct aren't checked + * + * This is effectively to check that is_zeroed_user() works. + */ + copy = malloc(misalign + sizeof(how_ext)); + how_copy = copy + misalign; + memset(copy, 0xff, misalign); + memcpy(how_copy, &how_ext, sizeof(how_ext)); + } + + fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size); + if (fd >= 0) { + fdpath = fdreadlink(_metadata, fd); + close(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)); + } + } + + free(copy); + free(fdpath); + } + } +} + +/* 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. */ + { .name = "incompatible flags (O_TMPFILE | O_PATH)", + .how.flags = O_TMPFILE | O_PATH | O_RDWR, .err = -EINVAL }, + { .name = "incompatible flags (O_TMPFILE | O_CREAT)", + .how.flags = O_TMPFILE | O_CREAT | O_RDWR, .err = -EINVAL }, + + /* O_PATH only permits certain other flags to be set ... */ + { .name = "compatible flags (O_PATH | O_CLOEXEC)", + .how.flags = O_PATH | O_CLOEXEC }, + { .name = "compatible flags (O_PATH | O_DIRECTORY)", + .how.flags = O_PATH | O_DIRECTORY }, + { .name = "compatible flags (O_PATH | O_NOFOLLOW)", + .how.flags = O_PATH | O_NOFOLLOW }, + /* ... and others are absolutely not permitted. */ + { .name = "incompatible flags (O_PATH | O_RDWR)", + .how.flags = O_PATH | O_RDWR, .err = -EINVAL }, + { .name = "incompatible flags (O_PATH | O_CREAT)", + .how.flags = O_PATH | O_CREAT, .err = -EINVAL }, + { .name = "incompatible flags (O_PATH | O_EXCL)", + .how.flags = O_PATH | O_EXCL, .err = -EINVAL }, + { .name = "incompatible flags (O_PATH | O_NOCTTY)", + .how.flags = O_PATH | O_NOCTTY, .err = -EINVAL }, + { .name = "incompatible flags (O_PATH | O_DIRECT)", + .how.flags = O_PATH | O_DIRECT, .err = -EINVAL }, + { .name = "incompatible flags (O_PATH | O_LARGEFILE)", + .how.flags = O_PATH | O_LARGEFILE, .err = -EINVAL }, + + /* ->mode must only be set with O_{CREAT,TMPFILE}. */ + { .name = "non-zero how.mode and O_RDONLY", + .how.flags = O_RDONLY, .how.mode = 0600, .err = -EINVAL }, + { .name = "non-zero how.mode and O_PATH", + .how.flags = O_PATH, .how.mode = 0600, .err = -EINVAL }, + { .name = "valid how.mode and O_CREAT", + .how.flags = O_CREAT, .how.mode = 0600 }, + { .name = "valid how.mode and O_TMPFILE", + .how.flags = O_TMPFILE | O_RDWR, .how.mode = 0600 }, + /* ->mode must only contain 0777 bits. */ + { .name = "invalid how.mode and O_CREAT", + .how.flags = O_CREAT, + .how.mode = 0xFFFF, .err = -EINVAL }, + { .name = "invalid (very large) how.mode and O_CREAT", + .how.flags = O_CREAT, + .how.mode = 0xC000000000000000ULL, .err = -EINVAL }, + { .name = "invalid how.mode and O_TMPFILE", + .how.flags = O_TMPFILE | O_RDWR, + .how.mode = 0x1337, .err = -EINVAL }, + { .name = "invalid (very large) how.mode and O_TMPFILE", + .how.flags = O_TMPFILE | O_RDWR, + .how.mode = 0x0000A00000000000ULL, .err = -EINVAL }, + + /* ->resolve flags must not conflict. */ + { .name = "incompatible resolve flags (BENEATH | IN_ROOT)", + .how.flags = O_RDONLY, + .how.resolve = RESOLVE_BENEATH | RESOLVE_IN_ROOT, + .err = -EINVAL }, + + /* ->resolve must only contain RESOLVE_* flags. */ + { .name = "invalid how.resolve and O_RDONLY", + .how.flags = O_RDONLY, + .how.resolve = 0x1337, .err = -EINVAL }, + { .name = "invalid how.resolve and O_CREAT", + .how.flags = O_CREAT, + .how.resolve = 0x1337, .err = -EINVAL }, + { .name = "invalid how.resolve and O_TMPFILE", + .how.flags = O_TMPFILE | O_RDWR, + .how.resolve = 0x1337, .err = -EINVAL }, + { .name = "invalid how.resolve and O_PATH", + .how.flags = O_PATH, + .how.resolve = 0x1337, .err = -EINVAL }, + + /* currently unknown upper 32 bit rejected. */ + { .name = "currently unknown bit (1 << 63)", + .how.flags = O_RDONLY | (1ULL << 63), + .how.resolve = 0, .err = -EINVAL }, + }; + + for (int i = 0; i < ARRAY_SIZE(tests); i++) { + int fd, fdflags = -1; + char *path, *fdpath = NULL; + struct flag_test *test = &tests[i]; + + path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : "."; + unlink(path); + + fd = sys_openat2(AT_FDCWD, path, &test->how); + if (fd < 0 && fd == -EOPNOTSUPP) { + /* + * Skip the testcase if it failed because not supported + * by FS. (e.g. a valid O_TMPFILE combination on NFS) + */ + TH_LOG("openat2 with %s not supported by FS -- skipping", + test->name); + continue; + } + + 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(_metadata, fd); + fdflags = fcntl(fd, F_GETFL); + otherflags = fcntl(fd, F_GETFD); + close(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; + + 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); + } + + free(fdpath); + } +} + +#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 diff --git a/tools/testing/selftests/filesystems/openat2/rename_attack_test.c b/tools/testing/selftests/filesystems/openat2/rename_attack_test.c new file mode 100644 index 000000000..1f33c34f5 --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/rename_attack_test.c @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: Aleksa Sarai <cyphar@cyphar.com> + * Copyright (C) 2018-2019 SUSE LLC. + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/mount.h> +#include <sys/mman.h> +#include <sys/prctl.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <syscall.h> +#include <limits.h> +#include <unistd.h> + +#include "helpers.h" +#include "kselftest_harness.h" + +#define ROUNDS 400000 + +/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */ +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. */ + ASSERT_EQ(prctl(PR_SET_PDEATHSIG, SIGKILL), 0); + + /* Swap @a and @b. */ + for (;;) + renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE); + exit(1); +} + +/* + * Construct a test directory with the following structure: + * + * root/ + * |-- a/ + * | `-- c/ + * `-- b/ + */ +FIXTURE(rename_attack) { + int dfd; + int afd; + pid_t child; +}; + +FIXTURE_SETUP(rename_attack) +{ + 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); +} + +FIXTURE_TEARDOWN(rename_attack) +{ + 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 = variant->resolve, + }; + + if (!openat2_supported) { + how.resolve = 0; + TH_LOG("openat2(2) unsupported -- using openat(2) instead"); + } + + for (int i = 0; i < ROUNDS; i++) { + int fd; + + if (openat2_supported) + fd = sys_openat2(self->afd, victim_path, &how); + else + fd = sys_openat(self->afd, victim_path, &how); + + if (fd < 0) { + if (fd == -EAGAIN) + eagains++; + else if (fd == -EXDEV) + exdevs++; + else if (fd == -ENOENT) + escapes++; /* escaped outside and got ENOENT... */ + else + other_errs++; /* unexpected error */ + } else { + if (fdequal(_metadata, fd, self->afd, NULL)) + successes++; + else + escapes++; /* we got an unexpected fd */ + } + if (fd >= 0) + close(fd); + } + + TH_LOG("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%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); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/openat2/resolve_test.c b/tools/testing/selftests/filesystems/openat2/resolve_test.c new file mode 100644 index 000000000..eacde59ce --- /dev/null +++ b/tools/testing/selftests/filesystems/openat2/resolve_test.c @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: Aleksa Sarai <cyphar@cyphar.com> + * Copyright (C) 2018-2019 SUSE LLC. + */ + +#define _GNU_SOURCE +#include <fcntl.h> +#include <sched.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/mount.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.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: + * + * root/ + * |-- procexe -> /proc/self/exe + * |-- procroot -> /proc/self/root + * |-- root/ + * |-- mnt/ [mountpoint] + * | |-- self -> ../mnt/ + * | `-- absself -> /mnt/ + * |-- etc/ + * | `-- passwd + * |-- creatlink -> /newfile3 + * |-- reletc -> etc/ + * |-- relsym -> etc/passwd + * |-- absetc -> /etc/ + * |-- abssym -> /etc/passwd + * |-- abscheeky -> /cheeky + * `-- cheeky/ + * |-- absself -> / + * |-- self -> ../../root/ + * |-- garbageself -> /../../root/ + * |-- passwd -> ../cheeky/../etc/../etc/passwd + * |-- abspasswd -> /../cheeky/../etc/../etc/passwd + * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd + * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd + */ +FIXTURE(openat2_resolve) { + int rootfd; + int hardcoded_fd; + char *hardcoded_fdpath; + char *procselfexe; +}; + +FIXTURE_SETUP(openat2_resolve) +{ + 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. */ + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount("", "/tmp", "", MS_PRIVATE, ""), 0); + + /* Make the top-level directory. */ + ASSERT_NE(mkdtemp(dirname), NULL); + dfd = open(dirname, O_PATH | O_DIRECTORY); + ASSERT_GE(dfd, 0); + + /* A sub-directory which is actually used for tests. */ + ASSERT_EQ(mkdirat(dfd, "root", 0755), 0); + tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + close(dfd); + dfd = tmpfd; + + 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. */ + 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); + + ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0); + ASSERT_GE(touchat(dfd, "etc/passwd"), 0); + + 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); + + ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0); + + ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0); + ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0); + ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0); + + ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd", + dfd, "cheeky/passwd"), 0); + ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd", + dfd, "cheeky/abspasswd"), 0); + + ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd", + dfd, "cheeky/dotdotlink"), 0); + ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd", + dfd, "cheeky/garbagelink"), 0); + + 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); +} + +FIXTURE_TEARDOWN(openat2_resolve) +{ + free(self->procselfexe); + free(self->hardcoded_fdpath); + if (self->hardcoded_fd >= 0) + close(self->hardcoded_fd); + if (self->rootfd >= 0) + close(self->rootfd); +} + +/* 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, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] absolute link to $root", + .path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] chained absolute links to $root", + .path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] jump outside $root", + .path = "..", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] temporary jump outside $root", + .path = "../root/", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] symlink temporary jump outside $root", + .path = "cheeky/self", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] chained symlink temporary jump outside $root", + .path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] garbage links to $root", + .path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] chained garbage links to $root", + .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + /* Only relative paths that stay inside dirfd should work. */ + { .name = "[beneath] ordinary path to 'root'", + .path = "root", .how.resolve = RESOLVE_BENEATH, + .out.path = "root", .pass = true }, + { .name = "[beneath] ordinary path to 'etc'", + .path = "etc", .how.resolve = RESOLVE_BENEATH, + .out.path = "etc", .pass = true }, + { .name = "[beneath] ordinary path to 'etc/passwd'", + .path = "etc/passwd", .how.resolve = RESOLVE_BENEATH, + .out.path = "etc/passwd", .pass = true }, + { .name = "[beneath] relative symlink inside $root", + .path = "relsym", .how.resolve = RESOLVE_BENEATH, + .out.path = "etc/passwd", .pass = true }, + { .name = "[beneath] chained-'..' relative symlink inside $root", + .path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH, + .out.path = "etc/passwd", .pass = true }, + { .name = "[beneath] absolute symlink component outside $root", + .path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] absolute symlink target outside $root", + .path = "abssym", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] absolute path outside $root", + .path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] cheeky absolute path outside $root", + .path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] chained cheeky absolute path outside $root", + .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + /* Tricky paths should fail. */ + { .name = "[beneath] tricky '..'-chained symlink outside $root", + .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] tricky absolute + '..'-chained symlink outside $root", + .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] tricky garbage link outside $root", + .path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + { .name = "[beneath] tricky absolute + garbage link outside $root", + .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH, + .out.err = -EXDEV, .pass = false }, + }; + + 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 }, + { .name = "[in_root] absolute symlink to /root", + .path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT, + .out.path = NULL, .pass = true }, + { .name = "[in_root] chained absolute symlinks to /root", + .path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT, + .out.path = NULL, .pass = true }, + { .name = "[in_root] '..' at root", + .path = "..", .how.resolve = RESOLVE_IN_ROOT, + .out.path = NULL, .pass = true }, + { .name = "[in_root] '../root' at root", + .path = "../root/", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "root", .pass = true }, + { .name = "[in_root] relative symlink containing '..' above root", + .path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "root", .pass = true }, + { .name = "[in_root] garbage link to /root", + .path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "root", .pass = true }, + { .name = "[in_root] chained garbage links to /root", + .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "root", .pass = true }, + { .name = "[in_root] relative path to 'root'", + .path = "root", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "root", .pass = true }, + { .name = "[in_root] relative path to 'etc'", + .path = "etc", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc", .pass = true }, + { .name = "[in_root] relative path to 'etc/passwd'", + .path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] relative symlink to 'etc/passwd'", + .path = "relsym", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'", + .path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'", + .path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] absolute symlink to 'etc/passwd'", + .path = "abssym", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] absolute path 'etc/passwd'", + .path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] cheeky absolute path 'etc/passwd'", + .path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] chained cheeky absolute path 'etc/passwd'", + .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] tricky '..'-chained symlink outside $root", + .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] tricky absolute + '..'-chained symlink outside $root", + .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root", + .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] tricky garbage link outside $root", + .path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] tricky absolute + garbage link outside $root", + .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + { .name = "[in_root] tricky absolute path + absolute + garbage link outside $root", + .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT, + .out.path = "etc/passwd", .pass = true }, + /* O_CREAT should handle trailing symlinks correctly. */ + { .name = "[in_root] O_CREAT of relative path inside $root", + .path = "newfile1", .how.flags = O_CREAT, + .how.mode = 0700, + .how.resolve = RESOLVE_IN_ROOT, + .out.path = "newfile1", .pass = true }, + { .name = "[in_root] O_CREAT of absolute path", + .path = "/newfile2", .how.flags = O_CREAT, + .how.mode = 0700, + .how.resolve = RESOLVE_IN_ROOT, + .out.path = "newfile2", .pass = true }, + { .name = "[in_root] O_CREAT of tricky symlink outside root", + .path = "/creatlink", .how.flags = O_CREAT, + .how.mode = 0700, + .how.resolve = RESOLVE_IN_ROOT, + .out.path = "newfile3", .pass = true }, + }; + + 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, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] cross into $mnt/", + .path = "mnt/", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] cross into $mnt/.", + .path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + /* Crossing *up* out of a mountpoint is disallowed. */ + { .name = "[no_xdev] goto mountpoint root", + .dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV, + .out.path = "mnt", .pass = true }, + { .name = "[no_xdev] cross up through '..'", + .dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] temporary cross up through '..'", + .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] temporary relative symlink cross up", + .dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] temporary absolute symlink cross up", + .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + /* Jumping to "/" is ok, but later components cannot cross. */ + { .name = "[no_xdev] jump to / directly", + .dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV, + .out.path = "/", .pass = true }, + { .name = "[no_xdev] jump to / (from /) directly", + .dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV, + .out.path = "/", .pass = true }, + { .name = "[no_xdev] jump to / then proc", + .path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] jump to / then tmp", + .path = "/tmp", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + /* Magic-links are blocked since they can switch vfsmounts. */ + { .name = "[no_xdev] cross through magic-link to self/root", + .dir = "/proc", .path = "self/root", .how.resolve = RESOLVE_NO_XDEV, + .out.err = -EXDEV, .pass = false }, + { .name = "[no_xdev] cross through magic-link to self/cwd", + .dir = "/proc", .path = "self/cwd", .how.resolve = RESOLVE_NO_XDEV, + .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 = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV, + .out.path = "/proc", .pass = true, }, + }; + + 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, + .out.path = "etc/passwd", .pass = true }, + /* Magic-links should not work. */ + { .name = "[no_magiclinks] symlink to magic-link", + .path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_magiclinks] normal path to magic-link", + .path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS, + .out.err = -ELOOP, .pass = false }, + { .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 = 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 }, + { .name = "[no_magiclinks] magic-link path component", + .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW", + .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW, + .how.resolve = RESOLVE_NO_MAGICLINKS, + .out.err = -ELOOP, .pass = false }, + }; + + 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, + .out.path = NULL, .pass = true }, + { .name = "[no_symlinks] ordinary path to 'root'", + .path = "root", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.path = "root", .pass = true }, + { .name = "[no_symlinks] ordinary path to 'etc'", + .path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.path = "etc", .pass = true }, + { .name = "[no_symlinks] ordinary path to 'etc/passwd'", + .path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.path = "etc/passwd", .pass = true }, + /* Regular symlinks are blocked. */ + { .name = "[no_symlinks] relative symlink target", + .path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] relative symlink component", + .path = "reletc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] absolute symlink target", + .path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] absolute symlink component", + .path = "absetc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] cheeky garbage link", + .path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] cheeky absolute + garbage link", + .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] cheeky absolute + absolute symlink", + .path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + /* Trailing symlinks with NO_FOLLOW. */ + { .name = "[no_symlinks] relative symlink with O_NOFOLLOW", + .path = "relsym", .how.flags = O_NOFOLLOW, + .how.resolve = RESOLVE_NO_SYMLINKS, + .out.path = "relsym", .pass = true }, + { .name = "[no_symlinks] absolute symlink with O_NOFOLLOW", + .path = "abssym", .how.flags = O_NOFOLLOW, + .how.resolve = RESOLVE_NO_SYMLINKS, + .out.path = "abssym", .pass = true }, + { .name = "[no_symlinks] trailing symlink with O_NOFOLLOW", + .path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW, + .how.resolve = RESOLVE_NO_SYMLINKS, + .out.path = "cheeky/garbagelink", .pass = true }, + { .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW", + .path = "abscheeky/absself", .how.flags = O_NOFOLLOW, + .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + { .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW", + .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW, + .how.resolve = RESOLVE_NO_SYMLINKS, + .out.err = -ELOOP, .pass = false }, + }; + + for (int i = 0; i < ARRAY_SIZE(tests); i++) + verify_resolve_test(_metadata, self->rootfd, + self->hardcoded_fd, &tests[i]); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/overlayfs/.gitignore b/tools/testing/selftests/filesystems/overlayfs/.gitignore new file mode 100644 index 000000000..e23a18c8b --- /dev/null +++ b/tools/testing/selftests/filesystems/overlayfs/.gitignore @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +dev_in_maps +set_layers_via_fds diff --git a/tools/testing/selftests/filesystems/overlayfs/Makefile b/tools/testing/selftests/filesystems/overlayfs/Makefile new file mode 100644 index 000000000..d3ad4a77d --- /dev/null +++ b/tools/testing/selftests/filesystems/overlayfs/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += -Wall +CFLAGS += $(KHDR_INCLUDES) +LDLIBS += -lcap + +LOCAL_HDRS += ../wrappers.h log.h + +TEST_GEN_PROGS := dev_in_maps +TEST_GEN_PROGS += set_layers_via_fds + +include ../../lib.mk + +$(OUTPUT)/set_layers_via_fds: ../utils.c diff --git a/tools/testing/selftests/filesystems/overlayfs/dev_in_maps.c b/tools/testing/selftests/filesystems/overlayfs/dev_in_maps.c new file mode 100644 index 000000000..8924cea6a --- /dev/null +++ b/tools/testing/selftests/filesystems/overlayfs/dev_in_maps.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ // Use ll64 + +#include <inttypes.h> +#include <unistd.h> +#include <stdio.h> + +#include <linux/unistd.h> +#include <linux/types.h> +#include <linux/mount.h> +#include <sys/syscall.h> +#include <sys/stat.h> +#include <sys/mman.h> +#include <sched.h> +#include <fcntl.h> + +#include "kselftest.h" +#include "log.h" +#include "../wrappers.h" + +static long get_file_dev_and_inode(void *addr, struct statx *stx) +{ + char buf[4096]; + FILE *mapf; + + mapf = fopen("/proc/self/maps", "r"); + if (mapf == NULL) + return pr_perror("fopen(/proc/self/maps)"); + + while (fgets(buf, sizeof(buf), mapf)) { + unsigned long start, end; + uint32_t maj, min; + __u64 ino; + + if (sscanf(buf, "%lx-%lx %*s %*s %x:%x %llu", + &start, &end, &maj, &min, &ino) != 5) + return pr_perror("unable to parse: %s", buf); + if (start == (unsigned long)addr) { + stx->stx_dev_major = maj; + stx->stx_dev_minor = min; + stx->stx_ino = ino; + return 0; + } + } + + return pr_err("unable to find the mapping"); +} + +static int ovl_mount(void) +{ + int tmpfs, fsfd, ovl; + + fsfd = sys_fsopen("tmpfs", 0); + if (fsfd == -1) + return pr_perror("fsopen(tmpfs)"); + + if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) == -1) + return pr_perror("FSCONFIG_CMD_CREATE"); + + tmpfs = sys_fsmount(fsfd, 0, 0); + if (tmpfs == -1) + return pr_perror("fsmount"); + + close(fsfd); + + /* overlayfs can't be constructed on top of a detached mount. */ + if (sys_move_mount(tmpfs, "", AT_FDCWD, "/tmp", MOVE_MOUNT_F_EMPTY_PATH)) + return pr_perror("move_mount"); + close(tmpfs); + + if (mkdir("/tmp/w", 0755) == -1 || + mkdir("/tmp/u", 0755) == -1 || + mkdir("/tmp/l", 0755) == -1) + return pr_perror("mkdir"); + + fsfd = sys_fsopen("overlay", 0); + if (fsfd == -1) + return pr_perror("fsopen(overlay)"); + if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "test", 0) == -1 || + sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "lowerdir", "/tmp/l", 0) == -1 || + sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "upperdir", "/tmp/u", 0) == -1 || + sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "workdir", "/tmp/w", 0) == -1) + return pr_perror("fsconfig"); + if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) == -1) + return pr_perror("fsconfig"); + ovl = sys_fsmount(fsfd, 0, 0); + if (ovl == -1) + return pr_perror("fsmount"); + + return ovl; +} + +/* + * Check that the file device and inode shown in /proc/pid/maps match values + * returned by stat(2). + */ +static int test(void) +{ + struct statx stx, mstx; + int ovl, fd; + void *addr; + + ovl = ovl_mount(); + if (ovl == -1) + return -1; + + fd = openat(ovl, "test", O_RDWR | O_CREAT, 0644); + if (fd == -1) + return pr_perror("openat"); + + addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) + return pr_perror("mmap"); + + if (get_file_dev_and_inode(addr, &mstx)) + return -1; + if (statx(fd, "", AT_EMPTY_PATH | AT_STATX_SYNC_AS_STAT, STATX_INO, &stx)) + return pr_perror("statx"); + + if (stx.stx_dev_major != mstx.stx_dev_major || + stx.stx_dev_minor != mstx.stx_dev_minor || + stx.stx_ino != mstx.stx_ino) + return pr_fail("unmatched dev:ino %x:%x:%llx (expected %x:%x:%llx)\n", + mstx.stx_dev_major, mstx.stx_dev_minor, mstx.stx_ino, + stx.stx_dev_major, stx.stx_dev_minor, stx.stx_ino); + + ksft_test_result_pass("devices are matched\n"); + return 0; +} + +int main(int argc, char **argv) +{ + int fsfd; + + fsfd = sys_fsopen("overlay", 0); + if (fsfd == -1) { + ksft_test_result_skip("unable to create overlay mount\n"); + return 1; + } + close(fsfd); + + /* Create a new mount namespace to not care about cleaning test mounts. */ + if (unshare(CLONE_NEWNS) == -1) { + ksft_test_result_skip("unable to create a new mount namespace\n"); + return 1; + } + if (sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) == -1) { + pr_perror("mount"); + return 1; + } + + ksft_set_plan(1); + + if (test()) + return 1; + + ksft_exit_pass(); + return 0; +} diff --git a/tools/testing/selftests/filesystems/overlayfs/log.h b/tools/testing/selftests/filesystems/overlayfs/log.h new file mode 100644 index 000000000..db64df2a8 --- /dev/null +++ b/tools/testing/selftests/filesystems/overlayfs/log.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#ifndef __SELFTEST_TIMENS_LOG_H__ +#define __SELFTEST_TIMENS_LOG_H__ + +#define pr_msg(fmt, lvl, ...) \ + ksft_print_msg("[%s] (%s:%d)\t" fmt "\n", \ + lvl, __FILE__, __LINE__, ##__VA_ARGS__) + +#define pr_p(func, fmt, ...) func(fmt ": %m", ##__VA_ARGS__) + +#define pr_err(fmt, ...) \ + ({ \ + ksft_test_result_error(fmt "\n", ##__VA_ARGS__); \ + -1; \ + }) + +#define pr_fail(fmt, ...) \ + ({ \ + ksft_test_result_fail(fmt, ##__VA_ARGS__); \ + -1; \ + }) + +#define pr_perror(fmt, ...) pr_p(pr_err, fmt, ##__VA_ARGS__) + +#endif diff --git a/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c b/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c new file mode 100644 index 000000000..3c0b93183 --- /dev/null +++ b/tools/testing/selftests/filesystems/overlayfs/set_layers_via_fds.c @@ -0,0 +1,720 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#define __SANE_USERSPACE_TYPES__ // Use ll64 + +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/mount.h> +#include <unistd.h> + +#include "kselftest_harness.h" +#include "../../pidfd/pidfd.h" +#include "log.h" +#include "../utils.h" +#include "../wrappers.h" + +FIXTURE(set_layers_via_fds) { + int pidfd; +}; + +FIXTURE_SETUP(set_layers_via_fds) +{ + self->pidfd = -EBADF; + EXPECT_EQ(mkdir("/set_layers_via_fds", 0755), 0); + EXPECT_EQ(mkdir("/set_layers_via_fds_tmpfs", 0755), 0); +} + +FIXTURE_TEARDOWN(set_layers_via_fds) +{ + if (self->pidfd >= 0) { + EXPECT_EQ(sys_pidfd_send_signal(self->pidfd, SIGKILL, NULL, 0), 0); + EXPECT_EQ(close(self->pidfd), 0); + } + umount2("/set_layers_via_fds", 0); + EXPECT_EQ(rmdir("/set_layers_via_fds"), 0); + + umount2("/set_layers_via_fds_tmpfs", 0); + EXPECT_EQ(rmdir("/set_layers_via_fds_tmpfs"), 0); +} + +TEST_F(set_layers_via_fds, set_layers_via_fds) +{ + int fd_context, fd_tmpfs, fd_overlay; + int layer_fds[] = { [0 ... 8] = -EBADF }; + bool layers_found[] = { [0 ... 8] = false }; + size_t len = 0; + char *line = NULL; + FILE *f_mountinfo; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l3", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l4", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "d1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "d2", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "d3", 0755), 0); + + layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY); + ASSERT_GE(layer_fds[0], 0); + + layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY); + ASSERT_GE(layer_fds[1], 0); + + layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY); + ASSERT_GE(layer_fds[2], 0); + + layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY); + ASSERT_GE(layer_fds[3], 0); + + layer_fds[4] = openat(fd_tmpfs, "l3", O_DIRECTORY); + ASSERT_GE(layer_fds[4], 0); + + layer_fds[5] = openat(fd_tmpfs, "l4", O_DIRECTORY); + ASSERT_GE(layer_fds[5], 0); + + layer_fds[6] = openat(fd_tmpfs, "d1", O_DIRECTORY); + ASSERT_GE(layer_fds[6], 0); + + layer_fds[7] = openat(fd_tmpfs, "d2", O_DIRECTORY); + ASSERT_GE(layer_fds[7], 0); + + layer_fds[8] = openat(fd_tmpfs, "d3", O_DIRECTORY); + ASSERT_GE(layer_fds[8], 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[4]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[5]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[6]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[7]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[8]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_STRING, "metacopy", "on", 0), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + + ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0); + + f_mountinfo = fopen("/proc/self/mountinfo", "r"); + ASSERT_NE(f_mountinfo, NULL); + + while (getline(&line, &len, f_mountinfo) != -1) { + char *haystack = line; + + if (strstr(haystack, "workdir=/tmp/w")) + layers_found[0] = true; + if (strstr(haystack, "upperdir=/tmp/u")) + layers_found[1] = true; + if (strstr(haystack, "lowerdir+=/tmp/l1")) + layers_found[2] = true; + if (strstr(haystack, "lowerdir+=/tmp/l2")) + layers_found[3] = true; + if (strstr(haystack, "lowerdir+=/tmp/l3")) + layers_found[4] = true; + if (strstr(haystack, "lowerdir+=/tmp/l4")) + layers_found[5] = true; + if (strstr(haystack, "datadir+=/tmp/d1")) + layers_found[6] = true; + if (strstr(haystack, "datadir+=/tmp/d2")) + layers_found[7] = true; + if (strstr(haystack, "datadir+=/tmp/d3")) + layers_found[8] = true; + } + free(line); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) { + ASSERT_EQ(layers_found[i], true); + ASSERT_EQ(close(layer_fds[i]), 0); + } + + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); + ASSERT_EQ(fclose(f_mountinfo), 0); +} + +TEST_F(set_layers_via_fds, set_500_layers_via_fds) +{ + int fd_context, fd_tmpfs, fd_overlay, fd_work, fd_upper, fd_lower; + int layer_fds[500] = { [0 ... 499] = -EBADF }; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) { + char path[100]; + + sprintf(path, "l%d", i); + ASSERT_EQ(mkdirat(fd_tmpfs, path, 0755), 0); + layer_fds[i] = openat(fd_tmpfs, path, O_DIRECTORY); + ASSERT_GE(layer_fds[i], 0); + } + + ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0); + fd_work = openat(fd_tmpfs, "w", O_DIRECTORY); + ASSERT_GE(fd_work, 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + fd_upper = openat(fd_tmpfs, "u", O_DIRECTORY); + ASSERT_GE(fd_upper, 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "l501", 0755), 0); + fd_lower = openat(fd_tmpfs, "l501", O_DIRECTORY); + ASSERT_GE(fd_lower, 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, fd_work), 0); + ASSERT_EQ(close(fd_work), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, fd_upper), 0); + ASSERT_EQ(close(fd_upper), 0); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) { + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[i]), 0); + ASSERT_EQ(close(layer_fds[i]), 0); + } + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, fd_lower), 0); + ASSERT_EQ(close(fd_lower), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); +} + +TEST_F(set_layers_via_fds, set_override_creds) +{ + int fd_context, fd_tmpfs, fd_overlay; + int layer_fds[] = { [0 ... 3] = -EBADF }; + pid_t pid; + int pidfd; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0); + + layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY); + ASSERT_GE(layer_fds[0], 0); + + layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY); + ASSERT_GE(layer_fds[1], 0); + + layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY); + ASSERT_GE(layer_fds[2], 0); + + layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY); + ASSERT_GE(layer_fds[3], 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_STRING, "metacopy", "on", 0), 0); + + pid = create_child(&pidfd, 0); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) { + TH_LOG("sys_fsconfig should have succeeded"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + ASSERT_GE(sys_waitid(P_PID, pid, NULL, WEXITED), 0); + ASSERT_GE(close(pidfd), 0); + + pid = create_child(&pidfd, 0); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "nooverride_creds", NULL, 0)) { + TH_LOG("sys_fsconfig should have succeeded"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + ASSERT_GE(sys_waitid(P_PID, pid, NULL, WEXITED), 0); + ASSERT_GE(close(pidfd), 0); + + pid = create_child(&pidfd, 0); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) { + TH_LOG("sys_fsconfig should have succeeded"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + ASSERT_GE(sys_waitid(P_PID, pid, NULL, WEXITED), 0); + ASSERT_GE(close(pidfd), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + + ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0); + + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); +} + +TEST_F(set_layers_via_fds, set_override_creds_invalid) +{ + int fd_context, fd_tmpfs, fd_overlay, ret; + int layer_fds[] = { [0 ... 3] = -EBADF }; + pid_t pid; + int fd_userns1, fd_userns2; + int ipc_sockets[2]; + char c; + const unsigned int predictable_fd_context_nr = 123; + + fd_userns1 = get_userns_fd(0, 0, 10000); + ASSERT_GE(fd_userns1, 0); + + fd_userns2 = get_userns_fd(0, 1234, 10000); + ASSERT_GE(fd_userns2, 0); + + ret = socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets); + ASSERT_GE(ret, 0); + + pid = create_child(&self->pidfd, 0); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (close(ipc_sockets[0])) { + TH_LOG("close should have succeeded"); + _exit(EXIT_FAILURE); + } + + if (!switch_userns(fd_userns2, 0, 0, false)) { + TH_LOG("switch_userns should have succeeded"); + _exit(EXIT_FAILURE); + } + + if (read_nointr(ipc_sockets[1], &c, 1) != 1) { + TH_LOG("read_nointr should have succeeded"); + _exit(EXIT_FAILURE); + } + + if (close(ipc_sockets[1])) { + TH_LOG("close should have succeeded"); + _exit(EXIT_FAILURE); + } + + if (!sys_fsconfig(predictable_fd_context_nr, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) { + TH_LOG("sys_fsconfig should have failed"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + ASSERT_EQ(close(ipc_sockets[1]), 0); + ASSERT_EQ(switch_userns(fd_userns1, 0, 0, false), true); + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0); + + layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY); + ASSERT_GE(layer_fds[0], 0); + + layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY); + ASSERT_GE(layer_fds[1], 0); + + layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY); + ASSERT_GE(layer_fds[2], 0); + + layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY); + ASSERT_GE(layer_fds[3], 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + ASSERT_EQ(dup3(fd_context, predictable_fd_context_nr, 0), predictable_fd_context_nr); + ASSERT_EQ(close(fd_context), 0); + fd_context = predictable_fd_context_nr; + ASSERT_EQ(write_nointr(ipc_sockets[0], "1", 1), 1); + ASSERT_EQ(close(ipc_sockets[0]), 0); + + ASSERT_EQ(wait_for_pid(pid), 0); + ASSERT_EQ(close(self->pidfd), 0); + self->pidfd = -EBADF; + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) + ASSERT_EQ(close(layer_fds[i]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "userxattr", NULL, 0), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + + ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0); + + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); + ASSERT_EQ(close(fd_userns1), 0); + ASSERT_EQ(close(fd_userns2), 0); +} + +TEST_F(set_layers_via_fds, set_override_creds_nomknod) +{ + int fd_context, fd_tmpfs, fd_overlay; + int layer_fds[] = { [0 ... 3] = -EBADF }; + pid_t pid; + int pidfd; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0); + + layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY); + ASSERT_GE(layer_fds[0], 0); + + layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY); + ASSERT_GE(layer_fds[1], 0); + + layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY); + ASSERT_GE(layer_fds[2], 0); + + layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY); + ASSERT_GE(layer_fds[3], 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "userxattr", NULL, 0), 0); + + pid = create_child(&pidfd, 0); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (!cap_down(CAP_MKNOD)) + _exit(EXIT_FAILURE); + + if (!cap_down(CAP_SYS_ADMIN)) + _exit(EXIT_FAILURE); + + if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) + _exit(EXIT_FAILURE); + + _exit(EXIT_SUCCESS); + } + ASSERT_EQ(sys_waitid(P_PID, pid, NULL, WEXITED), 0); + ASSERT_GE(close(pidfd), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + + ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(mknodat(fd_overlay, "dev-zero", S_IFCHR | 0644, makedev(1, 5)), -1); + ASSERT_EQ(errno, EPERM); + + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); +} + +TEST_F(set_layers_via_fds, set_500_layers_via_opath_fds) +{ + int fd_context, fd_tmpfs, fd_overlay, fd_work, fd_upper, fd_lower; + int layer_fds[500] = { [0 ... 499] = -EBADF }; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) { + char path[100]; + + sprintf(path, "l%d", i); + ASSERT_EQ(mkdirat(fd_tmpfs, path, 0755), 0); + layer_fds[i] = openat(fd_tmpfs, path, O_DIRECTORY | O_PATH); + ASSERT_GE(layer_fds[i], 0); + } + + ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0); + fd_work = openat(fd_tmpfs, "w", O_DIRECTORY | O_PATH); + ASSERT_GE(fd_work, 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + fd_upper = openat(fd_tmpfs, "u", O_DIRECTORY | O_PATH); + ASSERT_GE(fd_upper, 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "l501", 0755), 0); + fd_lower = openat(fd_tmpfs, "l501", O_DIRECTORY | O_PATH); + ASSERT_GE(fd_lower, 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0); + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, fd_work), 0); + ASSERT_EQ(close(fd_work), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, fd_upper), 0); + ASSERT_EQ(close(fd_upper), 0); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) { + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[i]), 0); + ASSERT_EQ(close(layer_fds[i]), 0); + } + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, fd_lower), 0); + ASSERT_EQ(close(fd_lower), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); +} + +TEST_F(set_layers_via_fds, set_layers_via_detached_mount_fds) +{ + int fd_context, fd_tmpfs, fd_overlay, fd_tmp; + int layer_fds[] = { [0 ... 8] = -EBADF }; + bool layers_found[] = { [0 ... 8] = false }; + size_t len = 0; + char *line = NULL; + FILE *f_mountinfo; + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); + + fd_context = sys_fsopen("tmpfs", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + fd_tmpfs = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_tmpfs, 0); + ASSERT_EQ(close(fd_context), 0); + + ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "u/upper", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "u/work", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l3", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "l4", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "d1", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "d2", 0755), 0); + ASSERT_EQ(mkdirat(fd_tmpfs, "d3", 0755), 0); + + ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/set_layers_via_fds_tmpfs", MOVE_MOUNT_F_EMPTY_PATH), 0); + + fd_tmp = open_tree(fd_tmpfs, "u", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(fd_tmp, 0); + + layer_fds[0] = openat(fd_tmp, "upper", O_CLOEXEC | O_DIRECTORY | O_PATH); + ASSERT_GE(layer_fds[0], 0); + + layer_fds[1] = openat(fd_tmp, "work", O_CLOEXEC | O_DIRECTORY | O_PATH); + ASSERT_GE(layer_fds[1], 0); + + layer_fds[2] = open_tree(fd_tmpfs, "l1", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[2], 0); + + layer_fds[3] = open_tree(fd_tmpfs, "l2", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[3], 0); + + layer_fds[4] = open_tree(fd_tmpfs, "l3", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[4], 0); + + layer_fds[5] = open_tree(fd_tmpfs, "l4", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[5], 0); + + layer_fds[6] = open_tree(fd_tmpfs, "d1", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[6], 0); + + layer_fds[7] = open_tree(fd_tmpfs, "d2", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[7], 0); + + layer_fds[8] = open_tree(fd_tmpfs, "d3", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + ASSERT_GE(layer_fds[8], 0); + + ASSERT_EQ(close(fd_tmpfs), 0); + + fd_context = sys_fsopen("overlay", 0); + ASSERT_GE(fd_context, 0); + + ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[0]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[1]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[4]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[5]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[6]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[7]), 0); + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[8]), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_STRING, "metacopy", "on", 0), 0); + + ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); + + fd_overlay = sys_fsmount(fd_context, 0, 0); + ASSERT_GE(fd_overlay, 0); + + ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0); + + f_mountinfo = fopen("/proc/self/mountinfo", "r"); + ASSERT_NE(f_mountinfo, NULL); + + while (getline(&line, &len, f_mountinfo) != -1) { + char *haystack = line; + + if (strstr(haystack, "workdir=/tmp/w")) + layers_found[0] = true; + if (strstr(haystack, "upperdir=/tmp/u")) + layers_found[1] = true; + if (strstr(haystack, "lowerdir+=/tmp/l1")) + layers_found[2] = true; + if (strstr(haystack, "lowerdir+=/tmp/l2")) + layers_found[3] = true; + if (strstr(haystack, "lowerdir+=/tmp/l3")) + layers_found[4] = true; + if (strstr(haystack, "lowerdir+=/tmp/l4")) + layers_found[5] = true; + if (strstr(haystack, "datadir+=/tmp/d1")) + layers_found[6] = true; + if (strstr(haystack, "datadir+=/tmp/d2")) + layers_found[7] = true; + if (strstr(haystack, "datadir+=/tmp/d3")) + layers_found[8] = true; + } + free(line); + + for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) { + ASSERT_EQ(layers_found[i], true); + ASSERT_EQ(close(layer_fds[i]), 0); + } + + ASSERT_EQ(close(fd_context), 0); + ASSERT_EQ(close(fd_overlay), 0); + ASSERT_EQ(fclose(f_mountinfo), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/statmount/.gitignore b/tools/testing/selftests/filesystems/statmount/.gitignore new file mode 100644 index 000000000..973363ad6 --- /dev/null +++ b/tools/testing/selftests/filesystems/statmount/.gitignore @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +statmount_test_ns +/*_test diff --git a/tools/testing/selftests/filesystems/statmount/Makefile b/tools/testing/selftests/filesystems/statmount/Makefile new file mode 100644 index 000000000..8e354fe99 --- /dev/null +++ b/tools/testing/selftests/filesystems/statmount/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +LDLIBS += -lcap + +TEST_GEN_PROGS := statmount_test statmount_test_ns listmount_test + +include ../../lib.mk + +$(OUTPUT)/statmount_test_ns: ../utils.c diff --git a/tools/testing/selftests/filesystems/statmount/listmount_test.c b/tools/testing/selftests/filesystems/statmount/listmount_test.c new file mode 100644 index 000000000..8bc82f38c --- /dev/null +++ b/tools/testing/selftests/filesystems/statmount/listmount_test.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2024 Christian Brauner <brauner@kernel.org> + +#define _GNU_SOURCE +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/mount.h> +#include <unistd.h> + +#include "statmount.h" +#include "kselftest_harness.h" + +#ifndef LISTMOUNT_REVERSE +#define LISTMOUNT_REVERSE (1 << 0) /* List later mounts first */ +#endif + +#define LISTMNT_BUFFER 10 + +/* Check that all mount ids are in increasing order. */ +TEST(listmount_forward) +{ + uint64_t list[LISTMNT_BUFFER], last_mnt_id = 0; + + for (;;) { + ssize_t nr_mounts; + + nr_mounts = listmount(LSMT_ROOT, 0, last_mnt_id, + list, LISTMNT_BUFFER, 0); + ASSERT_GE(nr_mounts, 0); + if (nr_mounts == 0) + break; + + for (size_t cur = 0; cur < nr_mounts; cur++) { + if (cur < nr_mounts - 1) + ASSERT_LT(list[cur], list[cur + 1]); + last_mnt_id = list[cur]; + } + } +} + +/* Check that all mount ids are in decreasing order. */ +TEST(listmount_backward) +{ + uint64_t list[LISTMNT_BUFFER], last_mnt_id = 0; + + for (;;) { + ssize_t nr_mounts; + + nr_mounts = listmount(LSMT_ROOT, 0, last_mnt_id, + list, LISTMNT_BUFFER, LISTMOUNT_REVERSE); + ASSERT_GE(nr_mounts, 0); + if (nr_mounts == 0) + break; + + for (size_t cur = 0; cur < nr_mounts; cur++) { + if (cur < nr_mounts - 1) + ASSERT_GT(list[cur], list[cur + 1]); + last_mnt_id = list[cur]; + } + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/statmount/statmount.h b/tools/testing/selftests/filesystems/statmount/statmount.h new file mode 100644 index 000000000..675f7cc00 --- /dev/null +++ b/tools/testing/selftests/filesystems/statmount/statmount.h @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#ifndef __STATMOUNT_H +#define __STATMOUNT_H + +#include <errno.h> +#include <stdint.h> +#include <stdlib.h> +#include <linux/mount.h> +#include <asm/unistd.h> + +#define STATMOUNT_BUFSIZE (1 << 15) + +#ifndef __NR_statmount + #if defined __alpha__ + #define __NR_statmount 567 + #elif defined _MIPS_SIM + #if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */ + #define __NR_statmount 4457 + #endif + #if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */ + #define __NR_statmount 6457 + #endif + #if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */ + #define __NR_statmount 5457 + #endif + #else + #define __NR_statmount 457 + #endif +#endif + +#ifndef __NR_listmount + #if defined __alpha__ + #define __NR_listmount 568 + #elif defined _MIPS_SIM + #if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */ + #define __NR_listmount 4458 + #endif + #if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */ + #define __NR_listmount 6458 + #endif + #if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */ + #define __NR_listmount 5458 + #endif + #else + #define __NR_listmount 458 + #endif +#endif + +static inline int statmount(uint64_t mnt_id, uint64_t mnt_ns_id, uint32_t fd, + uint64_t mask, struct statmount *buf, size_t bufsize, + unsigned int flags) +{ + struct mnt_id_req req = { + .size = MNT_ID_REQ_SIZE_VER0, + .param = mask, + }; + + if (flags & STATMOUNT_BY_FD) { + req.size = MNT_ID_REQ_SIZE_VER1; + req.mnt_fd = fd; + } else { + req.mnt_id = mnt_id; + if (mnt_ns_id) { + req.size = MNT_ID_REQ_SIZE_VER1; + req.mnt_ns_id = mnt_ns_id; + } + } + + return syscall(__NR_statmount, &req, buf, bufsize, flags); +} + +static inline ssize_t listmount(uint64_t mnt_id, uint64_t mnt_ns_id, + uint64_t last_mnt_id, uint64_t list[], size_t num, + unsigned int flags) +{ + struct mnt_id_req req = { + .size = MNT_ID_REQ_SIZE_VER0, + .mnt_id = mnt_id, + .param = last_mnt_id, + }; + + if (mnt_ns_id) { + req.size = MNT_ID_REQ_SIZE_VER1; + req.mnt_ns_id = mnt_ns_id; + } + + return syscall(__NR_listmount, &req, list, num, flags); +} + +static inline struct statmount *statmount_alloc(uint64_t mnt_id, uint64_t mnt_ns_id, + uint64_t mask, unsigned int flags) +{ + struct statmount *buf; + size_t bufsize = STATMOUNT_BUFSIZE; + int ret; + + for (;;) { + buf = malloc(bufsize); + if (!buf) + return NULL; + + ret = statmount(mnt_id, mnt_ns_id, 0, mask, buf, bufsize, flags); + if (ret == 0) + return buf; + + free(buf); + if (errno != EOVERFLOW) + return NULL; + + bufsize <<= 1; + } +} + +static inline struct statmount *statmount_alloc_by_fd(int fd, uint64_t mask) +{ + struct statmount *buf; + size_t bufsize = STATMOUNT_BUFSIZE; + int ret; + + for (;;) { + buf = malloc(bufsize); + if (!buf) + return NULL; + + ret = statmount(0, 0, fd, mask, buf, bufsize, STATMOUNT_BY_FD); + if (ret == 0) + return buf; + + free(buf); + if (errno != EOVERFLOW) + return NULL; + + bufsize <<= 1; + } +} + +#endif /* __STATMOUNT_H */ diff --git a/tools/testing/selftests/filesystems/statmount/statmount_test.c b/tools/testing/selftests/filesystems/statmount/statmount_test.c new file mode 100644 index 000000000..8dc018d47 --- /dev/null +++ b/tools/testing/selftests/filesystems/statmount/statmount_test.c @@ -0,0 +1,894 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#define _GNU_SOURCE + +#include <assert.h> +#include <stddef.h> +#include <sched.h> +#include <fcntl.h> +#include <sys/param.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/statfs.h> +#include <linux/stat.h> + +#include "statmount.h" +#include "kselftest.h" + +static const char *const known_fs[] = { + "9p", "adfs", "affs", "afs", "aio", "anon_inodefs", "apparmorfs", + "autofs", "bcachefs", "bdev", "befs", "bfs", "binder", "binfmt_misc", + "bpf", "btrfs", "btrfs_test_fs", "ceph", "cgroup", "cgroup2", "cifs", + "coda", "configfs", "cpuset", "cramfs", "cxl", "dax", "debugfs", + "devpts", "devtmpfs", "dmabuf", "drm", "ecryptfs", "efivarfs", "efs", + "erofs", "exfat", "ext2", "ext3", "ext4", "f2fs", "functionfs", + "fuse", "fuseblk", "fusectl", "gadgetfs", "gfs2", "gfs2meta", "hfs", + "hfsplus", "hostfs", "hpfs", "hugetlbfs", "ibmasmfs", "iomem", + "ipathfs", "iso9660", "jffs2", "jfs", "minix", "mqueue", "msdos", + "nfs", "nfs4", "nfsd", "nilfs2", "nsfs", "ntfs", "ntfs3", "ocfs2", + "ocfs2_dlmfs", "omfs", "openpromfs", "overlay", "pipefs", "proc", + "pstore", "pvfs2", "qnx4", "qnx6", "ramfs", "resctrl", "romfs", + "rootfs", "rpc_pipefs", "s390_hypfs", "secretmem", "securityfs", + "selinuxfs", "smackfs", "smb3", "sockfs", "spufs", "squashfs", "sysfs", + "sysv", "tmpfs", "tracefs", "ubifs", "udf", "ufs", "v7", "vboxsf", + "vfat", "virtiofs", "vxfs", "xenfs", "xfs", "zonefs", NULL }; + +static void write_file(const char *path, const char *val) +{ + int fd = open(path, O_WRONLY); + size_t len = strlen(val); + int ret; + + if (fd == -1) + ksft_exit_fail_msg("opening %s for write: %s\n", path, strerror(errno)); + + ret = write(fd, val, len); + if (ret == -1) + ksft_exit_fail_msg("writing to %s: %s\n", path, strerror(errno)); + if (ret != len) + ksft_exit_fail_msg("short write to %s\n", path); + + ret = close(fd); + if (ret == -1) + ksft_exit_fail_msg("closing %s\n", path); +} + +static uint64_t get_mnt_id(const char *name, const char *path, uint64_t mask) +{ + struct statx sx; + int ret; + + ret = statx(AT_FDCWD, path, 0, mask, &sx); + if (ret == -1) + ksft_exit_fail_msg("retrieving %s mount ID for %s: %s\n", + mask & STATX_MNT_ID_UNIQUE ? "unique" : "old", + name, strerror(errno)); + if (!(sx.stx_mask & mask)) + ksft_exit_fail_msg("no %s mount ID available for %s\n", + mask & STATX_MNT_ID_UNIQUE ? "unique" : "old", + name); + + return sx.stx_mnt_id; +} + + +static char root_mntpoint[] = "/tmp/statmount_test_root.XXXXXX"; +static int orig_root; +static uint64_t root_id, parent_id; +static uint32_t old_root_id, old_parent_id; +static FILE *f_mountinfo; + +static void cleanup_namespace(void) +{ + int ret; + + ret = fchdir(orig_root); + if (ret == -1) + ksft_perror("fchdir to original root"); + + ret = chroot("."); + if (ret == -1) + ksft_perror("chroot to original root"); + + umount2(root_mntpoint, MNT_DETACH); + rmdir(root_mntpoint); +} + +static void setup_namespace(void) +{ + int ret; + char buf[32]; + uid_t uid = getuid(); + gid_t gid = getgid(); + + ret = unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWPID); + if (ret == -1) + ksft_exit_fail_msg("unsharing mountns and userns: %s\n", + strerror(errno)); + + sprintf(buf, "0 %d 1", uid); + write_file("/proc/self/uid_map", buf); + write_file("/proc/self/setgroups", "deny"); + sprintf(buf, "0 %d 1", gid); + write_file("/proc/self/gid_map", buf); + + f_mountinfo = fopen("/proc/self/mountinfo", "re"); + if (!f_mountinfo) + ksft_exit_fail_msg("failed to open mountinfo: %s\n", + strerror(errno)); + + ret = mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL); + if (ret == -1) + ksft_exit_fail_msg("making mount tree private: %s\n", + strerror(errno)); + + if (!mkdtemp(root_mntpoint)) + ksft_exit_fail_msg("creating temporary directory %s: %s\n", + root_mntpoint, strerror(errno)); + + old_parent_id = get_mnt_id("parent", root_mntpoint, STATX_MNT_ID); + parent_id = get_mnt_id("parent", root_mntpoint, STATX_MNT_ID_UNIQUE); + + orig_root = open("/", O_PATH); + if (orig_root == -1) + ksft_exit_fail_msg("opening root directory: %s", + strerror(errno)); + + atexit(cleanup_namespace); + + ret = mount(root_mntpoint, root_mntpoint, NULL, MS_BIND, NULL); + if (ret == -1) + ksft_exit_fail_msg("mounting temp root %s: %s\n", + root_mntpoint, strerror(errno)); + + ret = chroot(root_mntpoint); + if (ret == -1) + ksft_exit_fail_msg("chroot to temp root %s: %s\n", + root_mntpoint, strerror(errno)); + + ret = chdir("/"); + if (ret == -1) + ksft_exit_fail_msg("chdir to root: %s\n", strerror(errno)); + + old_root_id = get_mnt_id("root", "/", STATX_MNT_ID); + root_id = get_mnt_id("root", "/", STATX_MNT_ID_UNIQUE); +} + +static int setup_mount_tree(int log2_num) +{ + int ret, i; + + ret = mount("", "/", NULL, MS_REC|MS_SHARED, NULL); + if (ret == -1) { + ksft_test_result_fail("making mount tree shared: %s\n", + strerror(errno)); + return -1; + } + + for (i = 0; i < log2_num; i++) { + ret = mount("/", "/", NULL, MS_BIND, NULL); + if (ret == -1) { + ksft_test_result_fail("mounting submount %s: %s\n", + root_mntpoint, strerror(errno)); + return -1; + } + } + return 0; +} + +static void test_listmount_empty_root(void) +{ + ssize_t res; + const unsigned int size = 32; + uint64_t list[size]; + + res = listmount(LSMT_ROOT, 0, 0, list, size, 0); + if (res == -1) { + ksft_test_result_fail("listmount: %s\n", strerror(errno)); + return; + } + if (res != 1) { + ksft_test_result_fail("listmount result is %zi != 1\n", res); + return; + } + + if (list[0] != root_id) { + ksft_test_result_fail("listmount ID doesn't match 0x%llx != 0x%llx\n", + (unsigned long long) list[0], + (unsigned long long) root_id); + return; + } + + ksft_test_result_pass("listmount empty root\n"); +} + +static void test_statmount_zero_mask(void) +{ + struct statmount sm; + int ret; + + ret = statmount(root_id, 0, 0, 0, &sm, sizeof(sm), 0); + if (ret == -1) { + ksft_test_result_fail("statmount zero mask: %s\n", + strerror(errno)); + return; + } + if (sm.size != sizeof(sm)) { + ksft_test_result_fail("unexpected size: %u != %u\n", + sm.size, (uint32_t) sizeof(sm)); + return; + } + if (sm.mask != 0) { + ksft_test_result_fail("unexpected mask: 0x%llx != 0x0\n", + (unsigned long long) sm.mask); + return; + } + + ksft_test_result_pass("statmount zero mask\n"); +} + +static void test_statmount_mnt_basic(void) +{ + struct statmount sm; + int ret; + uint64_t mask = STATMOUNT_MNT_BASIC; + + ret = statmount(root_id, 0, 0, mask, &sm, sizeof(sm), 0); + if (ret == -1) { + ksft_test_result_fail("statmount mnt basic: %s\n", + strerror(errno)); + return; + } + if (sm.size != sizeof(sm)) { + ksft_test_result_fail("unexpected size: %u != %u\n", + sm.size, (uint32_t) sizeof(sm)); + return; + } + if (sm.mask != mask) { + ksft_test_result_skip("statmount mnt basic unavailable\n"); + return; + } + + if (sm.mnt_id != root_id) { + ksft_test_result_fail("unexpected root ID: 0x%llx != 0x%llx\n", + (unsigned long long) sm.mnt_id, + (unsigned long long) root_id); + return; + } + + if (sm.mnt_id_old != old_root_id) { + ksft_test_result_fail("unexpected old root ID: %u != %u\n", + sm.mnt_id_old, old_root_id); + return; + } + + if (sm.mnt_parent_id != parent_id) { + ksft_test_result_fail("unexpected parent ID: 0x%llx != 0x%llx\n", + (unsigned long long) sm.mnt_parent_id, + (unsigned long long) parent_id); + return; + } + + if (sm.mnt_parent_id_old != old_parent_id) { + ksft_test_result_fail("unexpected old parent ID: %u != %u\n", + sm.mnt_parent_id_old, old_parent_id); + return; + } + + if (sm.mnt_propagation != MS_PRIVATE) { + ksft_test_result_fail("unexpected propagation: 0x%llx\n", + (unsigned long long) sm.mnt_propagation); + return; + } + + ksft_test_result_pass("statmount mnt basic\n"); +} + + +static void test_statmount_sb_basic(void) +{ + struct statmount sm; + int ret; + uint64_t mask = STATMOUNT_SB_BASIC; + struct statx sx; + struct statfs sf; + + ret = statmount(root_id, 0, 0, mask, &sm, sizeof(sm), 0); + if (ret == -1) { + ksft_test_result_fail("statmount sb basic: %s\n", + strerror(errno)); + return; + } + if (sm.size != sizeof(sm)) { + ksft_test_result_fail("unexpected size: %u != %u\n", + sm.size, (uint32_t) sizeof(sm)); + return; + } + if (sm.mask != mask) { + ksft_test_result_skip("statmount sb basic unavailable\n"); + return; + } + + ret = statx(AT_FDCWD, "/", 0, 0, &sx); + if (ret == -1) { + ksft_test_result_fail("stat root failed: %s\n", + strerror(errno)); + return; + } + + if (sm.sb_dev_major != sx.stx_dev_major || + sm.sb_dev_minor != sx.stx_dev_minor) { + ksft_test_result_fail("unexpected sb dev %u:%u != %u:%u\n", + sm.sb_dev_major, sm.sb_dev_minor, + sx.stx_dev_major, sx.stx_dev_minor); + return; + } + + ret = statfs("/", &sf); + if (ret == -1) { + ksft_test_result_fail("statfs root failed: %s\n", + strerror(errno)); + return; + } + + if (sm.sb_magic != sf.f_type) { + ksft_test_result_fail("unexpected sb magic: 0x%llx != 0x%lx\n", + (unsigned long long) sm.sb_magic, + sf.f_type); + return; + } + + ksft_test_result_pass("statmount sb basic\n"); +} + +static void test_statmount_mnt_point(void) +{ + struct statmount *sm; + + sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_POINT, 0); + if (!sm) { + ksft_test_result_fail("statmount mount point: %s\n", + strerror(errno)); + return; + } + + if (!(sm->mask & STATMOUNT_MNT_POINT)) { + ksft_test_result_fail("missing STATMOUNT_MNT_POINT in mask\n"); + return; + } + if (strcmp(sm->str + sm->mnt_point, "/") != 0) { + ksft_test_result_fail("unexpected mount point: '%s' != '/'\n", + sm->str + sm->mnt_point); + goto out; + } + ksft_test_result_pass("statmount mount point\n"); +out: + free(sm); +} + +static void test_statmount_mnt_root(void) +{ + struct statmount *sm; + const char *mnt_root, *last_dir, *last_root; + + last_dir = strrchr(root_mntpoint, '/'); + assert(last_dir); + last_dir++; + + sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_ROOT, 0); + if (!sm) { + ksft_test_result_fail("statmount mount root: %s\n", + strerror(errno)); + return; + } + if (!(sm->mask & STATMOUNT_MNT_ROOT)) { + ksft_test_result_fail("missing STATMOUNT_MNT_ROOT in mask\n"); + return; + } + mnt_root = sm->str + sm->mnt_root; + last_root = strrchr(mnt_root, '/'); + if (last_root) + last_root++; + else + last_root = mnt_root; + + if (strcmp(last_dir, last_root) != 0) { + ksft_test_result_fail("unexpected mount root last component: '%s' != '%s'\n", + last_root, last_dir); + goto out; + } + ksft_test_result_pass("statmount mount root\n"); +out: + free(sm); +} + +static void test_statmount_fs_type(void) +{ + struct statmount *sm; + const char *fs_type; + const char *const *s; + + sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0); + if (!sm) { + ksft_test_result_fail("statmount fs type: %s\n", + strerror(errno)); + return; + } + if (!(sm->mask & STATMOUNT_FS_TYPE)) { + ksft_test_result_fail("missing STATMOUNT_FS_TYPE in mask\n"); + return; + } + fs_type = sm->str + sm->fs_type; + for (s = known_fs; s != NULL; s++) { + if (strcmp(fs_type, *s) == 0) + break; + } + if (!s) + ksft_print_msg("unknown filesystem type: %s\n", fs_type); + + ksft_test_result_pass("statmount fs type\n"); + free(sm); +} + +static void test_statmount_mnt_opts(void) +{ + struct statmount *sm; + const char *statmount_opts; + char *line = NULL; + size_t len = 0; + + sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_BASIC | STATMOUNT_MNT_OPTS, + 0); + if (!sm) { + ksft_test_result_fail("statmount mnt opts: %s\n", + strerror(errno)); + return; + } + + if (!(sm->mask & STATMOUNT_MNT_BASIC)) { + ksft_test_result_fail("missing STATMOUNT_MNT_BASIC in mask\n"); + return; + } + + while (getline(&line, &len, f_mountinfo) != -1) { + int i; + char *p, *p2; + unsigned int old_mnt_id; + + old_mnt_id = atoi(line); + if (old_mnt_id != sm->mnt_id_old) + continue; + + for (p = line, i = 0; p && i < 5; i++) + p = strchr(p + 1, ' '); + if (!p) + continue; + + p2 = strchr(p + 1, ' '); + if (!p2) + continue; + *p2 = '\0'; + p = strchr(p2 + 1, '-'); + if (!p) + continue; + for (p++, i = 0; p && i < 2; i++) + p = strchr(p + 1, ' '); + if (!p) + continue; + p++; + + /* skip generic superblock options */ + if (strncmp(p, "ro", 2) == 0) + p += 2; + else if (strncmp(p, "rw", 2) == 0) + p += 2; + if (*p == ',') + p++; + if (strncmp(p, "sync", 4) == 0) + p += 4; + if (*p == ',') + p++; + if (strncmp(p, "dirsync", 7) == 0) + p += 7; + if (*p == ',') + p++; + if (strncmp(p, "lazytime", 8) == 0) + p += 8; + if (*p == ',') + p++; + p2 = strrchr(p, '\n'); + if (p2) + *p2 = '\0'; + + if (sm->mask & STATMOUNT_MNT_OPTS) + statmount_opts = sm->str + sm->mnt_opts; + else + statmount_opts = ""; + if (strcmp(statmount_opts, p) != 0) + ksft_test_result_fail( + "unexpected mount options: '%s' != '%s'\n", + statmount_opts, p); + else + ksft_test_result_pass("statmount mount options\n"); + free(sm); + free(line); + return; + } + + ksft_test_result_fail("didnt't find mount entry\n"); + free(sm); + free(line); +} + +static void test_statmount_string(uint64_t mask, size_t off, const char *name) +{ + struct statmount *sm; + size_t len, shortsize, exactsize; + uint32_t start, i; + int ret; + + sm = statmount_alloc(root_id, 0, mask, 0); + if (!sm) { + ksft_test_result_fail("statmount %s: %s\n", name, + strerror(errno)); + goto out; + } + if (sm->size < sizeof(*sm)) { + ksft_test_result_fail("unexpected size: %u < %u\n", + sm->size, (uint32_t) sizeof(*sm)); + goto out; + } + if (sm->mask != mask) { + ksft_test_result_skip("statmount %s unavailable\n", name); + goto out; + } + len = sm->size - sizeof(*sm); + start = ((uint32_t *) sm)[off]; + + for (i = start;; i++) { + if (i >= len) { + ksft_test_result_fail("string out of bounds\n"); + goto out; + } + if (!sm->str[i]) + break; + } + exactsize = sm->size; + shortsize = sizeof(*sm) + i; + + ret = statmount(root_id, 0, 0, mask, sm, exactsize, 0); + if (ret == -1) { + ksft_test_result_fail("statmount exact size: %s\n", + strerror(errno)); + goto out; + } + errno = 0; + ret = statmount(root_id, 0, 0, mask, sm, shortsize, 0); + if (ret != -1 || errno != EOVERFLOW) { + ksft_test_result_fail("should have failed with EOVERFLOW: %s\n", + strerror(errno)); + goto out; + } + + ksft_test_result_pass("statmount string %s\n", name); +out: + free(sm); +} + +static void test_listmount_tree(void) +{ + ssize_t res; + const unsigned int log2_num = 4; + const unsigned int step = 3; + const unsigned int size = (1 << log2_num) + step + 1; + size_t num, expect = 1 << log2_num; + uint64_t list[size]; + uint64_t list2[size]; + size_t i; + + + res = setup_mount_tree(log2_num); + if (res == -1) + return; + + num = res = listmount(LSMT_ROOT, 0, 0, list, size, 0); + if (res == -1) { + ksft_test_result_fail("listmount: %s\n", strerror(errno)); + return; + } + if (num != expect) { + ksft_test_result_fail("listmount result is %zi != %zi\n", + res, expect); + return; + } + + for (i = 0; i < size - step;) { + res = listmount(LSMT_ROOT, 0, i ? list2[i - 1] : 0, list2 + i, step, 0); + if (res == -1) + ksft_test_result_fail("short listmount: %s\n", + strerror(errno)); + i += res; + if (res < step) + break; + } + if (i != num) { + ksft_test_result_fail("different number of entries: %zu != %zu\n", + i, num); + return; + } + for (i = 0; i < num; i++) { + if (list2[i] != list[i]) { + ksft_test_result_fail("different value for entry %zu: 0x%llx != 0x%llx\n", + i, + (unsigned long long) list2[i], + (unsigned long long) list[i]); + } + } + + ksft_test_result_pass("listmount tree\n"); +} + +static void test_statmount_by_fd(void) +{ + struct statmount *sm = NULL; + char tmpdir[] = "/statmount.fd.XXXXXX"; + const char root[] = "/test"; + char subdir[PATH_MAX], tmproot[PATH_MAX]; + int fd; + + if (!mkdtemp(tmpdir)) { + ksft_perror("mkdtemp"); + return; + } + + if (mount("statmount.test", tmpdir, "tmpfs", 0, NULL)) { + ksft_perror("mount"); + rmdir(tmpdir); + return; + } + + snprintf(subdir, PATH_MAX, "%s%s", tmpdir, root); + snprintf(tmproot, PATH_MAX, "%s/%s", tmpdir, "chroot"); + + if (mkdir(subdir, 0755)) { + ksft_perror("mkdir"); + goto err_tmpdir; + } + + if (mount(subdir, subdir, NULL, MS_BIND, 0)) { + ksft_perror("mount"); + goto err_subdir; + } + + if (mkdir(tmproot, 0755)) { + ksft_perror("mkdir"); + goto err_subdir; + } + + fd = open(subdir, O_PATH); + if (fd < 0) { + ksft_perror("open"); + goto err_tmproot; + } + + if (chroot(tmproot)) { + ksft_perror("chroot"); + goto err_fd; + } + + sm = statmount_alloc_by_fd(fd, STATMOUNT_MNT_ROOT | STATMOUNT_MNT_POINT); + if (!sm) { + ksft_test_result_fail("statmount by fd failed: %s\n", strerror(errno)); + goto err_chroot; + } + + if (sm->size < sizeof(*sm)) { + ksft_test_result_fail("unexpected size: %u < %u\n", + sm->size, (uint32_t) sizeof(*sm)); + goto err_chroot; + } + + if (sm->mask & STATMOUNT_MNT_POINT) { + ksft_test_result_fail("STATMOUNT_MNT_POINT unexpectedly set in statmount\n"); + goto err_chroot; + } + + if (!(sm->mask & STATMOUNT_MNT_ROOT)) { + ksft_test_result_fail("STATMOUNT_MNT_ROOT not set in statmount\n"); + goto err_chroot; + } + + if (strcmp(root, sm->str + sm->mnt_root) != 0) { + ksft_test_result_fail("statmount returned incorrect mnt_root," + "statmount mnt_root: %s != %s\n", + sm->str + sm->mnt_root, root); + goto err_chroot; + } + + if (chroot(".")) { + ksft_perror("chroot"); + goto out; + } + + free(sm); + sm = statmount_alloc_by_fd(fd, STATMOUNT_MNT_ROOT | STATMOUNT_MNT_POINT); + if (!sm) { + ksft_test_result_fail("statmount by fd failed: %s\n", strerror(errno)); + goto err_fd; + } + + if (sm->size < sizeof(*sm)) { + ksft_test_result_fail("unexpected size: %u < %u\n", + sm->size, (uint32_t) sizeof(*sm)); + goto out; + } + + if (!(sm->mask & STATMOUNT_MNT_POINT)) { + ksft_test_result_fail("STATMOUNT_MNT_POINT not set in statmount\n"); + goto out; + } + + if (!(sm->mask & STATMOUNT_MNT_ROOT)) { + ksft_test_result_fail("STATMOUNT_MNT_ROOT not set in statmount\n"); + goto out; + } + + if (strcmp(subdir, sm->str + sm->mnt_point) != 0) { + ksft_test_result_fail("statmount returned incorrect mnt_point," + "statmount mnt_point: %s != %s\n", sm->str + sm->mnt_point, subdir); + goto out; + } + + if (strcmp(root, sm->str + sm->mnt_root) != 0) { + ksft_test_result_fail("statmount returned incorrect mnt_root," + "statmount mnt_root: %s != %s\n", sm->str + sm->mnt_root, root); + goto out; + } + + ksft_test_result_pass("statmount by fd\n"); + goto out; +err_chroot: + chroot("."); +out: + free(sm); +err_fd: + close(fd); +err_tmproot: + rmdir(tmproot); +err_subdir: + umount2(subdir, MNT_DETACH); + rmdir(subdir); +err_tmpdir: + umount2(tmpdir, MNT_DETACH); + rmdir(tmpdir); +} + +static void test_statmount_by_fd_unmounted(void) +{ + const char root[] = "/test.unmounted"; + char tmpdir[] = "/statmount.fd.XXXXXX"; + char subdir[PATH_MAX]; + int fd; + struct statmount *sm = NULL; + + if (!mkdtemp(tmpdir)) { + ksft_perror("mkdtemp"); + return; + } + + if (mount("statmount.test", tmpdir, "tmpfs", 0, NULL)) { + ksft_perror("mount"); + rmdir(tmpdir); + return; + } + + snprintf(subdir, PATH_MAX, "%s%s", tmpdir, root); + + if (mkdir(subdir, 0755)) { + ksft_perror("mkdir"); + goto err_tmpdir; + } + + if (mount(subdir, subdir, 0, MS_BIND, NULL)) { + ksft_perror("mount"); + goto err_subdir; + } + + fd = open(subdir, O_PATH); + if (fd < 0) { + ksft_perror("open"); + goto err_subdir; + } + + if (umount2(tmpdir, MNT_DETACH)) { + ksft_perror("umount2"); + goto err_fd; + } + + sm = statmount_alloc_by_fd(fd, STATMOUNT_MNT_POINT | STATMOUNT_MNT_ROOT); + if (!sm) { + ksft_test_result_fail("statmount by fd unmounted: %s\n", + strerror(errno)); + goto err_sm; + } + + if (sm->size < sizeof(*sm)) { + ksft_test_result_fail("unexpected size: %u < %u\n", + sm->size, (uint32_t) sizeof(*sm)); + goto err_sm; + } + + if (sm->mask & STATMOUNT_MNT_POINT) { + ksft_test_result_fail("STATMOUNT_MNT_POINT unexpectedly set in mask\n"); + goto err_sm; + } + + if (!(sm->mask & STATMOUNT_MNT_ROOT)) { + ksft_test_result_fail("STATMOUNT_MNT_ROOT not set in mask\n"); + goto err_sm; + } + + if (strcmp(sm->str + sm->mnt_root, root) != 0) { + ksft_test_result_fail("statmount returned incorrect mnt_root," + "statmount mnt_root: %s != %s\n", + sm->str + sm->mnt_root, root); + goto err_sm; + } + + ksft_test_result_pass("statmount by fd on unmounted mount\n"); +err_sm: + free(sm); +err_fd: + close(fd); +err_subdir: + umount2(subdir, MNT_DETACH); + rmdir(subdir); +err_tmpdir: + umount2(tmpdir, MNT_DETACH); + rmdir(tmpdir); +} + +#define str_off(memb) (offsetof(struct statmount, memb) / sizeof(uint32_t)) + +int main(void) +{ + int ret; + uint64_t all_mask = STATMOUNT_SB_BASIC | STATMOUNT_MNT_BASIC | + STATMOUNT_PROPAGATE_FROM | STATMOUNT_MNT_ROOT | + STATMOUNT_MNT_POINT | STATMOUNT_FS_TYPE | STATMOUNT_MNT_NS_ID; + + ksft_print_header(); + + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + assert(ret == -1); + if (errno == ENOSYS) + ksft_exit_skip("statmount() syscall not supported\n"); + + setup_namespace(); + + ksft_set_plan(17); + test_listmount_empty_root(); + test_statmount_zero_mask(); + test_statmount_mnt_basic(); + test_statmount_sb_basic(); + test_statmount_mnt_root(); + test_statmount_mnt_point(); + test_statmount_fs_type(); + test_statmount_mnt_opts(); + test_statmount_string(STATMOUNT_MNT_ROOT, str_off(mnt_root), "mount root"); + test_statmount_string(STATMOUNT_MNT_POINT, str_off(mnt_point), "mount point"); + test_statmount_string(STATMOUNT_FS_TYPE, str_off(fs_type), "fs type"); + test_statmount_string(all_mask, str_off(mnt_root), "mount root & all"); + test_statmount_string(all_mask, str_off(mnt_point), "mount point & all"); + test_statmount_string(all_mask, str_off(fs_type), "fs type & all"); + + test_listmount_tree(); + test_statmount_by_fd_unmounted(); + test_statmount_by_fd(); + + + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) + ksft_exit_fail(); + else + ksft_exit_pass(); +} diff --git a/tools/testing/selftests/filesystems/statmount/statmount_test_ns.c b/tools/testing/selftests/filesystems/statmount/statmount_test_ns.c new file mode 100644 index 000000000..e500905e4 --- /dev/null +++ b/tools/testing/selftests/filesystems/statmount/statmount_test_ns.c @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#define _GNU_SOURCE + +#include <assert.h> +#include <fcntl.h> +#include <limits.h> +#include <sched.h> +#include <stdlib.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <linux/nsfs.h> +#include <linux/stat.h> + +#include "statmount.h" +#include "../utils.h" +#include "kselftest.h" + +#define NSID_PASS 0 +#define NSID_FAIL 1 +#define NSID_SKIP 2 +#define NSID_ERROR 3 + +static void handle_result(int ret, const char *testname) +{ + if (ret == NSID_PASS) + ksft_test_result_pass("%s\n", testname); + else if (ret == NSID_FAIL) + ksft_test_result_fail("%s\n", testname); + else if (ret == NSID_ERROR) + ksft_exit_fail_msg("%s\n", testname); + else + ksft_test_result_skip("%s\n", testname); +} + +static int get_mnt_ns_id(const char *mnt_ns, uint64_t *mnt_ns_id) +{ + int fd = open(mnt_ns, O_RDONLY); + + if (fd < 0) { + ksft_print_msg("failed to open for ns %s: %s\n", + mnt_ns, strerror(errno)); + sleep(60); + return NSID_ERROR; + } + + if (ioctl(fd, NS_GET_MNTNS_ID, mnt_ns_id) < 0) { + ksft_print_msg("failed to get the nsid for ns %s: %s\n", + mnt_ns, strerror(errno)); + return NSID_ERROR; + } + close(fd); + return NSID_PASS; +} + +static int setup_namespace(void) +{ + if (setup_userns() != 0) + return NSID_ERROR; + + return NSID_PASS; +} + +static int _test_statmount_mnt_ns_id(void) +{ + struct statmount sm; + uint64_t mnt_ns_id; + uint64_t root_id; + int ret; + + ret = get_mnt_ns_id("/proc/self/ns/mnt", &mnt_ns_id); + if (ret != NSID_PASS) + return ret; + + root_id = get_unique_mnt_id("/"); + if (!root_id) + return NSID_ERROR; + + ret = statmount(root_id, 0, 0, STATMOUNT_MNT_NS_ID, &sm, sizeof(sm), 0); + if (ret == -1) { + ksft_print_msg("statmount mnt ns id: %s\n", strerror(errno)); + return NSID_ERROR; + } + + if (sm.size != sizeof(sm)) { + ksft_print_msg("unexpected size: %u != %u\n", sm.size, + (uint32_t)sizeof(sm)); + return NSID_FAIL; + } + if (sm.mask != STATMOUNT_MNT_NS_ID) { + ksft_print_msg("statmount mnt ns id unavailable\n"); + return NSID_SKIP; + } + + if (sm.mnt_ns_id != mnt_ns_id) { + ksft_print_msg("unexpected mnt ns ID: 0x%llx != 0x%llx\n", + (unsigned long long)sm.mnt_ns_id, + (unsigned long long)mnt_ns_id); + return NSID_FAIL; + } + + return NSID_PASS; +} + +static int _test_statmount_mnt_ns_id_by_fd(void) +{ + struct statmount sm; + uint64_t mnt_ns_id; + int ret, fd, mounted = 1, status = NSID_ERROR; + char mnt[] = "/statmount.fd.XXXXXX"; + + ret = get_mnt_ns_id("/proc/self/ns/mnt", &mnt_ns_id); + if (ret != NSID_PASS) + return ret; + + if (!mkdtemp(mnt)) { + ksft_print_msg("statmount by fd mnt ns id mkdtemp: %s\n", strerror(errno)); + return NSID_ERROR; + } + + if (mount(mnt, mnt, NULL, MS_BIND, 0)) { + ksft_print_msg("statmount by fd mnt ns id mount: %s\n", strerror(errno)); + status = NSID_ERROR; + goto err; + } + + fd = open(mnt, O_PATH); + if (fd < 0) { + ksft_print_msg("statmount by fd mnt ns id open: %s\n", strerror(errno)); + goto err; + } + + ret = statmount(0, 0, fd, STATMOUNT_MNT_NS_ID, &sm, sizeof(sm), STATMOUNT_BY_FD); + if (ret == -1) { + ksft_print_msg("statmount mnt ns id statmount: %s\n", strerror(errno)); + status = NSID_ERROR; + goto out; + } + + if (sm.size != sizeof(sm)) { + ksft_print_msg("unexpected size: %u != %u\n", sm.size, + (uint32_t)sizeof(sm)); + status = NSID_FAIL; + goto out; + } + if (sm.mask != STATMOUNT_MNT_NS_ID) { + ksft_print_msg("statmount mnt ns id unavailable\n"); + status = NSID_SKIP; + goto out; + } + + if (sm.mnt_ns_id != mnt_ns_id) { + ksft_print_msg("unexpected mnt ns ID: 0x%llx != 0x%llx\n", + (unsigned long long)sm.mnt_ns_id, + (unsigned long long)mnt_ns_id); + status = NSID_FAIL; + goto out; + } + + mounted = 0; + if (umount2(mnt, MNT_DETACH)) { + ksft_print_msg("statmount by fd mnt ns id umount2: %s\n", strerror(errno)); + goto out; + } + + ret = statmount(0, 0, fd, STATMOUNT_MNT_NS_ID, &sm, sizeof(sm), STATMOUNT_BY_FD); + if (ret == -1) { + ksft_print_msg("statmount mnt ns id statmount: %s\n", strerror(errno)); + status = NSID_ERROR; + goto out; + } + + if (sm.size != sizeof(sm)) { + ksft_print_msg("unexpected size: %u != %u\n", sm.size, + (uint32_t)sizeof(sm)); + status = NSID_FAIL; + goto out; + } + + if (sm.mask == STATMOUNT_MNT_NS_ID) { + ksft_print_msg("unexpected STATMOUNT_MNT_NS_ID in mask\n"); + status = NSID_FAIL; + goto out; + } + + status = NSID_PASS; +out: + close(fd); + if (mounted) + umount2(mnt, MNT_DETACH); +err: + rmdir(mnt); + return status; +} + + +static void test_statmount_mnt_ns_id(void) +{ + pid_t pid; + int ret; + + pid = fork(); + if (pid < 0) + ksft_exit_fail_msg("failed to fork: %s\n", strerror(errno)); + + /* We're the original pid, wait for the result. */ + if (pid != 0) { + ret = wait_for_pid(pid); + handle_result(ret, "test statmount ns id"); + return; + } + + ret = setup_namespace(); + if (ret != NSID_PASS) + exit(ret); + ret = _test_statmount_mnt_ns_id(); + if (ret != NSID_PASS) + exit(ret); + ret = _test_statmount_mnt_ns_id_by_fd(); + exit(ret); +} + +static int validate_external_listmount(pid_t pid, uint64_t child_nr_mounts) +{ + uint64_t list[256]; + uint64_t mnt_ns_id; + uint64_t nr_mounts; + char buf[256]; + int ret; + + /* Get the mount ns id for our child. */ + snprintf(buf, sizeof(buf), "/proc/%lu/ns/mnt", (unsigned long)pid); + ret = get_mnt_ns_id(buf, &mnt_ns_id); + + nr_mounts = listmount(LSMT_ROOT, mnt_ns_id, 0, list, 256, 0); + if (nr_mounts == (uint64_t)-1) { + ksft_print_msg("listmount: %s\n", strerror(errno)); + return NSID_ERROR; + } + + if (nr_mounts != child_nr_mounts) { + ksft_print_msg("listmount results is %zi != %zi\n", nr_mounts, + child_nr_mounts); + return NSID_FAIL; + } + + /* Validate that all of our entries match our mnt_ns_id. */ + for (int i = 0; i < nr_mounts; i++) { + struct statmount sm; + + ret = statmount(list[i], mnt_ns_id, 0, STATMOUNT_MNT_NS_ID, &sm, + sizeof(sm), 0); + if (ret < 0) { + ksft_print_msg("statmount mnt ns id: %s\n", strerror(errno)); + return NSID_ERROR; + } + + if (sm.mask != STATMOUNT_MNT_NS_ID) { + ksft_print_msg("statmount mnt ns id unavailable\n"); + return NSID_SKIP; + } + + if (sm.mnt_ns_id != mnt_ns_id) { + ksft_print_msg("listmount gave us the wrong ns id: 0x%llx != 0x%llx\n", + (unsigned long long)sm.mnt_ns_id, + (unsigned long long)mnt_ns_id); + return NSID_FAIL; + } + } + + return NSID_PASS; +} + +static void test_listmount_ns(void) +{ + uint64_t nr_mounts; + char pval; + int child_ready_pipe[2]; + int parent_ready_pipe[2]; + pid_t pid; + int ret, child_ret; + + if (pipe(child_ready_pipe) < 0) + ksft_exit_fail_msg("failed to create the child pipe: %s\n", + strerror(errno)); + if (pipe(parent_ready_pipe) < 0) + ksft_exit_fail_msg("failed to create the parent pipe: %s\n", + strerror(errno)); + + pid = fork(); + if (pid < 0) + ksft_exit_fail_msg("failed to fork: %s\n", strerror(errno)); + + if (pid == 0) { + char cval; + uint64_t list[256]; + + close(child_ready_pipe[0]); + close(parent_ready_pipe[1]); + + ret = setup_namespace(); + if (ret != NSID_PASS) + exit(ret); + + nr_mounts = listmount(LSMT_ROOT, 0, 0, list, 256, 0); + if (nr_mounts == (uint64_t)-1) { + ksft_print_msg("listmount: %s\n", strerror(errno)); + exit(NSID_FAIL); + } + + /* + * Tell our parent how many mounts we have, and then wait for it + * to tell us we're done. + */ + if (write(child_ready_pipe[1], &nr_mounts, sizeof(nr_mounts)) != + sizeof(nr_mounts)) + ret = NSID_ERROR; + if (read(parent_ready_pipe[0], &cval, sizeof(cval)) != sizeof(cval)) + ret = NSID_ERROR; + exit(NSID_PASS); + } + + close(child_ready_pipe[1]); + close(parent_ready_pipe[0]); + + /* Wait until the child has created everything. */ + if (read(child_ready_pipe[0], &nr_mounts, sizeof(nr_mounts)) != + sizeof(nr_mounts)) + ret = NSID_ERROR; + + ret = validate_external_listmount(pid, nr_mounts); + + if (write(parent_ready_pipe[1], &pval, sizeof(pval)) != sizeof(pval)) + ret = NSID_ERROR; + + child_ret = wait_for_pid(pid); + if (child_ret != NSID_PASS) + ret = child_ret; + handle_result(ret, "test listmount ns id"); +} + +int main(void) +{ + int ret; + + ksft_print_header(); + ret = statmount(0, 0, 0, 0, NULL, 0, 0); + assert(ret == -1); + if (errno == ENOSYS) + ksft_exit_skip("statmount() syscall not supported\n"); + + ksft_set_plan(2); + test_statmount_mnt_ns_id(); + test_listmount_ns(); + + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) + ksft_exit_fail(); + else + ksft_exit_pass(); +} diff --git a/tools/testing/selftests/filesystems/utils.c b/tools/testing/selftests/filesystems/utils.c new file mode 100644 index 000000000..d73d7d817 --- /dev/null +++ b/tools/testing/selftests/filesystems/utils.c @@ -0,0 +1,615 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include <fcntl.h> +#include <sys/types.h> +#include <dirent.h> +#include <grp.h> +#include <linux/limits.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/eventfd.h> +#include <sys/fsuid.h> +#include <sys/prctl.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/xattr.h> +#include <sys/mount.h> + +#include "kselftest.h" +#include "wrappers.h" +#include "utils.h" + +#define MAX_USERNS_LEVEL 32 + +#define syserror(format, ...) \ + ({ \ + fprintf(stderr, "%m - " format "\n", ##__VA_ARGS__); \ + (-errno); \ + }) + +#define syserror_set(__ret__, format, ...) \ + ({ \ + typeof(__ret__) __internal_ret__ = (__ret__); \ + errno = labs(__ret__); \ + fprintf(stderr, "%m - " format "\n", ##__VA_ARGS__); \ + __internal_ret__; \ + }) + +#define STRLITERALLEN(x) (sizeof(""x"") - 1) + +#define INTTYPE_TO_STRLEN(type) \ + (2 + (sizeof(type) <= 1 \ + ? 3 \ + : sizeof(type) <= 2 \ + ? 5 \ + : sizeof(type) <= 4 \ + ? 10 \ + : sizeof(type) <= 8 ? 20 : sizeof(int[-2 * (sizeof(type) > 8)]))) + +#define list_for_each(__iterator, __list) \ + for (__iterator = (__list)->next; __iterator != __list; __iterator = __iterator->next) + +typedef enum idmap_type_t { + ID_TYPE_UID, + ID_TYPE_GID +} idmap_type_t; + +struct id_map { + idmap_type_t map_type; + __u32 nsid; + __u32 hostid; + __u32 range; +}; + +struct list { + void *elem; + struct list *next; + struct list *prev; +}; + +struct userns_hierarchy { + int fd_userns; + int fd_event; + unsigned int level; + struct list id_map; +}; + +static inline void list_init(struct list *list) +{ + list->elem = NULL; + list->next = list->prev = list; +} + +static inline int list_empty(const struct list *list) +{ + return list == list->next; +} + +static inline void __list_add(struct list *new, struct list *prev, struct list *next) +{ + next->prev = new; + new->next = next; + new->prev = prev; + prev->next = new; +} + +static inline void list_add_tail(struct list *head, struct list *list) +{ + __list_add(list, head->prev, head); +} + +static inline void list_del(struct list *list) +{ + struct list *next, *prev; + + next = list->next; + prev = list->prev; + next->prev = prev; + prev->next = next; +} + +static ssize_t read_nointr(int fd, void *buf, size_t count) +{ + ssize_t ret; + + do { + ret = read(fd, buf, count); + } while (ret < 0 && errno == EINTR); + + return ret; +} + +static ssize_t write_nointr(int fd, const void *buf, size_t count) +{ + ssize_t ret; + + do { + ret = write(fd, buf, count); + } while (ret < 0 && errno == EINTR); + + return ret; +} + +#define __STACK_SIZE (8 * 1024 * 1024) +static pid_t do_clone(int (*fn)(void *), void *arg, int flags) +{ + void *stack; + + stack = malloc(__STACK_SIZE); + if (!stack) + return -ENOMEM; + +#ifdef __ia64__ + return __clone2(fn, stack, __STACK_SIZE, flags | SIGCHLD, arg, NULL); +#else + return clone(fn, stack + __STACK_SIZE, flags | SIGCHLD, arg, NULL); +#endif +} + +static int get_userns_fd_cb(void *data) +{ + for (;;) + pause(); + _exit(0); +} + +int wait_for_pid(pid_t pid) +{ + int status, ret; + +again: + ret = waitpid(pid, &status, 0); + if (ret == -1) { + if (errno == EINTR) + goto again; + + return -1; + } + + if (!WIFEXITED(status)) + return -1; + + return WEXITSTATUS(status); +} + +static int write_id_mapping(idmap_type_t map_type, pid_t pid, const char *buf, size_t buf_size) +{ + int fd = -EBADF, setgroups_fd = -EBADF; + int fret = -1; + int ret; + char path[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) + + STRLITERALLEN("/setgroups") + 1]; + + if (geteuid() != 0 && map_type == ID_TYPE_GID) { + ret = snprintf(path, sizeof(path), "/proc/%d/setgroups", pid); + if (ret < 0 || ret >= sizeof(path)) + goto out; + + setgroups_fd = open(path, O_WRONLY | O_CLOEXEC); + if (setgroups_fd < 0 && errno != ENOENT) { + syserror("Failed to open \"%s\"", path); + goto out; + } + + if (setgroups_fd >= 0) { + ret = write_nointr(setgroups_fd, "deny\n", STRLITERALLEN("deny\n")); + if (ret != STRLITERALLEN("deny\n")) { + syserror("Failed to write \"deny\" to \"/proc/%d/setgroups\"", pid); + goto out; + } + } + } + + ret = snprintf(path, sizeof(path), "/proc/%d/%cid_map", pid, map_type == ID_TYPE_UID ? 'u' : 'g'); + if (ret < 0 || ret >= sizeof(path)) + goto out; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) { + syserror("Failed to open \"%s\"", path); + goto out; + } + + ret = write_nointr(fd, buf, buf_size); + if (ret != buf_size) { + syserror("Failed to write %cid mapping to \"%s\"", + map_type == ID_TYPE_UID ? 'u' : 'g', path); + goto out; + } + + fret = 0; +out: + close(fd); + close(setgroups_fd); + + return fret; +} + +static int map_ids_from_idmap(struct list *idmap, pid_t pid) +{ + int fill, left; + char mapbuf[4096] = {}; + bool had_entry = false; + idmap_type_t map_type, u_or_g; + + if (list_empty(idmap)) + return 0; + + for (map_type = ID_TYPE_UID, u_or_g = 'u'; + map_type <= ID_TYPE_GID; map_type++, u_or_g = 'g') { + char *pos = mapbuf; + int ret; + struct list *iterator; + + + list_for_each(iterator, idmap) { + struct id_map *map = iterator->elem; + if (map->map_type != map_type) + continue; + + had_entry = true; + + left = 4096 - (pos - mapbuf); + fill = snprintf(pos, left, "%u %u %u\n", map->nsid, map->hostid, map->range); + /* + * The kernel only takes <= 4k for writes to + * /proc/<pid>/{g,u}id_map + */ + if (fill <= 0 || fill >= left) + return syserror_set(-E2BIG, "Too many %cid mappings defined", u_or_g); + + pos += fill; + } + if (!had_entry) + continue; + + ret = write_id_mapping(map_type, pid, mapbuf, pos - mapbuf); + if (ret < 0) + return syserror("Failed to write mapping: %s", mapbuf); + + memset(mapbuf, 0, sizeof(mapbuf)); + } + + return 0; +} + +static int get_userns_fd_from_idmap(struct list *idmap) +{ + int ret; + pid_t pid; + char path_ns[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) + + STRLITERALLEN("/ns/user") + 1]; + + pid = do_clone(get_userns_fd_cb, NULL, CLONE_NEWUSER | CLONE_NEWNS); + if (pid < 0) + return -errno; + + ret = map_ids_from_idmap(idmap, pid); + if (ret < 0) + return ret; + + ret = snprintf(path_ns, sizeof(path_ns), "/proc/%d/ns/user", pid); + if (ret < 0 || (size_t)ret >= sizeof(path_ns)) + ret = -EIO; + else + ret = open(path_ns, O_RDONLY | O_CLOEXEC | O_NOCTTY); + + (void)kill(pid, SIGKILL); + (void)wait_for_pid(pid); + return ret; +} + +int get_userns_fd(unsigned long nsid, unsigned long hostid, unsigned long range) +{ + struct list head, uid_mapl, gid_mapl; + struct id_map uid_map = { + .map_type = ID_TYPE_UID, + .nsid = nsid, + .hostid = hostid, + .range = range, + }; + struct id_map gid_map = { + .map_type = ID_TYPE_GID, + .nsid = nsid, + .hostid = hostid, + .range = range, + }; + + list_init(&head); + uid_mapl.elem = &uid_map; + gid_mapl.elem = &gid_map; + list_add_tail(&head, &uid_mapl); + list_add_tail(&head, &gid_mapl); + + return get_userns_fd_from_idmap(&head); +} + +bool switch_ids(uid_t uid, gid_t gid) +{ + if (setgroups(0, NULL)) + return syserror("failure: setgroups"); + + if (setresgid(gid, gid, gid)) + return syserror("failure: setresgid"); + + if (setresuid(uid, uid, uid)) + return syserror("failure: setresuid"); + + /* Ensure we can access proc files from processes we can ptrace. */ + if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) + return syserror("failure: make dumpable"); + + return true; +} + +static int create_userns_hierarchy(struct userns_hierarchy *h); + +static int userns_fd_cb(void *data) +{ + struct userns_hierarchy *h = data; + char c; + int ret; + + ret = read_nointr(h->fd_event, &c, 1); + if (ret < 0) + return syserror("failure: read from socketpair"); + + /* Only switch ids if someone actually wrote a mapping for us. */ + if (c == '1') { + if (!switch_ids(0, 0)) + return syserror("failure: switch ids to 0"); + } + + ret = write_nointr(h->fd_event, "1", 1); + if (ret < 0) + return syserror("failure: write to socketpair"); + + ret = create_userns_hierarchy(++h); + if (ret < 0) + return syserror("failure: userns level %d", h->level); + + return 0; +} + +static int create_userns_hierarchy(struct userns_hierarchy *h) +{ + int fret = -1; + char c; + int fd_socket[2]; + int fd_userns = -EBADF, ret = -1; + ssize_t bytes; + pid_t pid; + char path[256]; + + if (h->level == MAX_USERNS_LEVEL) + return 0; + + ret = socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, fd_socket); + if (ret < 0) + return syserror("failure: create socketpair"); + + /* Note the CLONE_FILES | CLONE_VM when mucking with fds and memory. */ + h->fd_event = fd_socket[1]; + pid = do_clone(userns_fd_cb, h, CLONE_NEWUSER | CLONE_FILES | CLONE_VM); + if (pid < 0) { + syserror("failure: userns level %d", h->level); + goto out_close; + } + + ret = map_ids_from_idmap(&h->id_map, pid); + if (ret < 0) { + kill(pid, SIGKILL); + syserror("failure: writing id mapping for userns level %d for %d", h->level, pid); + goto out_wait; + } + + if (!list_empty(&h->id_map)) + bytes = write_nointr(fd_socket[0], "1", 1); /* Inform the child we wrote a mapping. */ + else + bytes = write_nointr(fd_socket[0], "0", 1); /* Inform the child we didn't write a mapping. */ + if (bytes < 0) { + kill(pid, SIGKILL); + syserror("failure: write to socketpair"); + goto out_wait; + } + + /* Wait for child to set*id() and become dumpable. */ + bytes = read_nointr(fd_socket[0], &c, 1); + if (bytes < 0) { + kill(pid, SIGKILL); + syserror("failure: read from socketpair"); + goto out_wait; + } + + snprintf(path, sizeof(path), "/proc/%d/ns/user", pid); + fd_userns = open(path, O_RDONLY | O_CLOEXEC); + if (fd_userns < 0) { + kill(pid, SIGKILL); + syserror("failure: open userns level %d for %d", h->level, pid); + goto out_wait; + } + + fret = 0; + +out_wait: + if (!wait_for_pid(pid) && !fret) { + h->fd_userns = fd_userns; + fd_userns = -EBADF; + } + +out_close: + if (fd_userns >= 0) + close(fd_userns); + close(fd_socket[0]); + close(fd_socket[1]); + return fret; +} + +int write_file(const char *path, const char *val) +{ + int fd = open(path, O_WRONLY); + size_t len = strlen(val); + int ret; + + if (fd == -1) { + ksft_print_msg("opening %s for write: %s\n", path, strerror(errno)); + return -1; + } + + ret = write(fd, val, len); + if (ret == -1) { + ksft_print_msg("writing to %s: %s\n", path, strerror(errno)); + return -1; + } + if (ret != len) { + ksft_print_msg("short write to %s\n", path); + return -1; + } + + ret = close(fd); + if (ret == -1) { + ksft_print_msg("closing %s\n", path); + return -1; + } + + return 0; +} + +int setup_userns(void) +{ + int ret; + char buf[32]; + uid_t uid = getuid(); + gid_t gid = getgid(); + + ret = unshare(CLONE_NEWNS|CLONE_NEWUSER); + if (ret) { + ksft_exit_fail_msg("unsharing mountns and userns: %s\n", + strerror(errno)); + return ret; + } + + sprintf(buf, "0 %d 1", uid); + ret = write_file("/proc/self/uid_map", buf); + if (ret) + return ret; + ret = write_file("/proc/self/setgroups", "deny"); + if (ret) + return ret; + sprintf(buf, "0 %d 1", gid); + ret = write_file("/proc/self/gid_map", buf); + if (ret) + return ret; + + ret = mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL); + if (ret) { + ksft_print_msg("making mount tree private: %s\n", strerror(errno)); + return ret; + } + + return 0; +} + +int enter_userns(void) +{ + int ret; + char buf[32]; + uid_t uid = getuid(); + gid_t gid = getgid(); + + ret = unshare(CLONE_NEWUSER); + if (ret) + return ret; + + sprintf(buf, "0 %d 1", uid); + ret = write_file("/proc/self/uid_map", buf); + if (ret) + return ret; + ret = write_file("/proc/self/setgroups", "deny"); + if (ret) + return ret; + sprintf(buf, "0 %d 1", gid); + ret = write_file("/proc/self/gid_map", buf); + if (ret) + return ret; + + return 0; +} + +/* caps_down - lower all effective caps */ +int caps_down(void) +{ + bool fret = false; + cap_t caps = NULL; + int ret = -1; + + caps = cap_get_proc(); + if (!caps) + goto out; + + ret = cap_clear_flag(caps, CAP_EFFECTIVE); + if (ret) + goto out; + + ret = cap_set_proc(caps); + if (ret) + goto out; + + fret = true; + +out: + cap_free(caps); + return fret; +} + +/* cap_down - lower an effective cap */ +int cap_down(cap_value_t down) +{ + bool fret = false; + cap_t caps = NULL; + cap_value_t cap = down; + int ret = -1; + + caps = cap_get_proc(); + if (!caps) + goto out; + + ret = cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap, 0); + if (ret) + goto out; + + ret = cap_set_proc(caps); + if (ret) + goto out; + + fret = true; + +out: + cap_free(caps); + return fret; +} + +uint64_t get_unique_mnt_id(const char *path) +{ + struct statx sx; + int ret; + + ret = statx(AT_FDCWD, path, 0, STATX_MNT_ID_UNIQUE, &sx); + if (ret == -1) { + ksft_print_msg("retrieving unique mount ID for %s: %s\n", path, + strerror(errno)); + return 0; + } + + if (!(sx.stx_mask & STATX_MNT_ID_UNIQUE)) { + ksft_print_msg("no unique mount ID available for %s\n", path); + return 0; + } + + return sx.stx_mnt_id; +} diff --git a/tools/testing/selftests/filesystems/utils.h b/tools/testing/selftests/filesystems/utils.h new file mode 100644 index 000000000..d03085cef --- /dev/null +++ b/tools/testing/selftests/filesystems/utils.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#ifndef __IDMAP_UTILS_H +#define __IDMAP_UTILS_H + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include <errno.h> +#include <linux/types.h> +#include <sched.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syscall.h> +#include <sys/capability.h> +#include <sys/fsuid.h> +#include <sys/types.h> +#include <unistd.h> + +extern int get_userns_fd(unsigned long nsid, unsigned long hostid, + unsigned long range); + +extern int caps_down(void); +extern int cap_down(cap_value_t down); + +extern bool switch_ids(uid_t uid, gid_t gid); +extern int setup_userns(void); +extern int enter_userns(void); + +static inline bool switch_userns(int fd, uid_t uid, gid_t gid, bool drop_caps) +{ + if (setns(fd, CLONE_NEWUSER)) + return false; + + if (!switch_ids(uid, gid)) + return false; + + if (drop_caps && !caps_down()) + return false; + + return true; +} + +extern int wait_for_pid(pid_t pid); +extern int write_file(const char *path, const char *val); +extern uint64_t get_unique_mnt_id(const char *path); + +#endif /* __IDMAP_UTILS_H */ diff --git a/tools/testing/selftests/filesystems/wrappers.h b/tools/testing/selftests/filesystems/wrappers.h new file mode 100644 index 000000000..420ae4f90 --- /dev/null +++ b/tools/testing/selftests/filesystems/wrappers.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0 +// +#ifndef __SELFTEST_OVERLAYFS_WRAPPERS_H__ +#define __SELFTEST_OVERLAYFS_WRAPPERS_H__ + +#define _GNU_SOURCE + +#include <linux/types.h> +#include <linux/mount.h> +#include <sys/syscall.h> + +#ifndef STATX_MNT_ID_UNIQUE +#define STATX_MNT_ID_UNIQUE 0x00004000U /* Want/got extended stx_mount_id */ +#endif + +static inline int sys_fsopen(const char *fsname, unsigned int flags) +{ + return syscall(__NR_fsopen, fsname, flags); +} + +static inline int sys_fsconfig(int fd, unsigned int cmd, const char *key, + const char *value, int aux) +{ + return syscall(__NR_fsconfig, fd, cmd, key, value, aux); +} + +static inline int sys_fsmount(int fd, unsigned int flags, + unsigned int attr_flags) +{ + return syscall(__NR_fsmount, fd, flags, attr_flags); +} + +static inline int sys_mount(const char *src, const char *tgt, const char *fst, + unsigned long flags, const void *data) +{ + return syscall(__NR_mount, src, tgt, fst, flags, data); +} + +#ifndef MOVE_MOUNT_F_EMPTY_PATH +#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 /* Empty from path permitted */ +#endif + +#ifndef MOVE_MOUNT_T_EMPTY_PATH +#define MOVE_MOUNT_T_EMPTY_PATH 0x00000040 /* Empty to path permitted */ +#endif + +#ifndef __NR_move_mount + #if defined __alpha__ + #define __NR_move_mount 539 + #elif defined _MIPS_SIM + #if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */ + #define __NR_move_mount 4429 + #endif + #if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */ + #define __NR_move_mount 6429 + #endif + #if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */ + #define __NR_move_mount 5429 + #endif + #else + #define __NR_move_mount 429 + #endif +#endif + +static inline int sys_move_mount(int from_dfd, const char *from_pathname, + int to_dfd, const char *to_pathname, + unsigned int flags) +{ + return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd, + to_pathname, flags); +} + +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif + +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif + +#ifndef AT_RECURSIVE +#define AT_RECURSIVE 0x8000 /* Apply to the entire subtree */ +#endif + +#ifndef __NR_open_tree + #if defined __alpha__ + #define __NR_open_tree 538 + #elif defined _MIPS_SIM + #if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */ + #define __NR_open_tree 4428 + #endif + #if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */ + #define __NR_open_tree 6428 + #endif + #if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */ + #define __NR_open_tree 5428 + #endif + #else + #define __NR_open_tree 428 + #endif +#endif + +static inline int sys_open_tree(int dfd, const char *filename, unsigned int flags) +{ + return syscall(__NR_open_tree, dfd, filename, flags); +} + +#endif diff --git a/tools/testing/selftests/filesystems/xattr/.gitignore b/tools/testing/selftests/filesystems/xattr/.gitignore new file mode 100644 index 000000000..092d14094 --- /dev/null +++ b/tools/testing/selftests/filesystems/xattr/.gitignore @@ -0,0 +1,3 @@ +xattr_socket_test +xattr_sockfs_test +xattr_socket_types_test diff --git a/tools/testing/selftests/filesystems/xattr/Makefile b/tools/testing/selftests/filesystems/xattr/Makefile new file mode 100644 index 000000000..95364ffb1 --- /dev/null +++ b/tools/testing/selftests/filesystems/xattr/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += $(KHDR_INCLUDES) +TEST_GEN_PROGS := xattr_socket_test xattr_sockfs_test xattr_socket_types_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/xattr/xattr_socket_test.c b/tools/testing/selftests/filesystems/xattr/xattr_socket_test.c new file mode 100644 index 000000000..fac0a4c6b --- /dev/null +++ b/tools/testing/selftests/filesystems/xattr/xattr_socket_test.c @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2026 Christian Brauner <brauner@kernel.org> +/* + * Test extended attributes on path-based Unix domain sockets. + * + * Path-based Unix domain sockets are bound to a filesystem path and their + * inodes live on the underlying filesystem (e.g. tmpfs). These tests verify + * that user.* and trusted.* xattr operations work correctly on them using + * path-based syscalls (setxattr, getxattr, etc.). + * + * Covers SOCK_STREAM, SOCK_DGRAM, and SOCK_SEQPACKET socket types. + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <sys/xattr.h> +#include <unistd.h> + +#include "../../kselftest_harness.h" + +#define TEST_XATTR_NAME "user.testattr" +#define TEST_XATTR_VALUE "testvalue" +#define TEST_XATTR_VALUE2 "newvalue" + +/* + * Fixture for path-based Unix domain socket tests. + * Creates a SOCK_STREAM socket bound to a path in /tmp (typically tmpfs). + */ +FIXTURE(xattr_socket) +{ + char socket_path[PATH_MAX]; + int sockfd; +}; + +FIXTURE_VARIANT(xattr_socket) +{ + int sock_type; + const char *name; +}; + +FIXTURE_VARIANT_ADD(xattr_socket, stream) { + .sock_type = SOCK_STREAM, + .name = "stream", +}; + +FIXTURE_VARIANT_ADD(xattr_socket, dgram) { + .sock_type = SOCK_DGRAM, + .name = "dgram", +}; + +FIXTURE_VARIANT_ADD(xattr_socket, seqpacket) { + .sock_type = SOCK_SEQPACKET, + .name = "seqpacket", +}; + +FIXTURE_SETUP(xattr_socket) +{ + struct sockaddr_un addr; + int ret; + + self->sockfd = -1; + + snprintf(self->socket_path, sizeof(self->socket_path), + "/tmp/xattr_socket_test_%s.%d", variant->name, getpid()); + unlink(self->socket_path); + + self->sockfd = socket(AF_UNIX, variant->sock_type, 0); + ASSERT_GE(self->sockfd, 0) { + TH_LOG("Failed to create socket: %s", strerror(errno)); + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, self->socket_path, sizeof(addr.sun_path) - 1); + + ret = bind(self->sockfd, (struct sockaddr *)&addr, sizeof(addr)); + ASSERT_EQ(ret, 0) { + TH_LOG("Failed to bind socket to %s: %s", + self->socket_path, strerror(errno)); + } +} + +FIXTURE_TEARDOWN(xattr_socket) +{ + if (self->sockfd >= 0) + close(self->sockfd); + unlink(self->socket_path); +} + +TEST_F(xattr_socket, set_user_xattr) +{ + int ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr failed: %s (errno=%d)", strerror(errno), errno); + } +} + +TEST_F(xattr_socket, get_user_xattr) +{ + char buf[256]; + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr failed: %s", strerror(errno)); + } + + memset(buf, 0, sizeof(buf)); + ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)) { + TH_LOG("getxattr returned %zd, expected %zu: %s", + ret, strlen(TEST_XATTR_VALUE), strerror(errno)); + } + ASSERT_STREQ(buf, TEST_XATTR_VALUE); +} + +TEST_F(xattr_socket, list_user_xattr) +{ + char list[1024]; + ssize_t ret; + bool found = false; + char *ptr; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr failed: %s", strerror(errno)); + } + + memset(list, 0, sizeof(list)); + ret = listxattr(self->socket_path, list, sizeof(list)); + ASSERT_GT(ret, 0) { + TH_LOG("listxattr failed: %s", strerror(errno)); + } + + for (ptr = list; ptr < list + ret; ptr += strlen(ptr) + 1) { + if (strcmp(ptr, TEST_XATTR_NAME) == 0) { + found = true; + break; + } + } + ASSERT_TRUE(found) { + TH_LOG("xattr %s not found in list", TEST_XATTR_NAME); + } +} + +TEST_F(xattr_socket, remove_user_xattr) +{ + char buf[256]; + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr failed: %s", strerror(errno)); + } + + ret = removexattr(self->socket_path, TEST_XATTR_NAME); + ASSERT_EQ(ret, 0) { + TH_LOG("removexattr failed: %s", strerror(errno)); + } + + ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA) { + TH_LOG("Expected ENODATA, got %s", strerror(errno)); + } +} + +/* + * Test that xattrs persist across socket close and reopen. + * The xattr is on the filesystem inode, not the socket fd. + */ +TEST_F(xattr_socket, xattr_persistence) +{ + char buf[256]; + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr failed: %s", strerror(errno)); + } + + close(self->sockfd); + self->sockfd = -1; + + memset(buf, 0, sizeof(buf)); + ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)) { + TH_LOG("getxattr after close failed: %s", strerror(errno)); + } + ASSERT_STREQ(buf, TEST_XATTR_VALUE); +} + +TEST_F(xattr_socket, update_user_xattr) +{ + char buf[256]; + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), 0); + ASSERT_EQ(ret, 0); + + memset(buf, 0, sizeof(buf)); + ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE2)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE2); +} + +TEST_F(xattr_socket, xattr_create_flag) +{ + int ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), XATTR_CREATE); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, EEXIST); +} + +TEST_F(xattr_socket, xattr_replace_flag) +{ + int ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), XATTR_REPLACE); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +TEST_F(xattr_socket, multiple_xattrs) +{ + char buf[256]; + ssize_t ret; + int i; + char name[64], value[64]; + const int num_xattrs = 5; + + for (i = 0; i < num_xattrs; i++) { + snprintf(name, sizeof(name), "user.test%d", i); + snprintf(value, sizeof(value), "value%d", i); + ret = setxattr(self->socket_path, name, value, strlen(value), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr %s failed: %s", name, strerror(errno)); + } + } + + for (i = 0; i < num_xattrs; i++) { + snprintf(name, sizeof(name), "user.test%d", i); + snprintf(value, sizeof(value), "value%d", i); + memset(buf, 0, sizeof(buf)); + ret = getxattr(self->socket_path, name, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(value)); + ASSERT_STREQ(buf, value); + } +} + +TEST_F(xattr_socket, xattr_empty_value) +{ + char buf[256]; + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, "", 0, 0); + ASSERT_EQ(ret, 0); + + ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, 0); +} + +TEST_F(xattr_socket, xattr_get_size) +{ + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = getxattr(self->socket_path, TEST_XATTR_NAME, NULL, 0); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)); +} + +TEST_F(xattr_socket, xattr_buffer_too_small) +{ + char buf[2]; + ssize_t ret; + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ERANGE); +} + +TEST_F(xattr_socket, xattr_nonexistent) +{ + char buf[256]; + ssize_t ret; + + ret = getxattr(self->socket_path, "user.nonexistent", buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +TEST_F(xattr_socket, remove_nonexistent_xattr) +{ + int ret; + + ret = removexattr(self->socket_path, "user.nonexistent"); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +TEST_F(xattr_socket, large_xattr_value) +{ + char large_value[4096]; + char read_buf[4096]; + ssize_t ret; + + memset(large_value, 'A', sizeof(large_value)); + + ret = setxattr(self->socket_path, TEST_XATTR_NAME, + large_value, sizeof(large_value), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr with large value failed: %s", strerror(errno)); + } + + memset(read_buf, 0, sizeof(read_buf)); + ret = getxattr(self->socket_path, TEST_XATTR_NAME, + read_buf, sizeof(read_buf)); + ASSERT_EQ(ret, (ssize_t)sizeof(large_value)); + ASSERT_EQ(memcmp(large_value, read_buf, sizeof(large_value)), 0); +} + +/* + * Test lsetxattr/lgetxattr (don't follow symlinks). + * Socket files aren't symlinks, so this should work the same. + */ +TEST_F(xattr_socket, lsetxattr_lgetxattr) +{ + char buf[256]; + ssize_t ret; + + ret = lsetxattr(self->socket_path, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("lsetxattr failed: %s", strerror(errno)); + } + + memset(buf, 0, sizeof(buf)); + ret = lgetxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE); +} + +/* + * Fixture for trusted.* xattr tests. + * These require CAP_SYS_ADMIN. + */ +FIXTURE(xattr_socket_trusted) +{ + char socket_path[PATH_MAX]; + int sockfd; +}; + +FIXTURE_VARIANT(xattr_socket_trusted) +{ + int sock_type; + const char *name; +}; + +FIXTURE_VARIANT_ADD(xattr_socket_trusted, stream) { + .sock_type = SOCK_STREAM, + .name = "stream", +}; + +FIXTURE_VARIANT_ADD(xattr_socket_trusted, dgram) { + .sock_type = SOCK_DGRAM, + .name = "dgram", +}; + +FIXTURE_VARIANT_ADD(xattr_socket_trusted, seqpacket) { + .sock_type = SOCK_SEQPACKET, + .name = "seqpacket", +}; + +FIXTURE_SETUP(xattr_socket_trusted) +{ + struct sockaddr_un addr; + int ret; + + self->sockfd = -1; + + snprintf(self->socket_path, sizeof(self->socket_path), + "/tmp/xattr_socket_trusted_%s.%d", variant->name, getpid()); + unlink(self->socket_path); + + self->sockfd = socket(AF_UNIX, variant->sock_type, 0); + ASSERT_GE(self->sockfd, 0); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, self->socket_path, sizeof(addr.sun_path) - 1); + + ret = bind(self->sockfd, (struct sockaddr *)&addr, sizeof(addr)); + ASSERT_EQ(ret, 0); +} + +FIXTURE_TEARDOWN(xattr_socket_trusted) +{ + if (self->sockfd >= 0) + close(self->sockfd); + unlink(self->socket_path); +} + +TEST_F(xattr_socket_trusted, set_trusted_xattr) +{ + char buf[256]; + ssize_t len; + int ret; + + ret = setxattr(self->socket_path, "trusted.testattr", + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + if (ret == -1 && errno == EPERM) + SKIP(return, "Need CAP_SYS_ADMIN for trusted.* xattrs"); + ASSERT_EQ(ret, 0) { + TH_LOG("setxattr trusted.testattr failed: %s", strerror(errno)); + } + + memset(buf, 0, sizeof(buf)); + len = getxattr(self->socket_path, "trusted.testattr", + buf, sizeof(buf)); + ASSERT_EQ(len, (ssize_t)strlen(TEST_XATTR_VALUE)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE); +} + +TEST_F(xattr_socket_trusted, get_trusted_xattr_unprivileged) +{ + char buf[256]; + ssize_t ret; + + ret = getxattr(self->socket_path, "trusted.testattr", buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_TRUE(errno == ENODATA || errno == EPERM) { + TH_LOG("Expected ENODATA or EPERM, got %s", strerror(errno)); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/xattr/xattr_socket_types_test.c b/tools/testing/selftests/filesystems/xattr/xattr_socket_types_test.c new file mode 100644 index 000000000..bfabe91b2 --- /dev/null +++ b/tools/testing/selftests/filesystems/xattr/xattr_socket_types_test.c @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2026 Christian Brauner <brauner@kernel.org> +/* + * Test user.* xattrs on various socket families. + * + * All socket types use sockfs for their inodes, so user.* xattrs should + * work on any socket regardless of address family. This tests AF_INET, + * AF_INET6, AF_NETLINK, AF_PACKET, and abstract namespace AF_UNIX sockets. + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> +#include <sys/xattr.h> +#include <linux/netlink.h> +#include <unistd.h> + +#include "../../kselftest_harness.h" + +#define TEST_XATTR_NAME "user.testattr" +#define TEST_XATTR_VALUE "testvalue" + +FIXTURE(xattr_socket_types) +{ + int sockfd; +}; + +FIXTURE_VARIANT(xattr_socket_types) +{ + int family; + int type; + int protocol; +}; + +FIXTURE_VARIANT_ADD(xattr_socket_types, inet) { + .family = AF_INET, + .type = SOCK_STREAM, + .protocol = 0, +}; + +FIXTURE_VARIANT_ADD(xattr_socket_types, inet6) { + .family = AF_INET6, + .type = SOCK_STREAM, + .protocol = 0, +}; + +FIXTURE_VARIANT_ADD(xattr_socket_types, netlink) { + .family = AF_NETLINK, + .type = SOCK_RAW, + .protocol = NETLINK_USERSOCK, +}; + +FIXTURE_VARIANT_ADD(xattr_socket_types, packet) { + .family = AF_PACKET, + .type = SOCK_DGRAM, + .protocol = 0, +}; + +FIXTURE_SETUP(xattr_socket_types) +{ + self->sockfd = socket(variant->family, variant->type, + variant->protocol); + if (self->sockfd < 0 && + (errno == EAFNOSUPPORT || errno == EPERM || errno == EACCES)) + SKIP(return, "socket(%d, %d, %d) not available: %s", + variant->family, variant->type, variant->protocol, + strerror(errno)); + ASSERT_GE(self->sockfd, 0) { + TH_LOG("Failed to create socket(%d, %d, %d): %s", + variant->family, variant->type, variant->protocol, + strerror(errno)); + } +} + +FIXTURE_TEARDOWN(xattr_socket_types) +{ + if (self->sockfd >= 0) + close(self->sockfd); +} + +TEST_F(xattr_socket_types, set_get_list_remove) +{ + char buf[256], list[4096], *ptr; + ssize_t ret; + bool found; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("fsetxattr failed: %s", strerror(errno)); + } + + memset(buf, 0, sizeof(buf)); + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE); + + memset(list, 0, sizeof(list)); + ret = flistxattr(self->sockfd, list, sizeof(list)); + ASSERT_GT(ret, 0); + found = false; + for (ptr = list; ptr < list + ret; ptr += strlen(ptr) + 1) { + if (strcmp(ptr, TEST_XATTR_NAME) == 0) + found = true; + } + ASSERT_TRUE(found); + + ret = fremovexattr(self->sockfd, TEST_XATTR_NAME); + ASSERT_EQ(ret, 0); + + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +/* + * Test abstract namespace AF_UNIX socket. + * Abstract sockets don't have a filesystem path; their inodes live in + * sockfs so user.* xattrs should work via fsetxattr/fgetxattr. + */ +FIXTURE(xattr_abstract) +{ + int sockfd; +}; + +FIXTURE_SETUP(xattr_abstract) +{ + struct sockaddr_un addr; + char name[64]; + int ret, len; + + self->sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_GE(self->sockfd, 0); + + len = snprintf(name, sizeof(name), "xattr_test_abstract_%d", getpid()); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + addr.sun_path[0] = '\0'; + memcpy(&addr.sun_path[1], name, len); + + ret = bind(self->sockfd, (struct sockaddr *)&addr, + offsetof(struct sockaddr_un, sun_path) + 1 + len); + ASSERT_EQ(ret, 0); +} + +FIXTURE_TEARDOWN(xattr_abstract) +{ + if (self->sockfd >= 0) + close(self->sockfd); +} + +TEST_F(xattr_abstract, set_get) +{ + char buf[256]; + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("fsetxattr on abstract socket failed: %s", + strerror(errno)); + } + + memset(buf, 0, sizeof(buf)); + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/filesystems/xattr/xattr_sockfs_test.c b/tools/testing/selftests/filesystems/xattr/xattr_sockfs_test.c new file mode 100644 index 000000000..b4824b01a --- /dev/null +++ b/tools/testing/selftests/filesystems/xattr/xattr_sockfs_test.c @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2026 Christian Brauner <brauner@kernel.org> +/* + * Test extended attributes on sockfs sockets. + * + * Sockets created via socket() have their inodes in sockfs, which supports + * user.* xattrs with per-inode limits: up to 128 xattrs and 128KB total + * value size. These tests verify xattr operations via fsetxattr/fgetxattr/ + * flistxattr/fremovexattr on the socket fd, as well as limit enforcement. + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/xattr.h> +#include <unistd.h> + +#include "../../kselftest_harness.h" + +#define TEST_XATTR_NAME "user.testattr" +#define TEST_XATTR_VALUE "testvalue" +#define TEST_XATTR_VALUE2 "newvalue" + +/* Per-inode limits for user.* xattrs on sockfs (from include/linux/xattr.h) */ +#define SIMPLE_XATTR_MAX_NR 128 +#define SIMPLE_XATTR_MAX_SIZE (128 << 10) /* 128 KB */ + +#ifndef XATTR_SIZE_MAX +#define XATTR_SIZE_MAX 65536 +#endif + +/* + * Fixture for sockfs socket xattr tests. + * Creates an AF_UNIX socket (lives in sockfs, not bound to any path). + */ +FIXTURE(xattr_sockfs) +{ + int sockfd; +}; + +FIXTURE_SETUP(xattr_sockfs) +{ + self->sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_GE(self->sockfd, 0) { + TH_LOG("Failed to create socket: %s", strerror(errno)); + } +} + +FIXTURE_TEARDOWN(xattr_sockfs) +{ + if (self->sockfd >= 0) + close(self->sockfd); +} + +TEST_F(xattr_sockfs, set_get_user_xattr) +{ + char buf[256]; + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("fsetxattr failed: %s", strerror(errno)); + } + + memset(buf, 0, sizeof(buf)); + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)) { + TH_LOG("fgetxattr returned %zd: %s", ret, strerror(errno)); + } + ASSERT_STREQ(buf, TEST_XATTR_VALUE); +} + +/* + * Test listing xattrs on a sockfs socket. + * Should include user.* xattrs and system.sockprotoname. + */ +TEST_F(xattr_sockfs, list_user_xattr) +{ + char list[4096]; + ssize_t ret; + char *ptr; + bool found_user = false; + bool found_proto = false; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0) { + TH_LOG("fsetxattr failed: %s", strerror(errno)); + } + + memset(list, 0, sizeof(list)); + ret = flistxattr(self->sockfd, list, sizeof(list)); + ASSERT_GT(ret, 0) { + TH_LOG("flistxattr failed: %s", strerror(errno)); + } + + for (ptr = list; ptr < list + ret; ptr += strlen(ptr) + 1) { + if (strcmp(ptr, TEST_XATTR_NAME) == 0) + found_user = true; + if (strcmp(ptr, "system.sockprotoname") == 0) + found_proto = true; + } + ASSERT_TRUE(found_user) { + TH_LOG("user xattr not found in list"); + } + ASSERT_TRUE(found_proto) { + TH_LOG("system.sockprotoname not found in list"); + } +} + +TEST_F(xattr_sockfs, remove_user_xattr) +{ + char buf[256]; + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = fremovexattr(self->sockfd, TEST_XATTR_NAME); + ASSERT_EQ(ret, 0) { + TH_LOG("fremovexattr failed: %s", strerror(errno)); + } + + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +TEST_F(xattr_sockfs, update_user_xattr) +{ + char buf[256]; + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), 0); + ASSERT_EQ(ret, 0); + + memset(buf, 0, sizeof(buf)); + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE2)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE2); +} + +TEST_F(xattr_sockfs, xattr_create_flag) +{ + int ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), + XATTR_CREATE); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, EEXIST); +} + +TEST_F(xattr_sockfs, xattr_replace_flag) +{ + int ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), + XATTR_REPLACE); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +TEST_F(xattr_sockfs, get_nonexistent) +{ + char buf[256]; + ssize_t ret; + + ret = fgetxattr(self->sockfd, "user.nonexistent", buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); +} + +TEST_F(xattr_sockfs, empty_value) +{ + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, "", 0, 0); + ASSERT_EQ(ret, 0); + + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, NULL, 0); + ASSERT_EQ(ret, 0); +} + +TEST_F(xattr_sockfs, get_size) +{ + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, NULL, 0); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)); +} + +TEST_F(xattr_sockfs, buffer_too_small) +{ + char buf[2]; + ssize_t ret; + + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ERANGE); +} + +/* + * Test maximum number of user.* xattrs per socket. + * The kernel enforces SIMPLE_XATTR_MAX_NR (128), so the 129th should + * fail with ENOSPC. + */ +TEST_F(xattr_sockfs, max_nr_xattrs) +{ + char name[32]; + int i, ret; + + for (i = 0; i < SIMPLE_XATTR_MAX_NR; i++) { + snprintf(name, sizeof(name), "user.test%03d", i); + ret = fsetxattr(self->sockfd, name, "v", 1, 0); + ASSERT_EQ(ret, 0) { + TH_LOG("fsetxattr %s failed at i=%d: %s", + name, i, strerror(errno)); + } + } + + ret = fsetxattr(self->sockfd, "user.overflow", "v", 1, 0); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOSPC) { + TH_LOG("Expected ENOSPC for xattr %d, got %s", + SIMPLE_XATTR_MAX_NR + 1, strerror(errno)); + } +} + +/* + * Test maximum total value size for user.* xattrs. + * The kernel enforces SIMPLE_XATTR_MAX_SIZE (128KB). Individual xattr + * values are limited to XATTR_SIZE_MAX (64KB) by the VFS, so we need + * at least two xattrs to hit the total limit. + */ +TEST_F(xattr_sockfs, max_xattr_size) +{ + char *value; + int ret; + + value = malloc(XATTR_SIZE_MAX); + ASSERT_NE(value, NULL); + memset(value, 'A', XATTR_SIZE_MAX); + + /* First 64KB xattr - total = 64KB */ + ret = fsetxattr(self->sockfd, "user.big1", value, XATTR_SIZE_MAX, 0); + ASSERT_EQ(ret, 0) { + TH_LOG("first large xattr failed: %s", strerror(errno)); + } + + /* Second 64KB xattr - total = 128KB (exactly at limit) */ + ret = fsetxattr(self->sockfd, "user.big2", value, XATTR_SIZE_MAX, 0); + free(value); + ASSERT_EQ(ret, 0) { + TH_LOG("second large xattr failed: %s", strerror(errno)); + } + + /* Third xattr with 1 byte - total > 128KB, should fail */ + ret = fsetxattr(self->sockfd, "user.big3", "v", 1, 0); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOSPC) { + TH_LOG("Expected ENOSPC when exceeding size limit, got %s", + strerror(errno)); + } +} + +/* + * Test that removing an xattr frees limit space, allowing re-addition. + */ +TEST_F(xattr_sockfs, limit_remove_readd) +{ + char name[32]; + int i, ret; + + /* Fill up to the maximum count */ + for (i = 0; i < SIMPLE_XATTR_MAX_NR; i++) { + snprintf(name, sizeof(name), "user.test%03d", i); + ret = fsetxattr(self->sockfd, name, "v", 1, 0); + ASSERT_EQ(ret, 0); + } + + /* Verify we're at the limit */ + ret = fsetxattr(self->sockfd, "user.overflow", "v", 1, 0); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENOSPC); + + /* Remove one xattr */ + ret = fremovexattr(self->sockfd, "user.test000"); + ASSERT_EQ(ret, 0); + + /* Now we should be able to add one more */ + ret = fsetxattr(self->sockfd, "user.newattr", "v", 1, 0); + ASSERT_EQ(ret, 0) { + TH_LOG("re-add after remove failed: %s", strerror(errno)); + } +} + +/* + * Test that two different sockets have independent xattr limits. + */ +TEST_F(xattr_sockfs, limits_per_inode) +{ + char buf[256]; + int sock2; + ssize_t ret; + + sock2 = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_GE(sock2, 0); + + /* Set xattr on first socket */ + ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, + TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0); + ASSERT_EQ(ret, 0); + + /* First socket's xattr should not be visible on second socket */ + ret = fgetxattr(sock2, TEST_XATTR_NAME, NULL, 0); + ASSERT_EQ(ret, -1); + ASSERT_EQ(errno, ENODATA); + + /* Second socket should independently accept xattrs */ + ret = fsetxattr(sock2, TEST_XATTR_NAME, + TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), 0); + ASSERT_EQ(ret, 0); + + /* Verify each socket has its own value */ + memset(buf, 0, sizeof(buf)); + ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE); + + memset(buf, 0, sizeof(buf)); + ret = fgetxattr(sock2, TEST_XATTR_NAME, buf, sizeof(buf)); + ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE2)); + ASSERT_STREQ(buf, TEST_XATTR_VALUE2); + + close(sock2); +} + +TEST_HARNESS_MAIN |
