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>,
"Mickaël Salaün" <mic@digikod.net>,
"Günther Noack" <gnoack@google.com>,
netdev@vger.kernel.org,
"Linus Torvalds" <torvalds@linux-foundation.org>,
"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH v2 7/7] selftests/bpf: add a lifecycle test for the namespace LSM hooks
Date: Thu, 29 Oct 2026 17:41:02 +0100 [thread overview]
Message-ID: <20261029-work-security-namespace_free-v2-7-65591dbba7b3@kernel.org> (raw)
In-Reply-To: <20261029-work-security-namespace_free-v2-0-65591dbba7b3@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.
Acked-by: Alexei Starovoitov <ast@kernel.org>
Acked-by: Daniel Borkmann <daniel@iogearbox.net>
Tested-by: Daniel Borkmann <daniel@iogearbox.net>
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
next prev parent reply other threads:[~2026-10-29 16:41 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 16:40 [PATCH v2 0/7] ns: fixes for the namespace LSM hooks Christian Brauner
2026-10-29 16:40 ` [PATCH v2 1/7] lsm: always call the namespace_free hook Christian Brauner
2026-10-29 16:48 ` sashiko-bot
2026-10-29 16:40 ` [PATCH v2 2/7] bpf: don't mark namespace_init and namespace_free arguments as trusted Christian Brauner
2026-10-29 16:47 ` sashiko-bot
2026-10-29 16:40 ` [PATCH v2 3/7] selftests/bpf: add verifier tests for the namespace LSM hooks Christian Brauner
2026-10-29 16:46 ` sashiko-bot
2026-10-29 16:40 ` [PATCH v2 4/7] ns: allocate the namespace id before the namespace_init hook Christian Brauner
2026-10-29 16:51 ` sashiko-bot
2026-10-29 22:13 ` Mickaël Salaün
2026-10-29 16:41 ` [PATCH v2 5/7] nstree: drop ns_tree_add_raw() Christian Brauner
2026-10-29 16:49 ` sashiko-bot
2026-10-29 16:41 ` [PATCH v2 6/7] lsm: fix the security_namespace_init() kernel-doc Christian Brauner
2026-10-29 16:50 ` sashiko-bot
2026-10-29 16:41 ` Christian Brauner [this message]
2026-10-29 16:55 ` [PATCH v2 7/7] selftests/bpf: add a lifecycle test for the namespace LSM hooks sashiko-bot
2026-10-29 21:06 ` [PATCH v2 0/7] ns: fixes " Paul Moore
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=20261029-work-security-namespace_free-v2-7-65591dbba7b3@kernel.org \
--to=brauner@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=gnoack@google.com \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=mic@digikod.net \
--cc=netdev@vger.kernel.org \
--cc=paul@paul-moore.com \
--cc=torvalds@linux-foundation.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 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.