| // SPDX-License-Identifier: GPL-2.0-or-later |
| // Copyright (c) 2025 Miklos Szeredi <miklos@szeredi.hu> |
| |
| #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 <sys/syscall.h> |
| |
| #include "../../kselftest_harness.h" |
| #include "../statmount/statmount.h" |
| #include "../utils.h" |
| |
| // Needed for linux/fanotify.h |
| #ifndef __kernel_fsid_t |
| typedef struct { |
| int val[2]; |
| } __kernel_fsid_t; |
| #endif |
| |
| #include <sys/fanotify.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 |