From: Amir Goldstein <amir73il@gmail.com>
To: Jan Kara <jack@suse.cz>
Cc: Christian Brauner <brauner@kernel.org>, linux-fsdevel@vger.kernel.org
Subject: [PATCH v2 10/10] selftests/filesystems: add fanotify namespace notifications test
Date: Fri, 24 Apr 2026 19:05:03 +0200 [thread overview]
Message-ID: <20260424170503.2096847-11-amir73il@gmail.com> (raw)
In-Reply-To: <20260424170503.2096847-1-amir73il@gmail.com>
Test create and delete events for nsfs:
- For init userns and child userns
- Verify delete event is created regardless of vfs inode access
- Verify required ns capabilities
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
tools/include/uapi/linux/fanotify.h | 37 +-
.../selftests/filesystems/fanotify/Makefile | 3 +-
.../filesystems/fanotify/ns-notify_test.c | 330 ++++++++++++++++++
3 files changed, 362 insertions(+), 8 deletions(-)
create mode 100644 tools/testing/selftests/filesystems/fanotify/ns-notify_test.c
diff --git a/tools/include/uapi/linux/fanotify.h b/tools/include/uapi/linux/fanotify.h
index e710967c7c263..8a12db80f9d80 100644
--- a/tools/include/uapi/linux/fanotify.h
+++ b/tools/include/uapi/linux/fanotify.h
@@ -4,7 +4,9 @@
#include <linux/types.h>
-/* the following events that user-space can register for */
+/*
+ * Events that user-space can request when watching filesystems
+ */
#define FAN_ACCESS 0x00000001 /* File was accessed */
#define FAN_MODIFY 0x00000002 /* File was modified */
#define FAN_ATTRIB 0x00000004 /* Metadata changed */
@@ -28,19 +30,31 @@
/* #define FAN_DIR_MODIFY 0x00080000 */ /* Deprecated (reserved) */
#define FAN_PRE_ACCESS 0x00100000 /* Pre-content access hook */
-#define FAN_MNT_ATTACH 0x01000000 /* Mount was attached */
-#define FAN_MNT_DETACH 0x02000000 /* Mount was detached */
-
-#define FAN_EVENT_ON_CHILD 0x08000000 /* Interested in child events */
#define FAN_RENAME 0x10000000 /* File was renamed */
-#define FAN_ONDIR 0x40000000 /* Event occurred against dir */
-
/* helper events */
#define FAN_CLOSE (FAN_CLOSE_WRITE | FAN_CLOSE_NOWRITE) /* close */
#define FAN_MOVE (FAN_MOVED_FROM | FAN_MOVED_TO) /* moves */
+/*
+ * Filter flags for watching filesystems
+ */
+#define FAN_EVENT_ON_CHILD 0x08000000 /* Interested in child events */
+#define FAN_ONDIR 0x40000000 /* Event occurred against dir */
+
+/*
+ * Events that user-space can request when watching namespaces
+ *
+ * NOTE: These values may overload filesystem events, but not event flags
+ */
+#define FAN_NS_CREATE 0x00000100 /* Sub namespace was created */
+#define FAN_NS_DELETE 0x00000200 /* Sub namespace was deleted */
+
+#define FAN_MNT_ATTACH 0x01000000 /* Mount was attached */
+#define FAN_MNT_DETACH 0x02000000 /* Mount was detached */
+
+
/* flags used for fanotify_init() */
#define FAN_CLOEXEC 0x00000001
#define FAN_NONBLOCK 0x00000002
@@ -67,6 +81,7 @@
#define FAN_REPORT_TARGET_FID 0x00001000 /* Report dirent target id */
#define FAN_REPORT_FD_ERROR 0x00002000 /* event->fd can report error */
#define FAN_REPORT_MNT 0x00004000 /* Report mount events */
+#define FAN_REPORT_NSID 0x00008000 /* Report namespace events */
/* Convenience macro - FAN_REPORT_NAME requires FAN_REPORT_DIR_FID */
#define FAN_REPORT_DFID_NAME (FAN_REPORT_DIR_FID | FAN_REPORT_NAME)
@@ -98,6 +113,7 @@
#define FAN_MARK_MOUNT 0x00000010
#define FAN_MARK_FILESYSTEM 0x00000100
#define FAN_MARK_MNTNS 0x00000110
+#define FAN_MARK_USERNS 0x00001000
/*
* Convenience macro - FAN_MARK_IGNORE requires FAN_MARK_IGNORED_SURV_MODIFY
@@ -152,6 +168,7 @@ struct fanotify_event_metadata {
#define FAN_EVENT_INFO_TYPE_ERROR 5
#define FAN_EVENT_INFO_TYPE_RANGE 6
#define FAN_EVENT_INFO_TYPE_MNT 7
+#define FAN_EVENT_INFO_TYPE_NS 8
/* Special info types for FAN_RENAME */
#define FAN_EVENT_INFO_TYPE_OLD_DFID_NAME 10
@@ -210,6 +227,12 @@ struct fanotify_event_info_mnt {
__u64 mnt_id;
};
+struct fanotify_event_info_ns {
+ struct fanotify_event_info_header hdr;
+ __u64 self_nsid; /* ns_id of the namespace */
+ __u64 owner_nsid; /* ns_id of its owning user namespace */
+};
+
/*
* User space may need to record additional information about its decision.
* The extra information type records what kind of information is included.
diff --git a/tools/testing/selftests/filesystems/fanotify/Makefile b/tools/testing/selftests/filesystems/fanotify/Makefile
index 836a4eb7be062..d251249630985 100644
--- a/tools/testing/selftests/filesystems/fanotify/Makefile
+++ b/tools/testing/selftests/filesystems/fanotify/Makefile
@@ -3,9 +3,10 @@
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
-TEST_GEN_PROGS := mount-notify_test mount-notify_test_ns
+TEST_GEN_PROGS := mount-notify_test mount-notify_test_ns ns-notify_test
include ../../lib.mk
$(OUTPUT)/mount-notify_test: ../utils.c
$(OUTPUT)/mount-notify_test_ns: ../utils.c
+$(OUTPUT)/ns-notify_test: ../utils.c
diff --git a/tools/testing/selftests/filesystems/fanotify/ns-notify_test.c b/tools/testing/selftests/filesystems/fanotify/ns-notify_test.c
new file mode 100644
index 0000000000000..012a62c92ee4a
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fanotify/ns-notify_test.c
@@ -0,0 +1,330 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (c) 2025
+
+#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 <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/fanotify.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+#include "../utils.h"
+
+#include <linux/fanotify.h>
+
+/*
+ * Retrieve the ns_id of a namespace fd via name_to_handle_at().
+ * nsfs encodes { ns_id(u64), ns_type(u32), ns_inum(u32) } in f_handle.
+ */
+static uint64_t get_ns_id(int fd)
+{
+ struct {
+ struct file_handle fh;
+ uint64_t ns_id;
+ uint32_t ns_type;
+ uint32_t ns_inum;
+ } h = { .fh.handle_bytes = sizeof(uint64_t) + sizeof(uint32_t) * 2 };
+ int mnt_id;
+
+ if (name_to_handle_at(fd, "", &h.fh, &mnt_id, AT_EMPTY_PATH))
+ return 0;
+ return h.ns_id;
+}
+
+static void read_ns_event_fd(struct __test_metadata *const _metadata,
+ int fd, char *buf, size_t buf_size,
+ uint64_t expect_mask,
+ uint64_t *self_nsid_out, uint64_t *owner_nsid_out)
+{
+ struct fanotify_event_metadata *meta;
+ struct fanotify_event_info_ns *info;
+ ssize_t len;
+
+ len = read(fd, buf, buf_size);
+ ASSERT_GT(len, 0);
+
+ meta = (struct fanotify_event_metadata *)buf;
+ ASSERT_TRUE(FAN_EVENT_OK(meta, len));
+ ASSERT_EQ(meta->mask, expect_mask);
+ ASSERT_EQ(meta->fd, FAN_NOFD);
+ ASSERT_EQ(meta->event_len,
+ sizeof(*meta) + sizeof(struct fanotify_event_info_ns));
+
+ info = (struct fanotify_event_info_ns *)(meta + 1);
+ ASSERT_EQ(info->hdr.info_type, FAN_EVENT_INFO_TYPE_NS);
+ ASSERT_EQ(info->hdr.len, sizeof(*info));
+
+ *self_nsid_out = info->self_nsid;
+ *owner_nsid_out = info->owner_nsid;
+}
+
+/* =========================================================================
+ * Outer tests: watch init_user_ns from root context (no setup_userns).
+ * ========================================================================= */
+
+/*
+ * Root-only: watch init_user_ns, fork a child that creates a user namespace
+ * owned by init_user_ns, verify FAN_CREATE, let the child exit, verify
+ * FAN_DELETE. The watched namespace is created and destroyed entirely within
+ * the test body so both events are observable.
+ */
+TEST(outer_create_delete_userns)
+{
+ int fan_fd, ns_fd;
+ int pipefd[2];
+ pid_t pid;
+ uint64_t ns_nsid, create_self, create_owner;
+ uint64_t delete_self, delete_owner;
+ char buf[256];
+ char c;
+
+ if (geteuid() != 0)
+ SKIP(return, "requires root");
+
+ ns_fd = open("/proc/self/ns/user", O_RDONLY);
+ ASSERT_GE(ns_fd, 0);
+
+ ns_nsid = get_ns_id(ns_fd);
+ ASSERT_NE(ns_nsid, 0);
+
+ fan_fd = fanotify_init(FAN_REPORT_NSID, 0);
+ ASSERT_GE(fan_fd, 0);
+
+ errno = 0;
+ ASSERT_EQ(fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_USERNS,
+ FAN_NS_CREATE | FAN_NS_DELETE, ns_fd, NULL), 0)
+ TH_LOG("fanotify_mark errno=%d (%s)", errno, strerror(errno));
+
+ ASSERT_EQ(pipe(pipefd), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+
+ if (pid == 0) {
+ close(pipefd[0]);
+ if (unshare(CLONE_NEWUSER))
+ _exit(1);
+ if (write(pipefd[1], "r", 1) < 0)
+ _exit(1);
+ close(pipefd[1]);
+ pause();
+ _exit(0);
+ }
+
+ close(pipefd[1]);
+ ASSERT_EQ(read(pipefd[0], &c, 1), 1);
+ close(pipefd[0]);
+
+ /* --- FAN_NS_CREATE: new user namespace owned by init_user_ns --- */
+ read_ns_event_fd(_metadata, fan_fd, buf, sizeof(buf),
+ FAN_NS_CREATE, &create_self, &create_owner);
+ ASSERT_NE(create_self, 0);
+ ASSERT_EQ(create_owner, ns_nsid);
+
+ /* Let child exit, deactivating its user namespace */
+ kill(pid, SIGTERM);
+ waitpid(pid, NULL, 0);
+
+ /* --- FAN_NS_DELETE --- */
+ read_ns_event_fd(_metadata, fan_fd, buf, sizeof(buf),
+ FAN_NS_DELETE, &delete_self, &delete_owner);
+ ASSERT_EQ(delete_self, create_self);
+ ASSERT_EQ(delete_owner, ns_nsid);
+
+ close(fan_fd);
+ close(ns_fd);
+}
+
+/* =========================================================================
+ * Inner tests: watch a child userns from within it (via setup_userns).
+ * ========================================================================= */
+
+FIXTURE(userns_notify) {
+ int fan_fd;
+ int userns_fd;
+ int outer_ns_fd; /* init_user_ns fd, captured before setup_userns() */
+ uint64_t userns_nsid;
+ char buf[256];
+};
+
+FIXTURE_SETUP(userns_notify)
+{
+ int ret;
+
+ /* Capture the outer user namespace fd before setup_userns() */
+ self->outer_ns_fd = open("/proc/self/ns/user", O_RDONLY);
+ ASSERT_GE(self->outer_ns_fd, 0);
+
+ ret = setup_userns();
+ ASSERT_EQ(ret, 0);
+
+ self->userns_fd = open("/proc/self/ns/user", O_RDONLY);
+ ASSERT_GE(self->userns_fd, 0);
+
+ self->userns_nsid = get_ns_id(self->userns_fd);
+ ASSERT_NE(self->userns_nsid, 0);
+
+ self->fan_fd = fanotify_init(FAN_REPORT_NSID, 0);
+ ASSERT_GE(self->fan_fd, 0);
+
+ errno = 0;
+ ret = fanotify_mark(self->fan_fd, FAN_MARK_ADD | FAN_MARK_USERNS,
+ FAN_NS_CREATE | FAN_NS_DELETE,
+ self->userns_fd, NULL);
+ ASSERT_EQ(ret, 0)
+ TH_LOG("fanotify_mark errno=%d (%s)", errno, strerror(errno));
+}
+
+FIXTURE_TEARDOWN(userns_notify)
+{
+ close(self->fan_fd);
+ close(self->userns_fd);
+ close(self->outer_ns_fd);
+}
+
+static void read_ns_event(struct __test_metadata *const _metadata,
+ FIXTURE_DATA(userns_notify) *self,
+ uint64_t expect_mask,
+ uint64_t *self_nsid_out, uint64_t *owner_nsid_out)
+{
+ read_ns_event_fd(_metadata, self->fan_fd, self->buf, sizeof(self->buf),
+ expect_mask, self_nsid_out, owner_nsid_out);
+}
+
+/*
+ * Create a UTS namespace inside the watched user namespace, verify
+ * FAN_CREATE, then let the child exit and verify FAN_DELETE.
+ * Cross-check self_nsid against the actual ns_id obtained via
+ * name_to_handle_at() on the child's /proc/pid/ns/uts.
+ */
+TEST_F(userns_notify, inner_create_delete_uts)
+{
+ int pipefd[2];
+ pid_t pid;
+ uint64_t create_self, create_owner;
+ uint64_t delete_self, delete_owner;
+ char c;
+
+ ASSERT_EQ(pipe(pipefd), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+
+ if (pid == 0) {
+ close(pipefd[0]);
+ if (unshare(CLONE_NEWUTS))
+ _exit(1);
+ if (write(pipefd[1], "r", 1) < 0)
+ _exit(1);
+ close(pipefd[1]);
+ pause();
+ _exit(0);
+ }
+
+ close(pipefd[1]);
+ ASSERT_EQ(read(pipefd[0], &c, 1), 1);
+ close(pipefd[0]);
+
+ /* --- FAN_NS_CREATE --- */
+ read_ns_event(_metadata, self, FAN_NS_CREATE, &create_self, &create_owner);
+ ASSERT_NE(create_self, 0);
+ ASSERT_EQ(create_owner, self->userns_nsid);
+
+ /* Cross-check self_nsid against the child's actual UTS ns_id */
+ char path[64];
+ int ns_fd;
+ uint64_t uts_nsid;
+
+ snprintf(path, sizeof(path), "/proc/%d/ns/uts", pid);
+ ns_fd = open(path, O_RDONLY);
+ ASSERT_GE(ns_fd, 0);
+ uts_nsid = get_ns_id(ns_fd);
+ close(ns_fd);
+ ASSERT_EQ(uts_nsid, create_self);
+
+ kill(pid, SIGTERM);
+ waitpid(pid, NULL, 0);
+
+ /* --- FAN_NS_DELETE --- */
+ read_ns_event(_metadata, self, FAN_NS_DELETE, &delete_self, &delete_owner);
+ ASSERT_EQ(delete_self, create_self);
+ ASSERT_EQ(delete_owner, self->userns_nsid);
+}
+
+/*
+ * Same as inner_create_delete_uts but the namespace fd is never opened, so
+ * the stashed nsfs dentry/inode is never populated. Verifies that FAN_CREATE
+ * and FAN_DELETE are still delivered and carry a consistent self_nsid.
+ */
+TEST_F(userns_notify, inner_create_delete_uts_no_open)
+{
+ int pipefd[2];
+ pid_t pid;
+ uint64_t create_self, create_owner;
+ uint64_t delete_self, delete_owner;
+ char c;
+
+ ASSERT_EQ(pipe(pipefd), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+
+ if (pid == 0) {
+ close(pipefd[0]);
+ if (unshare(CLONE_NEWUTS))
+ _exit(1);
+ if (write(pipefd[1], "r", 1) < 0)
+ _exit(1);
+ close(pipefd[1]);
+ pause();
+ _exit(0);
+ }
+
+ close(pipefd[1]);
+ ASSERT_EQ(read(pipefd[0], &c, 1), 1);
+ close(pipefd[0]);
+
+ /* --- FAN_NS_CREATE (no open of /proc/pid/ns/uts) --- */
+ read_ns_event(_metadata, self, FAN_NS_CREATE, &create_self, &create_owner);
+ ASSERT_NE(create_self, 0);
+ ASSERT_EQ(create_owner, self->userns_nsid);
+
+ kill(pid, SIGTERM);
+ waitpid(pid, NULL, 0);
+
+ /* --- FAN_NS_DELETE --- */
+ read_ns_event(_metadata, self, FAN_NS_DELETE, &delete_self, &delete_owner);
+ ASSERT_EQ(delete_self, create_self);
+ ASSERT_EQ(delete_owner, self->userns_nsid);
+}
+
+/*
+ * Attempt to set a FAN_MARK_USERNS watch on the initial user namespace.
+ * Requires CAP_SYS_ADMIN in init_user_ns. Since FIXTURE_SETUP calls
+ * setup_userns(), the process lives in a child user namespace and cannot
+ * hold capabilities in init_user_ns, so the call must fail with EPERM
+ * regardless of the outer uid.
+ */
+TEST_F(userns_notify, inner_mark_init_userns_eperm)
+{
+ int ret;
+
+ ret = fanotify_mark(self->fan_fd, FAN_MARK_ADD | FAN_MARK_USERNS,
+ FAN_NS_CREATE | FAN_NS_DELETE,
+ self->outer_ns_fd, NULL);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, EPERM);
+}
+
+TEST_HARNESS_MAIN
--
2.54.0
prev parent reply other threads:[~2026-04-24 17:05 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-24 17:04 [PATCH v2 00/10] fanotify namespace monitoring Amir Goldstein
2026-04-24 17:04 ` [PATCH v2 01/10] fsnotify: rename fsnotify group flag macros Amir Goldstein
2026-04-24 17:04 ` [PATCH v2 02/10] fsnotify: introduce fsnotify group types Amir Goldstein
2026-04-24 17:04 ` [PATCH v2 03/10] fsnotify: separate the events bitmask macros by group type Amir Goldstein
2026-04-24 17:04 ` [PATCH v2 04/10] fanotify: test event->type instead of event mask when possible Amir Goldstein
2026-04-24 17:04 ` [PATCH v2 05/10] fsnotify: do not report mount events with fsnotify() Amir Goldstein
2026-04-24 17:04 ` [PATCH v2 06/10] fanotify: gate fs event classification by group type Amir Goldstein
2026-04-24 17:05 ` [PATCH v2 07/10] fanotify: gate fs events checks in fanotify_mark() " Amir Goldstein
2026-04-24 17:05 ` [PATCH v2 08/10] fanotify: add support for watching the namespaces tree Amir Goldstein
2026-04-24 17:05 ` [PATCH v2 09/10] selftests/filesystems: create fanotify test dir Amir Goldstein
2026-04-24 17:05 ` Amir Goldstein [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260424170503.2096847-11-amir73il@gmail.com \
--to=amir73il@gmail.com \
--cc=brauner@kernel.org \
--cc=jack@suse.cz \
--cc=linux-fsdevel@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox