All of lore.kernel.org
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org, bpf@vger.kernel.org
Cc: linux-security-module@vger.kernel.org,
	 Alexei Starovoitov <ast@kernel.org>,
	Daniel Borkmann <daniel@iogearbox.net>,
	 Paul Moore <paul@paul-moore.com>,
	netdev@vger.kernel.org,
	 "Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH 8/8] selftests/bpf: add a lifecycle test for the namespace LSM hooks
Date: Mon, 26 Oct 2026 10:57:16 +0100	[thread overview]
Message-ID: <20261026-work-security-namespace_free-v1-8-ffa3ebbf71bd@kernel.org> (raw)
In-Reply-To: <20261026-work-security-namespace_free-v1-0-ffa3ebbf71bd@kernel.org>

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


  parent reply	other threads:[~2026-10-26  9:57 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-26  9:57 [PATCH 0/8] ns: fixes for the namespace LSM hooks Christian Brauner
2026-10-26  9:57 ` [PATCH 1/8] lsm: always call the namespace_free hook Christian Brauner
2026-10-26 10:37   ` sashiko-bot
2026-10-26  9:57 ` [PATCH 2/8] bpf: don't mark namespace_init and namespace_free arguments as trusted Christian Brauner
2026-10-26 10:41   ` sashiko-bot
2026-10-26  9:57 ` [PATCH 3/8] selftests/bpf: add verifier tests for the namespace LSM hooks Christian Brauner
2026-10-26 10:44   ` sashiko-bot
2026-10-26  9:57 ` [PATCH 4/8] landlock: pass the namespace id to log to check_ns_type() Christian Brauner
2026-10-26 10:45   ` sashiko-bot
2026-10-29 11:27   ` Mickaël Salaün
2026-10-26  9:57 ` [PATCH 5/8] ns: allocate the namespace id before the namespace_init hook Christian Brauner
2026-10-26 10:51   ` sashiko-bot
2026-10-26  9:57 ` [PATCH 6/8] nstree: drop ns_tree_add_raw() Christian Brauner
2026-10-26 10:55   ` sashiko-bot
2026-10-26  9:57 ` [PATCH 7/8] lsm: fix the security_namespace_init() kernel-doc Christian Brauner
2026-10-26 10:57   ` sashiko-bot
2026-10-26  9:57 ` Christian Brauner [this message]
2026-10-26 11:03   ` [PATCH 8/8] selftests/bpf: add a lifecycle test for the namespace LSM hooks sashiko-bot
2026-10-26 20:31 ` [PATCH 0/8] ns: fixes " Paul Moore
2026-10-27  9:24   ` Christian Brauner
2026-10-27 14:36     ` Daniel Borkmann
2026-10-27 22:37     ` Paul Moore
2026-10-28 10:42       ` Christian Brauner
2026-10-28 15:59         ` Paul Moore
2026-10-26 23:31 ` Alexei Starovoitov

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=20261026-work-security-namespace_free-v1-8-ffa3ebbf71bd@kernel.org \
    --to=brauner@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=paul@paul-moore.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.