public inbox for linux-fsdevel@vger.kernel.org
 help / color / mirror / Atom feed
From: Amir Goldstein <amir73il@gmail.com>
To: Jan Kara <jack@suse.cz>
Cc: Christian Brauner <brauner@kernel.org>,
	Lennart Poettering <lennart@poettering.net>,
	Tejun Heo <tj@kernel.org>,
	"T . J . Mercier" <tjmercier@google.com>,
	linux-fsdevel@vger.kernel.org
Subject: [RFC][PATCH 5/5] selftests/filesystems: add fanotify namespace notifications test
Date: Sat,  7 Mar 2026 12:05:50 +0100	[thread overview]
Message-ID: <20260307110550.373762-6-amir73il@gmail.com> (raw)
In-Reply-To: <20260307110550.373762-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           |  79 +++--
 .../selftests/filesystems/fanotify/Makefile   |   3 +-
 .../filesystems/fanotify/ns-notify_test.c     | 330 ++++++++++++++++++
 3 files changed, 380 insertions(+), 32 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..45ad484ceb473 100644
--- a/tools/include/uapi/linux/fanotify.h
+++ b/tools/include/uapi/linux/fanotify.h
@@ -5,37 +5,45 @@
 #include <linux/types.h>
 
 /* the following events that user-space can register for */
-#define FAN_ACCESS		0x00000001	/* File was accessed */
-#define FAN_MODIFY		0x00000002	/* File was modified */
-#define FAN_ATTRIB		0x00000004	/* Metadata changed */
-#define FAN_CLOSE_WRITE		0x00000008	/* Writable file closed */
-#define FAN_CLOSE_NOWRITE	0x00000010	/* Unwritable file closed */
-#define FAN_OPEN		0x00000020	/* File was opened */
-#define FAN_MOVED_FROM		0x00000040	/* File was moved from X */
-#define FAN_MOVED_TO		0x00000080	/* File was moved to Y */
-#define FAN_CREATE		0x00000100	/* Subfile was created */
-#define FAN_DELETE		0x00000200	/* Subfile was deleted */
-#define FAN_DELETE_SELF		0x00000400	/* Self was deleted */
-#define FAN_MOVE_SELF		0x00000800	/* Self was moved */
-#define FAN_OPEN_EXEC		0x00001000	/* File was opened for exec */
-
-#define FAN_Q_OVERFLOW		0x00004000	/* Event queued overflowed */
-#define FAN_FS_ERROR		0x00008000	/* Filesystem error */
-
-#define FAN_OPEN_PERM		0x00010000	/* File open in perm check */
-#define FAN_ACCESS_PERM		0x00020000	/* File accessed in perm check */
-#define FAN_OPEN_EXEC_PERM	0x00040000	/* File open/exec in perm check */
-/* #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 */
+#define FAN_ACCESS		0x00000001ULL	/* File was accessed */
+#define FAN_MODIFY		0x00000002ULL	/* File was modified */
+#define FAN_ATTRIB		0x00000004ULL	/* Metadata changed */
+#define FAN_CLOSE_WRITE		0x00000008ULL	/* Writable file closed */
+#define FAN_CLOSE_NOWRITE	0x00000010ULL	/* Unwritable file closed */
+#define FAN_OPEN		0x00000020ULL	/* File was opened */
+#define FAN_MOVED_FROM		0x00000040ULL	/* File was moved from X */
+#define FAN_MOVED_TO		0x00000080ULL	/* File was moved to Y */
+#define FAN_CREATE		0x00000100ULL	/* Subfile was created */
+#define FAN_DELETE		0x00000200ULL	/* Subfile was deleted */
+#define FAN_DELETE_SELF		0x00000400ULL	/* Self was deleted */
+#define FAN_MOVE_SELF		0x00000800ULL	/* Self was moved */
+#define FAN_OPEN_EXEC		0x00001000ULL	/* File was opened for exec */
+
+#define FAN_Q_OVERFLOW		0x00004000ULL	/* Event queued overflowed */
+#define FAN_FS_ERROR		0x00008000ULL	/* Filesystem error */
+
+#define FAN_OPEN_PERM		0x00010000ULL	/* File open in perm check */
+#define FAN_ACCESS_PERM		0x00020000ULL	/* File accessed in perm check */
+#define FAN_OPEN_EXEC_PERM	0x00040000ULL	/* File open/exec in perm check */
+/* #define FAN_DIR_MODIFY	0x00080000ULL */	/* Deprecated (reserved) */
+
+#define FAN_PRE_ACCESS		0x00100000ULL	/* Pre-content access hook */
+#define FAN_MNT_ATTACH		0x01000000ULL	/* Mount was attached */
+#define FAN_MNT_DETACH		0x02000000ULL	/* Mount was detached */
+
+#define FAN_EVENT_ON_CHILD	0x08000000ULL	/* Interested in child events */
+
+#define FAN_RENAME		0x10000000ULL	/* File was renamed */
+
+#define FAN_ONDIR		0x40000000ULL	/* Event occurred against dir */
+
+/*
+ * Namespace lifecycle events use the upper 32 bits of the 64-bit mask
+ * to avoid confusion with the inode-level FAN_CREATE/FAN_DELETE events.
+ * They are only valid with FAN_MARK_USERNS and FAN_REPORT_NSID.
+ */
+#define FAN_NS_CREATE		(FAN_CREATE << 32)	/* Namespace became active */
+#define FAN_NS_DELETE		(FAN_DELETE << 32)	/* Namespace became inactive */
 
 /* helper events */
 #define FAN_CLOSE		(FAN_CLOSE_WRITE | FAN_CLOSE_NOWRITE) /* close */
@@ -67,6 +75,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 +107,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 +162,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 +221,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.53.0


  parent reply	other threads:[~2026-03-07 11:06 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-07 11:05 [RFC][PATCH 0/5] fanotify namespace monitoring Amir Goldstein
2026-03-07 11:05 ` [RFC][PATCH 1/5] fanotify: add support for watching the namespaces tree Amir Goldstein
2026-03-09 18:07   ` Amir Goldstein
2026-03-07 11:05 ` [RFC][PATCH 2/5] fanotify: use high bits for FAN_NS_CREATE/FAN_NS_DELETE Amir Goldstein
2026-03-07 11:05 ` [RFC][PATCH 3/5] selftests/filesystems: create fanotify test dir Amir Goldstein
2026-03-07 11:05 ` [RFC][PATCH 4/5] filesystems/statmount: update mount.h in tools include dir Amir Goldstein
2026-03-07 11:05 ` Amir Goldstein [this message]
2026-03-09 12:33 ` [RFC][PATCH 0/5] fanotify namespace monitoring Christian Brauner
2026-03-09 15:47   ` Amir Goldstein
2026-03-10 10:31     ` Christian Brauner
2026-03-10 11:14       ` Amir Goldstein
2026-03-16 10:05         ` Jan Kara

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=20260307110550.373762-6-amir73il@gmail.com \
    --to=amir73il@gmail.com \
    --cc=brauner@kernel.org \
    --cc=jack@suse.cz \
    --cc=lennart@poettering.net \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=tj@kernel.org \
    --cc=tjmercier@google.com \
    /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