From: "Mickaël Salaün" <mic@digikod.net>
To: "Christian Brauner" <brauner@kernel.org>,
"Günther Noack" <gnoack@google.com>,
"Paul Moore" <paul@paul-moore.com>,
"Serge E . Hallyn" <serge@hallyn.com>
Cc: "Mickaël Salaün" <mic@digikod.net>,
"Justin Suess" <utilityemal77@gmail.com>,
"Lennart Poettering" <lennart@poettering.net>,
"Mikhail Ivanov" <ivanov.mikhail1@huawei-partners.com>,
"Nicolas Bouchinet" <nicolas.bouchinet@oss.cyber.gouv.fr>,
"Shervin Oloumi" <enlightened@google.com>,
"Tingmao Wang" <m@maowtm.org>,
kernel-team@cloudflare.com, linux-fsdevel@vger.kernel.org,
linux-kernel@vger.kernel.org,
linux-security-module@vger.kernel.org
Subject: [RFC PATCH v1 08/11] selftests/landlock: Add namespace restriction tests
Date: Thu, 12 Mar 2026 11:04:41 +0100 [thread overview]
Message-ID: <20260312100444.2609563-9-mic@digikod.net> (raw)
In-Reply-To: <20260312100444.2609563-1-mic@digikod.net>
Add tests covering the two namespace-related Landlock permission types:
LANDLOCK_PERM_NAMESPACE_ENTER (namespace creation via unshare/clone and
namespace entry via setns) and its interaction with
LANDLOCK_PERM_CAPABILITY_USE.
Rule validation tests verify that the kernel correctly accepts known
CLONE_NEW* types, silently accepts unknown bits (including holes,
upper-range bits, and bit 63) for forward compatibility, and rejects an
empty namespace_types bitmask. Invalid allowed_perm combinations and
non-zero flags are also covered.
Namespace creation tests use FIXTURE_VARIANT to exercise all eight
namespace types (user, UTS, IPC, mount, cgroup, PID, network, time)
across allowed/denied and privileged/unprivileged combinations. This
verifies that security_namespace_alloc() is correctly called for every
type. Layer stacking tests verify that any-layer-denies semantics work
correctly, including the allow-over-allow case. A combined test
exercises both LANDLOCK_PERM_CAPABILITY_USE and
LANDLOCK_PERM_NAMESPACE_ENTER in a single domain.
Namespace entry tests verify that setns is subject to the same
type-based LANDLOCK_PERM_NAMESPACE_ENTER check via
security_namespace_install(), including cross-process setns denial and
the two-permission interaction where both LANDLOCK_PERM_NAMESPACE_ENTER
and LANDLOCK_PERM_CAPABILITY_USE must allow the operation for non-user
namespaces.
Audit tests verify that denied namespace creation, denied setns entry,
and allowed operations produce the expected audit records (or none).
Cc: Christian Brauner <brauner@kernel.org>
Cc: Günther Noack <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>
Cc: Serge E. Hallyn <serge@hallyn.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
tools/testing/selftests/landlock/common.h | 23 +
tools/testing/selftests/landlock/config | 5 +
tools/testing/selftests/landlock/ns_test.c | 1379 +++++++++++++++++++
tools/testing/selftests/landlock/wrappers.h | 6 +
4 files changed, 1413 insertions(+)
create mode 100644 tools/testing/selftests/landlock/ns_test.c
diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 90551650299c..e7d1d1e9df74 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -128,6 +128,29 @@ static void __maybe_unused clear_ambient_cap(
EXPECT_EQ(0, cap_get_ambient(cap));
}
+/*
+ * Returns true if the current process is in the initial user namespace.
+ * Compares the readlink targets of /proc/self/ns/user and /proc/1/ns/user.
+ */
+static bool __maybe_unused is_in_init_user_ns(void)
+{
+ char self_buf[64], init_buf[64];
+ ssize_t self_len, init_len;
+
+ self_len = readlink("/proc/self/ns/user", self_buf, sizeof(self_buf));
+ if (self_len <= 0 || self_len >= (ssize_t)sizeof(self_buf))
+ return false;
+
+ init_len = readlink("/proc/1/ns/user", init_buf, sizeof(init_buf));
+ if (init_len <= 0 || init_len >= (ssize_t)sizeof(init_buf))
+ return false;
+
+ if (self_len != init_len)
+ return false;
+
+ return memcmp(self_buf, init_buf, self_len) == 0;
+}
+
/* Receives an FD from a UNIX socket. Returns the received FD, or -errno. */
static int __maybe_unused recv_fd(int usock)
{
diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config
index 8fe9b461b1fd..d09b637bf6ca 100644
--- a/tools/testing/selftests/landlock/config
+++ b/tools/testing/selftests/landlock/config
@@ -3,6 +3,7 @@ CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
CONFIG_INET=y
+CONFIG_IPC_NS=y
CONFIG_IPV6=y
CONFIG_KEYS=y
CONFIG_MPTCP=y
@@ -10,10 +11,14 @@ CONFIG_MPTCP_IPV6=y
CONFIG_NET=y
CONFIG_NET_NS=y
CONFIG_OVERLAY_FS=y
+CONFIG_PID_NS=y
CONFIG_PROC_FS=y
CONFIG_SECURITY=y
CONFIG_SECURITY_LANDLOCK=y
CONFIG_SHMEM=y
CONFIG_SYSFS=y
+CONFIG_TIME_NS=y
CONFIG_TMPFS=y
CONFIG_TMPFS_XATTR=y
+CONFIG_USER_NS=y
+CONFIG_UTS_NS=y
diff --git a/tools/testing/selftests/landlock/ns_test.c b/tools/testing/selftests/landlock/ns_test.c
new file mode 100644
index 000000000000..5d968dd9f4f5
--- /dev/null
+++ b/tools/testing/selftests/landlock/ns_test.c
@@ -0,0 +1,1379 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - Namespace restriction
+ *
+ * Copyright © 2026 Cloudflare
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/capability.h>
+#include <linux/landlock.h>
+#include <sched.h>
+#include <stdio.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <syscall.h>
+#include <unistd.h>
+
+#include "audit.h"
+#include "common.h"
+
+/*
+ * Max length for /proc/self/ns/<name> paths (longest:
+ * "/proc/self/ns/cgroup").
+ */
+#define NS_PROC_PATH_MAX 32
+
+static int create_ns_ruleset(void)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ };
+
+ return landlock_create_ruleset(&attr, sizeof(attr), 0);
+}
+
+static int add_ns_rule(int ruleset_fd, __u64 ns_type)
+{
+ const struct landlock_namespace_attr attr = {
+ .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ .namespace_types = ns_type,
+ };
+
+ return landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE, &attr, 0);
+}
+
+/*
+ * Returns the /proc/self/NS entry name for a given CLONE_NEW* type, or NULL
+ * if unknown. Used to check kernel support without side effects.
+ */
+static const char *ns_proc_name(__u64 ns_type)
+{
+ switch (ns_type) {
+ case CLONE_NEWNS:
+ return "mnt";
+ case CLONE_NEWCGROUP:
+ return "cgroup";
+ case CLONE_NEWUTS:
+ return "uts";
+ case CLONE_NEWIPC:
+ return "ipc";
+ case CLONE_NEWUSER:
+ return "user";
+ case CLONE_NEWPID:
+ return "pid";
+ case CLONE_NEWNET:
+ return "net";
+ case CLONE_NEWTIME:
+ return "time";
+ default:
+ return NULL;
+ }
+}
+
+static bool ns_is_supported(__u64 ns_type, char *proc_path, size_t size)
+{
+ const char *ns_name;
+
+ ns_name = ns_proc_name(ns_type);
+ if (!ns_name)
+ return false;
+
+ snprintf(proc_path, size, "/proc/self/ns/%s", ns_name);
+ return access(proc_path, F_OK) == 0;
+}
+
+/* Rule validation tests */
+
+TEST(add_rule_bad_attr)
+{
+ const struct landlock_ruleset_attr cap_only_attr = {
+ .handled_perm = LANDLOCK_PERM_CAPABILITY_USE,
+ };
+ int ruleset_fd;
+ struct landlock_namespace_attr attr = {};
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Empty allowed_perm returns ENOMSG (useless deny rule). */
+ attr.allowed_perm = 0;
+ attr.namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(ENOMSG, errno);
+
+ /* allowed_perm with unhandled bit. */
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER |
+ LANDLOCK_PERM_CAPABILITY_USE;
+ attr.namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ /* allowed_perm with wrong type. */
+ attr.allowed_perm = LANDLOCK_PERM_CAPABILITY_USE;
+ attr.namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ /*
+ * Unknown namespace bits (e.g. bit 63) are silently accepted
+ * for forward compatibility. Only known CLONE_NEW* bits are stored.
+ */
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER;
+ attr.namespace_types = 1ULL << 63;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /* Useless rule: empty namespace_types bitmask. */
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER;
+ attr.namespace_types = 0;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(ENOMSG, errno);
+
+ /*
+ * Bit 1 is not a CLONE_NEW* value but is silently accepted
+ * for forward compatibility (no hole rejection).
+ */
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER;
+ attr.namespace_types = (1ULL << 1);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /* Multi-bit values are valid (bitmask allows multiple types). */
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER;
+ attr.namespace_types = CLONE_NEWUTS | CLONE_NEWNET;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /* Non-zero flags must be rejected. */
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER;
+ attr.namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 1));
+ ASSERT_EQ(EINVAL, errno);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * Ruleset handles PERM_CAPABILITY_USE but not PERM_NAMESPACE_ENTER:
+ * adding a namespace rule must be rejected.
+ */
+ ruleset_fd = landlock_create_ruleset(&cap_only_attr,
+ sizeof(cap_only_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ attr.allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER;
+ attr.namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(EINVAL, errno);
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+/*
+ * Unknown namespace types in the upper range are silently accepted
+ * (allow-list: they have no effect since the kernel never checks them).
+ */
+TEST(add_rule_unknown)
+{
+ int ruleset_fd;
+ struct landlock_namespace_attr attr = {
+ .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ };
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ /*
+ * Bit 31 is in the lower 32 bits but not a CLONE_NEW* value.
+ * Silently accepted for forward compatibility (no hole rejection).
+ */
+ attr.namespace_types = 1ULL << 31;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /* Bit 32 is in the unknown upper range: silently accepted. */
+ attr.namespace_types = 1ULL << 32;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+/* Namespace creation tests (variant-based positive/negative) */
+
+/* clang-format off */
+FIXTURE(ns_create) {
+ char proc_path[NS_PROC_PATH_MAX];
+};
+/* clang-format on */
+
+FIXTURE_VARIANT(ns_create)
+{
+ const __u64 namespace_types;
+ const bool is_sandboxed;
+ const bool has_rule;
+ const bool drop_all_caps;
+ const int expected_result;
+};
+
+/*
+ * Unsandboxed baseline: no Landlock domain is enforced.
+ * User namespace creation should succeed without any restriction.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, user_unsandboxed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUSER,
+ .is_sandboxed = false,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+
+/*
+ * User namespace creation denied: handled by Landlock but no rule
+ * allows CLONE_NEWUSER.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, user_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUSER,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/*
+ * User namespace creation allowed: Landlock rule permits CLONE_NEWUSER.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, user_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUSER,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+
+/*
+ * User namespace creation while unprivileged: the process has no
+ * capabilities but unshare(CLONE_NEWUSER) is an unprivileged
+ * operation so it still succeeds. The Landlock rule allows it.
+ * For setns, the capability check (CAP_SYS_ADMIN) fails first
+ * since the process has no capabilities, yielding EPERM.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, user_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUSER,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = 0,
+};
+
+/*
+ * Unsandboxed baseline for non-user namespace: no Landlock domain,
+ * process has CAP_SYS_ADMIN. UTS creation should succeed.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, uts_unsandboxed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUTS,
+ .is_sandboxed = false,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+
+/*
+ * Non-user namespace denied: process has CAP_SYS_ADMIN (passes
+ * ns_capable), but Landlock denies (no rule).
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, uts_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUTS,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/*
+ * Non-user namespace allowed: process has CAP_SYS_ADMIN and Landlock
+ * rule permits CLONE_NEWUTS.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, uts_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUTS, .is_sandboxed = true, .has_rule = true,
+ .drop_all_caps = false, .expected_result = 0,
+};
+
+/*
+ * Unprivileged namespace creation: process lacks CAP_SYS_ADMIN, so the
+ * kernel denies creation regardless of Landlock rules. Landlock cannot
+ * authorize what the kernel denied (LSM hooks are restriction-only).
+ * The rule is present to verify Landlock does not change the error code.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, uts_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWUTS,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, ipc_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWIPC,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, ipc_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWIPC, .is_sandboxed = true, .has_rule = true,
+ .drop_all_caps = false, .expected_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, ipc_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWIPC,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, mnt_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWNS,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, mnt_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWNS, .is_sandboxed = true, .has_rule = true,
+ .drop_all_caps = false, .expected_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, mnt_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWNS,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, cgroup_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWCGROUP,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, cgroup_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWCGROUP,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, cgroup_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWCGROUP,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, pid_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWPID,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, pid_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWPID, .is_sandboxed = true, .has_rule = true,
+ .drop_all_caps = false, .expected_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, pid_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWPID,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, net_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWNET,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, net_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWNET, .is_sandboxed = true, .has_rule = true,
+ .drop_all_caps = false, .expected_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, net_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWNET,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, time_denied) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWTIME,
+ .is_sandboxed = true,
+ .has_rule = false,
+ .drop_all_caps = false,
+ .expected_result = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, time_allowed) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWTIME,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create, time_unprivileged) {
+ /* clang-format on */
+ .namespace_types = CLONE_NEWTIME,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = true,
+ .expected_result = EPERM,
+};
+
+FIXTURE_SETUP(ns_create)
+{
+ if (!ns_is_supported(variant->namespace_types, self->proc_path,
+ sizeof(self->proc_path))) {
+ /* UML does not support the time namespace. */
+ if (variant->namespace_types == CLONE_NEWTIME)
+ SKIP(return, "CLONE_NEWTIME not supported");
+
+ ASSERT_TRUE(false)
+ {
+ TH_LOG("Namespace type 0x%llx not supported",
+ (unsigned long long)variant->namespace_types);
+ }
+ }
+
+ if (variant->drop_all_caps)
+ drop_caps(_metadata);
+ else
+ disable_caps(_metadata);
+}
+
+FIXTURE_TEARDOWN(ns_create)
+{
+}
+
+TEST_F(ns_create, unshare)
+{
+ int ruleset_fd, err;
+
+ if (variant->is_sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd,
+ variant->namespace_types));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ /*
+ * Non-user namespaces need CAP_SYS_ADMIN for the privileged path.
+ * User namespaces and unprivileged tests skip this.
+ */
+ if (!variant->drop_all_caps &&
+ variant->namespace_types != CLONE_NEWUSER)
+ set_cap(_metadata, CAP_SYS_ADMIN);
+
+ err = unshare(variant->namespace_types);
+ if (variant->expected_result) {
+ EXPECT_EQ(-1, err);
+ EXPECT_EQ(variant->expected_result, errno);
+ } else {
+ EXPECT_EQ(0, err);
+ }
+
+ if (!variant->drop_all_caps &&
+ variant->namespace_types != CLONE_NEWUSER)
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * clone3 exercises a different kernel entry point than unshare: it goes
+ * through kernel_clone() -> copy_process() -> copy_namespaces() ->
+ * create_new_namespaces(). Both paths converge at __ns_common_init() ->
+ * security_namespace_alloc(), but the entry point and argument handling
+ * differ.
+ */
+TEST_F(ns_create, clone3)
+{
+ int ruleset_fd, status;
+ pid_t pid;
+ struct clone_args args = {};
+
+ if (variant->is_sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd,
+ variant->namespace_types));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ if (!variant->drop_all_caps &&
+ variant->namespace_types != CLONE_NEWUSER)
+ set_cap(_metadata, CAP_SYS_ADMIN);
+
+ args.flags = variant->namespace_types;
+ args.exit_signal = SIGCHLD;
+ pid = sys_clone3(&args, sizeof(args));
+ if (pid == 0)
+ _exit(EXIT_SUCCESS);
+
+ if (variant->expected_result) {
+ EXPECT_EQ(-1, pid);
+ EXPECT_EQ(variant->expected_result, errno);
+ } else {
+ EXPECT_LE(0, pid);
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_EQ(1, WIFEXITED(status));
+ ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+ }
+
+ if (!variant->drop_all_caps &&
+ variant->namespace_types != CLONE_NEWUSER)
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * setns exercises the namespace install path: validate_ns() ->
+ * security_namespace_install() -> hook_namespace_install(). This is a
+ * different LSM hook than creation, so it must be tested separately for
+ * each type.
+ *
+ * Mount namespace setns requires both CAP_SYS_ADMIN and CAP_SYS_CHROOT
+ * (checked by mntns_install), so the allowed variant sets both.
+ */
+TEST_F(ns_create, setns)
+{
+ int ruleset_fd, ns_fd, err, expected;
+
+ /*
+ * setns into the process's own user NS always returns EINVAL:
+ * userns_install() rejects re-entry before checking capabilities.
+ */
+ if (variant->namespace_types == CLONE_NEWUSER) {
+ expected = EINVAL;
+ } else {
+ expected = variant->expected_result;
+ }
+
+ /* Open the NS FD before enforcing the domain. */
+ ns_fd = open(self->proc_path, O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ if (variant->is_sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd,
+ variant->namespace_types));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ if (!variant->drop_all_caps) {
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ /*
+ * mntns_install() requires CAP_SYS_CHROOT in addition to
+ * CAP_SYS_ADMIN.
+ */
+ if (variant->namespace_types == CLONE_NEWNS)
+ set_cap(_metadata, CAP_SYS_CHROOT);
+ }
+
+ err = setns(ns_fd, variant->namespace_types);
+ if (expected) {
+ EXPECT_EQ(-1, err);
+ EXPECT_EQ(expected, errno);
+ } else {
+ EXPECT_EQ(0, err);
+ }
+
+ if (!variant->drop_all_caps) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ if (variant->namespace_types == CLONE_NEWNS)
+ clear_cap(_metadata, CAP_SYS_CHROOT);
+ }
+
+ EXPECT_EQ(0, close(ns_fd));
+}
+
+/* Additional namespace creation tests */
+
+/*
+ * When LANDLOCK_PERM_NAMESPACE_ENTER is not handled by any domain, namespace
+ * creation must produce the same result as without Landlock. Unlike the
+ * unsandboxed variants of ns_create (which have no domain at all), this test
+ * verifies that a domain handling only FS access does not interfere with
+ * namespace operations.
+ */
+TEST(ns_create_unhandled)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
+ };
+ int ruleset_fd;
+
+ disable_caps(_metadata);
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* User namespace creation should still work (unhandled). */
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER));
+}
+
+/*
+ * Layer stacking: layer 1 always allows CLONE_NEWUSER. Layer 2
+ * either allows (both layers agree -> success) or denies (any layer
+ * can deny -> failure).
+ */
+/* clang-format off */
+FIXTURE(ns_stacking) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(ns_stacking)
+{
+ bool second_layer_allows;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_stacking, deny) {
+ /* clang-format on */
+ .second_layer_allows = false,
+};
+
+/* Both layers allow CLONE_NEWUSER -> operation succeeds. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_stacking, allow) {
+ /* clang-format on */
+ .second_layer_allows = true,
+};
+
+FIXTURE_SETUP(ns_stacking)
+{
+ disable_caps(_metadata);
+}
+
+FIXTURE_TEARDOWN(ns_stacking)
+{
+}
+
+/*
+ * Verify that a second Landlock layer cannot override the first layer's
+ * denial. Each layer stores its permission bitmask independently, and
+ * enforcement requires all layers to allow an operation. This ensures
+ * the correct intersection: layer 1 allows CLONE_NEWUSER, but if layer
+ * 2 does not also allow it, the operation is denied.
+ */
+TEST_F(ns_stacking, two_layers)
+{
+ int ruleset_fd;
+
+ /* First layer: allow CLONE_NEWUSER. */
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWUSER));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* Second layer: allow or deny depending on variant. */
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->second_layer_allows)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWUSER));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ if (variant->second_layer_allows) {
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER));
+ } else {
+ EXPECT_EQ(-1, unshare(CLONE_NEWUSER));
+ EXPECT_EQ(EPERM, errno);
+ }
+}
+
+/*
+ * Combined capability and namespace permissions in a single domain.
+ * Verifies that both permission types can coexist and are enforced
+ * independently.
+ */
+TEST(combined_cap_ns)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_perm = LANDLOCK_PERM_CAPABILITY_USE |
+ LANDLOCK_PERM_NAMESPACE_ENTER,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .allowed_perm = LANDLOCK_PERM_CAPABILITY_USE,
+ .capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ .namespace_types = CLONE_NEWUSER,
+ };
+ int ruleset_fd;
+
+ /* Isolate hostname changes from other tests. */
+ ASSERT_EQ(0, unshare(CLONE_NEWUTS));
+
+ disable_caps(_metadata);
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY,
+ &cap_attr, 0));
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &ns_attr, 0));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* CAP_SYS_ADMIN use allowed by capability rule. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, sethostname("test", 4));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* CAP_SYS_CHROOT denied (not in allowed capability rules). */
+ set_cap(_metadata, CAP_SYS_CHROOT);
+ EXPECT_EQ(-1, chroot("/"));
+ EXPECT_EQ(EPERM, errno);
+
+ /*
+ * UTS namespace creation denied by Landlock (not in allowed namespace
+ * rules). CAP_SYS_ADMIN is needed for the kernel's ns_capable()
+ * check to pass, so that Landlock's hook is actually reached.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* User namespace creation allowed by namespace rule. */
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER));
+}
+
+/*
+ * Partial allow: one namespace type is allowed, another is denied.
+ * Verifies that rules are per-type.
+ */
+TEST(ns_create_partial)
+{
+ int ruleset_fd;
+
+ disable_caps(_metadata);
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Only allow UTS namespace creation. */
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWUTS));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* UTS namespace should be allowed. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, unshare(CLONE_NEWUTS));
+
+ /* User namespace should be denied (no rule). */
+ EXPECT_EQ(-1, unshare(CLONE_NEWUSER));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/* clang-format off */
+FIXTURE(setns_cross_process) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(setns_cross_process)
+{
+ bool is_sandboxed;
+ int expected_setns;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(setns_cross_process, denied) {
+ /* clang-format on */
+ .is_sandboxed = true,
+ .expected_setns = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(setns_cross_process, allowed) {
+ /* clang-format on */
+ .is_sandboxed = false,
+ .expected_setns = 0,
+};
+
+FIXTURE_SETUP(setns_cross_process)
+{
+}
+
+FIXTURE_TEARDOWN(setns_cross_process)
+{
+}
+
+/*
+ * setns into a child's UTS namespace: when sandboxed with
+ * LANDLOCK_PERM_NAMESPACE_ENTER denying UTS, the rule-based check
+ * applies regardless of which process created the namespace.
+ */
+TEST_F(setns_cross_process, setns)
+{
+ int ruleset_fd, ns_fd, status;
+ pid_t child;
+ int pipe_parent[2], pipe_child[2];
+ char buf, path[64];
+
+ disable_caps(_metadata);
+
+ /*
+ * Enable dumpable so the parent can read /proc/<child>/ns/uts.
+ * Without this, ptrace access checks (PTRACE_MODE_READ) prevent
+ * opening another process's namespace entries.
+ */
+ ASSERT_EQ(0, prctl(PR_SET_DUMPABLE, 1, 0, 0, 0));
+
+ ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ EXPECT_EQ(0, close(pipe_parent[1]));
+ EXPECT_EQ(0, close(pipe_child[0]));
+
+ /* Child: create a UTS namespace. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWUTS));
+
+ drop_caps(_metadata);
+ ASSERT_EQ(0, prctl(PR_SET_DUMPABLE, 1, 0, 0, 0));
+
+ /* Signal parent that the namespace is ready. */
+ ASSERT_EQ(1, write(pipe_child[1], ".", 1));
+
+ /* Wait for parent to finish testing. */
+ ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
+ _exit(_metadata->exit_code);
+ }
+
+ EXPECT_EQ(0, close(pipe_parent[0]));
+ EXPECT_EQ(0, close(pipe_child[1]));
+
+ /* Wait for child namespace. */
+ ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
+ EXPECT_EQ(0, close(pipe_child[0]));
+
+ /* Open the child's NS FD BEFORE creating the domain. */
+ snprintf(path, sizeof(path), "/proc/%d/ns/uts", child);
+ ns_fd = open(path, O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ if (variant->is_sandboxed) {
+ /* Create domain denying UTS entry (no allow rule). */
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ if (variant->expected_setns) {
+ EXPECT_EQ(-1, setns(ns_fd, CLONE_NEWUTS));
+ EXPECT_EQ(variant->expected_setns, errno);
+ } else {
+ EXPECT_EQ(0, setns(ns_fd, CLONE_NEWUTS));
+ }
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, close(ns_fd));
+
+ /* Release child. */
+ ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+ EXPECT_EQ(0, close(pipe_parent[1]));
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_EQ(1, WIFEXITED(status));
+ ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+/*
+ * Verify that both LANDLOCK_PERM_NAMESPACE_ENTER and LANDLOCK_PERM_CAPABILITY_USE
+ * apply simultaneously: creating/entering a non-user namespace
+ * requires both the namespace type to be allowed AND CAP_SYS_ADMIN
+ * to be allowed. User namespace creation is the exception (no
+ * capable() call from the kernel).
+ */
+TEST(setns_and_create)
+{
+ int ruleset_fd, ns_fd;
+ const struct landlock_ruleset_attr attr = {
+ .handled_perm = LANDLOCK_PERM_NAMESPACE_ENTER |
+ LANDLOCK_PERM_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ .namespace_types = CLONE_NEWUTS,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .allowed_perm = LANDLOCK_PERM_CAPABILITY_USE,
+ .capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+
+ disable_caps(_metadata);
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &ns_attr, 0));
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY,
+ &cap_attr, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* UTS unshare: allowed by NS rule + CAP_SYS_ADMIN allowed. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWUTS));
+
+ /* IPC unshare: denied by NS rule (type not allowed). */
+ EXPECT_EQ(-1, unshare(CLONE_NEWIPC));
+ EXPECT_EQ(EPERM, errno);
+
+ /* setns into current UTS: allowed by NS rule. */
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+ EXPECT_EQ(0, setns(ns_fd, CLONE_NEWUTS));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, close(ns_fd));
+
+ /*
+ * User namespace creation: only LANDLOCK_PERM_NAMESPACE_ENTER needed
+ * (no capable() call from the kernel for user NS). Denied
+ * because CLONE_NEWUSER is not in the allowed namespace types.
+ */
+ EXPECT_EQ(-1, unshare(CLONE_NEWUSER));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/*
+ * Verify that LANDLOCK_PERM_CAPABILITY_USE can deny the CAP_SYS_ADMIN check
+ * that the kernel performs before the Landlock namespace hook is
+ * reached. The NS type is allowed but the required capability is not,
+ * so the operation fails on the capability check.
+ *
+ * User namespace creation is the exception: no capable() call, so the
+ * operation succeeds with just LANDLOCK_PERM_NAMESPACE_ENTER.
+ */
+TEST(two_perm_cap_denied)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_perm = LANDLOCK_PERM_NAMESPACE_ENTER |
+ LANDLOCK_PERM_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ .namespace_types = CLONE_NEWUTS | CLONE_NEWUSER,
+ };
+ /* CAP_SYS_ADMIN is NOT allowed. */
+ const struct landlock_capability_attr cap_attr = {
+ .allowed_perm = LANDLOCK_PERM_CAPABILITY_USE,
+ .capabilities = (1ULL << CAP_SYS_CHROOT),
+ };
+ int ruleset_fd;
+
+ disable_caps(_metadata);
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &ns_attr, 0));
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY,
+ &cap_attr, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * UTS creation: the process holds CAP_SYS_ADMIN but Landlock
+ * denies it (not in the cap rule), so the kernel's
+ * ns_capable(CAP_SYS_ADMIN) gate fails before the namespace
+ * hook is reached.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * User NS creation: no capable() call from the kernel, so
+ * only LANDLOCK_PERM_NAMESPACE_ENTER applies. CLONE_NEWUSER is in the
+ * allowed set, so this succeeds.
+ */
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER));
+}
+
+/*
+ * Mount namespace setns is unique: the kernel checks both
+ * CAP_SYS_ADMIN and CAP_SYS_CHROOT in mntns_install(). Verify that
+ * allowing CAP_SYS_ADMIN alone is not sufficient.
+ */
+TEST(two_perm_mnt_setns)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_perm = LANDLOCK_PERM_NAMESPACE_ENTER |
+ LANDLOCK_PERM_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
+ .namespace_types = CLONE_NEWNS,
+ };
+ const struct landlock_capability_attr cap_admin = {
+ .allowed_perm = LANDLOCK_PERM_CAPABILITY_USE,
+ .capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+ const struct landlock_capability_attr cap_admin_chroot = {
+ .allowed_perm = LANDLOCK_PERM_CAPABILITY_USE,
+ .capabilities = (1ULL << CAP_SYS_ADMIN) |
+ (1ULL << CAP_SYS_CHROOT),
+ };
+ int ruleset_fd, ns_fd;
+
+ disable_caps(_metadata);
+
+ /* Layer 1: allow mount NS + CAP_SYS_ADMIN only (no CAP_SYS_CHROOT). */
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &ns_attr, 0));
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY,
+ &cap_admin, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ ns_fd = open("/proc/self/ns/mnt", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ /*
+ * Fails: mntns_install() checks CAP_SYS_ADMIN (allowed) then
+ * CAP_SYS_CHROOT (denied by LANDLOCK_PERM_CAPABILITY_USE).
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ set_cap(_metadata, CAP_SYS_CHROOT);
+ EXPECT_EQ(-1, setns(ns_fd, CLONE_NEWNS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ clear_cap(_metadata, CAP_SYS_CHROOT);
+
+ /* Layer 2: also allows CAP_SYS_CHROOT. */
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &ns_attr, 0));
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY,
+ &cap_admin_chroot, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * Still fails: layer 1 still denies CAP_SYS_CHROOT.
+ * Landlock layer stacking means the most restrictive layer wins.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ set_cap(_metadata, CAP_SYS_CHROOT);
+ EXPECT_EQ(-1, setns(ns_fd, CLONE_NEWNS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ clear_cap(_metadata, CAP_SYS_CHROOT);
+ EXPECT_EQ(0, close(ns_fd));
+}
+
+/* Audit tests */
+
+static int matches_log_ns_create(int audit_fd, __u64 ns_type)
+{
+ static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ " blockers=perm\\.namespace_enter"
+ " namespace_type=0x%x"
+ " namespace_inum=0$";
+ char log_match[sizeof(log_template) + 10];
+ int log_match_len;
+
+ log_match_len = snprintf(log_match, sizeof(log_match), log_template,
+ (unsigned int)ns_type);
+ if (log_match_len >= sizeof(log_match))
+ return -E2BIG;
+
+ return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
+ NULL);
+}
+
+static int matches_log_ns_setns(int audit_fd, __u64 ns_type)
+{
+ static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ " blockers=perm\\.namespace_enter"
+ " namespace_type=0x%x"
+ " namespace_inum=[0-9]\\+$";
+ char log_match[sizeof(log_template) + 10];
+ int log_match_len;
+
+ log_match_len = snprintf(log_match, sizeof(log_match), log_template,
+ (unsigned int)ns_type);
+ if (log_match_len >= sizeof(log_match))
+ return -E2BIG;
+
+ return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
+ NULL);
+}
+
+FIXTURE(ns_audit)
+{
+ struct audit_filter audit_filter;
+ int audit_fd;
+};
+
+FIXTURE_SETUP(ns_audit)
+{
+ ASSERT_TRUE(is_in_init_user_ns());
+
+ disable_caps(_metadata);
+
+ set_cap(_metadata, CAP_AUDIT_CONTROL);
+ self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
+ EXPECT_LE(0, self->audit_fd);
+ clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+FIXTURE_TEARDOWN(ns_audit)
+{
+ set_cap(_metadata, CAP_AUDIT_CONTROL);
+ EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter));
+}
+
+/*
+ * Verifies that a denied namespace creation produces the expected audit
+ * record with the perm.namespace_enter blocker string and namespace_type.
+ */
+TEST_F(ns_audit, create_denied)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ EXPECT_EQ(0, matches_log_ns_create(self->audit_fd, CLONE_NEWUTS));
+
+ /*
+ * No extra access records: the denial was already consumed by
+ * matches_log_ns_create above. One domain allocation record,
+ * emitted in the same event as the first access denial for this
+ * domain.
+ */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+}
+
+TEST_F(ns_audit, create_allowed)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWUTS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, unshare(CLONE_NEWUTS));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* No records: allowed operations never trigger audit logging. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+TEST_F(ns_audit, setns_allowed)
+{
+ struct audit_records records;
+ int ruleset_fd, ns_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWUTS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ /* Allowed: should succeed with no audit record. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, setns(ns_fd, CLONE_NEWUTS));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, close(ns_fd));
+
+ /* No records: allowed setns never triggers audit logging. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+TEST_F(ns_audit, setns_denied)
+{
+ struct audit_records records;
+ int ruleset_fd, ns_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ /* No rule allows UTS -> denied. */
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(-1, setns(ns_fd, CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, close(ns_fd));
+
+ /* Verify the audit record for setns denial. */
+ EXPECT_EQ(0, matches_log_ns_setns(self->audit_fd, CLONE_NEWUTS));
+
+ /*
+ * No extra access records: the denial was already consumed by
+ * matches_log_ns_setns above. One domain allocation record,
+ * emitted in the same event as the first access denial for this
+ * domain.
+ */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+}
+
+TEST_F(ns_audit, unshare_denied)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* Deny UTS namespace creation (no allow rule). */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* Verify the audit record for namespace creation denial. */
+ EXPECT_EQ(0, matches_log_ns_create(self->audit_fd, CLONE_NEWUTS));
+
+ /*
+ * No extra access records: the denial was already consumed by
+ * matches_log_ns_create above. One domain allocation record,
+ * emitted in the same event as the first access denial for this
+ * domain.
+ */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/wrappers.h b/tools/testing/selftests/landlock/wrappers.h
index 65548323e45d..a3266fdb43da 100644
--- a/tools/testing/selftests/landlock/wrappers.h
+++ b/tools/testing/selftests/landlock/wrappers.h
@@ -9,6 +9,7 @@
#define _GNU_SOURCE
#include <linux/landlock.h>
+#include <linux/sched.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
@@ -45,3 +46,8 @@ static inline pid_t sys_gettid(void)
{
return syscall(__NR_gettid);
}
+
+static inline pid_t sys_clone3(struct clone_args *args, size_t size)
+{
+ return syscall(__NR_clone3, args, size);
+}
--
2.53.0
next prev parent reply other threads:[~2026-03-12 10:05 UTC|newest]
Thread overview: 51+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-12 10:04 [RFC PATCH v1 00/11] Landlock: Namespace and capability control Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 01/11] security: add LSM blob and hooks for namespaces Mickaël Salaün
2026-03-25 12:31 ` Christian Brauner
2026-04-09 16:40 ` Mickaël Salaün
2026-04-10 9:35 ` Christian Brauner
2026-04-22 21:21 ` Günther Noack
2026-04-23 0:19 ` Paul Moore
2026-04-24 18:56 ` Mickaël Salaün
2026-04-24 19:28 ` Paul Moore
2026-04-27 14:57 ` Christian Brauner
2026-04-27 21:46 ` Paul Moore
2026-03-12 10:04 ` [RFC PATCH v1 02/11] security: Add LSM_AUDIT_DATA_NS for namespace audit records Mickaël Salaün
2026-03-25 12:32 ` Christian Brauner
2026-04-01 16:38 ` Mickaël Salaün
2026-04-01 18:48 ` Mickaël Salaün
2026-04-09 13:29 ` Christian Brauner
2026-04-22 21:21 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 03/11] nsproxy: Add FOR_EACH_NS_TYPE() X-macro and CLONE_NS_ALL Mickaël Salaün
2026-03-25 12:33 ` Christian Brauner
2026-03-25 15:26 ` Mickaël Salaün
2026-03-26 14:22 ` (subset) " Christian Brauner
2026-03-12 10:04 ` [RFC PATCH v1 04/11] landlock: Wrap per-layer access masks in struct layer_rights Mickaël Salaün
2026-04-10 1:45 ` Tingmao Wang
2026-04-22 21:29 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 05/11] landlock: Enforce namespace entry restrictions Mickaël Salaün
2026-03-29 13:15 ` kernel test robot
2026-04-10 1:45 ` Tingmao Wang
2026-05-08 15:46 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 06/11] landlock: Enforce capability restrictions Mickaël Salaün
2026-04-22 21:36 ` Günther Noack
2026-05-08 15:54 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 07/11] selftests/landlock: Drain stale audit records on init Mickaël Salaün
2026-03-24 13:27 ` Günther Noack
2026-03-12 10:04 ` Mickaël Salaün [this message]
2026-03-12 10:04 ` [RFC PATCH v1 09/11] selftests/landlock: Add capability restriction tests Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 10/11] samples/landlock: Add capability and namespace restriction support Mickaël Salaün
2026-04-22 21:20 ` Günther Noack
2026-04-23 13:51 ` Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 11/11] landlock: Add documentation for capability and namespace restrictions Mickaël Salaün
2026-03-12 14:48 ` Justin Suess
2026-04-23 13:51 ` Mickaël Salaün
2026-04-23 16:01 ` Justin Suess
2026-04-23 16:08 ` Justin Suess
2026-04-22 20:38 ` Günther Noack
2026-04-23 13:52 ` Mickaël Salaün
2026-05-08 15:13 ` Günther Noack
2026-03-25 12:34 ` [RFC PATCH v1 00/11] Landlock: Namespace and capability control Christian Brauner
2026-04-20 15:06 ` Günther Noack
2026-04-21 8:24 ` Mickaël Salaün
2026-04-22 21:16 ` Günther Noack
2026-04-23 13:50 ` Mickaël Salaün
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=20260312100444.2609563-9-mic@digikod.net \
--to=mic@digikod.net \
--cc=brauner@kernel.org \
--cc=enlightened@google.com \
--cc=gnoack@google.com \
--cc=ivanov.mikhail1@huawei-partners.com \
--cc=kernel-team@cloudflare.com \
--cc=lennart@poettering.net \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=m@maowtm.org \
--cc=nicolas.bouchinet@oss.cyber.gouv.fr \
--cc=paul@paul-moore.com \
--cc=serge@hallyn.com \
--cc=utilityemal77@gmail.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.