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>,
"Daniel Durning" <danieldurning.work@gmail.com>,
"Jonathan Corbet" <corbet@lwn.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: [PATCH v4 5/8] selftests/landlock: Add namespace restriction tests
Date: Fri, 2 Oct 2026 14:43:58 +0200 [thread overview]
Message-ID: <20261002124409.1277970-6-mic@digikod.net> (raw)
In-Reply-To: <20261002124409.1277970-1-mic@digikod.net>
Add tests covering LANDLOCK_PERMISSION_NAMESPACE_USE (namespace creation
via unshare/clone and namespace entry via setns) and its interaction
with LANDLOCK_PERMISSION_CAPABILITY_USE.
Rule validation covers the forward-compatibility contract: unknown
CLONE_NEW* bits, including holes, upper-range bits and bit 63, are
accepted at rule-add time yet have no runtime effect, because
deny-by-default still applies once the domain is enforced. Empty masks,
invalid permission selectors and non-zero flags are rejected.
Creation tests use FIXTURE_VARIANT to exercise all eight namespace types
across allowed/denied and privileged/unprivileged combinations, so every
type is shown to reach security_namespace_init(). Stacking tests cover
the three per-layer allow/deny combinations that exercise distinct
walker paths, and one domain handles both permissions at once.
Entry tests subject setns to the same type-based check through
security_namespace_install(), across processes, and require both
permissions to allow a non-user namespace.
The four open_tree(2) and fsmount(2) variants, anonymous and not, all
create a mount namespace: they converge in security_namespace_init()
with CLONE_NEWNS through different alloc_mnt_ns() paths. A
CLONE_NEWUSER | CLONE_NEWUTS unshare under partial-allow rules shows
that creation is sequential, so a denial on either type rolls the whole
syscall back with EPERM.
Audit tests cover allowed and denied creation and entry, per-member
quiet masks and the youngest-denying-layer rule. Trace tests cover
handled permissions, successful and rejected rules, version history,
namespace IDs, and quiet denials, including repeated and unknown-only
effective no-ops.
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>
---
Changes since v3:
https://patch.msgid.link/20260726161400.3010511-10-mic@digikod.net
- Cover the EFAULT result for an invalid namespace rule pointer.
- Drop CAP_AUDIT_CONTROL after cleaning up the audit fixture.
- Add trace tests for handled permissions, successful and rejected
rules, namespace IDs, and quiet denials, including repeated and
unknown-only effective no-ops.
- Make the successful clone3 assertion fatal before passing its PID to
waitpid().
Changes since v2:
https://patch.msgid.link/20260527181127.879771-7-mic@digikod.net
- Rebased for ABI 11 and the renamed namespace rule attribute (perm
plus allowed_namespace_types/quiet_namespace_types).
- Add per-member quiet audit tests for the namespace permission:
create_quieted, quiet_is_per_member, allow_and_quiet,
allow_and_quiet_same_member (quiet on an allowed member is inert),
quiet_youngest_layer_wins, and quiet_parent_only_denying_layer, plus a
quiet-only-rule leg in add_rule_bad_attr and the add_ns_rule_full
helper.
- Add a positive both-caps mount setns leg (two_perm_mnt_setns_allowed)
and the both-perms inverse test two_perm_ns_denied (the capability is
allowed but the namespace type is not, so the namespace hook denies).
- Add two_perm_combined_unshare_denied and
two_perm_combined_unshare_allowed: combining CLONE_NEWUSER with
another type in a single unshare(2) does not exempt the non-user
namespace from the capability check, so it still requires an allowed
CAP_SYS_ADMIN when the domain handles LANDLOCK_PERM_CAPABILITY_USE.
- Add add_rule_unknown_no_runtime_effect_setns, the install-hook mirror
of the creation-hook unknown-bit test.
- Add ns_audit.setns_quieted (a quieted setns denial is still EPERM but
unlogged) and ns_audit.quiet_unknown_bit_no_effect (an unknown quiet
bit does not suppress a known denial).
- Trim ns_proc_open to two representative types (user and mnt): opening
/proc/self/ns/* never reaches a Landlock hook and the path is
namespace-type-agnostic, so the other per-type variants added no
coverage.
Changes since v1:
https://patch.msgid.link/20260312100444.2609563-9-mic@digikod.net
- Update audit test patterns: namespace_inum replaced by namespace_id.
- Fix user_denied.setns expected error: security_namespace_install()
now runs before userns_install(), so Landlock returns EPERM before
userns_install() returns EINVAL.
- Rename LANDLOCK_PERM_NAMESPACE_ENTER references to
LANDLOCK_PERM_NAMESPACE_USE (companion change to the introducing
commit).
- Add ns_proc_open fixture covering all 8 namespace types: verify that
open("/proc/self/ns/<type>", O_RDONLY) does not trigger Landlock
denial under LANDLOCK_PERM_NAMESPACE_USE. Defensive boundary
documentation that the procfs ns/<type> open path is outside the
per-category permission's scope; catches future regressions if a hook
is misplaced.
- Add ns_mount_fd fixture covering open_tree(OPEN_TREE_CLONE),
open_tree(OPEN_TREE_NAMESPACE), fsmount(FSMOUNT_CLOEXEC), and
fsmount(FSMOUNT_NAMESPACE) with denied/allowed/unsandboxed
variants. All four converge in security_namespace_init() with
CLONE_NEWNS but exercise different code paths through alloc_mnt_ns().
- Add ns_create_multi_flag fixture covering a combined unshare with
CLONE_NEWUSER | CLONE_NEWUTS under partial-allow rules: documents
that any Landlock denial in the sequential namespace creation
rolls back the entire syscall with EPERM.
- Reshape setns_cross_process variants: rename allowed to
sandboxed_allowed and add an unsandboxed variant, so the fixture
now covers denied, sandboxed_allowed, and unsandboxed.
- Add sys_open_tree, sys_fsopen, sys_fsconfig, sys_fsmount syscall
wrappers in wrappers.h, and update fs_test.c to use sys_open_tree
instead of its local open_tree wrapper.
- Rename comments referencing security_namespace_alloc() and
hook_namespace_alloc() to security_namespace_init() and
hook_namespace_init() (companion change to the LSM hook rename in
the introducing commit).
- Document that setns_cross_process exercises only CLONE_NEWUTS:
the same enforcement applies to every namespace type via the
unified hook_namespace_install() helper.
- Add add_rule_unknown_no_runtime_effect: assert that a rule
listing only unknown namespace bits is accepted at rule-add time
but has no runtime effect, so an actual CLONE_NEW* operation is
still denied by deny-by-default once the domain is enforced.
- Add ns_stacking parent_denies variant covering the inverse
direction of stacking: layer 1 denies, layer 2 allows, operation
still denied. Completes the per-layer walker direction coverage.
---
tools/testing/selftests/landlock/common.h | 23 +
tools/testing/selftests/landlock/config | 5 +
tools/testing/selftests/landlock/fs_test.c | 13 +-
tools/testing/selftests/landlock/ns_test.c | 2643 +++++++++++++++++++
tools/testing/selftests/landlock/trace.h | 25 +
tools/testing/selftests/landlock/wrappers.h | 29 +
6 files changed, 2728 insertions(+), 10 deletions(-)
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 c5124de68a51..25aa4777cc33 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -130,6 +130,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 d86321936fd8..5568d7a4b2d7 100644
--- a/tools/testing/selftests/landlock/config
+++ b/tools/testing/selftests/landlock/config
@@ -5,6 +5,7 @@ CONFIG_CGROUP_SCHED=y
CONFIG_ENABLE_DEFAULT_TRACERS=y
CONFIG_FTRACE=y
CONFIG_INET=y
+CONFIG_IPC_NS=y
CONFIG_IPV6=y
CONFIG_KEYS=y
CONFIG_MPTCP=y
@@ -12,10 +13,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/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 6e979cef884d..7e25a6f6073c 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -57,13 +57,6 @@ int renameat2(int olddirfd, const char *oldpath, int newdirfd,
}
#endif
-#ifndef open_tree
-int open_tree(int dfd, const char *filename, unsigned int flags)
-{
- return syscall(__NR_open_tree, dfd, filename, flags);
-}
-#endif
-
static int sys_execveat(int dirfd, const char *pathname, char *const argv[],
char *const envp[], int flags)
{
@@ -2628,9 +2621,9 @@ TEST_F_FORK(layout1, refer_mount_root_deny)
/* Creates a mount object from a non-mount point. */
set_cap(_metadata, CAP_SYS_ADMIN);
- root_fd =
- open_tree(AT_FDCWD, dir_s1d1,
- AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ root_fd = sys_open_tree(AT_FDCWD, dir_s1d1,
+ AT_EMPTY_PATH | OPEN_TREE_CLONE |
+ OPEN_TREE_CLOEXEC);
clear_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_LE(0, root_fd);
diff --git a/tools/testing/selftests/landlock/ns_test.c b/tools/testing/selftests/landlock/ns_test.c
new file mode 100644
index 000000000000..6faafc110844
--- /dev/null
+++ b/tools/testing/selftests/landlock/ns_test.c
@@ -0,0 +1,2643 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - Namespace restriction
+ *
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/capability.h>
+#include <linux/landlock.h>
+#include <linux/mount.h>
+#include <linux/nsfs.h>
+#include <sched.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <syscall.h>
+#include <unistd.h>
+
+#include "audit.h"
+#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "ns_test"
+
+/*
+ * 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_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ };
+
+ 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 = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = ns_type,
+ };
+
+ return landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE, &attr, 0);
+}
+
+static int add_ns_rule_full(int ruleset_fd, __u64 allowed, __u64 quiet)
+{
+ const struct landlock_namespace_attr attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = allowed,
+ .quiet_namespace_types = quiet,
+ };
+
+ 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_permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ int ruleset_fd;
+ struct landlock_namespace_attr attr = {};
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Invalid rule pointer returns EFAULT. */
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ NULL, 0));
+ ASSERT_EQ(EFAULT, errno);
+
+ /* Empty permissions selector returns ENOMSG. */
+ attr.permissions = 0;
+ attr.allowed_namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(ENOMSG, errno);
+
+ /* Valid namespace selector plus an extra unhandled selector bit. */
+ attr.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE;
+ attr.allowed_namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(EINVAL, errno);
+
+ /* permissions with wrong type. */
+ attr.permissions = LANDLOCK_PERMISSION_CAPABILITY_USE;
+ attr.allowed_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.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_namespace_types = 1ULL << 63;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /* Useless rule: neither allowed nor quiet types set returns ENOMSG. */
+ attr.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_namespace_types = 0;
+ attr.quiet_namespace_types = 0;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ ASSERT_EQ(ENOMSG, errno);
+
+ /* Quiet-only rule (empty allowed set) is legal. */
+ attr.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_namespace_types = 0;
+ attr.quiet_namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /* Allow and quiet in the same rule. */
+ attr.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_namespace_types = CLONE_NEWNET;
+ attr.quiet_namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+ attr.quiet_namespace_types = 0;
+
+ /*
+ * Bit 1 is not a CLONE_NEW* value but is silently accepted for forward
+ * compatibility (no hole rejection).
+ */
+ attr.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_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.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_namespace_types = CLONE_NEWUTS | CLONE_NEWNET;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /*
+ * LANDLOCK_ADD_RULE_QUIET (and any other flag) is filesystem/network
+ * only and must be rejected for namespace rules, even when every attr
+ * field is otherwise valid.
+ */
+ attr.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_namespace_types = CLONE_NEWUTS;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, LANDLOCK_ADD_RULE_QUIET));
+ ASSERT_EQ(EINVAL, errno);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * Ruleset handles LANDLOCK_PERMISSION_CAPABILITY_USE but not
+ * LANDLOCK_PERMISSION_NAMESPACE_USE: 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.permissions = LANDLOCK_PERMISSION_NAMESPACE_USE;
+ attr.allowed_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 = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ };
+
+ 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.allowed_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.allowed_namespace_types = 1ULL << 32;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+/*
+ * A rule that lists only namespace bits unknown to the running kernel is
+ * accepted by landlock_add_rule() but has no runtime effect: once the domain is
+ * enforced, any actual CLONE_NEW* operation is still denied by the per-category
+ * deny-by-default behaviour. This documents the forward-compatibility
+ * contract: unknown bits are silently accepted so the same policy can be loaded
+ * across kernels, but they never grant a permission that the running kernel
+ * knows nothing about.
+ */
+TEST(add_rule_unknown_no_runtime_effect)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ };
+ struct landlock_namespace_attr attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ /* Only unknown bits: bit 31 (in lower 32) and bit 32. */
+ .allowed_namespace_types = (1ULL << 31) | (1ULL << 32),
+ };
+ int ruleset_fd;
+
+ disable_caps(_metadata);
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /*
+ * Baseline: absent the domain, creating CLONE_NEWUTS succeeds. This
+ * ensures the EPERM below is attributable to Landlock rather than to a
+ * missing capability, and fails if the hook always allows.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWUTS));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * CLONE_NEWUTS is a real, known CLONE_NEW* type but was not authorised
+ * by the rule above; deny-by-default applies.
+ */
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * The install-hook mirror of add_rule_unknown_no_runtime_effect: a rule listing
+ * only unknown namespace bits has no runtime effect on setns(2) either. Once
+ * enforced, entering a known namespace type is still denied by the per-category
+ * deny-by-default behaviour.
+ */
+TEST(add_rule_unknown_no_runtime_effect_setns)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ };
+ struct landlock_namespace_attr attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ /* Only unknown bits: bit 31 (in lower 32) and bit 32. */
+ .allowed_namespace_types = (1ULL << 31) | (1ULL << 32),
+ };
+ int ruleset_fd, ns_fd;
+
+ disable_caps(_metadata);
+
+ /* Open the NS FD before enforcing the domain. */
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &attr, 0));
+
+ /*
+ * Baseline: absent the domain, entering the process's own UTS namespace
+ * succeeds, so the later EPERM is attributable to Landlock rather than
+ * to a missing capability, and fails if the hook always allows.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, setns(ns_fd, CLONE_NEWUTS));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * CLONE_NEWUTS is a real, known CLONE_NEW* type but was not authorised
+ * by the rule above; deny-by-default applies at the install hook.
+ */
+ EXPECT_EQ(-1, setns(ns_fd, CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, close(ns_fd));
+}
+
+/* Namespace creation tests (variant-based positive/negative) */
+
+/* clang-format off */
+FIXTURE(ns_create) {
+ /* clang-format on */
+ char proc_path[NS_PROC_PATH_MAX];
+};
+
+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) {
+ .namespace_types = CLONE_NEWUTS,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+/* clang-format on */
+
+/*
+ * 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) {
+ .namespace_types = CLONE_NEWIPC,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+/* clang-format on */
+
+/* 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) {
+ .namespace_types = CLONE_NEWNS,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+/* clang-format on */
+
+/* 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) {
+ .namespace_types = CLONE_NEWPID,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+/* clang-format on */
+
+/* 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) {
+ .namespace_types = CLONE_NEWNET,
+ .is_sandboxed = true,
+ .has_rule = true,
+ .drop_all_caps = false,
+ .expected_result = 0,
+};
+/* clang-format on */
+
+/* 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)
+{
+ ASSERT_TRUE(ns_is_supported(variant->namespace_types, self->proc_path,
+ sizeof(self->proc_path)))
+ {
+ 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_init(), 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 {
+ ASSERT_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 returns EINVAL from
+ * userns_install() (rejects re-entry), but when Landlock denies the
+ * operation, security_namespace_install() returns EPERM before
+ * userns_install() runs.
+ */
+ if (variant->namespace_types == CLONE_NEWUSER &&
+ !variant->expected_result) {
+ 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_PERMISSION_NAMESPACE_USE 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: both layers must allow CLONE_NEWUSER for the operation to
+ * succeed. Variants exercise the three combinations of per-layer allow/deny
+ * that exercise distinct semantics; the (deny, deny) combination is omitted
+ * because it is covered by every other "deny" test in this file.
+ */
+/* clang-format off */
+FIXTURE(ns_stacking) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(ns_stacking)
+{
+ bool first_layer_allows;
+ bool second_layer_allows;
+};
+
+/* Layer 1 allows, layer 2 denies -> child denies. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_stacking, deny) {
+ /* clang-format on */
+ .first_layer_allows = true,
+ .second_layer_allows = false,
+};
+
+/* Both layers allow CLONE_NEWUSER -> operation succeeds. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_stacking, allow) {
+ /* clang-format on */
+ .first_layer_allows = true,
+ .second_layer_allows = true,
+};
+
+/*
+ * Layer 1 denies, layer 2 allows -> still denied: a child layer cannot grant
+ * what an ancestor layer withheld. This complements the
+ * parent-allows/child-denies variant above; together they verify the walker
+ * checks both layers and accepts only the (allow, allow) cell.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_stacking, parent_denies) {
+ /* clang-format on */
+ .first_layer_allows = false,
+ .second_layer_allows = true,
+};
+
+FIXTURE_SETUP(ns_stacking)
+{
+ disable_caps(_metadata);
+}
+
+FIXTURE_TEARDOWN(ns_stacking)
+{
+}
+
+/*
+ * Verify that any layer can deny an operation: enforcement requires all layers
+ * to allow. Variants exercise the three combinations that exercise distinct
+ * walker paths (allow/deny, allow/allow, deny/allow); only allow/allow lets the
+ * operation through.
+ */
+TEST_F(ns_stacking, two_layers)
+{
+ int ruleset_fd;
+ const bool expect_success = variant->first_layer_allows &&
+ variant->second_layer_allows;
+
+ /* First layer: allow or deny depending on variant. */
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->first_layer_allows)
+ 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 (expect_success) {
+ 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_permissions = LANDLOCK_PERMISSION_CAPABILITY_USE |
+ LANDLOCK_PERMISSION_NAMESPACE_USE,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_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);
+}
+
+/*
+ * open_tree(2) and fsmount(2) acquire a file descriptor referring to a
+ * newly-created mount namespace. Both call paths funnel into
+ * security_namespace_init() with CLONE_NEWNS, gated by
+ * LANDLOCK_PERMISSION_NAMESPACE_USE. Without coverage here, regressions in
+ * those paths would slip past the suite.
+ */
+/* clang-format off */
+FIXTURE(ns_mount_fd) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(ns_mount_fd)
+{
+ bool sandboxed;
+ bool has_rule;
+ int expected_errno;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_mount_fd, denied) {
+ /* clang-format on */
+ .sandboxed = true,
+ .has_rule = false,
+ .expected_errno = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_mount_fd, allowed) {
+ /* clang-format on */
+ .sandboxed = true,
+ .has_rule = true,
+ .expected_errno = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_mount_fd, unsandboxed) {
+ /* clang-format on */
+ .sandboxed = false,
+ .has_rule = false,
+ .expected_errno = 0,
+};
+
+FIXTURE_SETUP(ns_mount_fd)
+{
+}
+
+FIXTURE_TEARDOWN(ns_mount_fd)
+{
+}
+
+/*
+ * open_tree(OPEN_TREE_CLONE) creates an anonymous mount namespace to hold the
+ * cloned mount tree. hook_namespace_init() fires with CLONE_NEWNS.
+ */
+TEST_F(ns_mount_fd, open_tree_clone)
+{
+ int ruleset_fd, fd;
+
+ disable_caps(_metadata);
+
+ if (variant->sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWNS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ fd = sys_open_tree(AT_FDCWD, "/",
+ OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
+ if (variant->expected_errno) {
+ EXPECT_EQ(-1, fd);
+ EXPECT_EQ(variant->expected_errno, errno);
+ } else {
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(0, close(fd));
+ }
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * open_tree(OPEN_TREE_NAMESPACE) clones the mount tree into a new
+ * (non-anonymous) mount namespace. Same hook (CLONE_NEWNS) but a different
+ * code path inside fs/namespace.c (open_new_namespace -> alloc_mnt_ns).
+ * OPEN_TREE_NAMESPACE and OPEN_TREE_CLONE are mutually exclusive.
+ */
+TEST_F(ns_mount_fd, open_tree_namespace)
+{
+ int ruleset_fd, fd;
+
+ disable_caps(_metadata);
+
+ if (variant->sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWNS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ fd = sys_open_tree(AT_FDCWD, "/",
+ OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC |
+ AT_RECURSIVE);
+ if (variant->expected_errno) {
+ EXPECT_EQ(-1, fd);
+ EXPECT_EQ(variant->expected_errno, errno);
+ } else {
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(0, close(fd));
+ }
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * fsmount(2) without FSMOUNT_NAMESPACE creates an anonymous mount namespace to
+ * attach the new superblock. hook_namespace_init() fires with CLONE_NEWNS.
+ * The fs context (fsopen + fsconfig) is set up before sandboxing because
+ * Landlock here only handles the namespace permission.
+ */
+TEST_F(ns_mount_fd, fsmount_default)
+{
+ int ruleset_fd, fs_fd, mnt_fd;
+
+ disable_caps(_metadata);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ fs_fd = sys_fsopen("tmpfs", 0);
+ ASSERT_LE(0, fs_fd);
+ ASSERT_EQ(0, sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0));
+
+ if (variant->sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWNS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ mnt_fd = sys_fsmount(fs_fd, FSMOUNT_CLOEXEC, 0);
+ if (variant->expected_errno) {
+ EXPECT_EQ(-1, mnt_fd);
+ EXPECT_EQ(variant->expected_errno, errno);
+ } else {
+ ASSERT_LE(0, mnt_fd);
+ EXPECT_EQ(0, close(mnt_fd));
+ }
+ EXPECT_EQ(0, close(fs_fd));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * fsmount(2) with FSMOUNT_NAMESPACE creates a (non-anonymous) mount namespace
+ * for the new mount. Same hook as the default path, different code path.
+ */
+TEST_F(ns_mount_fd, fsmount_namespace)
+{
+ int ruleset_fd, fs_fd, mnt_fd;
+
+ disable_caps(_metadata);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ fs_fd = sys_fsopen("tmpfs", 0);
+ ASSERT_LE(0, fs_fd);
+ ASSERT_EQ(0, sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0));
+
+ if (variant->sandboxed) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->has_rule)
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, CLONE_NEWNS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ mnt_fd = sys_fsmount(fs_fd, FSMOUNT_CLOEXEC | FSMOUNT_NAMESPACE, 0);
+ if (variant->expected_errno) {
+ EXPECT_EQ(-1, mnt_fd);
+ EXPECT_EQ(variant->expected_errno, errno);
+ } else {
+ ASSERT_LE(0, mnt_fd);
+ EXPECT_EQ(0, close(mnt_fd));
+ }
+ EXPECT_EQ(0, close(fs_fd));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * unshare(2) with multiple CLONE_NEW* flags: when
+ * LANDLOCK_PERMISSION_NAMESPACE_USE denies any of the requested types, the
+ * entire syscall fails with EPERM. This documents the kernel's atomic
+ * behavior: create_new_namespaces(), called by copy_namespaces(), creates
+ * namespaces sequentially, and the first Landlock denial rolls back the whole
+ * operation. Mixing CLONE_NEWUSER (no capability check) with another
+ * CLONE_NEW* type is the typical container-runtime bootstrap pattern.
+ */
+/* clang-format off */
+FIXTURE(ns_create_multi_flag) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(ns_create_multi_flag)
+{
+ __u64 allowed_types;
+ int expected_errno;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create_multi_flag, partial_denied) {
+ /* clang-format on */
+ /* User namespace allowed; UTS namespace denied. */
+ .allowed_types = CLONE_NEWUSER,
+ .expected_errno = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_create_multi_flag, both_allowed) {
+ /* clang-format on */
+ .allowed_types = CLONE_NEWUSER | CLONE_NEWUTS,
+ .expected_errno = 0,
+};
+
+FIXTURE_SETUP(ns_create_multi_flag)
+{
+}
+
+FIXTURE_TEARDOWN(ns_create_multi_flag)
+{
+}
+
+TEST_F(ns_create_multi_flag, unshare)
+{
+ int ruleset_fd, status, err;
+ pid_t child;
+
+ disable_caps(_metadata);
+
+ /* Run unshare(2) in a child to avoid polluting the test process. */
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, variant->allowed_types));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ err = unshare(CLONE_NEWUSER | CLONE_NEWUTS);
+ if (variant->expected_errno) {
+ EXPECT_EQ(-1, err);
+ EXPECT_EQ(variant->expected_errno, errno);
+ } else {
+ EXPECT_EQ(0, err);
+ }
+ _exit(_metadata->exit_code);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_EQ(1, WIFEXITED(status));
+ ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+/* clang-format off */
+FIXTURE(setns_cross_process) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(setns_cross_process)
+{
+ bool is_sandboxed;
+ bool has_rule;
+ int expected_setns;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(setns_cross_process, denied) {
+ /* clang-format on */
+ .is_sandboxed = true,
+ .has_rule = false,
+ .expected_setns = EPERM,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(setns_cross_process, sandboxed_allowed) {
+ /* clang-format on */
+ .is_sandboxed = true,
+ .has_rule = true,
+ .expected_setns = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(setns_cross_process, unsandboxed) {
+ /* clang-format on */
+ .is_sandboxed = false,
+ .has_rule = 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_PERMISSION_NAMESPACE_USE denying UTS, the rule-based check applies
+ * regardless of which process created the namespace. This fixture exercises
+ * only CLONE_NEWUTS; the same enforcement applies to every namespace type (see
+ * hook_namespace_install() in security/landlock/ns.c), so per-type variants
+ * would not exercise different code paths.
+ */
+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) {
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (variant->has_rule)
+ 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);
+ 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 LANDLOCK_PERMISSION_NAMESPACE_USE and
+ * LANDLOCK_PERMISSION_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_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWUTS,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_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_PERMISSION_NAMESPACE_USE
+ * 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_PERMISSION_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_PERMISSION_NAMESPACE_USE.
+ */
+TEST(two_permissions_cap_denied)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWUTS | CLONE_NEWUSER,
+ };
+ /* CAP_SYS_ADMIN is NOT allowed. */
+ const struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_capabilities = (1ULL << CAP_SYS_CHROOT),
+ };
+ int ruleset_fd, ns_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);
+
+ /*
+ * setns into the current UTS namespace: the type is allowed but
+ * CAP_SYS_ADMIN is denied, so the kernel's ns_capable(CAP_SYS_ADMIN)
+ * gate fails before the namespace hook is reached (the setns capability
+ * leg, complementing the unshare/create leg above).
+ */
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+ EXPECT_EQ(-1, setns(ns_fd, CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ EXPECT_EQ(0, close(ns_fd));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * User NS creation: no capable() call from the kernel, so only
+ * LANDLOCK_PERMISSION_NAMESPACE_USE applies. CLONE_NEWUSER is in the
+ * allowed set, so this succeeds.
+ */
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER));
+}
+
+/*
+ * Combining CLONE_NEWUSER with another namespace type in a single unshare(2)
+ * call does not exempt the non-user namespace from the capability check: the
+ * kernel checks ns_capable(new_user_ns, CAP_SYS_ADMIN) for it, and the
+ * namespace-agnostic capability hook denies it when the domain handles
+ * LANDLOCK_PERMISSION_CAPABILITY_USE without allowing CAP_SYS_ADMIN. Both
+ * namespace types are allowed here, yet the combined call fails atomically,
+ * exactly like the equivalent two-call sequence.
+ */
+TEST(two_permissions_combined_unshare_denied)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ /* Both namespace types are allowed; no capability is allowed. */
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWUSER | CLONE_NEWUTS,
+ };
+ 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));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /*
+ * CAP_SYS_ADMIN is held so the kernel's commoncap check passes (via
+ * ownership of the new user namespace) and the denial is attributable
+ * to Landlock. The UTS leg needs CAP_SYS_ADMIN, which the capability
+ * hook denies, so the atomic unshare(2) fails and no namespace is
+ * created.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(-1, unshare(CLONE_NEWUSER | CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* The user namespace alone has no capability check, so it succeeds. */
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER));
+}
+
+/*
+ * Positive complement to two_permissions_combined_unshare_denied: allowing
+ * CAP_SYS_ADMIN lets the same combined unshare(CLONE_NEWUSER | CLONE_NEWUTS)
+ * succeed, proving the denial above comes from the missing capability allowance
+ * and not the namespace rule. Run in a child because a successful unshare(2)
+ * mutates the caller.
+ */
+TEST(two_permissions_combined_unshare_allowed)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWUSER | CLONE_NEWUTS,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+ int ruleset_fd, status;
+ pid_t child;
+
+ disable_caps(_metadata);
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ 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));
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, unshare(CLONE_NEWUSER | CLONE_NEWUTS));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ _exit(_metadata->exit_code);
+ }
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_EQ(1, WIFEXITED(status));
+ ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+/*
+ * Inverse of two_permissions_cap_denied: the required capability is allowed but
+ * the namespace type is not, so the Landlock namespace hook denies the
+ * operation (the capability leg passes). A positive control (an allowed type
+ * creates successfully with the same allowed capability) proves CAP_SYS_ADMIN
+ * is genuinely exercisable, so the UTS denial is attributable to the namespace
+ * hook rather than the capability hook.
+ */
+TEST(two_permissions_ns_denied)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ /* CLONE_NEWUTS is NOT allowed; CLONE_NEWNET is. */
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWNET,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+ 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));
+
+ /*
+ * Positive control: CLONE_NEWNET is allowed and CAP_SYS_ADMIN is
+ * allowed, so the kernel's ns_capable() gate passes and the namespace
+ * hook allows it.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, unshare(CLONE_NEWNET));
+
+ /*
+ * CAP_SYS_ADMIN is allowed (ns_capable passes), but CLONE_NEWUTS is not
+ * in the allowed namespace types, so the Landlock namespace hook denies
+ * it.
+ */
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * 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_permissions_mnt_setns)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWNS,
+ };
+ const struct landlock_capability_attr cap_admin = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_capabilities = (1ULL << CAP_SYS_ADMIN),
+ };
+ const struct landlock_capability_attr cap_admin_chroot = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_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_PERMISSION_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));
+}
+
+/*
+ * Positive complement to two_permissions_mnt_setns: a single layer that allows
+ * the mount namespace type AND both CAP_SYS_ADMIN and CAP_SYS_CHROOT lets
+ * setns(CLONE_NEWNS) into the process's own mount namespace succeed. Proves
+ * the denial legs above are caused by the withheld CAP_SYS_CHROOT, not an
+ * unconditional block of mount setns.
+ */
+TEST(two_permissions_mnt_setns_allowed)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+ const struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWNS,
+ };
+ const struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_capabilities = (1ULL << CAP_SYS_ADMIN) |
+ (1ULL << CAP_SYS_CHROOT),
+ };
+ int ruleset_fd, ns_fd;
+
+ disable_caps(_metadata);
+
+ /* Open the NS FD before enforcing the domain. */
+ ns_fd = open("/proc/self/ns/mnt", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ 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));
+
+ /*
+ * mntns_install() checks CAP_SYS_ADMIN then CAP_SYS_CHROOT (both
+ * allowed by the rule) and the mount NS type is allowed, so setns into
+ * the process's own mount namespace succeeds.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ set_cap(_metadata, CAP_SYS_CHROOT);
+ EXPECT_EQ(0, setns(ns_fd, CLONE_NEWNS));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ clear_cap(_metadata, CAP_SYS_CHROOT);
+ EXPECT_EQ(0, close(ns_fd));
+}
+
+/*
+ * setns() through a namespace fd inherited across fork() is still governed by
+ * the child's own Landlock domain: the child enforces a domain denying
+ * CLONE_NEWUTS, then joining the inherited /proc/self/ns/uts fd is denied.
+ */
+TEST(setns_inherited_fd)
+{
+ int ns_fd, status;
+ pid_t child;
+
+ disable_caps(_metadata);
+
+ /* Open the UTS ns fd before fork() so the child inherits it. */
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY);
+ ASSERT_LE(0, ns_fd);
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ int ruleset_fd;
+
+ /*
+ * Baseline: joining the inherited fd works before the domain,
+ * so the later EPERM is attributable to Landlock.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, setns(ns_fd, CLONE_NEWUTS));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ /* No rule allows UTS. */
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_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);
+
+ _exit(_metadata->exit_code);
+ return;
+ }
+
+ EXPECT_EQ(0, close(ns_fd));
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ if (WIFSIGNALED(status) || !WIFEXITED(status) ||
+ WEXITSTATUS(status) != EXIT_SUCCESS)
+ _metadata->exit_code = KSFT_FAIL;
+}
+
+/* Audit tests */
+
+static int matches_log_ns_create(int audit_fd, __u64 ns_type)
+{
+ static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ " blockers=namespace\\.use namespace_type=0x%x namespace_id=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, __u64 ns_id)
+{
+ static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ " blockers=namespace\\.use namespace_type=0x%x namespace_id=%llu$";
+ char log_match[sizeof(log_template) + 32];
+ int log_match_len;
+
+ log_match_len = snprintf(log_match, sizeof(log_match), log_template,
+ (unsigned int)ns_type,
+ (unsigned long long)ns_id);
+ 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));
+ clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+/*
+ * Verifies that a denied namespace creation produces the expected audit record
+ * with the namespace.use blocker string and namespace_type.
+ */
+TEST_F(ns_audit, create_denied)
+{
+ struct audit_records records;
+ int ruleset_fd;
+ __u64 domain_id;
+
+ 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));
+
+ /*
+ * The domain allocation record is emitted in the same event as the
+ * first denial; anchor its status=allocated and enforcing pid. Both
+ * access and domain records are now consumed, so none remain.
+ */
+ EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(),
+ &domain_id));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, 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);
+ EXPECT_EQ(0, records.domain);
+}
+
+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);
+ EXPECT_EQ(0, records.domain);
+}
+
+TEST_F(ns_audit, setns_denied)
+{
+ struct audit_records records;
+ int ruleset_fd, ns_fd;
+ __u64 ns_id, domain_id;
+
+ 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);
+
+ /* The audit record logs the target namespace's exact ns_id. */
+ ASSERT_EQ(0, ioctl(ns_fd, NS_GET_ID, &ns_id));
+
+ 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, anchored to the ns_id. */
+ EXPECT_EQ(0, matches_log_ns_setns(self->audit_fd, CLONE_NEWUTS, ns_id));
+
+ /*
+ * The domain allocation record is emitted in the same event as the
+ * first denial; anchor its status=allocated and enforcing pid. Both
+ * access and domain records are now consumed, so none remain.
+ */
+ EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(),
+ &domain_id));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+}
+
+/*
+ * A quieted denial is still denied (EPERM); only its audit record is
+ * suppressed. Quiet-only rule (empty allowed set).
+ */
+TEST_F(ns_audit, create_quieted)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, 0, CLONE_NEWUTS));
+ 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);
+
+ /* The denial is suppressed: no access record. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+/*
+ * A quieted setns denial is still denied (EPERM); only its audit record is
+ * suppressed. The install-hook mirror of create_quieted.
+ */
+TEST_F(ns_audit, setns_quieted)
+{
+ 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_full(ruleset_fd, 0, 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);
+
+ 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));
+
+ /* The denial is suppressed: no access record. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+/*
+ * Quiet is per-member: quieting one namespace type does not silence the denial
+ * of another type denied by the same layer.
+ */
+TEST_F(ns_audit, quiet_is_per_member)
+{
+ struct audit_records records;
+ int ruleset_fd;
+ __u64 domain_id;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ /*
+ * Quiet CLONE_NEWNET only; CLONE_NEWUTS is neither allowed nor quiet.
+ */
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, 0, CLONE_NEWNET));
+ 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);
+
+ /* The unquieted CLONE_NEWUTS denial is still logged. */
+ EXPECT_EQ(0, matches_log_ns_create(self->audit_fd, CLONE_NEWUTS));
+ EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(),
+ &domain_id));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+}
+
+/*
+ * Quieting an unknown namespace bit has no effect: a rule that quiets only a
+ * bit the running kernel does not know still logs the denial of a known type.
+ * Mirrors the forward-compatibility contract for the quiet mask.
+ */
+TEST_F(ns_audit, quiet_unknown_bit_no_effect)
+{
+ struct audit_records records;
+ int ruleset_fd;
+ __u64 domain_id;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ /* Quiet only an unknown bit (bit 31 is not a CLONE_NEW* value). */
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, 0, (1ULL << 31)));
+ 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);
+
+ /* The unknown quiet bit does not suppress the known denial. */
+ EXPECT_EQ(0, matches_log_ns_create(self->audit_fd, CLONE_NEWUTS));
+ EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(),
+ &domain_id));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+}
+
+/* A single rule can both allow one member and quiet the denial of another. */
+TEST_F(ns_audit, allow_and_quiet)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, CLONE_NEWNET, CLONE_NEWUTS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* Allowed member: succeeds. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, unshare(CLONE_NEWNET));
+ /* Quieted member: denied but not logged. */
+ EXPECT_EQ(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+/*
+ * Quieting a member that is also allowed is inert: an allowed member is never
+ * denied, so its quiet bit has no effect.
+ */
+TEST_F(ns_audit, allow_and_quiet_same_member)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ /* Same member both allowed and quieted. */
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, CLONE_NEWNET, CLONE_NEWNET));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* The member is allowed (quiet is inert), so it succeeds. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ EXPECT_EQ(0, unshare(CLONE_NEWNET));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* Nothing denied, nothing logged. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+/*
+ * Only the youngest denying layer decides quieting: a parent that quiets a
+ * member cannot silence a denial by a deeper layer that handles but does not
+ * quiet it.
+ */
+TEST_F(ns_audit, quiet_youngest_layer_wins)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ /* Parent layer: handles namespaces and quiets CLONE_NEWUTS. */
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, 0, CLONE_NEWUTS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* Child layer: handles namespaces but does not quiet CLONE_NEWUTS. */
+ 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);
+
+ /* The child layer denies and does not quiet: logged. */
+ EXPECT_EQ(0, matches_log_ns_create(self->audit_fd, CLONE_NEWUTS));
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+/*
+ * A parent layer that quiets and denies a member suppresses its own denial even
+ * when it is not the youngest layer: a child layer allows the member, so the
+ * parent is the youngest (and only) denying layer and its stored quiet mask
+ * fires. Complements quiet_youngest_layer_wins.
+ */
+TEST_F(ns_audit, quiet_parent_only_denying_layer)
+{
+ struct audit_records records;
+ int ruleset_fd;
+
+ /* Parent layer: quiet-only, so it denies CLONE_NEWUTS. */
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, 0, CLONE_NEWUTS));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ /* Child layer: allows CLONE_NEWUTS, so only the parent denies it. */
+ 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(-1, unshare(CLONE_NEWUTS));
+ EXPECT_EQ(EPERM, errno);
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* The parent quiets and is the youngest denying layer: suppressed. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+}
+
+/* Trace tests */
+
+/* clang-format off */
+FIXTURE(ns_trace) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(ns_trace)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NAMESPACE_ENABLE,
+ true));
+ ASSERT_EQ(0, tracefs_enable_event(
+ TRACEFS_DENY_PERMISSION_NAMESPACE_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(ns_trace)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_ADD_RULE_NAMESPACE_ENABLE, false);
+ tracefs_enable_event(TRACEFS_DENY_PERMISSION_NAMESPACE_ENABLE, false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Check the permission fields and version history, including two rejected calls
+ * and two successful calls that do not extend the effective policy: a repeated
+ * rule and an unknown-only rule.
+ */
+TEST_F(ns_trace, rule_events)
+{
+ const char *const version_1 =
+ REGEX_ADD_RULE_NAMESPACE_VERSION(TRACE_TASK, "1");
+ const char *const version_2 =
+ REGEX_ADD_RULE_NAMESPACE_VERSION(TRACE_TASK, "2");
+ const char *const version_3 =
+ REGEX_ADD_RULE_NAMESPACE_VERSION(TRACE_TASK, "3");
+ const struct landlock_namespace_attr valid_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ .allowed_namespace_types = CLONE_NEWUTS,
+ .quiet_namespace_types = CLONE_NEWNET,
+ };
+ const struct landlock_namespace_attr invalid_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ .allowed_namespace_types = CLONE_NEWUTS,
+ };
+ char expected[32], field[64];
+ char *buf;
+ int ruleset_fd;
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, CLONE_NEWUTS, CLONE_NEWNET));
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &invalid_attr, 0));
+ ASSERT_EQ(EINVAL, errno);
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
+ &valid_attr, LANDLOCK_ADD_RULE_QUIET));
+ ASSERT_EQ(EINVAL, errno);
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, CLONE_NEWUTS, CLONE_NEWNET));
+ ASSERT_EQ(0, add_ns_rule(ruleset_fd, 1ULL << 63));
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ ASSERT_EQ(1,
+ tracefs_count_matches(buf, REGEX_CREATE_RULESET(TRACE_TASK)));
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "handled_permissions", field, sizeof(field)));
+ EXPECT_STREQ("namespace.use", field);
+
+ ASSERT_EQ(3,
+ tracefs_count_matches(buf, "landlock_add_rule_namespace: "));
+ ASSERT_EQ(3, tracefs_count_matches(
+ buf, REGEX_ADD_RULE_NAMESPACE(TRACE_TASK)));
+ EXPECT_EQ(1, tracefs_count_matches(buf, version_1));
+ EXPECT_EQ(1, tracefs_count_matches(buf, version_2));
+ EXPECT_EQ(1, tracefs_count_matches(buf, version_3));
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, version_1, "permissions", field,
+ sizeof(field)));
+ EXPECT_STREQ("namespace.use", field);
+ ASSERT_EQ(0, tracefs_extract_field(buf, version_1,
+ "allowed_namespace_types", field,
+ sizeof(field)));
+ /* Trace fields use raw CLONE_NEW* values, not internal bit indexes. */
+ snprintf(expected, sizeof(expected), "0x%llx",
+ (unsigned long long)CLONE_NEWUTS);
+ EXPECT_STREQ(expected, field);
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, version_1, "quiet_namespace_types",
+ field, sizeof(field)));
+ snprintf(expected, sizeof(expected), "0x%llx",
+ (unsigned long long)CLONE_NEWNET);
+ EXPECT_STREQ(expected, field);
+
+ /* The rejected calls emit no event and do not consume version 2. */
+ ASSERT_EQ(0, tracefs_extract_field(buf, version_2, "permissions", field,
+ sizeof(field)));
+ EXPECT_STREQ("namespace.use", field);
+ ASSERT_EQ(0, tracefs_extract_field(buf, version_2,
+ "allowed_namespace_types", field,
+ sizeof(field)));
+ snprintf(expected, sizeof(expected), "0x%llx",
+ (unsigned long long)CLONE_NEWUTS);
+ EXPECT_STREQ(expected, field);
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, version_2, "quiet_namespace_types",
+ field, sizeof(field)));
+ snprintf(expected, sizeof(expected), "0x%llx",
+ (unsigned long long)CLONE_NEWNET);
+ EXPECT_STREQ(expected, field);
+
+ /* The unknown-only call succeeds but contributes no effective bits. */
+ ASSERT_EQ(0, tracefs_extract_field(buf, version_3, "permissions", field,
+ sizeof(field)));
+ EXPECT_STREQ("namespace.use", field);
+ ASSERT_EQ(0, tracefs_extract_field(buf, version_3,
+ "allowed_namespace_types", field,
+ sizeof(field)));
+ EXPECT_STREQ("0x0", field);
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, version_3, "quiet_namespace_types",
+ field, sizeof(field)));
+ EXPECT_STREQ("0x0", field);
+
+ free(buf);
+}
+
+static void test_ns_trace_denial(struct __test_metadata *const _metadata,
+ const bool use_setns, const bool quiet)
+{
+ char expected[32], field[64];
+ int ruleset_fd, ns_fd = -1, ret;
+ __u64 expected_ns_id = 0;
+ char *buf;
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ if (use_setns) {
+ ns_fd = open("/proc/self/ns/uts", O_RDONLY | O_CLOEXEC);
+ ASSERT_LE(0, ns_fd);
+ ASSERT_EQ(0, ioctl(ns_fd, NS_GET_ID, &expected_ns_id));
+ }
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ if (quiet)
+ ASSERT_EQ(0, add_ns_rule_full(ruleset_fd, 0, CLONE_NEWUTS));
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ if (use_setns)
+ ret = setns(ns_fd, CLONE_NEWUTS);
+ else
+ ret = unshare(CLONE_NEWUTS);
+ EXPECT_EQ(-1, ret);
+ EXPECT_EQ(EPERM, errno);
+
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ if (ns_fd >= 0)
+ EXPECT_EQ(0, close(ns_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+ ASSERT_EQ(1, tracefs_count_matches(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK)))
+ {
+ TH_LOG("Expected one namespace denial event\n%s", buf);
+ }
+
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK),
+ "domain", field, sizeof(field)));
+ EXPECT_STRNE("0", field);
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK),
+ "logged", field, sizeof(field)));
+ EXPECT_STREQ(quiet ? "0" : "1", field);
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK),
+ "blockers", field, sizeof(field)));
+ EXPECT_STREQ("use", field);
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK),
+ "namespace_type", field, sizeof(field)));
+ snprintf(expected, sizeof(expected), "0x%llx",
+ (unsigned long long)CLONE_NEWUTS);
+ EXPECT_STREQ(expected, field);
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PERMISSION_NAMESPACE(TRACE_TASK),
+ "namespace_id", field, sizeof(field)));
+ EXPECT_EQ(use_setns ? expected_ns_id : 0, strtoull(field, NULL, 10));
+
+ free(buf);
+}
+
+TEST_F(ns_trace, deny_create)
+{
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+ test_ns_trace_denial(_metadata, false, false);
+}
+
+TEST_F(ns_trace, deny_create_quiet)
+{
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+ test_ns_trace_denial(_metadata, false, true);
+}
+
+TEST_F(ns_trace, deny_setns)
+{
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+ test_ns_trace_denial(_metadata, true, false);
+}
+
+/* clang-format off */
+FIXTURE(ns_proc_open) {
+ /* clang-format on */
+ struct audit_filter audit_filter;
+ int audit_fd;
+};
+
+FIXTURE_VARIANT(ns_proc_open)
+{
+ __u64 ns_type;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_proc_open, mnt) {
+ /* clang-format on */
+ .ns_type = CLONE_NEWNS,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ns_proc_open, user) {
+ /* clang-format on */
+ .ns_type = CLONE_NEWUSER,
+};
+
+FIXTURE_SETUP(ns_proc_open)
+{
+ 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_proc_open)
+{
+ set_cap(_metadata, CAP_AUDIT_CONTROL);
+ EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter));
+ clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+/*
+ * Opening /proc/self/ns/<type> only acquires a procfs reference, not membership
+ * or an acquired fd of the kind LANDLOCK_PERMISSION_NAMESPACE_USE gates.
+ * Verify the open is unrestricted even when the permission is handled with no
+ * rules.
+ */
+TEST_F(ns_proc_open, open_unrestricted)
+{
+ char proc_path[NS_PROC_PATH_MAX];
+ struct audit_records records;
+ int ruleset_fd, fd;
+
+ ASSERT_TRUE(
+ ns_is_supported(variant->ns_type, proc_path, sizeof(proc_path)))
+ {
+ TH_LOG("Namespace type 0x%llx not supported",
+ (unsigned long long)variant->ns_type);
+ }
+
+ ruleset_fd = create_ns_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ fd = open(proc_path, O_RDONLY);
+ ASSERT_LE(0, fd)
+ {
+ TH_LOG("open(%s) failed: %s", proc_path, strerror(errno));
+ }
+
+ /* No Landlock denial. */
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+
+ EXPECT_EQ(0, close(fd));
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h
index fe2d1a0d8de3..568c13d8eee4 100644
--- a/tools/testing/selftests/landlock/trace.h
+++ b/tools/testing/selftests/landlock/trace.h
@@ -31,6 +31,8 @@
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_path_beneath/enable"
#define TRACEFS_ADD_RULE_NET_PORT_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net_port/enable"
+#define TRACEFS_ADD_RULE_NAMESPACE_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_add_rule_namespace/enable"
#define TRACEFS_CHECK_RULE_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_inode/enable"
#define TRACEFS_CHECK_RULE_NET_ENABLE \
@@ -39,6 +41,8 @@
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
#define TRACEFS_DENY_ACCESS_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
+#define TRACEFS_DENY_PERMISSION_NAMESPACE_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_permission_namespace/enable"
#define TRACEFS_DENY_PTRACE_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
#define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
@@ -95,6 +99,17 @@
"access_rights=[a-z_|]* " \
"port=[0-9]\\+$"
+#define REGEX_ADD_RULE_NAMESPACE_VERSION(task, version) \
+ TRACE_PREFIX(task) \
+ "landlock_add_rule_namespace: " \
+ "ruleset=[0-9a-f]\\+\\." version " " \
+ "permissions=[a-z._|]* " \
+ "allowed_namespace_types=0x[0-9a-f]\\+ " \
+ "quiet_namespace_types=0x[0-9a-f]\\+$"
+
+#define REGEX_ADD_RULE_NAMESPACE(task) \
+ REGEX_ADD_RULE_NAMESPACE_VERSION(task, "[0-9]\\+")
+
#define REGEX_CREATE_RULESET(task) \
TRACE_PREFIX(task) \
"landlock_create_ruleset: " \
@@ -148,6 +163,16 @@
"blockers=[a-z_|]* " \
"port=-\\?[0-9]\\+$"
+#define REGEX_DENY_PERMISSION_NAMESPACE(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_permission_namespace: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "logged=[01] " \
+ "blockers=[a-z_|]* " \
+ "namespace_type=0x[0-9a-f]\\+ " \
+ "namespace_id=[0-9]\\+$"
+
#define REGEX_DENY_PTRACE(task) \
TRACE_PREFIX(task) \
"landlock_deny_ptrace: " \
diff --git a/tools/testing/selftests/landlock/wrappers.h b/tools/testing/selftests/landlock/wrappers.h
index 65548323e45d..e6fe46b7c2cc 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,31 @@ 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);
+}
+
+static inline int sys_open_tree(int dfd, const char *filename,
+ unsigned int flags)
+{
+ return syscall(__NR_open_tree, dfd, filename, flags);
+}
+
+static inline int sys_fsopen(const char *fsname, unsigned int flags)
+{
+ return syscall(__NR_fsopen, fsname, flags);
+}
+
+static inline int sys_fsconfig(int fs_fd, unsigned int cmd, const char *key,
+ const void *value, int aux)
+{
+ return syscall(__NR_fsconfig, fs_fd, cmd, key, value, aux);
+}
+
+static inline int sys_fsmount(int fs_fd, unsigned int flags,
+ unsigned int attr_flags)
+{
+ return syscall(__NR_fsmount, fs_fd, flags, attr_flags);
+}
--
2.55.0
next prev parent reply other threads:[~2026-10-02 12:44 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 12:43 [PATCH v4 0/8] Landlock: Namespace and capability control Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 1/8] landlock: Rename quiet_masks to quiet_access Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 2/8] landlock: Wrap per-layer access masks in struct layer_config Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 3/8] landlock: Enforce namespace use restrictions Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 4/8] landlock: Enforce capability restrictions Mickaël Salaün
2026-10-02 12:43 ` Mickaël Salaün [this message]
2026-10-02 12:43 ` [PATCH v4 6/8] selftests/landlock: Add capability restriction tests Mickaël Salaün
2026-10-02 12:44 ` [PATCH v4 7/8] samples/landlock: Add capability and namespace restriction support Mickaël Salaün
2026-10-02 12:44 ` [PATCH v4 8/8] landlock: Add documentation for capability and namespace restrictions 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=20261002124409.1277970-6-mic@digikod.net \
--to=mic@digikod.net \
--cc=brauner@kernel.org \
--cc=corbet@lwn.net \
--cc=danieldurning.work@gmail.com \
--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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®