[PATCH 8/8] selftests/bpf: add a lifecycle test for the namespace LSM hooks

Christian Brauner brauner at kernel.org
Mon Oct 26 09:57:16 UTC 2026


Track new uts, ipc and net namespaces from a bpf LSM by their namespace
id and check that:

- namespace_init already sees the id that NS_GET_ID reports

- namespace_init, namespace_install and namespace_free fire exactly once
  for each of them

The test fails if the namespace_free hook isn't called or if the
namespace id is allocated after namespace_init.

Signed-off-by: Christian Brauner (Amutable) <brauner at kernel.org>
---
 .../selftests/bpf/prog_tests/ns_lifecycle.c        | 145 +++++++++++++++++++++
 .../selftests/bpf/progs/test_ns_lifecycle.c        |  88 +++++++++++++
 2 files changed, 233 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/ns_lifecycle.c b/tools/testing/selftests/bpf/prog_tests/ns_lifecycle.c
new file mode 100644
index 000000000000..8ea4939efb23
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/ns_lifecycle.c
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Exercise all namespace LSM hooks from a BPF LSM: unshare() new
+ * namespaces (namespace_init), setns() into them (namespace_install) and
+ * drop the last reference (namespace_free).
+ */
+
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <sched.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <linux/nsfs.h>
+#include "task_local_storage_helpers.h"
+#include "test_ns_lifecycle.skel.h"
+
+struct ns_stat {
+	__u32 ns_type;
+	__u32 init_count;
+	__u32 install_count;
+	__u32 free_count;
+};
+
+static const struct {
+	const char *path;
+	int type;
+} namespaces[] = {
+	{ "/proc/self/ns/uts", CLONE_NEWUTS },
+	{ "/proc/self/ns/ipc", CLONE_NEWIPC },
+	{ "/proc/self/ns/net", CLONE_NEWNET },
+};
+
+#define NR_NS ARRAY_SIZE(namespaces)
+
+static bool all_freed(int map_fd, const __u64 *ns_id)
+{
+	struct ns_stat stat;
+	int i;
+
+	for (i = 0; i < NR_NS; i++) {
+		if (bpf_map_lookup_elem(map_fd, &ns_id[i], &stat) || !stat.free_count)
+			return false;
+	}
+	return true;
+}
+
+void test_ns_lifecycle(void)
+{
+	int orig_fd[NR_NS] = { -1, -1, -1 }, new_fd[NR_NS] = { -1, -1, -1 };
+	struct test_ns_lifecycle *skel = NULL;
+	int pidfd, map_fd, err, i, tries;
+	struct ns_stat stat;
+	__u64 ns_id[NR_NS];
+	struct stat st;
+
+	for (i = 0; i < NR_NS; i++) {
+		orig_fd[i] = open(namespaces[i].path, O_RDONLY | O_CLOEXEC);
+		if (!ASSERT_OK_FD(orig_fd[i], "open orig ns"))
+			goto close_fds;
+	}
+
+	pidfd = sys_pidfd_open(getpid(), 0);
+	if (!ASSERT_OK_FD(pidfd, "pidfd_open"))
+		goto close_fds;
+	err = fstat(pidfd, &st);
+	close(pidfd);
+	if (!ASSERT_OK(err, "fstat pidfd"))
+		goto close_fds;
+
+	skel = test_ns_lifecycle__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+		goto close_fds;
+
+	err = test_ns_lifecycle__attach(skel);
+	if (!ASSERT_OK(err, "skel attach"))
+		goto destroy;
+
+	map_fd = bpf_map__fd(skel->maps.ns_lifecycle);
+	skel->bss->monitored_pidfd_ino = st.st_ino;
+
+	err = unshare(CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWNET);
+	if (!ASSERT_OK(err, "unshare"))
+		goto disarm;
+
+	for (i = 0; i < NR_NS; i++) {
+		new_fd[i] = open(namespaces[i].path, O_RDONLY | O_CLOEXEC);
+		if (!ASSERT_OK_FD(new_fd[i], "open new ns"))
+			goto restore;
+		if (!ASSERT_OK(ioctl(new_fd[i], NS_GET_ID, &ns_id[i]), "NS_GET_ID"))
+			goto restore;
+	}
+
+	/* Leave the new namespaces and install them again. */
+	for (i = 0; i < NR_NS; i++) {
+		if (!ASSERT_OK(setns(orig_fd[i], namespaces[i].type), "setns orig"))
+			goto restore;
+		if (!ASSERT_OK(setns(new_fd[i], namespaces[i].type), "setns new"))
+			goto restore;
+		if (!ASSERT_OK(setns(orig_fd[i], namespaces[i].type), "setns orig again"))
+			goto restore;
+	}
+
+	skel->bss->monitored_pidfd_ino = 0;
+
+	/* Drop the last references. Network namespaces are freed asynchronously. */
+	for (i = 0; i < NR_NS; i++) {
+		close(new_fd[i]);
+		new_fd[i] = -1;
+	}
+
+	for (tries = 0; tries < 1000 && !all_freed(map_fd, ns_id); tries++) {
+		kern_sync_rcu();
+		usleep(10000);
+	}
+
+	for (i = 0; i < NR_NS; i++) {
+		/* namespace_init must already have seen the id. */
+		if (!ASSERT_OK(bpf_map_lookup_elem(map_fd, &ns_id[i], &stat), "lookup ns_stat"))
+			continue;
+
+		ASSERT_EQ(stat.ns_type, namespaces[i].type, "ns_type");
+		ASSERT_EQ(stat.init_count, 1, "init_count");
+		ASSERT_EQ(stat.install_count, 1, "install_count");
+		ASSERT_EQ(stat.free_count, 1, "free_count");
+	}
+	goto destroy;
+
+restore:
+	for (i = 0; i < NR_NS; i++)
+		setns(orig_fd[i], namespaces[i].type);
+disarm:
+	skel->bss->monitored_pidfd_ino = 0;
+destroy:
+	test_ns_lifecycle__destroy(skel);
+close_fds:
+	for (i = 0; i < NR_NS; i++) {
+		if (new_fd[i] >= 0)
+			close(new_fd[i]);
+		if (orig_fd[i] >= 0)
+			close(orig_fd[i]);
+	}
+}
diff --git a/tools/testing/selftests/bpf/progs/test_ns_lifecycle.c b/tools/testing/selftests/bpf/progs/test_ns_lifecycle.c
new file mode 100644
index 000000000000..88c5d40ba27a
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_ns_lifecycle.c
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+struct ns_stat {
+	__u32 ns_type;
+	__u32 init_count;
+	__u32 install_count;
+	__u32 free_count;
+};
+
+/* Unlike inode numbers namespace ids are never recycled. */
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(max_entries, 16);
+	__type(key, __u64);
+	__type(value, struct ns_stat);
+} ns_lifecycle SEC(".maps");
+
+__u64 monitored_pidfd_ino = 0;
+
+char _license[] SEC("license") = "GPL";
+
+static bool is_monitored(void)
+{
+	struct task_struct *task;
+
+	if (!monitored_pidfd_ino)
+		return false;
+
+	task = bpf_get_current_task_btf();
+	return task->thread_pid->ino == monitored_pidfd_ino;
+}
+
+SEC("lsm/namespace_init")
+int BPF_PROG(ns_init, struct ns_common *ns, int ret)
+{
+	struct ns_stat new = {}, *stat;
+	__u64 ns_id;
+
+	if (ret)
+		return ret;
+
+	if (!is_monitored())
+		return 0;
+
+	ns_id = ns->ns_id;
+	new.ns_type = ns->ns_type;
+	bpf_map_update_elem(&ns_lifecycle, &ns_id, &new, BPF_NOEXIST);
+	stat = bpf_map_lookup_elem(&ns_lifecycle, &ns_id);
+	if (stat)
+		__sync_fetch_and_add(&stat->init_count, 1);
+	return 0;
+}
+
+SEC("lsm/namespace_install")
+int BPF_PROG(ns_install, const struct nsset *nsset, struct ns_common *ns, int ret)
+{
+	struct ns_stat *stat;
+	__u64 ns_id;
+
+	if (ret)
+		return ret;
+
+	if (!is_monitored())
+		return 0;
+
+	ns_id = ns->ns_id;
+	stat = bpf_map_lookup_elem(&ns_lifecycle, &ns_id);
+	if (stat)
+		__sync_fetch_and_add(&stat->install_count, 1);
+	return 0;
+}
+
+SEC("lsm/namespace_free")
+int BPF_PROG(ns_free, struct ns_common *ns)
+{
+	struct ns_stat *stat;
+	__u64 ns_id;
+
+	ns_id = ns->ns_id;
+	stat = bpf_map_lookup_elem(&ns_lifecycle, &ns_id);
+	if (stat)
+		__sync_fetch_and_add(&stat->free_count, 1);
+	return 0;
+}

-- 
2.53.0




More information about the Linux-security-module-archive mailing list