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>, 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


      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