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) --- .../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 +#include +#include +#include +#include +#include +#include +#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 +#include + +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