summaryrefslogtreecommitdiff
path: root/security
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-08-21 12:28:35 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-08-21 12:28:35 -0700
commit7199989f3f3194d653b024ce8e79cea6b15e38b9 (patch)
tree00801d41a37df6bdd737ede8798f68006be3ca88 /security
parent614b9fb585f143d65162f17f1a4b4ec4cbdf5794 (diff)
parent172b6a6d8463562b0cbebfd66f770b078f81966b (diff)
downloadlinux-7199989f3f3194d653b024ce8e79cea6b15e38b9.tar.gz
linux-7199989f3f3194d653b024ce8e79cea6b15e38b9.zip
Merge tag 'landlock-7.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux
Pull Landlock update from Mickaël Salaün: "This improves observability with Landlock tracepoints support, which required some refactoring for dedicated domain types and common helpers shared with audit code. A LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag is also added to improve process-wide domain enforcement consistency. Whiteout files are now correctly handled and tested, and a few other fixes" * tag 'landlock-7.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: (34 commits) landlock: Document tracepoints selftests/landlock: Add landlock_enforce_domain trace tests selftests/landlock: Add scope and ptrace tracepoint tests selftests/landlock: Add network tracepoint tests selftests/landlock: Add filesystem tracepoint tests selftests/landlock: Add trace event test infrastructure and tests landlock: Add tracepoints for ptrace and scope denials landlock: Add landlock_deny_access_fs and landlock_deny_access_net landlock: Add tracepoints for rule checking landlock: Add landlock_enforce_domain tracepoint landlock: Add create_domain and free_domain tracepoints landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints landlock: Add create_ruleset and free_ruleset tracepoints landlock: Consolidate access-right and scope names in a shared header landlock: Decouple the per-denial logging decision from CONFIG_AUDIT landlock: Split denial logging from audit into common framework landlock: Split struct landlock_domain from struct landlock_ruleset landlock: Move domain query functions to domain.c landlock: Prepare ruleset and domain type split samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler ...
Diffstat (limited to 'security')
-rw-r--r--security/landlock/Kconfig5
-rw-r--r--security/landlock/Makefile12
-rw-r--r--security/landlock/access.h6
-rw-r--r--security/landlock/audit.c641
-rw-r--r--security/landlock/audit.h57
-rw-r--r--security/landlock/cred.c14
-rw-r--r--security/landlock/cred.h29
-rw-r--r--security/landlock/domain.c472
-rw-r--r--security/landlock/domain.h163
-rw-r--r--security/landlock/errata/abi-1.h23
-rw-r--r--security/landlock/fs.c259
-rw-r--r--security/landlock/fs.h40
-rw-r--r--security/landlock/id.h6
-rw-r--r--security/landlock/limits.h2
-rw-r--r--security/landlock/log.c587
-rw-r--r--security/landlock/log.h86
-rw-r--r--security/landlock/net.c38
-rw-r--r--security/landlock/ruleset.c546
-rw-r--r--security/landlock/ruleset.h250
-rw-r--r--security/landlock/syscalls.c125
-rw-r--r--security/landlock/task.c87
-rw-r--r--security/landlock/trace.c185
-rw-r--r--security/landlock/trace.h44
-rw-r--r--security/landlock/tsync.c24
-rw-r--r--security/landlock/tsync.h4
25 files changed, 2277 insertions, 1428 deletions
diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig
index 3f1493402052..7aeac29160e8 100644
--- a/security/landlock/Kconfig
+++ b/security/landlock/Kconfig
@@ -21,6 +21,11 @@ config SECURITY_LANDLOCK
you should also prepend "landlock," to the content of CONFIG_LSM to
enable Landlock at boot time.
+config SECURITY_LANDLOCK_LOG
+ bool
+ depends on SECURITY_LANDLOCK
+ default y if AUDIT || TRACEPOINTS
+
config SECURITY_LANDLOCK_KUNIT_TEST
bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS
depends on KUNIT=y
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index ffa7646d99f3..2711f4876939 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -8,11 +8,15 @@ landlock-y := \
cred.o \
task.o \
fs.o \
- tsync.o
+ tsync.o \
+ domain.o
landlock-$(CONFIG_INET) += net.o
-landlock-$(CONFIG_AUDIT) += \
+landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
id.o \
- audit.o \
- domain.o
+ log.o
+
+landlock-$(CONFIG_AUDIT) += audit.o
+
+landlock-$(CONFIG_TRACEPOINTS) += trace.o
diff --git a/security/landlock/access.h b/security/landlock/access.h
index d926078bf0a5..bbbb41f41147 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -19,7 +19,7 @@
/*
* All access rights that are denied by default whether they are handled or not
- * by a ruleset/layer. This must be ORed with all ruleset->access_masks[]
+ * by a ruleset/layer. This must be ORed with all domain->handled_masks[]
* entries when we need to get the absolute handled access masks, see
* landlock_upgrade_handled_access_masks().
*/
@@ -74,13 +74,13 @@ struct layer_mask {
* @access: The unfulfilled access rights for this layer.
*/
access_mask_t access : LANDLOCK_NUM_ACCESS_MAX;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @quiet: Whether we have encountered a rule with the quiet flag for
* this layer. Used to control logging.
*/
access_mask_t quiet : 1;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
} __packed __aligned(sizeof(access_mask_t));
/*
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 50536c568526..e02963834e48 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -5,9 +5,9 @@
* Copyright © 2023-2025 Microsoft Corporation
*/
-#include <kunit/test.h>
#include <linux/audit.h>
#include <linux/bitops.h>
+#include <linux/landlock.h>
#include <linux/lsm_audit.h>
#include <linux/pid.h>
#include <uapi/linux/landlock.h>
@@ -18,40 +18,30 @@
#include "cred.h"
#include "domain.h"
#include "limits.h"
-#include "ruleset.h"
-
-static const char *const fs_access_strings[] = {
- [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
-};
+#include "log.h"
+
+/*
+ * Access-right and scope names are built from the lists shared with the trace
+ * events (see <linux/landlock.h>). The designated initializer places each name
+ * at its bit index, so the lookup stays O(1) and does not depend on the entry
+ * order. log_blockers() adds the "fs."/"net."/"scope." category prefix.
+ */
+#define _LANDLOCK_NAME_ENTRY(mask, name) [BIT_INDEX(mask)] = name
+
+static const char *const fs_access_strings[] = { _LANDLOCK_ACCESS_FS_NAMES };
static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
-static const char *const net_access_strings[] = {
- [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
- [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
- [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_UDP)] = "net.bind_udp",
- [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)] =
- "net.connect_send_udp",
-};
+static const char *const net_access_strings[] = { _LANDLOCK_ACCESS_NET_NAMES };
static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
+static const char *const scope_strings[] = { _LANDLOCK_SCOPE_NAMES };
+
+static_assert(ARRAY_SIZE(scope_strings) == LANDLOCK_NUM_SCOPE);
+
+#undef _LANDLOCK_NAME_ENTRY
+
static __attribute_const__ const char *
get_blocker(const enum landlock_request_type type,
const unsigned long access_bit)
@@ -63,7 +53,7 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
WARN_ON_ONCE(access_bit != -1);
- return "fs.change_topology";
+ return "change_topology";
case LANDLOCK_REQUEST_FS_ACCESS:
if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
@@ -77,32 +67,63 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
WARN_ON_ONCE(access_bit != -1);
- return "scope.abstract_unix_socket";
+ return scope_strings[BIT_INDEX(
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)];
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
WARN_ON_ONCE(access_bit != -1);
- return "scope.signal";
+ return scope_strings[BIT_INDEX(LANDLOCK_SCOPE_SIGNAL)];
}
WARN_ON_ONCE(1);
return "unknown";
}
+/*
+ * Returns the audit category prefix prepended to the unprefixed blocker name
+ * returned by get_blocker() (filesystem and network access rights,
+ * change_topology, and scopes). The ptrace blocker is standalone and carries
+ * its full name in get_blocker(), so it uses no prefix.
+ */
+static __attribute_const__ const char *
+blocker_prefix(const enum landlock_request_type type)
+{
+ switch (type) {
+ case LANDLOCK_REQUEST_PTRACE:
+ return "";
+
+ case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ return "fs.";
+
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ return "net.";
+
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ return "scope.";
+ }
+
+ WARN_ON_ONCE(1);
+ return "";
+}
+
static void log_blockers(struct audit_buffer *const ab,
const enum landlock_request_type type,
const access_mask_t access)
{
const unsigned long access_mask = access;
+ const char *const prefix = blocker_prefix(type);
unsigned long access_bit;
bool is_first = true;
for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
- audit_log_format(ab, "%s%s", is_first ? "" : ",",
+ audit_log_format(ab, "%s%s%s", is_first ? "" : ",", prefix,
get_blocker(type, access_bit));
is_first = false;
}
if (is_first)
- audit_log_format(ab, "%s", get_blocker(type, -1));
+ audit_log_format(ab, "%s%s", prefix, get_blocker(type, -1));
}
static void log_domain(struct landlock_hierarchy *const hierarchy)
@@ -137,526 +158,32 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
}
-static struct landlock_hierarchy *
-get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
-{
- struct landlock_hierarchy *hierarchy = domain->hierarchy;
- ssize_t i;
-
- if (WARN_ON_ONCE(layer >= domain->num_layers))
- return hierarchy;
-
- for (i = domain->num_layers - 1; i > layer; i--) {
- if (WARN_ON_ONCE(!hierarchy->parent))
- break;
-
- hierarchy = hierarchy->parent;
- }
-
- return hierarchy;
-}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static void test_get_hierarchy(struct kunit *const test)
-{
- struct landlock_hierarchy dom0_hierarchy = {
- .id = 10,
- };
- struct landlock_hierarchy dom1_hierarchy = {
- .parent = &dom0_hierarchy,
- .id = 20,
- };
- struct landlock_hierarchy dom2_hierarchy = {
- .parent = &dom1_hierarchy,
- .id = 30,
- };
- struct landlock_ruleset dom2 = {
- .hierarchy = &dom2_hierarchy,
- .num_layers = 3,
- };
-
- KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
- KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
- KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
- /* KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); */
-}
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-
-/* Get the youngest layer that denied the access_request. */
-static size_t get_denied_layer(const struct landlock_ruleset *const domain,
- access_mask_t *const access_request,
- const struct layer_masks *masks)
-{
- for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
- if (masks->layers[i].access & *access_request) {
- *access_request &= masks->layers[i].access;
- return i;
- }
- }
-
- /* Not found - fall back to default values */
- *access_request = 0;
- return domain->num_layers - 1;
-}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static void test_get_denied_layer(struct kunit *const test)
-{
- const struct landlock_ruleset dom = {
- .num_layers = 5,
- };
- const struct layer_masks masks = {
- .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
- LANDLOCK_ACCESS_FS_READ_DIR,
- .layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
- LANDLOCK_ACCESS_FS_READ_DIR,
- .layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
- };
- access_mask_t access;
-
- access = LANDLOCK_ACCESS_FS_EXECUTE;
- KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &masks));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
-
- access = LANDLOCK_ACCESS_FS_READ_FILE;
- KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
-
- access = LANDLOCK_ACCESS_FS_READ_DIR;
- KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
-
- access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
- KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
- KUNIT_EXPECT_EQ(test, access,
- LANDLOCK_ACCESS_FS_READ_FILE |
- LANDLOCK_ACCESS_FS_READ_DIR);
-
- access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
- KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
-
- access = LANDLOCK_ACCESS_FS_WRITE_FILE;
- KUNIT_EXPECT_EQ(test, 4, get_denied_layer(&dom, &access, &masks));
- KUNIT_EXPECT_EQ(test, access, 0);
-}
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-
-static size_t
-get_layer_from_deny_masks(access_mask_t *const access_request,
- const access_mask_t all_existing_optional_access,
- const deny_masks_t deny_masks,
- optional_access_t quiet_optional_accesses,
- bool *quiet)
-{
- const unsigned long access_opt = all_existing_optional_access;
- const unsigned long access_req = *access_request;
- access_mask_t missing = 0;
- size_t youngest_layer = 0;
- size_t access_index = 0;
- unsigned long access_bit;
- bool should_quiet = false;
-
- /* This will require change with new object types. */
- WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
-
- for_each_set_bit(access_bit, &access_opt,
- BITS_PER_TYPE(access_mask_t)) {
- if (access_req & BIT(access_bit)) {
- const size_t layer =
- (deny_masks >>
- (access_index *
- HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
- (LANDLOCK_MAX_NUM_LAYERS - 1);
- const bool layer_has_quiet =
- !!(quiet_optional_accesses & BIT(access_index));
-
- if (layer > youngest_layer) {
- youngest_layer = layer;
- missing = BIT(access_bit);
- should_quiet = layer_has_quiet;
- } else if (layer == youngest_layer) {
- missing |= BIT(access_bit);
- /*
- * Whether the layer has rules with quiet flag
- * covering the file accessed does not depend on
- * the access, and so the following
- * WARN_ON_ONCE() should not fail.
- */
- WARN_ON_ONCE(should_quiet && !layer_has_quiet);
- should_quiet = layer_has_quiet;
- }
- }
- access_index++;
- }
-
- *access_request = missing;
- *quiet = should_quiet;
- return youngest_layer;
-}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static void test_get_layer_from_deny_masks(struct kunit *const test)
-{
- deny_masks_t deny_mask;
- access_mask_t access;
- optional_access_t quiet_optional_accesses;
- bool quiet;
-
- /* truncate:0 ioctl_dev:2 */
- deny_mask = 0x20;
- quiet_optional_accesses = 0;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 0,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- /* layer denying truncate: quiet, ioctl: not quiet */
- quiet_optional_accesses = 0b01;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 0,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, true);
-
- access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- /* Reverse order - truncate:2 ioctl_dev:0 */
- deny_mask = 0x02;
- quiet_optional_accesses = 0;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 0,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- /* layer denying truncate: quiet, ioctl: not quiet */
- quiet_optional_accesses = 0b01;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, true);
-
- access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 0,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, true);
-
- /* layer denying truncate: not quiet, ioctl: quiet */
- quiet_optional_accesses = 0b10;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 0,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, true);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 2,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- /* truncate:15 ioctl_dev:15 */
- deny_mask = 0xff;
- quiet_optional_accesses = 0;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 15,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 15,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access,
- LANDLOCK_ACCESS_FS_TRUNCATE |
- LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, false);
-
- /* Both quiet (same layer so quietness must be the same) */
- quiet_optional_accesses = 0b11;
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE;
- KUNIT_EXPECT_EQ(test, 15,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
- KUNIT_EXPECT_EQ(test, quiet, true);
-
- access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
- KUNIT_EXPECT_EQ(test, 15,
- get_layer_from_deny_masks(
- &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
- deny_mask, quiet_optional_accesses, &quiet));
- KUNIT_EXPECT_EQ(test, access,
- LANDLOCK_ACCESS_FS_TRUNCATE |
- LANDLOCK_ACCESS_FS_IOCTL_DEV);
- KUNIT_EXPECT_EQ(test, quiet, true);
-}
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-
-static bool is_valid_request(const struct landlock_request *const request)
-{
- if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
- return false;
-
- if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
- return false;
-
- if (request->access) {
- if (WARN_ON_ONCE(!(!!request->layer_masks ^
- !!request->all_existing_optional_access)))
- return false;
- } else {
- if (WARN_ON_ONCE(request->layer_masks ||
- request->all_existing_optional_access))
- return false;
- }
-
- if (request->deny_masks) {
- if (WARN_ON_ONCE(!request->all_existing_optional_access))
- return false;
- static_assert(sizeof(request->all_existing_optional_access) ==
- sizeof(u32));
- if (WARN_ON_ONCE(
- request->quiet_optional_accesses >=
- BIT(hweight32(
- request->all_existing_optional_access))))
- return false;
- }
-
- return true;
-}
-
-static access_mask_t
-pick_access_mask_for_request_type(const enum landlock_request_type type,
- const struct access_masks access_masks)
-{
- switch (type) {
- case LANDLOCK_REQUEST_FS_ACCESS:
- return access_masks.fs;
- case LANDLOCK_REQUEST_NET_ACCESS:
- return access_masks.net;
- default:
- WARN_ONCE(1, "Invalid request type %d passed to %s", type,
- __func__);
- return 0;
- }
-}
-
/**
- * landlock_log_denial - Create audit records related to a denial
+ * landlock_audit_denial - Create an audit record for a denied access request
*
- * @subject: The Landlock subject's credential denying an action.
* @request: Detail of the user space request.
+ * @youngest_denied: The youngest hierarchy node that denied the access.
+ * @missing: The set of denied access rights.
+ * @logged: Whether the denial is selected for logging, as computed by
+ * landlock_log_denial() (domain policy and quiet rules).
+ *
+ * Emits the record when audit is enabled and the denial is selected for
+ * logging.
*/
-void landlock_log_denial(const struct landlock_cred_security *const subject,
- const struct landlock_request *const request)
+void landlock_audit_denial(const struct landlock_request *const request,
+ struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool logged)
{
struct audit_buffer *ab;
- struct landlock_hierarchy *youngest_denied;
- size_t youngest_layer;
- access_mask_t missing;
- bool object_quiet_flag = false, quiet_applicable_to_access = false;
-
- if (WARN_ON_ONCE(!subject || !subject->domain ||
- !subject->domain->hierarchy || !request))
- return;
-
- if (!is_valid_request(request))
- return;
-
- missing = request->access;
- if (missing) {
- /* Gets the nearest domain that denies the request. */
- if (request->layer_masks) {
- youngest_layer = get_denied_layer(subject->domain,
- &missing,
- request->layer_masks);
- object_quiet_flag =
- request->layer_masks->layers[youngest_layer]
- .quiet;
- } else {
- youngest_layer = get_layer_from_deny_masks(
- &missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
- request->deny_masks,
- request->quiet_optional_accesses,
- &object_quiet_flag);
- }
- youngest_denied =
- get_hierarchy(subject->domain, youngest_layer);
- } else {
- youngest_layer = request->layer_plus_one - 1;
- youngest_denied =
- get_hierarchy(subject->domain, youngest_layer);
- }
-
- if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
- return;
-
- /*
- * Consistently keeps track of the number of denied access requests
- * even if audit is currently disabled, or if audit rules currently
- * exclude this record type, or if landlock_restrict_self(2)'s flags
- * quiet logs.
- */
- atomic64_inc(&youngest_denied->num_denials);
if (!audit_enabled)
return;
- /* Checks if the current exec was restricting itself. */
- if (subject->domain_exec & BIT(youngest_layer)) {
- /* Ignores denials for the same execution. */
- if (!youngest_denied->log_same_exec)
- return;
- } else {
- /* Ignores denials after a new execution. */
- if (!youngest_denied->log_new_exec)
- return;
- }
-
/*
- * Checks if the object is marked quiet by the layer that denied the
- * request. If it's a different layer that marked it as quiet, but that
- * layer is not the one that denied the request, we should still audit
- * log the denial.
+ * Skips denials the domain's policy or a quiet rule excludes from
+ * logging (folded into @logged by landlock_log_denial()).
*/
- if (object_quiet_flag) {
- /*
- * We now check if the denied requests are all covered by the
- * layer's quiet access bits.
- */
- const access_mask_t quiet_mask =
- pick_access_mask_for_request_type(
- request->type, youngest_denied->quiet_masks);
-
- quiet_applicable_to_access = (quiet_mask & missing) == missing;
- } else {
- /*
- * Either the object is not quiet, or this is a scope request.
- * We check request->type to distinguish between the two cases.
- */
- const access_mask_t quiet_mask =
- youngest_denied->quiet_masks.scope;
-
- switch (request->type) {
- case LANDLOCK_REQUEST_SCOPE_SIGNAL:
- quiet_applicable_to_access =
- !!(quiet_mask & LANDLOCK_SCOPE_SIGNAL);
- break;
- case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
- quiet_applicable_to_access =
- !!(quiet_mask &
- LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
- break;
- /*
- * Leave LANDLOCK_REQUEST_PTRACE and
- * LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they
- * are never quiet.
- */
- default:
- break;
- }
- }
-
- if (quiet_applicable_to_access)
+ if (!logged)
return;
/* Uses consistent allocation flags wrt common_lsm_audit(). */
@@ -675,23 +202,19 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
}
/**
- * landlock_log_drop_domain - Create an audit record on domain deallocation
+ * landlock_audit_free_domain - Create an audit record on domain deallocation
*
* @hierarchy: The domain's hierarchy being deallocated.
*
* Only domains which previously appeared in the audit logs are logged again.
* This is useful to know when a domain will never show again in the audit log.
*
- * Called in a work queue scheduled by landlock_put_ruleset_deferred() called
- * by hook_cred_free().
+ * Called from landlock_log_free_domain().
*/
-void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+void landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy)
{
struct audit_buffer *ab;
- if (WARN_ON_ONCE(!hierarchy))
- return;
-
if (!audit_enabled)
return;
@@ -712,23 +235,3 @@ void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
hierarchy->id, atomic64_read(&hierarchy->num_denials));
audit_log_end(ab);
}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static struct kunit_case test_cases[] = {
- /* clang-format off */
- KUNIT_CASE(test_get_hierarchy),
- KUNIT_CASE(test_get_denied_layer),
- KUNIT_CASE(test_get_layer_from_deny_masks),
- {}
- /* clang-format on */
-};
-
-static struct kunit_suite test_suite = {
- .name = "landlock_audit",
- .test_cases = test_cases,
-};
-
-kunit_test_suite(test_suite);
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 620f8a24291d..14a514065e08 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -8,66 +8,33 @@
#ifndef _SECURITY_LANDLOCK_AUDIT_H
#define _SECURITY_LANDLOCK_AUDIT_H
-#include <linux/audit.h>
-#include <linux/lsm_audit.h>
+#include <linux/types.h>
#include "access.h"
-#include "cred.h"
-enum landlock_request_type {
- LANDLOCK_REQUEST_PTRACE = 1,
- LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
- LANDLOCK_REQUEST_FS_ACCESS,
- LANDLOCK_REQUEST_NET_ACCESS,
- LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
- LANDLOCK_REQUEST_SCOPE_SIGNAL,
-};
-
-/*
- * We should be careful to only use a variable of this type for
- * landlock_log_denial(). This way, the compiler can remove it entirely if
- * CONFIG_AUDIT is not set.
- */
-struct landlock_request {
- /* Mandatory fields. */
- enum landlock_request_type type;
- struct common_audit_data audit;
-
- /**
- * layer_plus_one: First layer level that denies the request + 1. The
- * extra one is useful to detect uninitialized field.
- */
- size_t layer_plus_one;
-
- /* Required field for configurable access control. */
- access_mask_t access;
-
- /* Required fields for requests with layer masks. */
- const struct layer_masks *layer_masks;
-
- /* Required fields for requests with deny masks. */
- const access_mask_t all_existing_optional_access;
- deny_masks_t deny_masks;
- optional_access_t quiet_optional_accesses;
-};
+struct landlock_hierarchy;
+struct landlock_request;
#ifdef CONFIG_AUDIT
-void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
+void landlock_audit_denial(const struct landlock_request *const request,
+ struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool logged);
-void landlock_log_denial(const struct landlock_cred_security *const subject,
- const struct landlock_request *const request);
+void landlock_audit_free_domain(
+ const struct landlock_hierarchy *const hierarchy);
#else /* CONFIG_AUDIT */
static inline void
-landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+landlock_audit_denial(const struct landlock_request *const request,
+ struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool logged)
{
}
static inline void
-landlock_log_denial(const struct landlock_cred_security *const subject,
- const struct landlock_request *const request)
+landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy)
{
}
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index cc419de75cd6..03449c26247e 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -22,7 +22,7 @@ static void hook_cred_transfer(struct cred *const new,
const struct landlock_cred_security *const old_llcred =
landlock_cred(old);
- landlock_get_ruleset(old_llcred->domain);
+ landlock_get_domain(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
@@ -35,13 +35,13 @@ static int hook_cred_prepare(struct cred *const new,
static void hook_cred_free(struct cred *const cred)
{
- struct landlock_ruleset *const dom = landlock_cred(cred)->domain;
+ struct landlock_domain *const dom = landlock_cred(cred)->domain;
if (dom)
- landlock_put_ruleset_deferred(dom);
+ landlock_put_domain_deferred(dom);
}
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
{
@@ -50,16 +50,16 @@ static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
return 0;
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(cred_prepare, hook_cred_prepare),
LSM_HOOK_INIT(cred_transfer, hook_cred_transfer),
LSM_HOOK_INIT(cred_free, hook_cred_free),
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
};
__init void landlock_add_cred_hooks(void)
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index f287c56b5fd4..a5ff9957949a 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -16,6 +16,7 @@
#include <linux/rcupdate.h>
#include "access.h"
+#include "domain.h"
#include "limits.h"
#include "ruleset.h"
#include "setup.h"
@@ -31,11 +32,11 @@
*/
struct landlock_cred_security {
/**
- * @domain: Immutable ruleset enforced on a task.
+ * @domain: Immutable domain enforced on a task.
*/
- struct landlock_ruleset *domain;
+ struct landlock_domain *domain;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @domain_exec: Bitmask identifying the domain layers that were enforced by
* the current task's executed file (i.e. no new execve(2) since
@@ -49,17 +50,17 @@ struct landlock_cred_security {
* not require a current domain.
*/
u8 log_subdomains_off : 1;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
} __packed;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* Makes sure all layer executions can be stored. */
static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security,
domain_exec)) >=
LANDLOCK_MAX_NUM_LAYERS);
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline struct landlock_cred_security *
landlock_cred(const struct cred *cred)
@@ -70,22 +71,20 @@ landlock_cred(const struct cred *cred)
static inline void landlock_cred_copy(struct landlock_cred_security *dst,
const struct landlock_cred_security *src)
{
- landlock_put_ruleset(dst->domain);
+ landlock_put_domain(dst->domain);
*dst = *src;
- landlock_get_ruleset(src->domain);
+ landlock_get_domain(src->domain);
}
-static inline struct landlock_ruleset *landlock_get_current_domain(void)
+static inline struct landlock_domain *landlock_get_current_domain(void)
{
return landlock_cred(current_cred())->domain;
}
-/*
- * The call needs to come from an RCU read-side critical section.
- */
-static inline const struct landlock_ruleset *
+/* The call needs to come from an RCU read-side critical section. */
+static inline const struct landlock_domain *
landlock_get_task_domain(const struct task_struct *const task)
{
return landlock_cred(__task_cred(task))->domain;
@@ -126,7 +125,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
const union access_masks_all masks_all = {
.masks = masks,
};
- const struct landlock_ruleset *domain;
+ const struct landlock_domain *domain;
ssize_t layer_level;
if (!cred)
@@ -139,7 +138,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
for (layer_level = domain->num_layers - 1; layer_level >= 0;
layer_level--) {
union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
+ .masks = domain->handled_masks[layer_level],
};
if (layer.all & masks_all.all) {
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 9a8355fccd26..93c7104fd6b2 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -5,26 +5,486 @@
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
*/
#include <kunit/test.h>
#include <linux/bitops.h>
#include <linux/bits.h>
+#include <linux/cleanup.h>
#include <linux/cred.h>
+#include <linux/err.h>
#include <linux/file.h>
+#include <linux/lockdep.h>
#include <linux/mm.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
#include <linux/path.h>
#include <linux/pid.h>
+#include <linux/rbtree.h>
+#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/signal.h>
+#include <linux/slab.h>
#include <linux/uidgid.h>
+#include <linux/workqueue.h>
#include "access.h"
#include "common.h"
#include "domain.h"
#include "id.h"
+#include "limits.h"
+#include "ruleset.h"
-#ifdef CONFIG_AUDIT
+static void build_check_domain(void)
+{
+ const struct landlock_domain domain = {
+ .num_layers = ~0,
+ };
+
+ BUILD_BUG_ON(domain.num_layers < LANDLOCK_MAX_NUM_LAYERS);
+}
+
+static struct landlock_domain *create_domain(const u32 num_layers)
+{
+ struct landlock_domain *new_domain;
+
+ build_check_domain();
+ new_domain = kzalloc_flex(*new_domain, handled_masks, num_layers,
+ GFP_KERNEL_ACCOUNT);
+ if (!new_domain)
+ return ERR_PTR(-ENOMEM);
+
+ refcount_set(&new_domain->usage, 1);
+ new_domain->rules.root_inode = RB_ROOT;
+
+#if IS_ENABLED(CONFIG_INET)
+ new_domain->rules.root_net_port = RB_ROOT;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ new_domain->num_layers = num_layers;
+ return new_domain;
+}
+
+static void free_domain(struct landlock_domain *const domain)
+{
+ might_sleep();
+ landlock_free_rules(&domain->rules);
+ landlock_put_hierarchy(domain->hierarchy);
+ kfree(domain);
+}
+
+void landlock_put_domain(struct landlock_domain *const domain)
+{
+ might_sleep();
+ if (domain && refcount_dec_and_test(&domain->usage))
+ free_domain(domain);
+}
+
+static void free_domain_work(struct work_struct *const work)
+{
+ struct landlock_domain *domain;
+
+ domain = container_of(work, struct landlock_domain, work_free);
+ free_domain(domain);
+}
+
+void landlock_put_domain_deferred(struct landlock_domain *const domain)
+{
+ if (domain && refcount_dec_and_test(&domain->usage)) {
+ INIT_WORK(&domain->work_free, free_domain_work);
+ schedule_work(&domain->work_free);
+ }
+}
+
+/* The returned access has the same lifetime as the domain. */
+static const struct landlock_rule *
+find_rule(const struct landlock_domain *const domain,
+ const struct landlock_id id)
+{
+ const struct rb_root *root;
+ const struct rb_node *node;
+
+ root = landlock_get_rule_root((struct landlock_rules *)&domain->rules,
+ id.type);
+ if (IS_ERR(root))
+ return NULL;
+ node = root->rb_node;
+
+ while (node) {
+ struct landlock_rule *this =
+ rb_entry(node, struct landlock_rule, node);
+
+ if (this->key.data == id.key.data)
+ return this;
+ if (this->key.data < id.key.data)
+ node = node->rb_right;
+ else
+ node = node->rb_left;
+ }
+ return NULL;
+}
+
+/**
+ * landlock_unmask_layers - Remove the access rights in @masks which are
+ * granted by a matching rule
+ *
+ * Looks up the rule matching @id in @domain, then updates the set of
+ * (per-layer) unfulfilled access rights @masks so that all the access rights
+ * granted by that rule are removed (because they are now fulfilled).
+ *
+ * @domain: The Landlock domain to search for a matching rule.
+ * @id: Identifier for the rule target (e.g. inode, port).
+ * @masks: A matrix of unfulfilled access rights for each layer.
+ * @matched_rule: Optional output for the matched rule (for tracing); set to
+ * the matching rule when non-NULL, unchanged otherwise.
+ *
+ * Return: True if the request is allowed (i.e. the access rights granted all
+ * remaining unfulfilled access rights and masks has no leftover set bits).
+ */
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_masks *masks,
+ const struct landlock_rule **matched_rule)
+{
+ const struct landlock_rule *rule;
+
+ if (!masks)
+ return true;
+
+ rule = find_rule(domain, id);
+ if (!rule)
+ return false;
+
+ if (matched_rule)
+ *matched_rule = rule;
+
+ /*
+ * An access is granted if, for each policy layer, at least one rule
+ * encountered on the pathwalk grants the requested access, regardless
+ * of its position in the layer stack. We must then check the remaining
+ * layers for each inode, from the first added layer to the last one.
+ * When there are multiple requested accesses, for each policy layer,
+ * the full set of requested accesses may not be granted by only one
+ * rule, but by the union (binary OR) of multiple rules. For example,
+ * /a/b <execute> + /a <read> grants /a/b <execute + read>.
+ *
+ * This function is called once per matching rule during the pathwalk,
+ * progressively clearing bits in @masks. The overall access decision
+ * is per-layer: access is granted iff masks->layers[l].access == 0 for
+ * all layers l. When two independent mechanisms can each grant access
+ * within a layer (e.g. a path rule OR a scope exception), the
+ * composition must evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not
+ * (FOR-ALL l A(l)) OR (FOR-ALL l B(l)), to prevent bypass when
+ * different layers grant via different mechanisms.
+ */
+ for (size_t i = 0; i < rule->num_layers; i++) {
+ const struct landlock_layer *const layer = &rule->layers[i];
+
+ /* Clear the bits where the layer in the rule grants access. */
+ masks->layers[layer->level - 1].access &= ~layer->access;
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ /* Collect rule flags for each layer. */
+ if (layer->flags.quiet)
+ masks->layers[layer->level - 1].quiet = true;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
+ if (masks->layers[i].access)
+ return false;
+ }
+ return true;
+}
+
+typedef access_mask_t
+get_access_mask_t(const struct landlock_domain *const domain,
+ const u16 layer_level);
+
+/**
+ * landlock_init_layer_masks - Initialize layer masks from an access request
+ *
+ * Populates @masks such that for each access right in @access_request, the bits
+ * for all the layers are set where this access right is handled. Rule flags
+ * are also zeroed.
+ *
+ * @domain: The domain that defines the current restrictions.
+ * @access_request: The requested access rights to check.
+ * @masks: Layer access masks to populate.
+ * @key_type: The key type to switch between access masks of different types.
+ *
+ * Return: An access mask where each access right bit is set which is handled in
+ * any of the active layers in @domain.
+ */
+access_mask_t
+landlock_init_layer_masks(const struct landlock_domain *const domain,
+ const access_mask_t access_request,
+ struct layer_masks *const masks,
+ const enum landlock_key_type key_type)
+{
+ access_mask_t handled_accesses = 0;
+ get_access_mask_t *get_access_mask;
+
+ switch (key_type) {
+ case LANDLOCK_KEY_INODE:
+ get_access_mask = landlock_get_fs_access_mask;
+ break;
+
+#if IS_ENABLED(CONFIG_INET)
+ case LANDLOCK_KEY_NET_PORT:
+ get_access_mask = landlock_get_net_access_mask;
+ break;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ default:
+ WARN_ON_ONCE(1);
+ return 0;
+ }
+
+ /* An empty access request can happen because of O_WRONLY | O_RDWR. */
+ if (!access_request)
+ return 0;
+
+ for (size_t i = 0; i < domain->num_layers; i++) {
+ const access_mask_t handled = get_access_mask(domain, i);
+
+ masks->layers[i].access = access_request & handled;
+ handled_accesses |= masks->layers[i].access;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ masks->layers[i].quiet = false;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+ }
+ for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
+ i++) {
+ masks->layers[i].access = 0;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ masks->layers[i].quiet = false;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+ }
+
+ return handled_accesses;
+}
+
+static int merge_tree(struct landlock_domain *const dst,
+ struct landlock_ruleset *const src,
+ const enum landlock_key_type key_type)
+{
+ struct landlock_rule *walker_rule, *next_rule;
+ struct rb_root *src_root;
+ int err = 0;
+
+ might_sleep();
+ lockdep_assert_held(&src->lock);
+
+ src_root = landlock_get_rule_root(&src->rules, key_type);
+ if (IS_ERR(src_root))
+ return PTR_ERR(src_root);
+
+ /* Merges the @src tree. */
+ rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
+ node) {
+ struct landlock_layer layers[] = { {
+ .level = dst->num_layers,
+ } };
+ const struct landlock_id id = {
+ .key = walker_rule->key,
+ .type = key_type,
+ };
+
+ if (WARN_ON_ONCE(walker_rule->num_layers != 1))
+ return -EINVAL;
+
+ if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
+ return -EINVAL;
+
+ layers[0].access = walker_rule->layers[0].access;
+ layers[0].flags = walker_rule->layers[0].flags;
+
+ err = landlock_store_rule(&dst->rules, id, &layers,
+ ARRAY_SIZE(layers));
+ if (err)
+ return err;
+ }
+ return err;
+}
+
+static int merge_ruleset(struct landlock_domain *const dst,
+ struct landlock_ruleset *const src)
+{
+ int err = 0;
+
+ might_sleep();
+ /* Should already be checked by landlock_merge_ruleset() */
+ if (WARN_ON_ONCE(!src))
+ return 0;
+ /* Only merge into a domain. */
+ if (WARN_ON_ONCE(!dst || !dst->hierarchy))
+ return -EINVAL;
+
+ lockdep_assert_held(&src->lock);
+
+ /* Stacks the new layer. */
+ if (WARN_ON_ONCE(dst->num_layers < 1))
+ return -EINVAL;
+
+ dst->handled_masks[dst->num_layers - 1] =
+ landlock_upgrade_handled_access_masks(src->handled_masks);
+
+ /* Merges the @src inode tree. */
+ err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
+ if (err)
+ return err;
+
+#if IS_ENABLED(CONFIG_INET)
+ /* Merges the @src network port tree. */
+ err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
+ if (err)
+ return err;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ return 0;
+}
+
+static int inherit_tree(struct landlock_domain *const parent,
+ struct landlock_domain *const child,
+ const enum landlock_key_type key_type)
+{
+ struct landlock_rule *walker_rule, *next_rule;
+ struct rb_root *parent_root;
+ int err = 0;
+
+ might_sleep();
+
+ parent_root = landlock_get_rule_root(&parent->rules, key_type);
+ if (IS_ERR(parent_root))
+ return PTR_ERR(parent_root);
+
+ /* Copies the @parent inode or network tree. */
+ rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
+ parent_root, node) {
+ const struct landlock_id id = {
+ .key = walker_rule->key,
+ .type = key_type,
+ };
+
+ err = landlock_store_rule(&child->rules, id,
+ &walker_rule->layers,
+ walker_rule->num_layers);
+ if (err)
+ return err;
+ }
+ return err;
+}
+
+static int inherit_ruleset(struct landlock_domain *const parent,
+ struct landlock_domain *const child)
+{
+ int err = 0;
+
+ might_sleep();
+ if (!parent)
+ return 0;
+
+ /* Copies the @parent inode tree. */
+ err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
+ if (err)
+ return err;
+
+#if IS_ENABLED(CONFIG_INET)
+ /* Copies the @parent network port tree. */
+ err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
+ if (err)
+ return err;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ if (WARN_ON_ONCE(child->num_layers <= parent->num_layers))
+ return -EINVAL;
+
+ /*
+ * Copies the parent layer stack and leaves a space for the new layer.
+ */
+ memcpy(child->handled_masks, parent->handled_masks,
+ flex_array_size(parent, handled_masks, parent->num_layers));
+
+ if (WARN_ON_ONCE(!parent->hierarchy))
+ return -EINVAL;
+
+ landlock_get_hierarchy(parent->hierarchy);
+ child->hierarchy->parent = parent->hierarchy;
+
+ return 0;
+}
+
+/**
+ * landlock_merge_ruleset - Merge a ruleset with a domain
+ *
+ * @parent: Parent domain.
+ * @ruleset: New ruleset to be merged.
+ *
+ * The current task is requesting to be restricted. The subjective credentials
+ * must not be in an overridden state. cf. landlock_init_hierarchy_log().
+ *
+ * The caller must hold @ruleset->lock.
+ *
+ * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
+ * failure. If @parent is NULL, the new domain duplicates @ruleset.
+ */
+struct landlock_domain *
+landlock_merge_ruleset(struct landlock_domain *const parent,
+ struct landlock_ruleset *const ruleset)
+{
+ struct landlock_domain *new_dom __free(landlock_put_domain) = NULL;
+ u32 num_layers;
+ int err;
+
+ might_sleep();
+ lockdep_assert_held(&ruleset->lock);
+ if (WARN_ON_ONCE(!ruleset))
+ return ERR_PTR(-EINVAL);
+
+ if (parent) {
+ if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
+ return ERR_PTR(-E2BIG);
+ num_layers = parent->num_layers + 1;
+ } else {
+ num_layers = 1;
+ }
+
+ /* Creates a new domain... */
+ new_dom = create_domain(num_layers);
+ if (IS_ERR(new_dom))
+ return new_dom;
+
+ new_dom->hierarchy =
+ kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
+ if (!new_dom->hierarchy)
+ return ERR_PTR(-ENOMEM);
+
+ refcount_set(&new_dom->hierarchy->usage, 1);
+
+ /* ...as a child of @parent... */
+ err = inherit_ruleset(parent, new_dom);
+ if (err)
+ return ERR_PTR(err);
+
+ /* ...and including @ruleset. */
+ err = merge_ruleset(new_dom, ruleset);
+ if (err)
+ return ERR_PTR(err);
+
+ err = landlock_init_hierarchy_log(new_dom->hierarchy);
+ if (err)
+ return ERR_PTR(err);
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+ return no_free_ptr(new_dom);
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* get_current_exe - Get the current's executable path, if any
@@ -128,7 +588,13 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
hierarchy->details = details;
hierarchy->id = landlock_get_id_range(1);
- hierarchy->log_status = LANDLOCK_LOG_PENDING;
+ /*
+ * The hierarchy is born unobservable: landlock_restrict_self() moves it
+ * out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation
+ * event, so the matching free_domain event fires for it and not for a
+ * hierarchy whose creation was never observed.
+ */
+ hierarchy->log_status = LANDLOCK_LOG_UNCOMMITTED;
hierarchy->log_same_exec = true;
hierarchy->log_new_exec = false;
atomic64_set(&hierarchy->num_denials, 0);
@@ -306,4 +772,4 @@ kunit_test_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 2a1660e3dea7..5ce2f91488d5 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -5,11 +5,13 @@
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
#define _SECURITY_LANDLOCK_DOMAIN_H
+#include <linux/cleanup.h>
#include <linux/limits.h>
#include <linux/mm.h>
#include <linux/path.h>
@@ -17,12 +19,28 @@
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/slab.h>
+#include <linux/workqueue.h>
#include "access.h"
-#include "audit.h"
+#include "log.h"
+#include "ruleset.h"
enum landlock_log_status {
- LANDLOCK_LOG_PENDING = 0,
+ /*
+ * Hierarchy whose creation event has not been emitted, so it is not yet
+ * observable from user space. A hierarchy is born in this state (the
+ * zero value, so a partially initialized hierarchy defaults to "not
+ * observable") and leaves it when landlock_restrict_self() emits its
+ * creation event, right after the merge and before the thread-sync
+ * wait. No trace free_domain event (and no audit deallocation record)
+ * fires while a hierarchy is in this state, so a hierarchy that never
+ * became observable (e.g. its initialization failed) is freed silently.
+ * A domain aborted by a thread-sync failure already emitted its
+ * creation event, so it is no longer UNCOMMITTED and does fire
+ * free_domain.
+ */
+ LANDLOCK_LOG_UNCOMMITTED = 0,
+ LANDLOCK_LOG_PENDING,
LANDLOCK_LOG_RECORDED,
LANDLOCK_LOG_DISABLED,
};
@@ -81,7 +99,7 @@ struct landlock_hierarchy {
*/
refcount_t usage;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @log_status: Whether this domain should be logged or not. Because
* concurrent log entries may be created at the same time, it is still
@@ -116,10 +134,10 @@ struct landlock_hierarchy {
* logged) if the related object is marked as quiet.
*/
struct access_masks quiet_masks;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
};
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
deny_masks_t
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
@@ -142,7 +160,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
kfree(hierarchy->details);
}
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline int
landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
@@ -155,7 +173,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
{
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void
landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy)
@@ -169,11 +187,140 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
const struct landlock_hierarchy *const freeme = hierarchy;
- landlock_log_drop_domain(hierarchy);
+ landlock_log_free_domain(hierarchy);
landlock_free_hierarchy_details(hierarchy);
hierarchy = hierarchy->parent;
kfree(freeme);
}
}
+/**
+ * struct landlock_domain - Immutable Landlock domain
+ *
+ * A domain is created from a ruleset by landlock_merge_ruleset() and enforced
+ * on a task. Once created, its rules and access masks are immutable. Unlike
+ * &struct landlock_ruleset, a domain has no lock field.
+ */
+struct landlock_domain {
+ /**
+ * @rules: Red-black tree storage for rules.
+ */
+ struct landlock_rules rules;
+ /**
+ * @hierarchy: Enables hierarchy identification even when a parent
+ * domain vanishes. This is needed for the ptrace and scope
+ * restrictions.
+ */
+ struct landlock_hierarchy *hierarchy;
+ union {
+ /**
+ * @work_free: Enables to free a domain within a lockless
+ * section. This is only used by landlock_put_domain_deferred()
+ * when @usage reaches zero. The fields @usage, @num_layers and
+ * @handled_masks are then unused.
+ */
+ struct work_struct work_free;
+ struct {
+ /**
+ * @usage: Number of credentials referencing this
+ * domain.
+ */
+ refcount_t usage;
+ /**
+ * @num_layers: Number of layers that are used in this
+ * domain. This enables to check that all the layers
+ * allow an access request.
+ */
+ u32 num_layers;
+ /**
+ * @handled_masks: Contains the subset of filesystem and
+ * network actions that are restricted by a domain. A
+ * domain saves all layers of merged rulesets in a stack
+ * (FAM), starting from the first layer to the last one.
+ * These layers are used when merging rulesets, for user
+ * space backward compatibility (i.e. future-proof), and
+ * to properly handle merged rulesets without
+ * overlapping access rights. These layers are set once
+ * and never changed for the lifetime of the domain.
+ */
+ struct access_masks handled_masks[];
+ };
+ };
+};
+
+static inline access_mask_t
+landlock_get_fs_access_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ /* Handles all initially denied by default access rights. */
+ return domain->handled_masks[layer_level].fs |
+ _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
+}
+
+static inline access_mask_t
+landlock_get_net_access_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ return domain->handled_masks[layer_level].net;
+}
+
+static inline access_mask_t
+landlock_get_scope_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ return domain->handled_masks[layer_level].scope;
+}
+
+/**
+ * landlock_union_access_masks - Return all access rights handled in the
+ * domain
+ *
+ * @domain: Landlock domain
+ *
+ * Return: An access_masks result of the OR of all the domain's access masks.
+ */
+static inline struct access_masks
+landlock_union_access_masks(const struct landlock_domain *const domain)
+{
+ union access_masks_all matches = {};
+ size_t layer_level;
+
+ for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
+ union access_masks_all layer = {
+ .masks = domain->handled_masks[layer_level],
+ };
+
+ matches.all |= layer.all;
+ }
+
+ return matches.masks;
+}
+
+void landlock_put_domain(struct landlock_domain *const domain);
+void landlock_put_domain_deferred(struct landlock_domain *const domain);
+
+DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
+ if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+
+struct landlock_domain *
+landlock_merge_ruleset(struct landlock_domain *const parent,
+ struct landlock_ruleset *const ruleset);
+
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_masks *masks,
+ const struct landlock_rule **matched_rule);
+
+access_mask_t
+landlock_init_layer_masks(const struct landlock_domain *const domain,
+ const access_mask_t access_request,
+ struct layer_masks *masks,
+ const enum landlock_key_type key_type);
+
+static inline void landlock_get_domain(struct landlock_domain *const domain)
+{
+ if (domain)
+ refcount_inc(&domain->usage);
+}
+
#endif /* _SECURITY_LANDLOCK_DOMAIN_H */
diff --git a/security/landlock/errata/abi-1.h b/security/landlock/errata/abi-1.h
index 3f099555f059..315ea7e0fe50 100644
--- a/security/landlock/errata/abi-1.h
+++ b/security/landlock/errata/abi-1.h
@@ -22,3 +22,26 @@
* from their original mount points.
*/
LANDLOCK_ERRATUM(3)
+
+/**
+ * DOC: erratum_4
+ *
+ * Erratum 4: Creation of whiteout objects
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * This fix changes the access rights required for the creation of whiteout
+ * objects through :manpage:`mknod(2)`, :manpage:`renameat2(2)`, or
+ * :manpage:`link(2)`. Creating whiteout objects is now guarded by
+ * ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of ``LANDLOCK_ACCESS_FS_MAKE_CHAR``.
+ *
+ * Whiteout objects are used in OverlayFS to mark the absence of a file in an
+ * upper file system. Despite being created with ``S_IFCHR``, whiteout objects
+ * do not count as character devices.
+ *
+ * Impact:
+ *
+ * Sandboxed programs that create OverlayFS whiteouts (such as fuse-overlayfs)
+ * now require ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of
+ * ``LANDLOCK_ACCESS_FS_MAKE_CHAR``.
+ */
+LANDLOCK_ERRATUM(4)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index f7e5e4ef9eac..30aa6ce13590 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -20,6 +20,7 @@
#include <linux/falloc.h>
#include <linux/fs.h>
#include <linux/init.h>
+#include <linux/kdev_t.h>
#include <linux/kernel.h>
#include <linux/limits.h>
#include <linux/list.h>
@@ -42,16 +43,18 @@
#include <uapi/linux/landlock.h>
#include "access.h"
-#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "limits.h"
+#include "log.h"
#include "object.h"
#include "ruleset.h"
#include "setup.h"
+#include <trace/events/landlock.h>
+
/* Underlying object management */
static void release_inode(struct landlock_object *const object)
@@ -336,18 +339,34 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
if (!d_is_dir(path->dentry) &&
!access_mask_subset(access_rights, ACCESS_FILE))
return -EINVAL;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return -EINVAL;
/* Transforms relative access rights to absolute ones. */
access_rights |= LANDLOCK_MASK_ACCESS_FS &
- ~landlock_get_fs_access_mask(ruleset, 0);
+ ~(ruleset->handled_masks.fs |
+ _LANDLOCK_ACCESS_FS_INITIALLY_DENIED);
id.key.object = get_inode_object(d_backing_inode(path->dentry));
if (IS_ERR(id.key.object))
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
+
+ /*
+ * Emit after the rule insertion succeeds, so every event corresponds to
+ * a rule that is actually in the ruleset. The ruleset lock is still
+ * held for BTF consistency (enforced by lockdep_assert_held in
+ * TP_fast_assign).
+ */
+ if (!err && trace_landlock_add_rule_fs_enabled()) {
+ char *buffer __free(__putname) = __getname();
+ const char *pathname =
+ buffer ? resolve_path_for_trace(path, buffer) :
+ "<no_mem>";
+
+ trace_landlock_add_rule_fs(ruleset, access_rights, path,
+ pathname);
+ }
mutex_unlock(&ruleset->lock);
+
/*
* No need to check for an error because landlock_insert_rule()
* increments the refcount for the new object if needed.
@@ -358,31 +377,55 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
/* Access-control management */
-/*
- * The lifetime of the returned rule is tied to @domain.
+/**
+ * get_inode_id - Look up the Landlock object for a dentry
+ * @dentry: The dentry to look up.
+ * @id: Filled with the inode's Landlock object pointer on success.
*
- * Returns NULL if no rule is found or if @dentry is negative.
+ * Extracts the Landlock object pointer from @dentry's inode security blob and
+ * stores it in @id for use as a rule-tree lookup key.
+ *
+ * When this returns false (negative dentry or no Landlock object), no rule can
+ * match this inode, so landlock_unmask_layers() need not be called. Callers
+ * that gate landlock_unmask_layers() on this function must handle the NULL
+ * masks case independently, since the !masks-returns-true early-return in
+ * landlock_unmask_layers() will not be reached. See the allowed_parent2
+ * initialization in is_access_to_paths_allowed().
+ *
+ * Return: True if a Landlock object exists for @dentry, false otherwise.
*/
-static const struct landlock_rule *
-find_rule(const struct landlock_ruleset *const domain,
- const struct dentry *const dentry)
+static bool get_inode_id(const struct dentry *const dentry,
+ struct landlock_id *id)
{
- const struct landlock_rule *rule;
- const struct inode *inode;
- struct landlock_id id = {
- .type = LANDLOCK_KEY_INODE,
- };
-
/* Ignores nonexistent leafs. */
if (d_is_negative(dentry))
- return NULL;
+ return false;
- inode = d_backing_inode(dentry);
- rcu_read_lock();
- id.key.object = rcu_dereference(landlock_inode(inode)->object);
- rule = landlock_find_rule(domain, id);
- rcu_read_unlock();
- return rule;
+ /*
+ * rcu_access_pointer() is sufficient: the pointer is used only as a
+ * numeric comparison key for rule lookup, not dereferenced. The object
+ * cannot be freed while the domain exists because the domain's rule
+ * tree holds its own reference to it.
+ */
+ id->key.object = rcu_access_pointer(
+ landlock_inode(d_backing_inode(dentry))->object);
+ return !!id->key.object;
+}
+
+static bool unmask_layers_fs(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ const access_mask_t access_request,
+ struct layer_masks *masks,
+ const struct dentry *const dentry)
+{
+ const struct landlock_rule *rule = NULL;
+ bool ret;
+
+ ret = landlock_unmask_layers(domain, id, masks, &rule);
+ if (rule)
+ trace_landlock_check_rule_fs(domain, rule, access_request,
+ dentry);
+ return ret;
}
/*
@@ -749,7 +792,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
* Return: True if the access request is granted, false otherwise.
*/
static bool
-is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
+is_access_to_paths_allowed(const struct landlock_domain *const domain,
const struct path *const path,
const access_mask_t access_request_parent1,
struct layer_masks *layer_masks_parent1,
@@ -763,6 +806,9 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
child1_is_directory = true, child2_is_directory = true;
struct path walker_path;
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
access_mask_t access_masked_parent1, access_masked_parent2;
struct layer_masks _layer_masks_child1, _layer_masks_child2;
struct layer_masks *layer_masks_child1 = NULL,
@@ -802,28 +848,46 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
/* For a simple request, only check for requested accesses. */
access_masked_parent1 = access_request_parent1;
access_masked_parent2 = access_request_parent2;
+ /*
+ * Simple requests have no parent2 to check, so parent2 is
+ * trivially allowed. This must be set explicitly because the
+ * get_inode_id() gate in the pathwalk loop may prevent
+ * landlock_unmask_layers() from being called (which would
+ * otherwise return true for NULL masks as a side effect).
+ */
+ allowed_parent2 = true;
is_dom_check = false;
}
if (unlikely(dentry_child1)) {
- /*
- * Get the layer masks for the child dentries for use by domain
- * check later.
- */
- if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- &_layer_masks_child1,
- LANDLOCK_KEY_INODE))
- landlock_unmask_layers(find_rule(domain, dentry_child1),
- &_layer_masks_child1);
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ access_mask_t handled;
+
+ handled = landlock_init_layer_masks(domain,
+ LANDLOCK_MASK_ACCESS_FS,
+ &_layer_masks_child1,
+ LANDLOCK_KEY_INODE);
+ if (handled && get_inode_id(dentry_child1, &id))
+ unmask_layers_fs(domain, id, handled,
+ &_layer_masks_child1, dentry_child1);
layer_masks_child1 = &_layer_masks_child1;
child1_is_directory = d_is_dir(dentry_child1);
}
if (unlikely(dentry_child2)) {
- if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- &_layer_masks_child2,
- LANDLOCK_KEY_INODE))
- landlock_unmask_layers(find_rule(domain, dentry_child2),
- &_layer_masks_child2);
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ access_mask_t handled;
+
+ handled = landlock_init_layer_masks(domain,
+ LANDLOCK_MASK_ACCESS_FS,
+ &_layer_masks_child2,
+ LANDLOCK_KEY_INODE);
+ if (handled && get_inode_id(dentry_child2, &id))
+ unmask_layers_fs(domain, id, handled,
+ &_layer_masks_child2, dentry_child2);
layer_masks_child2 = &_layer_masks_child2;
child2_is_directory = d_is_dir(dentry_child2);
}
@@ -835,8 +899,6 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
* restriction.
*/
while (true) {
- const struct landlock_rule *rule;
-
/*
* If at least all accesses allowed on the destination are
* already allowed on the source, respectively if there is at
@@ -877,13 +939,20 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
break;
}
- rule = find_rule(domain, walker_path.dentry);
- allowed_parent1 =
- allowed_parent1 ||
- landlock_unmask_layers(rule, layer_masks_parent1);
- allowed_parent2 =
- allowed_parent2 ||
- landlock_unmask_layers(rule, layer_masks_parent2);
+ if (get_inode_id(walker_path.dentry, &id)) {
+ allowed_parent1 =
+ allowed_parent1 ||
+ unmask_layers_fs(domain, id,
+ access_masked_parent1,
+ layer_masks_parent1,
+ walker_path.dentry);
+ allowed_parent2 =
+ allowed_parent2 ||
+ unmask_layers_fs(domain, id,
+ access_masked_parent2,
+ layer_masks_parent2,
+ walker_path.dentry);
+ }
/* Stops when a rule from each layer grants access. */
if (allowed_parent1 && allowed_parent2)
@@ -933,10 +1002,11 @@ jump_up:
path_put(&walker_path);
/*
- * Check CONFIG_AUDIT to enable elision of log_request_parent* and
- * associated caller's stack variables thanks to dead code elimination.
+ * Check CONFIG_SECURITY_LANDLOCK_LOG to enable elision of
+ * log_request_parent* and associated caller's stack variables thanks to
+ * dead code elimination.
*/
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
if (!allowed_parent1 && log_request_parent1) {
log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS;
log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH;
@@ -952,7 +1022,7 @@ jump_up:
log_request_parent2->access = access_masked_parent2;
log_request_parent2->layer_masks = layer_masks_parent2;
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return allowed_parent1 && allowed_parent2;
}
@@ -983,7 +1053,8 @@ static int current_check_access_path(const struct path *const path,
return -EACCES;
}
-static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
+static __attribute_const__ access_mask_t get_mode_access(const umode_t mode,
+ const dev_t dev)
{
switch (mode & S_IFMT) {
case S_IFLNK:
@@ -991,6 +1062,9 @@ static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
case S_IFDIR:
return LANDLOCK_ACCESS_FS_MAKE_DIR;
case S_IFCHR:
+ /* Whiteout objects are guarded with MAKE_REG. */
+ if (dev == WHITEOUT_DEV)
+ return LANDLOCK_ACCESS_FS_MAKE_REG;
return LANDLOCK_ACCESS_FS_MAKE_CHAR;
case S_IFBLK:
return LANDLOCK_ACCESS_FS_MAKE_BLOCK;
@@ -1007,6 +1081,13 @@ static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
}
}
+static access_mask_t get_dentry_access(const struct dentry *const dentry)
+{
+ const struct inode *const inode = d_backing_inode(dentry);
+
+ return get_mode_access(inode->i_mode, inode->i_rdev);
+}
+
static access_mask_t maybe_remove(const struct dentry *const dentry)
{
if (d_is_negative(dentry))
@@ -1039,29 +1120,36 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
* Return: True if all the domain access rights are allowed for @dir, false if
* the walk reached @mnt_root.
*/
-static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
+static bool collect_domain_accesses(const struct landlock_domain *const domain,
const struct dentry *const mnt_root,
struct dentry *dir,
struct layer_masks *layer_masks_dom)
{
bool ret = false;
+ access_mask_t access_masked_dom;
if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
return true;
if (is_nouser_or_private(dir))
return true;
- if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- layer_masks_dom, LANDLOCK_KEY_INODE))
+ access_masked_dom =
+ landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
+ layer_masks_dom, LANDLOCK_KEY_INODE);
+ if (!access_masked_dom)
return true;
dget(dir);
while (true) {
struct dentry *parent_dentry;
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
/* Gets all layers allowing all domain accesses. */
- if (landlock_unmask_layers(find_rule(domain, dir),
- layer_masks_dom)) {
+ if (get_inode_id(dir, &id) &&
+ unmask_layers_fs(domain, id, access_masked_dom,
+ layer_masks_dom, dir)) {
/*
* Stops when all handled accesses are allowed by at
* least one rule in each layer.
@@ -1093,6 +1181,7 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
* @new_dentry: Destination file or directory.
* @removable: Sets to true if it is a rename operation.
* @exchange: Sets to true if it is a rename operation with RENAME_EXCHANGE.
+ * @whiteout: Sets to true if it is a rename operation with RENAME_WHITEOUT.
*
* Because of its unprivileged constraints, Landlock relies on file hierarchies
* (and not only inodes) to tie access rights to files. Being able to link or
@@ -1140,7 +1229,8 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
static int current_check_refer_path(struct dentry *const old_dentry,
const struct path *const new_dir,
struct dentry *const new_dentry,
- const bool removable, const bool exchange)
+ const bool removable, const bool exchange,
+ const bool whiteout)
{
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs, NULL);
@@ -1159,18 +1249,25 @@ static int current_check_refer_path(struct dentry *const old_dentry,
if (exchange) {
if (unlikely(d_is_negative(new_dentry)))
return -ENOENT;
- access_request_parent1 =
- get_mode_access(d_backing_inode(new_dentry)->i_mode);
+ access_request_parent1 = get_dentry_access(new_dentry);
} else {
access_request_parent1 = 0;
}
- access_request_parent2 =
- get_mode_access(d_backing_inode(old_dentry)->i_mode);
+ access_request_parent2 = get_dentry_access(old_dentry);
if (removable) {
access_request_parent1 |= maybe_remove(old_dentry);
access_request_parent2 |= maybe_remove(new_dentry);
}
+ /*
+ * In case of renameat2(2) with RENAME_WHITEOUT, a whiteout object is
+ * created in the source location, so we require an additional access
+ * right there.
+ */
+ if (whiteout)
+ access_request_parent1 |=
+ get_mode_access(S_IFCHR | WHITEOUT_MODE, WHITEOUT_DEV);
+
/* The mount points are the same for old and new paths, cf. EXDEV. */
if (old_dentry->d_parent == new_dir->dentry) {
/*
@@ -1520,7 +1617,7 @@ static int hook_path_link(struct dentry *const old_dentry,
struct dentry *const new_dentry)
{
return current_check_refer_path(old_dentry, new_dir, new_dentry, false,
- false);
+ false, false);
}
static int hook_path_rename(const struct path *const old_dir,
@@ -1531,7 +1628,8 @@ static int hook_path_rename(const struct path *const old_dir,
{
/* old_dir refers to old_dentry->d_parent and new_dir->mnt */
return current_check_refer_path(old_dentry, new_dir, new_dentry, true,
- !!(flags & RENAME_EXCHANGE));
+ !!(flags & RENAME_EXCHANGE),
+ !!(flags & RENAME_WHITEOUT));
}
static int hook_path_mkdir(const struct path *const dir,
@@ -1544,7 +1642,8 @@ static int hook_path_mknod(const struct path *const dir,
struct dentry *const dentry, const umode_t mode,
const unsigned int dev)
{
- return current_check_access_path(dir, get_mode_access(mode));
+ return current_check_access_path(
+ dir, get_mode_access(mode, new_decode_dev(dev)));
}
static int hook_path_symlink(const struct path *const dir,
@@ -1589,8 +1688,8 @@ static int hook_path_truncate(const struct path *const path)
* @masks: Layer access masks to unmask
* @access: Access bits that control scoping
*/
-static void unmask_scoped_access(const struct landlock_ruleset *const client,
- const struct landlock_ruleset *const server,
+static void unmask_scoped_access(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
struct layer_masks *const masks,
const access_mask_t access)
{
@@ -1644,7 +1743,7 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client,
static int hook_unix_find(const struct path *const path, struct sock *other,
int flags)
{
- const struct landlock_ruleset *dom_other;
+ const struct landlock_domain *dom_other;
const struct landlock_cred_security *subject;
struct layer_masks layer_masks;
struct landlock_request request = {};
@@ -1802,14 +1901,14 @@ static int hook_file_open(struct file *const file)
* file access rights in the opened struct file.
*/
landlock_file(file)->allowed_access = allowed_access;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
landlock_file(file)->deny_masks = landlock_get_deny_masks(
_LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks);
landlock_file(file)->quiet_optional_accesses =
landlock_get_quiet_optional_accesses(
_LANDLOCK_ACCESS_FS_OPTIONAL,
landlock_file(file)->deny_masks, &layer_masks);
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
if (access_mask_subset(open_access_request, allowed_access))
return 0;
@@ -1843,10 +1942,10 @@ static int hook_file_truncate(struct file *const file)
},
.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
.access = LANDLOCK_ACCESS_FS_TRUNCATE,
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.deny_masks = landlock_file(file)->deny_masks,
.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
});
return -EACCES;
}
@@ -1883,10 +1982,10 @@ static int hook_file_ioctl_common(const struct file *const file,
},
.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
.access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.deny_masks = landlock_file(file)->deny_masks,
.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
});
return -EACCES;
}
@@ -1939,7 +2038,7 @@ static bool control_current_fowner(struct fown_struct *const fown)
static void hook_file_set_fowner(struct file *file)
{
- struct landlock_ruleset *prev_dom;
+ struct landlock_domain *prev_dom;
struct landlock_cred_security fown_subject = {};
struct pid *prev_tg, *fown_tg = NULL;
size_t fown_layer = 0;
@@ -1952,7 +2051,7 @@ static void hook_file_set_fowner(struct file *file)
landlock_get_applicable_subject(
current_cred(), signal_scope, &fown_layer);
if (new_subject) {
- landlock_get_ruleset(new_subject->domain);
+ landlock_get_domain(new_subject->domain);
fown_subject = *new_subject;
fown_tg = get_pid(task_tgid(current));
}
@@ -1962,19 +2061,19 @@ static void hook_file_set_fowner(struct file *file)
prev_tg = landlock_file(file)->fown_tg;
landlock_file(file)->fown_subject = fown_subject;
landlock_file(file)->fown_tg = fown_tg;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
landlock_file(file)->fown_layer = fown_layer;
-#endif /* CONFIG_AUDIT*/
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/* May be called in an RCU read-side critical section. */
- landlock_put_ruleset_deferred(prev_dom);
+ landlock_put_domain_deferred(prev_dom);
put_pid(prev_tg);
}
static void hook_file_free_security(struct file *file)
{
put_pid(landlock_file(file)->fown_tg);
- landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain);
+ landlock_put_domain_deferred(landlock_file(file)->fown_subject.domain);
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index b4421d9df68f..50b3ddb8ac1c 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -11,6 +11,7 @@
#define _SECURITY_LANDLOCK_FS_H
#include <linux/build_bug.h>
+#include <linux/cleanup.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
@@ -20,6 +21,8 @@
#include "ruleset.h"
#include "setup.h"
+DEFINE_FREE(__putname, char *, if (_T) __putname(_T))
+
/**
* struct landlock_inode_security - Inode security blob
*
@@ -57,7 +60,7 @@ struct landlock_file_security {
*/
access_mask_t allowed_access;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @deny_masks: Domain layer levels that deny an optional access (see
* _LANDLOCK_ACCESS_FS_OPTIONAL).
@@ -75,7 +78,7 @@ struct landlock_file_security {
* LANDLOCK_SCOPE_SIGNAL.
*/
u8 fown_layer;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/**
* @fown_subject: Landlock credential of the task that set the PID that
@@ -97,7 +100,7 @@ struct landlock_file_security {
struct pid *fown_tg;
};
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* Makes sure all layers can be identified. */
/* clang-format off */
@@ -113,7 +116,7 @@ static_assert(BITS_PER_TYPE(typeof_member(struct landlock_file_security,
quiet_optional_accesses)) >=
HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL));
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/**
* struct landlock_superblock_security - Superblock security blob
@@ -153,4 +156,33 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
const struct path *const path,
access_mask_t access_hierarchy, const u32 flags);
+/**
+ * resolve_path_for_trace - Resolve a path for tracepoint display
+ *
+ * @path: The path to resolve.
+ * @buf: A buffer of at least PATH_MAX bytes for the resolved path.
+ *
+ * Uses d_absolute_path() to produce a namespace-independent absolute path,
+ * unlike d_path() which resolves relative to the process's chroot. This
+ * ensures trace output is deterministic regardless of the tracer's mount
+ * namespace.
+ *
+ * Return: A pointer into @buf with the resolved path, or an error string
+ * ("<too_long>", "<unreachable>").
+ */
+static inline const char *resolve_path_for_trace(const struct path *path,
+ char *buf)
+{
+ const char *p;
+
+ p = d_absolute_path(path, buf, PATH_MAX);
+ if (!IS_ERR_OR_NULL(p))
+ return p;
+
+ if (PTR_ERR(p) == -ENAMETOOLONG)
+ return "<too_long>";
+
+ return "<unreachable>";
+}
+
#endif /* _SECURITY_LANDLOCK_FS_H */
diff --git a/security/landlock/id.h b/security/landlock/id.h
index 45dcfb9e9a8b..2a43c2b523a8 100644
--- a/security/landlock/id.h
+++ b/security/landlock/id.h
@@ -8,18 +8,18 @@
#ifndef _SECURITY_LANDLOCK_ID_H
#define _SECURITY_LANDLOCK_ID_H
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
void __init landlock_init_id(void);
u64 landlock_get_id_range(size_t number_of_ids);
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void __init landlock_init_id(void)
{
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* _SECURITY_LANDLOCK_ID_H */
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 08d5f2f6d321..1a7c5fb8f6fd 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -34,7 +34,7 @@
#define LANDLOCK_NUM_ACCESS_MAX \
MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE)
-#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_TSYNC
+#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
/* clang-format on */
diff --git a/security/landlock/log.c b/security/landlock/log.c
new file mode 100644
index 000000000000..a8578a6f2ce9
--- /dev/null
+++ b/security/landlock/log.c
@@ -0,0 +1,587 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Log helpers
+ *
+ * Copyright © 2023-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#include <kunit/test.h>
+#include <linux/bitops.h>
+#include <uapi/linux/landlock.h>
+
+#include "access.h"
+#include "audit.h"
+#include "common.h"
+#include "cred.h"
+#include "domain.h"
+#include "limits.h"
+#include "log.h"
+#include "ruleset.h"
+#include "trace.h"
+
+static struct landlock_hierarchy *
+get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
+{
+ struct landlock_hierarchy *hierarchy = domain->hierarchy;
+ ssize_t i;
+
+ if (WARN_ON_ONCE(layer >= domain->num_layers))
+ return hierarchy;
+
+ for (i = domain->num_layers - 1; i > layer; i--) {
+ if (WARN_ON_ONCE(!hierarchy->parent))
+ break;
+
+ hierarchy = hierarchy->parent;
+ }
+
+ return hierarchy;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_hierarchy(struct kunit *const test)
+{
+ struct landlock_hierarchy dom0_hierarchy = {
+ .id = 10,
+ };
+ struct landlock_hierarchy dom1_hierarchy = {
+ .parent = &dom0_hierarchy,
+ .id = 20,
+ };
+ struct landlock_hierarchy dom2_hierarchy = {
+ .parent = &dom1_hierarchy,
+ .id = 30,
+ };
+ struct landlock_domain dom2 = {
+ .hierarchy = &dom2_hierarchy,
+ .num_layers = 3,
+ };
+
+ KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
+ KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
+ KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
+ /* KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); */
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+/* Get the youngest layer that denied the access_request. */
+static size_t get_denied_layer(const struct landlock_domain *const domain,
+ access_mask_t *const access_request,
+ const struct layer_masks *masks)
+{
+ for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
+ if (masks->layers[i].access & *access_request) {
+ *access_request &= masks->layers[i].access;
+ return i;
+ }
+ }
+
+ /* Not found - fall back to default values */
+ *access_request = 0;
+ return domain->num_layers - 1;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_denied_layer(struct kunit *const test)
+{
+ const struct landlock_domain dom = {
+ .num_layers = 5,
+ };
+ const struct layer_masks masks = {
+ .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ .layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ .layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ };
+ access_mask_t access;
+
+ access = LANDLOCK_ACCESS_FS_EXECUTE;
+ KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &masks));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
+
+ access = LANDLOCK_ACCESS_FS_READ_FILE;
+ KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
+
+ access = LANDLOCK_ACCESS_FS_READ_DIR;
+ KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
+
+ access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
+ KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+ KUNIT_EXPECT_EQ(test, access,
+ LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_READ_DIR);
+
+ access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
+ KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
+
+ access = LANDLOCK_ACCESS_FS_WRITE_FILE;
+ KUNIT_EXPECT_EQ(test, 4, get_denied_layer(&dom, &access, &masks));
+ KUNIT_EXPECT_EQ(test, access, 0);
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+static size_t
+get_layer_from_deny_masks(access_mask_t *const access_request,
+ const access_mask_t all_existing_optional_access,
+ const deny_masks_t deny_masks,
+ optional_access_t quiet_optional_accesses,
+ bool *quiet)
+{
+ const unsigned long access_opt = all_existing_optional_access;
+ const unsigned long access_req = *access_request;
+ access_mask_t missing = 0;
+ size_t youngest_layer = 0;
+ size_t access_index = 0;
+ unsigned long access_bit;
+ bool should_quiet = false;
+
+ /* This will require change with new object types. */
+ WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
+
+ for_each_set_bit(access_bit, &access_opt,
+ BITS_PER_TYPE(access_mask_t)) {
+ if (access_req & BIT(access_bit)) {
+ const size_t layer =
+ (deny_masks >>
+ (access_index *
+ HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
+ (LANDLOCK_MAX_NUM_LAYERS - 1);
+ const bool layer_has_quiet =
+ !!(quiet_optional_accesses & BIT(access_index));
+
+ if (layer > youngest_layer) {
+ youngest_layer = layer;
+ missing = BIT(access_bit);
+ should_quiet = layer_has_quiet;
+ } else if (layer == youngest_layer) {
+ missing |= BIT(access_bit);
+ /*
+ * Whether the layer has rules with quiet flag
+ * covering the file accessed does not depend on
+ * the access, and so the following
+ * WARN_ON_ONCE() should not fail.
+ */
+ WARN_ON_ONCE(should_quiet && !layer_has_quiet);
+ should_quiet = layer_has_quiet;
+ }
+ }
+ access_index++;
+ }
+
+ *access_request = missing;
+ *quiet = should_quiet;
+ return youngest_layer;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_layer_from_deny_masks(struct kunit *const test)
+{
+ deny_masks_t deny_mask;
+ access_mask_t access;
+ optional_access_t quiet_optional_accesses;
+ bool quiet;
+
+ /* truncate:0 ioctl_dev:2 */
+ deny_mask = 0x20;
+ quiet_optional_accesses = 0;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* layer denying truncate: quiet, ioctl: not quiet */
+ quiet_optional_accesses = 0b01;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* Reverse order - truncate:2 ioctl_dev:0 */
+ deny_mask = 0x02;
+ quiet_optional_accesses = 0;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* layer denying truncate: quiet, ioctl: not quiet */
+ quiet_optional_accesses = 0b01;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ /* layer denying truncate: not quiet, ioctl: quiet */
+ quiet_optional_accesses = 0b10;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 2,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* truncate:15 ioctl_dev:15 */
+ deny_mask = 0xff;
+ quiet_optional_accesses = 0;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access,
+ LANDLOCK_ACCESS_FS_TRUNCATE |
+ LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, false);
+
+ /* Both quiet (same layer so quietness must be the same) */
+ quiet_optional_accesses = 0b11;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(
+ &access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask, quiet_optional_accesses, &quiet));
+ KUNIT_EXPECT_EQ(test, access,
+ LANDLOCK_ACCESS_FS_TRUNCATE |
+ LANDLOCK_ACCESS_FS_IOCTL_DEV);
+ KUNIT_EXPECT_EQ(test, quiet, true);
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+static bool is_valid_request(const struct landlock_request *const request)
+{
+ if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
+ return false;
+
+ if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
+ return false;
+
+ if (request->access) {
+ if (WARN_ON_ONCE(!(!!request->layer_masks ^
+ !!request->all_existing_optional_access)))
+ return false;
+ } else {
+ if (WARN_ON_ONCE(request->layer_masks ||
+ request->all_existing_optional_access))
+ return false;
+ }
+
+ if (request->deny_masks) {
+ if (WARN_ON_ONCE(!request->all_existing_optional_access))
+ return false;
+ static_assert(sizeof(request->all_existing_optional_access) ==
+ sizeof(u32));
+ if (WARN_ON_ONCE(
+ request->quiet_optional_accesses >=
+ BIT(hweight32(
+ request->all_existing_optional_access))))
+ return false;
+ }
+
+ return true;
+}
+
+static access_mask_t
+pick_access_mask_for_request_type(const enum landlock_request_type type,
+ const struct access_masks access_masks)
+{
+ switch (type) {
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ return access_masks.fs;
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ return access_masks.net;
+ default:
+ WARN_ONCE(1, "Invalid request type %d passed to %s", type,
+ __func__);
+ return 0;
+ }
+}
+
+/*
+ * Whether a quiet rule silences the denial: the rule must cover the whole
+ * denied access in the layer that denied it (a quiet rule in a non-denying
+ * layer does not suppress the denial).
+ */
+static bool
+is_denial_quieted(const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool object_quiet_flag)
+{
+ if (object_quiet_flag) {
+ const access_mask_t quiet_mask =
+ pick_access_mask_for_request_type(
+ request->type, youngest_denied->quiet_masks);
+
+ return (quiet_mask & missing) == missing;
+ }
+
+ /*
+ * Either the object is not quiet, or this is a scope request. We check
+ * request->type to distinguish between the two cases.
+ */
+ switch (request->type) {
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ return !!(youngest_denied->quiet_masks.scope &
+ LANDLOCK_SCOPE_SIGNAL);
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ return !!(youngest_denied->quiet_masks.scope &
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+ /*
+ * Leave LANDLOCK_REQUEST_PTRACE and LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY
+ * unhandled for now - they are never quiet.
+ */
+ default:
+ return false;
+ }
+}
+
+/*
+ * Computes whether a denial from youngest_denied is selected for logging by the
+ * domain's policy: its logging must not be disabled (by both per-execution
+ * flags being off, or by an ancestor's
+ * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF), the per-execution flag matching
+ * same_exec must be set, and no quiet rule may cover the denied access.
+ * landlock_log_denial() computes this once and passes it to
+ * landlock_audit_denial(), which additionally requires audit_enabled.
+ */
+static bool
+is_denial_logged(const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool same_exec,
+ const bool object_quiet_flag)
+{
+ if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
+ return false;
+
+ if (!(same_exec ? youngest_denied->log_same_exec :
+ youngest_denied->log_new_exec))
+ return false;
+
+ return !is_denial_quieted(request, youngest_denied, missing,
+ object_quiet_flag);
+}
+
+/**
+ * landlock_log_denial - Log a denied access
+ *
+ * @subject: The Landlock subject's credential denying an action.
+ * @request: Detail of the user space request.
+ */
+void landlock_log_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request)
+{
+ struct landlock_hierarchy *youngest_denied;
+ size_t youngest_layer;
+ access_mask_t missing;
+ bool object_quiet_flag = false;
+
+ if (WARN_ON_ONCE(!subject || !subject->domain ||
+ !subject->domain->hierarchy || !request))
+ return;
+
+ if (!is_valid_request(request))
+ return;
+
+ missing = request->access;
+ if (missing) {
+ /* Gets the nearest domain that denies the request. */
+ if (request->layer_masks) {
+ youngest_layer = get_denied_layer(subject->domain,
+ &missing,
+ request->layer_masks);
+ object_quiet_flag =
+ request->layer_masks->layers[youngest_layer]
+ .quiet;
+ } else {
+ youngest_layer = get_layer_from_deny_masks(
+ &missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
+ request->deny_masks,
+ request->quiet_optional_accesses,
+ &object_quiet_flag);
+ }
+ youngest_denied =
+ get_hierarchy(subject->domain, youngest_layer);
+ } else {
+ youngest_layer = request->layer_plus_one - 1;
+ youngest_denied =
+ get_hierarchy(subject->domain, youngest_layer);
+ }
+
+ const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer));
+ const bool logged = is_denial_logged(request, youngest_denied, missing,
+ same_exec, object_quiet_flag);
+
+ /*
+ * Consistently keeps track of the number of denied access requests even
+ * if audit is currently disabled, or if audit rules currently exclude
+ * this record type, or if landlock_restrict_self(2)'s flags quiet logs.
+ */
+ atomic64_inc(&youngest_denied->num_denials);
+
+ landlock_trace_denial(request, youngest_denied, missing, same_exec,
+ logged);
+ landlock_audit_denial(request, youngest_denied, missing, logged);
+}
+
+/**
+ * landlock_log_free_domain - Log domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Called from landlock_put_domain_deferred() (via a work queue scheduled by
+ * hook_cred_free()) or directly from landlock_put_domain().
+ */
+void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ if (WARN_ON_ONCE(!hierarchy))
+ return;
+
+ landlock_trace_free_domain(hierarchy);
+ landlock_audit_free_domain(hierarchy);
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static struct kunit_case test_cases[] = {
+ /* clang-format off */
+ KUNIT_CASE(test_get_hierarchy),
+ KUNIT_CASE(test_get_denied_layer),
+ KUNIT_CASE(test_get_layer_from_deny_masks),
+ {}
+ /* clang-format on */
+};
+
+static struct kunit_suite test_suite = {
+ .name = "landlock_log",
+ .test_cases = test_cases,
+};
+
+kunit_test_suite(test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
diff --git a/security/landlock/log.h b/security/landlock/log.h
new file mode 100644
index 000000000000..e0a6e44f3ddd
--- /dev/null
+++ b/security/landlock/log.h
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Log helpers
+ *
+ * Copyright © 2023-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#ifndef _SECURITY_LANDLOCK_LOG_H
+#define _SECURITY_LANDLOCK_LOG_H
+
+#include <linux/lsm_audit.h>
+
+#include "access.h"
+
+struct landlock_cred_security;
+struct landlock_hierarchy;
+
+enum landlock_request_type {
+ LANDLOCK_REQUEST_PTRACE = 1,
+ LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
+ LANDLOCK_REQUEST_FS_ACCESS,
+ LANDLOCK_REQUEST_NET_ACCESS,
+ LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
+ LANDLOCK_REQUEST_SCOPE_SIGNAL,
+};
+
+/*
+ * We should be careful to only use a variable of this type for
+ * landlock_log_denial(). This way, the compiler can remove it entirely if
+ * CONFIG_SECURITY_LANDLOCK_LOG is not set.
+ */
+struct landlock_request {
+ /* Mandatory fields. */
+ enum landlock_request_type type;
+ struct common_audit_data audit;
+
+ /**
+ * layer_plus_one: First layer level that denies the request + 1. The
+ * extra one is useful to detect uninitialized field.
+ */
+ size_t layer_plus_one;
+
+ /* Required field for configurable access control. */
+ access_mask_t access;
+
+ /* Required fields for requests with layer masks. */
+ const struct layer_masks *layer_masks;
+
+ /* Required fields for requests with deny masks. */
+ const access_mask_t all_existing_optional_access;
+ deny_masks_t deny_masks;
+ optional_access_t quiet_optional_accesses;
+
+ /*
+ * Other-party domain ID for a relational (scope/ptrace) denial, or 0 if
+ * that party is unsandboxed. An ID, not a pointer: the other task can
+ * replace its credential and free the domain it referenced. Trace path
+ * only; audit ignores it.
+ */
+ u64 other_domain_id;
+};
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+
+void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy);
+
+void landlock_log_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request);
+
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+static inline void
+landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+}
+
+static inline void
+landlock_log_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request)
+{
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+#endif /* _SECURITY_LANDLOCK_LOG_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 46c17116fcf4..8f2aaac54b33 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -12,13 +12,16 @@
#include <linux/socket.h>
#include <net/ipv6.h>
-#include "audit.h"
#include "common.h"
#include "cred.h"
+#include "domain.h"
#include "limits.h"
+#include "log.h"
#include "net.h"
#include "ruleset.h"
+#include <trace/events/landlock.h>
+
int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights,
const u32 flags)
@@ -32,16 +35,40 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
/* Transforms relative access rights to absolute ones. */
- access_rights |= LANDLOCK_MASK_ACCESS_NET &
- ~landlock_get_net_access_mask(ruleset, 0);
+ access_rights |= LANDLOCK_MASK_ACCESS_NET & ~ruleset->handled_masks.net;
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
+
+ /*
+ * Emit after the rule insertion succeeds, so every event corresponds to
+ * a rule that is actually in the ruleset. The ruleset lock is still
+ * held for BTF consistency (enforced by lockdep_assert_held in
+ * TP_fast_assign).
+ */
+ if (!err)
+ trace_landlock_add_rule_net(ruleset, access_rights, port);
mutex_unlock(&ruleset->lock);
return err;
}
+static bool unmask_layers_net(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_masks *masks,
+ access_mask_t access_request)
+{
+ const struct landlock_rule *rule = NULL;
+ bool ret;
+
+ ret = landlock_unmask_layers(domain, id, masks, &rule);
+ if (rule)
+ trace_landlock_check_rule_net(
+ domain, rule, access_request,
+ ntohs((__force __be16)id.key.data));
+ return ret;
+}
+
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
@@ -51,7 +78,6 @@ static int current_check_access_socket(struct socket *const sock,
unsigned short sock_family;
__be16 port;
struct layer_masks layer_masks = {};
- const struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
};
@@ -237,14 +263,14 @@ static int current_check_access_socket(struct socket *const sock,
id.key.data = (__force uintptr_t)port;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
- rule = landlock_find_rule(subject->domain, id);
access_request = landlock_init_layer_masks(subject->domain,
access_request, &layer_masks,
LANDLOCK_KEY_NET_PORT);
if (!access_request)
return 0;
- if (landlock_unmask_layers(rule, &layer_masks))
+ if (unmask_layers_net(subject->domain, id, &layer_masks,
+ access_request))
return 0;
audit_net.family = address->sa_family;
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 4dd09ea22c84..0d07707523cd 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare, Inc.
*/
#include <linux/bits.h>
@@ -20,59 +21,64 @@
#include <linux/refcount.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
-#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
#include "access.h"
-#include "domain.h"
+#include "id.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
-static struct landlock_ruleset *create_ruleset(const u32 num_layers)
+#include <trace/events/landlock.h>
+
+struct landlock_ruleset *
+landlock_create_ruleset(const access_mask_t fs_access_mask,
+ const access_mask_t net_access_mask,
+ const access_mask_t scope_mask)
{
struct landlock_ruleset *new_ruleset;
- new_ruleset = kzalloc_flex(*new_ruleset, access_masks, num_layers,
- GFP_KERNEL_ACCOUNT);
+ /* Informs about useless ruleset. */
+ if (!fs_access_mask && !net_access_mask && !scope_mask)
+ return ERR_PTR(-ENOMSG);
+
+ new_ruleset = kzalloc_obj(*new_ruleset, GFP_KERNEL_ACCOUNT);
if (!new_ruleset)
return ERR_PTR(-ENOMEM);
+
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
- new_ruleset->root_inode = RB_ROOT;
+ new_ruleset->rules.root_inode = RB_ROOT;
#if IS_ENABLED(CONFIG_INET)
- new_ruleset->root_net_port = RB_ROOT;
+ new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
- new_ruleset->num_layers = num_layers;
- /*
- * hierarchy = NULL
- * num_rules = 0
- * access_masks[] = 0
- */
- return new_ruleset;
-}
+#ifdef CONFIG_TRACEPOINTS
+ new_ruleset->id = landlock_get_id_range(1);
+#endif /* CONFIG_TRACEPOINTS */
-struct landlock_ruleset *
-landlock_create_ruleset(const access_mask_t fs_access_mask,
- const access_mask_t net_access_mask,
- const access_mask_t scope_mask)
-{
- struct landlock_ruleset *new_ruleset;
+ /* Should already be checked in landlock_create_ruleset(). */
+ if (fs_access_mask) {
+ const access_mask_t mask = fs_access_mask &
+ LANDLOCK_MASK_ACCESS_FS;
- /* Informs about useless ruleset. */
- if (!fs_access_mask && !net_access_mask && !scope_mask)
- return ERR_PTR(-ENOMSG);
- new_ruleset = create_ruleset(1);
- if (IS_ERR(new_ruleset))
- return new_ruleset;
- if (fs_access_mask)
- landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0);
- if (net_access_mask)
- landlock_add_net_access_mask(new_ruleset, net_access_mask, 0);
- if (scope_mask)
- landlock_add_scope_mask(new_ruleset, scope_mask, 0);
+ WARN_ON_ONCE(fs_access_mask != mask);
+ new_ruleset->handled_masks.fs |= mask;
+ }
+ if (net_access_mask) {
+ const access_mask_t mask = net_access_mask &
+ LANDLOCK_MASK_ACCESS_NET;
+
+ WARN_ON_ONCE(net_access_mask != mask);
+ new_ruleset->handled_masks.net |= mask;
+ }
+ if (scope_mask) {
+ const access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
+
+ WARN_ON_ONCE(scope_mask != mask);
+ new_ruleset->handled_masks.scope |= mask;
+ }
return new_ruleset;
}
@@ -129,7 +135,7 @@ create_rule(const struct landlock_id id,
return ERR_PTR(-ENOMEM);
RB_CLEAR_NODE(&new_rule->node);
if (is_object_pointer(id.type)) {
- /* This should have been caught by insert_rule(). */
+ /* This should have been caught by landlock_store_rule(). */
WARN_ON_ONCE(!id.key.object);
landlock_get_object(id.key.object);
}
@@ -145,24 +151,6 @@ create_rule(const struct landlock_id id,
return new_rule;
}
-static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
- const enum landlock_key_type key_type)
-{
- switch (key_type) {
- case LANDLOCK_KEY_INODE:
- return &ruleset->root_inode;
-
-#if IS_ENABLED(CONFIG_INET)
- case LANDLOCK_KEY_NET_PORT:
- return &ruleset->root_net_port;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- default:
- WARN_ON_ONCE(1);
- return ERR_PTR(-EINVAL);
- }
-}
-
static void free_rule(struct landlock_rule *const rule,
const enum landlock_key_type key_type)
{
@@ -176,19 +164,20 @@ static void free_rule(struct landlock_rule *const rule,
static void build_check_ruleset(void)
{
- const struct landlock_ruleset ruleset = {
+ const struct landlock_rules rules = {
.num_rules = ~0,
- .num_layers = ~0,
};
- BUILD_BUG_ON(ruleset.num_rules < LANDLOCK_MAX_NUM_RULES);
- BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
+ BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
}
/**
- * insert_rule - Create and insert a rule in a ruleset
+ * landlock_store_rule - Create and insert a rule into the rule storage
*
- * @ruleset: The ruleset to be updated.
+ * @rules: The rule storage to be updated. The caller is responsible for
+ * any required locking. For rulesets, this means holding
+ * &landlock_ruleset.lock. For domains under construction, no lock is
+ * needed because the domain is not yet visible to other tasks.
* @id: The ID to build the new rule with. The underlying kernel object, if
* any, must be held by the caller.
* @layers: One or multiple layers to be copied into the new rule.
@@ -196,19 +185,19 @@ static void build_check_ruleset(void)
*
* When user space requests to add a new rule to a ruleset, @layers only
* contains one entry and this entry is not assigned to any level. In this
- * case, the new rule will extend @ruleset, similarly to a boolean OR between
+ * case, the new rule will extend @rules, similarly to a boolean OR between
* access rights.
*
* When merging a ruleset in a domain, or copying a domain, @layers will be
- * added to @ruleset as new constraints, similarly to a boolean AND between
- * access rights.
+ * added to @rules as new constraints, similarly to a boolean AND between access
+ * rights.
*
* Return: 0 on success, -errno on failure.
*/
-static int insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const struct landlock_layer (*layers)[],
- const size_t num_layers)
+int landlock_store_rule(struct landlock_rules *const rules,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers)
{
struct rb_node **walker_node;
struct rb_node *parent_node = NULL;
@@ -216,14 +205,13 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
struct rb_root *root;
might_sleep();
- lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!layers))
return -ENOENT;
if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
return -ENOENT;
- root = get_root(ruleset, id.type);
+ root = landlock_get_rule_root(rules, id.type);
if (IS_ERR(root))
return PTR_ERR(root);
@@ -249,7 +237,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
if ((*layers)[0].level == 0) {
/*
* Extends access rights when the request comes from
- * landlock_add_rule(2), i.e. @ruleset is not a domain.
+ * landlock_add_rule(2), i.e. @rules is not a domain.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
return -EINVAL;
@@ -278,14 +266,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
/* There is no match for @id. */
build_check_ruleset();
- if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
+ if (rules->num_rules >= LANDLOCK_MAX_NUM_RULES)
return -E2BIG;
new_rule = create_rule(id, layers, num_layers, NULL);
if (IS_ERR(new_rule))
return PTR_ERR(new_rule);
rb_link_node(&new_rule->node, parent_node, walker_node);
rb_insert_color(&new_rule->node, root);
- ruleset->num_rules++;
+ rules->num_rules++;
return 0;
}
@@ -311,197 +299,50 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
{
struct landlock_layer layers[] = { {
.access = access,
- /* When @level is zero, insert_rule() extends @ruleset. */
+ /*
+ * When @level is zero, landlock_store_rule() extends @ruleset.
+ */
.level = 0,
.flags = {
.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
},
} };
+ int err;
build_check_layer();
- return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers));
-}
-
-static int merge_tree(struct landlock_ruleset *const dst,
- struct landlock_ruleset *const src,
- const enum landlock_key_type key_type)
-{
- struct landlock_rule *walker_rule, *next_rule;
- struct rb_root *src_root;
- int err = 0;
-
- might_sleep();
- lockdep_assert_held(&dst->lock);
- lockdep_assert_held(&src->lock);
-
- src_root = get_root(src, key_type);
- if (IS_ERR(src_root))
- return PTR_ERR(src_root);
-
- /* Merges the @src tree. */
- rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
- node) {
- struct landlock_layer layers[] = { {
- .level = dst->num_layers,
- } };
- const struct landlock_id id = {
- .key = walker_rule->key,
- .type = key_type,
- };
-
- if (WARN_ON_ONCE(walker_rule->num_layers != 1))
- return -EINVAL;
-
- if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
- return -EINVAL;
-
- layers[0].access = walker_rule->layers[0].access;
- layers[0].flags = walker_rule->layers[0].flags;
-
- err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
- if (err)
- return err;
- }
- return err;
-}
-
-static int merge_ruleset(struct landlock_ruleset *const dst,
- struct landlock_ruleset *const src)
-{
- int err = 0;
-
- might_sleep();
- /* Should already be checked by landlock_merge_ruleset() */
- if (WARN_ON_ONCE(!src))
- return 0;
- /* Only merge into a domain. */
- if (WARN_ON_ONCE(!dst || !dst->hierarchy))
- return -EINVAL;
-
- /* Locks @dst first because we are its only owner. */
- mutex_lock(&dst->lock);
- mutex_lock_nested(&src->lock, SINGLE_DEPTH_NESTING);
-
- /* Stacks the new layer. */
- if (WARN_ON_ONCE(src->num_layers != 1 || dst->num_layers < 1)) {
- err = -EINVAL;
- goto out_unlock;
- }
- dst->access_masks[dst->num_layers - 1] =
- landlock_upgrade_handled_access_masks(src->access_masks[0]);
-
- /* Merges the @src inode tree. */
- err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
- if (err)
- goto out_unlock;
-
-#if IS_ENABLED(CONFIG_INET)
- /* Merges the @src network port tree. */
- err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
- if (err)
- goto out_unlock;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
-out_unlock:
- mutex_unlock(&src->lock);
- mutex_unlock(&dst->lock);
- return err;
-}
-
-static int inherit_tree(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const child,
- const enum landlock_key_type key_type)
-{
- struct landlock_rule *walker_rule, *next_rule;
- struct rb_root *parent_root;
- int err = 0;
-
- might_sleep();
- lockdep_assert_held(&parent->lock);
- lockdep_assert_held(&child->lock);
-
- parent_root = get_root(parent, key_type);
- if (IS_ERR(parent_root))
- return PTR_ERR(parent_root);
-
- /* Copies the @parent inode or network tree. */
- rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
- parent_root, node) {
- const struct landlock_id id = {
- .key = walker_rule->key,
- .type = key_type,
- };
-
- err = insert_rule(child, id, &walker_rule->layers,
- walker_rule->num_layers);
- if (err)
- return err;
- }
- return err;
-}
-
-static int inherit_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const child)
-{
- int err = 0;
-
- might_sleep();
- if (!parent)
- return 0;
-
- /* Locks @child first because we are its only owner. */
- mutex_lock(&child->lock);
- mutex_lock_nested(&parent->lock, SINGLE_DEPTH_NESTING);
-
- /* Copies the @parent inode tree. */
- err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
- if (err)
- goto out_unlock;
-
-#if IS_ENABLED(CONFIG_INET)
- /* Copies the @parent network port tree. */
- err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
- if (err)
- goto out_unlock;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- if (WARN_ON_ONCE(child->num_layers <= parent->num_layers)) {
- err = -EINVAL;
- goto out_unlock;
- }
- /* Copies the parent layer stack and leaves a space for the new layer. */
- memcpy(child->access_masks, parent->access_masks,
- flex_array_size(parent, access_masks, parent->num_layers));
+ lockdep_assert_held(&ruleset->lock);
+ err = landlock_store_rule(&ruleset->rules, id, &layers,
+ ARRAY_SIZE(layers));
- if (WARN_ON_ONCE(!parent->hierarchy)) {
- err = -EINVAL;
- goto out_unlock;
- }
- landlock_get_hierarchy(parent->hierarchy);
- child->hierarchy->parent = parent->hierarchy;
+#ifdef CONFIG_TRACEPOINTS
+ if (!err)
+ ruleset->version++;
+#endif /* CONFIG_TRACEPOINTS */
-out_unlock:
- mutex_unlock(&parent->lock);
- mutex_unlock(&child->lock);
return err;
}
-static void free_ruleset(struct landlock_ruleset *const ruleset)
+void landlock_free_rules(struct landlock_rules *const rules)
{
struct landlock_rule *freeme, *next;
might_sleep();
- rbtree_postorder_for_each_entry_safe(freeme, next, &ruleset->root_inode,
+ rbtree_postorder_for_each_entry_safe(freeme, next, &rules->root_inode,
node)
free_rule(freeme, LANDLOCK_KEY_INODE);
#if IS_ENABLED(CONFIG_INET)
rbtree_postorder_for_each_entry_safe(freeme, next,
- &ruleset->root_net_port, node)
+ &rules->root_net_port, node)
free_rule(freeme, LANDLOCK_KEY_NET_PORT);
#endif /* IS_ENABLED(CONFIG_INET) */
+}
- landlock_put_hierarchy(ruleset->hierarchy);
+static void free_ruleset(struct landlock_ruleset *const ruleset)
+{
+ might_sleep();
+ trace_landlock_free_ruleset(ruleset);
+ landlock_free_rules(&ruleset->rules);
kfree(ruleset);
}
@@ -511,234 +352,3 @@ void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
if (ruleset && refcount_dec_and_test(&ruleset->usage))
free_ruleset(ruleset);
}
-
-static void free_ruleset_work(struct work_struct *const work)
-{
- struct landlock_ruleset *ruleset;
-
- ruleset = container_of(work, struct landlock_ruleset, work_free);
- free_ruleset(ruleset);
-}
-
-/* Only called by hook_cred_free(). */
-void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
-{
- if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
- INIT_WORK(&ruleset->work_free, free_ruleset_work);
- schedule_work(&ruleset->work_free);
- }
-}
-
-/**
- * landlock_merge_ruleset - Merge a ruleset with a domain
- *
- * @parent: Parent domain.
- * @ruleset: New ruleset to be merged.
- *
- * The current task is requesting to be restricted. The subjective credentials
- * must not be in an overridden state. cf. landlock_init_hierarchy_log().
- *
- * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR()
- * on failure. If @parent is NULL, the new domain duplicates @ruleset.
- */
-struct landlock_ruleset *
-landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset)
-{
- struct landlock_ruleset *new_dom __free(landlock_put_ruleset) = NULL;
- u32 num_layers;
- int err;
-
- might_sleep();
- if (WARN_ON_ONCE(!ruleset || parent == ruleset))
- return ERR_PTR(-EINVAL);
-
- if (parent) {
- if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
- return ERR_PTR(-E2BIG);
- num_layers = parent->num_layers + 1;
- } else {
- num_layers = 1;
- }
-
- /* Creates a new domain... */
- new_dom = create_ruleset(num_layers);
- if (IS_ERR(new_dom))
- return new_dom;
-
- new_dom->hierarchy =
- kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
- if (!new_dom->hierarchy)
- return ERR_PTR(-ENOMEM);
-
- refcount_set(&new_dom->hierarchy->usage, 1);
-
- /* ...as a child of @parent... */
- err = inherit_ruleset(parent, new_dom);
- if (err)
- return ERR_PTR(err);
-
- /* ...and including @ruleset. */
- err = merge_ruleset(new_dom, ruleset);
- if (err)
- return ERR_PTR(err);
-
- err = landlock_init_hierarchy_log(new_dom->hierarchy);
- if (err)
- return ERR_PTR(err);
-
-#ifdef CONFIG_AUDIT
- new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
-#endif /* CONFIG_AUDIT */
-
- return no_free_ptr(new_dom);
-}
-
-/*
- * The returned access has the same lifetime as @ruleset.
- */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
- const struct landlock_id id)
-{
- const struct rb_root *root;
- const struct rb_node *node;
-
- root = get_root((struct landlock_ruleset *)ruleset, id.type);
- if (IS_ERR(root))
- return NULL;
- node = root->rb_node;
-
- while (node) {
- struct landlock_rule *this =
- rb_entry(node, struct landlock_rule, node);
-
- if (this->key.data == id.key.data)
- return this;
- if (this->key.data < id.key.data)
- node = node->rb_right;
- else
- node = node->rb_left;
- }
- return NULL;
-}
-
-/**
- * landlock_unmask_layers - Remove the access rights in @masks
- * which are granted in @rule
- *
- * Updates the set of (per-layer) unfulfilled access rights @masks
- * so that all the access rights granted in @rule are removed from it
- * (because they are now fulfilled).
- *
- * @rule: A rule that grants a set of access rights for each layer
- * @masks: A matrix of unfulfilled access rights for each layer
- *
- * Return: True if the request is allowed (i.e. the access rights granted all
- * remaining unfulfilled access rights and masks has no leftover set bits).
- */
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_masks *masks)
-{
- if (!masks)
- return true;
- if (!rule)
- return false;
-
- /*
- * An access is granted if, for each policy layer, at least one rule
- * encountered on the pathwalk grants the requested access,
- * regardless of its position in the layer stack. We must then check
- * the remaining layers for each inode, from the first added layer to
- * the last one. When there is multiple requested accesses, for each
- * policy layer, the full set of requested accesses may not be granted
- * by only one rule, but by the union (binary OR) of multiple rules.
- * E.g. /a/b <execute> + /a <read> => /a/b <execute + read>
- */
- for (size_t i = 0; i < rule->num_layers; i++) {
- const struct landlock_layer *const layer = &rule->layers[i];
-
- /* Clear the bits where the layer in the rule grants access. */
- masks->layers[layer->level - 1].access &= ~layer->access;
-
-#ifdef CONFIG_AUDIT
- /* Collect rule flags for each layer. */
- if (layer->flags.quiet)
- masks->layers[layer->level - 1].quiet = true;
-#endif /* CONFIG_AUDIT */
- }
-
- for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
- if (masks->layers[i].access)
- return false;
- }
- return true;
-}
-
-typedef access_mask_t
-get_access_mask_t(const struct landlock_ruleset *const ruleset,
- const u16 layer_level);
-
-/**
- * landlock_init_layer_masks - Initialize layer masks from an access request
- *
- * Populates @masks such that for each access right in @access_request, the bits
- * for all the layers are set where this access right is handled. Rule flags
- * are also zeroed.
- *
- * @domain: The domain that defines the current restrictions.
- * @access_request: The requested access rights to check.
- * @masks: Layer access masks to populate.
- * @key_type: The key type to switch between access masks of different types.
- *
- * Return: An access mask where each access right bit is set which is handled
- * in any of the active layers in @domain.
- */
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
- const access_mask_t access_request,
- struct layer_masks *const masks,
- const enum landlock_key_type key_type)
-{
- access_mask_t handled_accesses = 0;
- get_access_mask_t *get_access_mask;
-
- switch (key_type) {
- case LANDLOCK_KEY_INODE:
- get_access_mask = landlock_get_fs_access_mask;
- break;
-
-#if IS_ENABLED(CONFIG_INET)
- case LANDLOCK_KEY_NET_PORT:
- get_access_mask = landlock_get_net_access_mask;
- break;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- default:
- WARN_ON_ONCE(1);
- return 0;
- }
-
- /* An empty access request can happen because of O_WRONLY | O_RDWR. */
- if (!access_request)
- return 0;
-
- for (size_t i = 0; i < domain->num_layers; i++) {
- const access_mask_t handled = get_access_mask(domain, i);
-
- masks->layers[i].access = access_request & handled;
- handled_accesses |= masks->layers[i].access;
-#ifdef CONFIG_AUDIT
- masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
- }
- for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
- i++) {
- masks->layers[i].access = 0;
-#ifdef CONFIG_AUDIT
- masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
- }
-
- return handled_accesses;
-}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 0437adf17428..b536fa0425b7 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_RULESET_H
@@ -14,14 +15,11 @@
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
-#include <linux/workqueue.h>
#include "access.h"
#include "limits.h"
#include "object.h"
-struct landlock_hierarchy;
-
/**
* struct landlock_layer - Access rights for a given layer
*/
@@ -68,13 +66,12 @@ union landlock_key {
*/
enum landlock_key_type {
/**
- * @LANDLOCK_KEY_INODE: Type of &landlock_ruleset.root_inode's node
- * keys.
+ * @LANDLOCK_KEY_INODE: Type of &landlock_rules.root_inode's node keys.
*/
LANDLOCK_KEY_INODE = 1,
/**
- * @LANDLOCK_KEY_NET_PORT: Type of &landlock_ruleset.root_net_port's
- * node keys.
+ * @LANDLOCK_KEY_NET_PORT: Type of &landlock_rules.root_net_port's node
+ * keys.
*/
LANDLOCK_KEY_NET_PORT,
};
@@ -122,88 +119,78 @@ struct landlock_rule {
};
/**
- * struct landlock_ruleset - Landlock ruleset
+ * struct landlock_rules - Red-black tree storage for Landlock rules
*
- * This data structure must contain unique entries, be updatable, and quick to
- * match an object.
+ * This structure holds the rule trees shared by both rulesets and domains.
*/
-struct landlock_ruleset {
+struct landlock_rules {
/**
* @root_inode: Root of a red-black tree containing &struct
- * landlock_rule nodes with inode object. Once a ruleset is tied to a
- * process (i.e. as a domain), this tree is immutable until @usage
- * reaches zero.
+ * landlock_rule nodes with inode object. Immutable for domains.
*/
struct rb_root root_inode;
#if IS_ENABLED(CONFIG_INET)
/**
* @root_net_port: Root of a red-black tree containing &struct
- * landlock_rule nodes with network port. Once a ruleset is tied to a
- * process (i.e. as a domain), this tree is immutable until @usage
- * reaches zero.
+ * landlock_rule nodes with network port. Immutable for domains.
*/
struct rb_root root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
/**
- * @hierarchy: Enables hierarchy identification even when a parent
- * domain vanishes. This is needed for the ptrace protection.
+ * @num_rules: Number of non-overlapping (i.e. not for the same object)
+ * rules in this tree storage.
*/
- struct landlock_hierarchy *hierarchy;
- union {
- /**
- * @work_free: Enables to free a ruleset within a lockless
- * section. This is only used by
- * landlock_put_ruleset_deferred() when @usage reaches zero.
- * The fields @lock, @usage, @num_rules, @num_layers,
- * @quiet_masks and @access_masks are then unused.
- */
- struct work_struct work_free;
- struct {
- /**
- * @lock: Protects against concurrent modifications of
- * @root, if @usage is greater than zero.
- */
- struct mutex lock;
- /**
- * @usage: Number of processes (i.e. domains) or file
- * descriptors referencing this ruleset.
- */
- refcount_t usage;
- /**
- * @num_rules: Number of non-overlapping (i.e. not for
- * the same object) rules in this ruleset.
- */
- u32 num_rules;
- /**
- * @num_layers: Number of layers that are used in this
- * ruleset. This enables to check that all the layers
- * allow an access request. A value of 0 identifies a
- * non-merged ruleset (i.e. not a domain).
- */
- u32 num_layers;
- /**
- * @quiet_masks: Stores the quiet flags for an unmerged
- * ruleset. For a merged domain, this is stored in each
- * layer's struct landlock_hierarchy instead.
- */
- struct access_masks quiet_masks;
- /**
- * @access_masks: Contains the subset of filesystem and
- * network actions that are restricted by a ruleset.
- * A domain saves all layers of merged rulesets in a
- * stack (FAM), starting from the first layer to the
- * last one. These layers are used when merging
- * rulesets, for user space backward compatibility
- * (i.e. future-proof), and to properly handle merged
- * rulesets without overlapping access rights. These
- * layers are set once and never changed for the
- * lifetime of the ruleset.
- */
- struct access_masks access_masks[];
- };
- };
+ u32 num_rules;
+};
+
+/**
+ * struct landlock_ruleset - Landlock ruleset
+ *
+ * This data structure must contain unique entries, be updatable, and quick to
+ * match an object.
+ */
+struct landlock_ruleset {
+ /**
+ * @rules: Red-black tree storage for rules.
+ */
+ struct landlock_rules rules;
+ /**
+ * @lock: Protects against concurrent modifications of @rules, if @usage
+ * is greater than zero.
+ */
+ struct mutex lock;
+ /**
+ * @usage: Number of file descriptors referencing this ruleset.
+ */
+ refcount_t usage;
+
+#ifdef CONFIG_TRACEPOINTS
+ /**
+ * @version: Counter incremented on each successful
+ * landlock_add_rule(2), including when it only extends an existing
+ * rule's access rights. Used by tracepoints to correlate a domain with
+ * the exact ruleset state it was created from. Protected by @lock.
+ */
+ u32 version;
+ /**
+ * @id: Unique identifier for this ruleset, used for tracing.
+ */
+ u64 id;
+#endif /* CONFIG_TRACEPOINTS */
+
+ /**
+ * @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a
+ * merged domain, this is stored in each layer's struct
+ * landlock_hierarchy instead.
+ */
+ struct access_masks quiet_masks;
+ /**
+ * @handled_masks: Contains the subset of filesystem and network actions
+ * that are handled by this ruleset.
+ */
+ struct access_masks handled_masks;
};
struct landlock_ruleset *
@@ -212,7 +199,6 @@ landlock_create_ruleset(const access_mask_t access_mask_fs,
const access_mask_t scope_mask);
void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
-void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T))
@@ -221,110 +207,44 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access, const u32 flags);
-struct landlock_ruleset *
-landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset);
+int landlock_store_rule(struct landlock_rules *const rules,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers);
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
- const struct landlock_id id);
-
-static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
-{
- if (ruleset)
- refcount_inc(&ruleset->usage);
-}
+void landlock_free_rules(struct landlock_rules *const rules);
/**
- * landlock_union_access_masks - Return all access rights handled in the
- * domain
+ * landlock_get_rule_root - Get the root of a rule tree by key type
*
- * @domain: Landlock ruleset (used as a domain)
+ * @rules: The rules storage to look up.
+ * @key_type: The type of key to select the tree for.
*
- * Return: An access_masks result of the OR of all the domain's access masks.
+ * Return: A pointer to the rb_root, or ERR_PTR(-EINVAL) on unknown type.
*/
-static inline struct access_masks
-landlock_union_access_masks(const struct landlock_ruleset *const domain)
+static inline struct rb_root *
+landlock_get_rule_root(struct landlock_rules *const rules,
+ const enum landlock_key_type key_type)
{
- union access_masks_all matches = {};
- size_t layer_level;
+ switch (key_type) {
+ case LANDLOCK_KEY_INODE:
+ return &rules->root_inode;
- for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
- union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
- };
+#if IS_ENABLED(CONFIG_INET)
+ case LANDLOCK_KEY_NET_PORT:
+ return &rules->root_net_port;
+#endif /* IS_ENABLED(CONFIG_INET) */
- matches.all |= layer.all;
+ default:
+ WARN_ON_ONCE(1);
+ return ERR_PTR(-EINVAL);
}
-
- return matches.masks;
-}
-
-static inline void
-landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t fs_access_mask,
- const u16 layer_level)
-{
- access_mask_t fs_mask = fs_access_mask & LANDLOCK_MASK_ACCESS_FS;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(fs_access_mask != fs_mask);
- ruleset->access_masks[layer_level].fs |= fs_mask;
}
-static inline void
-landlock_add_net_access_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t net_access_mask,
- const u16 layer_level)
-{
- access_mask_t net_mask = net_access_mask & LANDLOCK_MASK_ACCESS_NET;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(net_access_mask != net_mask);
- ruleset->access_masks[layer_level].net |= net_mask;
-}
-
-static inline void
-landlock_add_scope_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t scope_mask, const u16 layer_level)
-{
- access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(scope_mask != mask);
- ruleset->access_masks[layer_level].scope |= mask;
-}
-
-static inline access_mask_t
-landlock_get_fs_access_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- /* Handles all initially denied by default access rights. */
- return ruleset->access_masks[layer_level].fs |
- _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
-}
-
-static inline access_mask_t
-landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- return ruleset->access_masks[layer_level].net;
-}
-
-static inline access_mask_t
-landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
+static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
{
- return ruleset->access_masks[layer_level].scope;
+ if (ruleset)
+ refcount_inc(&ruleset->usage);
}
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_masks *masks);
-
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
- const access_mask_t access_request,
- struct layer_masks *masks,
- const enum landlock_key_type key_type);
-
#endif /* _SECURITY_LANDLOCK_RULESET_H */
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 36b02892c62f..1d02d57f4c48 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -22,6 +22,7 @@
#include <linux/mount.h>
#include <linux/path.h>
#include <linux/sched.h>
+#include <linux/sched/signal.h>
#include <linux/security.h>
#include <linux/stddef.h>
#include <linux/syscalls.h>
@@ -38,6 +39,8 @@
#include "setup.h"
#include "tsync.h"
+#include <trace/events/landlock.h>
+
static bool is_initialized(void)
{
if (likely(landlock_initialized))
@@ -169,7 +172,7 @@ static const struct file_operations ruleset_fops = {
* If the change involves a fix that requires userspace awareness, also update
* the errata documentation in Documentation/userspace-api/landlock.rst .
*/
-const int landlock_abi_version = 10;
+const int landlock_abi_version = 11;
/**
* sys_landlock_create_ruleset - Create a new ruleset
@@ -281,6 +284,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
+ /*
+ * Emits before anon_inode_getfd() installs the file descriptor, while
+ * the ruleset is still private to this thread: no lock is needed, and
+ * the event cannot race a concurrent close() freeing the ruleset under
+ * the tracepoint's BTF read. This is the last point at which the
+ * ruleset is guaranteed alive and unshared.
+ */
+ trace_landlock_create_ruleset(ruleset);
+
/* Creates anonymous FD referring to the ruleset. */
ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
ruleset, O_RDWR | O_CLOEXEC);
@@ -308,8 +320,6 @@ static struct landlock_ruleset *get_ruleset_from_fd(const int fd,
if (!(fd_file(ruleset_f)->f_mode & mode))
return ERR_PTR(-EPERM);
ruleset = fd_file(ruleset_f)->private_data;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return ERR_PTR(-EINVAL);
landlock_get_ruleset(ruleset);
return ruleset;
}
@@ -367,7 +377,7 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
- mask = ruleset->access_masks[0].fs;
+ mask = ruleset->handled_masks.fs;
if ((path_beneath_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -408,7 +418,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
- mask = landlock_get_net_access_mask(ruleset, 0);
+ mask = ruleset->handled_masks.net;
if ((net_port_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -502,21 +512,28 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
* - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
* - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
* - %LANDLOCK_RESTRICT_SELF_TSYNC
+ * - %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
*
* This system call enforces a Landlock ruleset on the current thread.
* Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its
* namespace or is running with no_new_privs. This avoids scenarios where
* unprivileged tasks can affect the behavior of privileged children.
*
+ * With %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS, the no_new_privs attribute of the
+ * calling thread is set only once the enforcement of the ruleset succeeded,
+ * which fulfills the above requirement: no_new_privs is set if and only if the
+ * call succeeds.
+ *
* Return: 0 on success, or -errno on failure. Possible returned errors are:
*
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EINVAL: @flags contains an unknown bit.
* - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
* - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
- * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
- * current thread is not running with no_new_privs, or it doesn't have
- * %CAP_SYS_ADMIN in its namespace.
+ * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or
+ * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread
+ * is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its
+ * namespace.
* - %E2BIG: The maximum number of stacked rulesets is reached for the current
* thread.
*
@@ -527,26 +544,30 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
+ struct landlock_domain *new_dom = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
+ bool process_wide;
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
prev_log_subdomains;
if (!is_initialized())
return -EOPNOTSUPP;
+ if ((flags | LANDLOCK_MASK_RESTRICT_SELF) !=
+ LANDLOCK_MASK_RESTRICT_SELF)
+ return -EINVAL;
+
/*
* Similar checks as for seccomp(2), except that an -EPERM may be
- * returned.
+ * returned. LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this
+ * requirement.
*/
- if (!task_no_new_privs(current) &&
+ if (!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
+ !task_no_new_privs(current) &&
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
- if ((flags | LANDLOCK_MASK_RESTRICT_SELF) !=
- LANDLOCK_MASK_RESTRICT_SELF)
- return -EINVAL;
-
/* Translates "off" flag to boolean. */
log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
/* Translates "on" flag to boolean. */
@@ -576,11 +597,11 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
new_llcred = landlock_cred(new_cred);
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
prev_log_subdomains = !new_llcred->log_subdomains_off;
new_llcred->log_subdomains_off = !prev_log_subdomains ||
!log_subdomains;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/*
* The only case when a ruleset may not be set is if
@@ -595,37 +616,91 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* manipulating the current credentials because they are
* dedicated per thread.
*/
- struct landlock_ruleset *const new_dom =
- landlock_merge_ruleset(new_llcred->domain, ruleset);
+ mutex_lock(&ruleset->lock);
+ new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
+ mutex_unlock(&ruleset->lock);
abort_creds(new_cred);
return PTR_ERR(new_dom);
}
+ /*
+ * Emits the domain-creation event while @ruleset->lock is still
+ * held, right after the merge, so an eBPF program attached to
+ * the tracepoint reads the exact ruleset that was merged into
+ * the domain: a consistent snapshot that a concurrent
+ * landlock_add_rule() (which holds the same lock) cannot
+ * modify.
+ *
+ * This must come before the thread-sync wait below. Holding
+ * @ruleset->lock across landlock_restrict_sibling_threads()
+ * would hang: a sibling thread blocked in landlock_add_rule()
+ * on the same @ruleset->lock cannot run the task_work that
+ * thread-sync waits for (the lock wait is uninterruptible).
+ * Emitting here keeps the lock off the thread-sync path.
+ *
+ * The trade-off is that the event fires for a domain that a
+ * later (rare) thread-sync failure aborts. That path emits the
+ * matching free_domain event so the create/free pair stays
+ * balanced (see the thread-sync error path below).
+ */
+ trace_landlock_create_domain(new_dom, ruleset);
+ mutex_unlock(&ruleset->lock);
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
new_dom->hierarchy->log_same_exec = log_same_exec;
new_dom->hierarchy->log_new_exec = log_new_exec;
+ /*
+ * The creation event fired above, so move the domain out of
+ * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
+ * too, even if a thread-sync failure aborts it below. Audit
+ * logging may still be disabled (DISABLED); tracing observes it
+ * anyway.
+ */
if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
-#endif /* CONFIG_AUDIT */
+ else
+ new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/* Replaces the old (prepared) domain. */
- landlock_put_ruleset(new_llcred->domain);
+ landlock_put_domain(new_llcred->domain);
new_llcred->domain = new_dom;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
new_llcred->domain_exec |= BIT(new_dom->num_layers - 1);
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
const int err = landlock_restrict_sibling_threads(
- current_cred(), new_cred);
+ current_cred(), new_cred, flags);
if (err) {
+ /*
+ * Thread-sync failed (rare), so the new domain is
+ * aborted instead of committed. Its creation event
+ * already fired above, so the imminent free must emit
+ * the matching free_domain event to keep the
+ * create/free pair balanced; no special log_status is
+ * set here.
+ */
abort_creds(new_cred);
return err;
}
}
- return commit_creds(new_cred);
+ /* Sets no_new_privs past the last point of failure. */
+ if (flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS)
+ task_set_no_new_privs(current);
+
+ /* Whole process: thread-sync swept siblings, or single-threaded. */
+ process_wide = (flags & LANDLOCK_RESTRICT_SELF_TSYNC) ||
+ get_nr_threads(current) == 1;
+ commit_creds(new_cred);
+
+ /* The caller commits last, so its event concludes the operation. */
+ if (ruleset)
+ trace_landlock_enforce_domain(new_dom, true, process_wide,
+ task_no_new_privs(current));
+
+ return 0;
}
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 360d226d0f51..4491ce31ae04 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -20,11 +20,11 @@
#include <net/af_unix.h>
#include <net/sock.h>
-#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
+#include "log.h"
#include "ruleset.h"
#include "setup.h"
#include "task.h"
@@ -41,8 +41,8 @@
* Return: True if @parent is an ancestor of or equal to @child, false
* otherwise.
*/
-static bool domain_scope_le(const struct landlock_ruleset *const parent,
- const struct landlock_ruleset *const child)
+static bool domain_scope_le(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
{
const struct landlock_hierarchy *walker;
@@ -63,8 +63,8 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,
return false;
}
-static int domain_ptrace(const struct landlock_ruleset *const parent,
- const struct landlock_ruleset *const child)
+static int domain_ptrace(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
{
if (domain_scope_le(parent, child))
return 0;
@@ -88,6 +88,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
const struct landlock_cred_security *parent_subject;
+ u64 tracee_domain_id = 0;
int err;
/* Quick return for non-landlocked tasks. */
@@ -96,9 +97,13 @@ static int hook_ptrace_access_check(struct task_struct *const child,
return 0;
scoped_guard(rcu) {
- const struct landlock_ruleset *const child_dom =
+ const struct landlock_domain *const child_dom =
landlock_get_task_domain(child);
err = domain_ptrace(parent_subject->domain, child_dom);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (child_dom)
+ tracee_domain_id = child_dom->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!err)
@@ -116,6 +121,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
.u.tsk = child,
},
.layer_plus_one = parent_subject->domain->num_layers,
+ .other_domain_id = tracee_domain_id,
});
return err;
@@ -135,7 +141,8 @@ static int hook_ptrace_access_check(struct task_struct *const child,
static int hook_ptrace_traceme(struct task_struct *const parent)
{
const struct landlock_cred_security *parent_subject;
- const struct landlock_ruleset *child_dom;
+ const struct landlock_domain *child_dom;
+ u64 tracee_domain_id = 0;
int err;
child_dom = landlock_get_current_domain();
@@ -147,6 +154,12 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
if (!err)
return 0;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ /* The tracee is the current task; its domain is stable here. */
+ if (child_dom)
+ tracee_domain_id = child_dom->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
/*
* For the ptrace_traceme case, we log the domain which is the cause of
* the denial, which means the parent domain instead of the current
@@ -161,6 +174,7 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
.u.tsk = current,
},
.layer_plus_one = parent_subject->domain->num_layers,
+ .other_domain_id = tracee_domain_id,
});
return err;
}
@@ -176,8 +190,8 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
* Return: True if @server is in a different domain from @client and @client
* is scoped to access @server (i.e. access should be denied), false otherwise.
*/
-static bool domain_is_scoped(const struct landlock_ruleset *const client,
- const struct landlock_ruleset *const server,
+static bool domain_is_scoped(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
access_mask_t scope)
{
int client_layer, server_layer;
@@ -236,13 +250,28 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
}
static bool sock_is_scoped(struct sock *const other,
- const struct landlock_ruleset *const domain)
+ const struct landlock_domain *const domain,
+ u64 *const peer_domain_id)
{
- const struct landlock_ruleset *dom_other;
+ const struct landlock_domain *dom_other;
/* The credentials will not change. */
lockdep_assert_held(&unix_sk(other)->lock);
+
+ /*
+ * A live kernel socket (e.g. from sock_create_kern()) has no backing
+ * file, hence no Landlock domain, so treat it as unscoped. The
+ * sk_socket check only guards that dereference; sk_socket is NULL
+ * solely for a dead peer, which the caller already excludes under the
+ * held lock, so no separate SOCK_DEAD check is needed.
+ */
+ if (unlikely(!other->sk_socket || !other->sk_socket->file))
+ return false;
+
dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ *peer_domain_id = dom_other ? dom_other->hierarchy->id : 0;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return domain_is_scoped(domain, dom_other,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
}
@@ -270,6 +299,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
struct sock *const newsk)
{
size_t handle_layer;
+ u64 peer_domain_id = 0;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
@@ -281,7 +311,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
if (!is_abstract_socket(other))
return 0;
- if (!sock_is_scoped(other, subject->domain))
+ if (!sock_is_scoped(other, subject->domain, &peer_domain_id))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@@ -293,6 +323,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
},
},
.layer_plus_one = handle_layer + 1,
+ .other_domain_id = peer_domain_id,
});
return -EPERM;
}
@@ -301,6 +332,7 @@ static int hook_unix_may_send(struct socket *const sock,
struct socket *const other)
{
size_t handle_layer;
+ u64 peer_domain_id = 0;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
@@ -318,7 +350,7 @@ static int hook_unix_may_send(struct socket *const sock,
if (!is_abstract_socket(other->sk))
return 0;
- if (!sock_is_scoped(other->sk, subject->domain))
+ if (!sock_is_scoped(other->sk, subject->domain, &peer_domain_id))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@@ -330,6 +362,7 @@ static int hook_unix_may_send(struct socket *const sock,
},
},
.layer_plus_one = handle_layer + 1,
+ .other_domain_id = peer_domain_id,
});
return -EPERM;
}
@@ -344,6 +377,7 @@ static int hook_task_kill(struct task_struct *const p,
{
bool is_scoped;
size_t handle_layer;
+ u64 target_domain_id = 0;
const struct landlock_cred_security *subject;
if (!cred) {
@@ -370,9 +404,15 @@ static int hook_task_kill(struct task_struct *const p,
return 0;
scoped_guard(rcu) {
- is_scoped = domain_is_scoped(subject->domain,
- landlock_get_task_domain(p),
+ const struct landlock_domain *const other =
+ landlock_get_task_domain(p);
+
+ is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (other)
+ target_domain_id = other->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!is_scoped)
@@ -385,6 +425,7 @@ static int hook_task_kill(struct task_struct *const p,
.u.tsk = p,
},
.layer_plus_one = handle_layer + 1,
+ .other_domain_id = target_domain_id,
});
return -EPERM;
}
@@ -394,6 +435,7 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
{
const struct landlock_cred_security *subject;
bool is_scoped = false;
+ u64 target_domain_id = 0;
/* Lock already held by send_sigio() and send_sigurg(). */
lockdep_assert_held(&fown->lock);
@@ -421,9 +463,15 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
return 0;
scoped_guard(rcu) {
- is_scoped = domain_is_scoped(subject->domain,
- landlock_get_task_domain(tsk),
+ const struct landlock_domain *const other =
+ landlock_get_task_domain(tsk);
+
+ is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (other)
+ target_domain_id = other->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!is_scoped)
@@ -435,9 +483,10 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
.type = LSM_AUDIT_DATA_TASK,
.u.tsk = tsk,
},
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+ .other_domain_id = target_domain_id,
});
return -EPERM;
}
diff --git a/security/landlock/trace.c b/security/landlock/trace.c
new file mode 100644
index 000000000000..2ea7aac8d75d
--- /dev/null
+++ b/security/landlock/trace.c
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/dcache.h>
+#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/lsm_audit.h>
+#include <net/sock.h>
+
+#include "access.h"
+#include "domain.h"
+#include "fs.h"
+#include "log.h"
+#include "ruleset.h"
+#include "trace.h"
+
+/*
+ * Generates the tracepoint definitions in this translation unit. The trace
+ * event header dereferences the traced objects in TP_fast_assign, so the full
+ * struct definitions (e.g. ruleset.h, domain.h) must be included before it.
+ */
+#define CREATE_TRACE_POINTS
+#include <trace/events/landlock.h>
+
+/**
+ * landlock_trace_free_domain - Emit a tracepoint on domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Fires only for a hierarchy whose creation event was emitted, i.e. one that
+ * left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). This keeps the
+ * create/free pair balanced: a hierarchy that never became observable is freed
+ * silently, while a domain that landlock_restrict_self() created and a
+ * thread-sync failure then aborted still fires free_domain, because its
+ * creation event already fired.
+ *
+ * Called from landlock_log_free_domain().
+ */
+void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ /*
+ * The log_status read is a correctness guard (keep the create/free pair
+ * balanced), not a cost guard, so this cold path needs no
+ * trace_..._enabled() check: the tracepoint is a static-branch no-op
+ * when disabled. The denial path guards trace_..._enabled() instead
+ * because it does expensive __getname()/path work before emitting.
+ */
+ if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED)
+ trace_landlock_free_domain(hierarchy);
+}
+
+/**
+ * landlock_trace_denial - Emit a tracepoint for a denied access request
+ *
+ * @request: Detail of the user space request.
+ * @youngest_denied: The youngest hierarchy node that denied the access.
+ * @missing: The set of denied access rights.
+ * @same_exec: Whether the current task is the same executable that called
+ * landlock_restrict_self() for the denying domain, as computed
+ * by landlock_log_denial().
+ * @logged: Whether the domain's policy selects this denial for logging, as
+ * computed by landlock_log_denial().
+ *
+ * Emits the tracepoint matching @request->type when its event is enabled.
+ * Unlike audit, fires regardless of @logged; the value is recorded in the event
+ * so consumers can filter on it.
+ *
+ * Called from landlock_log_denial().
+ */
+void landlock_trace_denial(
+ const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool same_exec, const bool logged)
+{
+ switch (request->type) {
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
+ if (trace_landlock_deny_access_fs_enabled()) {
+ char *buf __free(__putname) = __getname();
+ struct path dentry_path;
+ const char *pathname;
+ const struct path *path = NULL;
+
+ /*
+ * Selects the path from the audit data type, as
+ * dump_common_audit_data() does. A FS_ACCESS denial
+ * carries a file (hook_file_truncate) or an ioctl op
+ * (hook_file_ioctl) rather than a path;
+ * FS_CHANGE_TOPOLOGY carries a path or a bare dentry.
+ * Reading the wrong union member would dereference
+ * garbage, so every reachable type is handled here.
+ */
+ switch (request->audit.type) {
+ case LSM_AUDIT_DATA_FILE:
+ path = &request->audit.u.file->f_path;
+ break;
+ case LSM_AUDIT_DATA_IOCTL_OP:
+ path = &request->audit.u.op->path;
+ break;
+ case LSM_AUDIT_DATA_DENTRY:
+ /*
+ * Build a path on the stack with the real
+ * dentry so TP_fast_assign can extract dev and
+ * ino; the mnt field is unused there.
+ */
+ dentry_path = (struct path){
+ .dentry = request->audit.u.dentry,
+ };
+ path = &dentry_path;
+ break;
+ case LSM_AUDIT_DATA_PATH:
+ path = &request->audit.u.path;
+ break;
+ default:
+ WARN_ONCE(1,
+ "Unhandled Landlock FS audit type %d",
+ request->audit.type);
+ break;
+ }
+
+ if (!path)
+ break;
+
+ if (!buf) {
+ pathname = "<no_mem>";
+ } else if (request->audit.type ==
+ LSM_AUDIT_DATA_DENTRY) {
+ /* No vfsmount: render the dentry path alone. */
+ pathname = dentry_path_raw(
+ request->audit.u.dentry, buf, PATH_MAX);
+ if (IS_ERR(pathname))
+ pathname =
+ PTR_ERR(pathname) ==
+ -ENAMETOOLONG ?
+ "<too_long>" :
+ "<unreachable>";
+ } else {
+ pathname = resolve_path_for_trace(path, buf);
+ }
+
+ trace_landlock_deny_access_fs(youngest_denied,
+ same_exec, logged,
+ missing, path, pathname);
+ }
+ break;
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ if (trace_landlock_deny_access_net_enabled())
+ trace_landlock_deny_access_net(
+ youngest_denied, same_exec, logged, missing,
+ request->audit.u.net->sk,
+ ntohs(request->audit.u.net->sport),
+ ntohs(request->audit.u.net->dport));
+ break;
+ case LANDLOCK_REQUEST_PTRACE:
+ if (trace_landlock_deny_ptrace_enabled())
+ trace_landlock_deny_ptrace(youngest_denied, same_exec,
+ logged,
+ request->other_domain_id,
+ request->audit.u.tsk);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ if (trace_landlock_deny_scope_signal_enabled())
+ trace_landlock_deny_scope_signal(
+ youngest_denied, same_exec, logged,
+ request->other_domain_id, request->audit.u.tsk);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ if (trace_landlock_deny_scope_abstract_unix_socket_enabled())
+ trace_landlock_deny_scope_abstract_unix_socket(
+ youngest_denied, same_exec, logged,
+ request->other_domain_id,
+ request->audit.u.net->sk);
+ break;
+ default:
+ WARN_ONCE(1, "Unhandled Landlock request type %d",
+ request->type);
+ break;
+ }
+}
diff --git a/security/landlock/trace.h b/security/landlock/trace.h
new file mode 100644
index 000000000000..7be98e748855
--- /dev/null
+++ b/security/landlock/trace.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#ifndef _SECURITY_LANDLOCK_TRACE_H
+#define _SECURITY_LANDLOCK_TRACE_H
+
+#include "access.h"
+
+struct landlock_hierarchy;
+struct landlock_request;
+
+#ifdef CONFIG_TRACEPOINTS
+
+void landlock_trace_free_domain(
+ const struct landlock_hierarchy *const hierarchy);
+
+void landlock_trace_denial(
+ const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool same_exec, const bool logged);
+
+#else /* CONFIG_TRACEPOINTS */
+
+static inline void
+landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+}
+
+static inline void
+landlock_trace_denial(const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool same_exec,
+ const bool logged)
+{
+}
+
+#endif /* CONFIG_TRACEPOINTS */
+
+#endif /* _SECURITY_LANDLOCK_TRACE_H */
diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
index c5730bbd9ed3..cfa73a2a67ca 100644
--- a/security/landlock/tsync.c
+++ b/security/landlock/tsync.c
@@ -17,10 +17,13 @@
#include <linux/sched/task.h>
#include <linux/slab.h>
#include <linux/task_work.h>
+#include <uapi/linux/landlock.h>
#include "cred.h"
#include "tsync.h"
+#include <trace/events/landlock.h>
+
/*
* Shared state between multiple threads which are enforcing Landlock rulesets
* in lockstep with each other.
@@ -78,6 +81,8 @@ struct tsync_work {
*/
static void restrict_one_thread(struct tsync_shared_context *ctx)
{
+ const struct landlock_domain *new_dom =
+ landlock_cred(ctx->new_cred)->domain;
int err;
struct cred *cred = NULL;
@@ -146,6 +151,18 @@ static void restrict_one_thread(struct tsync_shared_context *ctx)
commit_creds(cred);
+ /*
+ * Emitted strictly after commit_creds() and before the out: label, so
+ * it fires only for a thread now enforcing new_dom, and every
+ * non-concluding (complete == false) event happens-before the
+ * operation's single concluding one. Skipped on the flags-only path,
+ * where old_cred and new_cred carry the same domain. A sibling never
+ * concludes the operation and its enforcement is always process-wide.
+ */
+ if (new_dom != landlock_cred(ctx->old_cred)->domain)
+ trace_landlock_enforce_domain(new_dom, false, true,
+ task_no_new_privs(current));
+
out:
/* Notify the calling thread once all threads are done */
if (atomic_dec_return(&ctx->num_unfinished) == 0)
@@ -466,7 +483,8 @@ static void cancel_tsync_works(const struct tsync_works *works,
* restrict_sibling_threads - enables a Landlock policy for all sibling threads
*/
int landlock_restrict_sibling_threads(const struct cred *old_cred,
- const struct cred *new_cred)
+ const struct cred *new_cred,
+ const u32 restrict_flags)
{
int err;
struct tsync_shared_context shared_ctx;
@@ -481,7 +499,9 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
init_completion(&shared_ctx.all_finished);
shared_ctx.old_cred = old_cred;
shared_ctx.new_cred = new_cred;
- shared_ctx.set_no_new_privs = task_no_new_privs(current);
+ shared_ctx.set_no_new_privs =
+ (restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) ||
+ task_no_new_privs(current);
/*
* Serialize concurrent TSYNC operations to prevent deadlocks when
diff --git a/security/landlock/tsync.h b/security/landlock/tsync.h
index ef86bb61c2f6..2ae4f938ca00 100644
--- a/security/landlock/tsync.h
+++ b/security/landlock/tsync.h
@@ -9,8 +9,10 @@
#define _SECURITY_LANDLOCK_TSYNC_H
#include <linux/cred.h>
+#include <linux/types.h>
int landlock_restrict_sibling_threads(const struct cred *old_cred,
- const struct cred *new_cred);
+ const struct cred *new_cred,
+ u32 restrict_flags);
#endif /* _SECURITY_LANDLOCK_TSYNC_H */