summaryrefslogtreecommitdiff
path: root/security
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2025-03-28 12:37:13 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2025-03-28 12:37:13 -0700
commit72885116069abdd05c245707c3989fc605632970 (patch)
tree7afe47997b8768f2a2300c08d69416d1524360af /security
parent78fb88eca684ed6f09f01a232c925b6da75d8131 (diff)
parent8e2dd47b10e77452733eae23cc83078fa29c1e9a (diff)
downloadlinux-72885116069abdd05c245707c3989fc605632970.tar.gz
linux-72885116069abdd05c245707c3989fc605632970.tar.bz2
linux-72885116069abdd05c245707c3989fc605632970.zip
Merge tag 'landlock-6.15-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux
Pull landlock updates from Mickaël Salaün: "This brings two main changes to Landlock: - A signal scoping fix with a new interface for user space to know if it is compatible with the running kernel. - Audit support to give visibility on why access requests are denied, including the origin of the security policy, missing access rights, and description of object(s). This was designed to limit log spam as much as possible while still alerting about unexpected blocked access. With these changes come new and improved documentation, and a lot of new tests" * tag 'landlock-6.15-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: (36 commits) landlock: Add audit documentation selftests/landlock: Add audit tests for network selftests/landlock: Add audit tests for filesystem selftests/landlock: Add audit tests for abstract UNIX socket scoping selftests/landlock: Add audit tests for ptrace selftests/landlock: Test audit with restrict flags selftests/landlock: Add tests for audit flags and domain IDs selftests/landlock: Extend tests for landlock_restrict_self(2)'s flags selftests/landlock: Add test for invalid ruleset file descriptor samples/landlock: Enable users to log sandbox denials landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF landlock: Add LANDLOCK_RESTRICT_SELF_LOG_*_EXEC_* flags landlock: Log scoped denials landlock: Log TCP bind and connect denials landlock: Log truncate and IOCTL denials landlock: Factor out IOCTL hooks landlock: Log file-related denials landlock: Log mount-related denials landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status landlock: Add AUDIT_LANDLOCK_ACCESS and log ptrace denials ...
Diffstat (limited to 'security')
-rw-r--r--security/landlock/.kunitconfig2
-rw-r--r--security/landlock/Makefile5
-rw-r--r--security/landlock/access.h25
-rw-r--r--security/landlock/audit.c522
-rw-r--r--security/landlock/audit.h76
-rw-r--r--security/landlock/cred.c28
-rw-r--r--security/landlock/cred.h92
-rw-r--r--security/landlock/domain.c264
-rw-r--r--security/landlock/domain.h174
-rw-r--r--security/landlock/errata.h99
-rw-r--r--security/landlock/errata/abi-4.h15
-rw-r--r--security/landlock/errata/abi-6.h19
-rw-r--r--security/landlock/fs.c321
-rw-r--r--security/landlock/fs.h40
-rw-r--r--security/landlock/id.c251
-rw-r--r--security/landlock/id.h25
-rw-r--r--security/landlock/limits.h7
-rw-r--r--security/landlock/net.c78
-rw-r--r--security/landlock/ruleset.c30
-rw-r--r--security/landlock/ruleset.h48
-rw-r--r--security/landlock/setup.c40
-rw-r--r--security/landlock/setup.h3
-rw-r--r--security/landlock/syscalls.c99
-rw-r--r--security/landlock/task.c257
-rw-r--r--security/lsm_audit.c27
25 files changed, 2283 insertions, 264 deletions
diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig
index 03e119466604..f9423f01ac5b 100644
--- a/security/landlock/.kunitconfig
+++ b/security/landlock/.kunitconfig
@@ -1,4 +1,6 @@
+CONFIG_AUDIT=y
CONFIG_KUNIT=y
+CONFIG_NET=y
CONFIG_SECURITY=y
CONFIG_SECURITY_LANDLOCK=y
CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index b4538b7cf7d2..3160c2bdac1d 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -4,3 +4,8 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \
cred.o task.o fs.o
landlock-$(CONFIG_INET) += net.o
+
+landlock-$(CONFIG_AUDIT) += \
+ id.o \
+ audit.o \
+ domain.o
diff --git a/security/landlock/access.h b/security/landlock/access.h
index 74fd8f399fbd..7961c6630a2d 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
- * Landlock LSM - Access types and helpers
+ * Landlock - Access types and helpers
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
@@ -28,6 +28,12 @@
LANDLOCK_ACCESS_FS_REFER)
/* clang-format on */
+/* clang-format off */
+#define _LANDLOCK_ACCESS_FS_OPTIONAL ( \
+ LANDLOCK_ACCESS_FS_TRUNCATE | \
+ LANDLOCK_ACCESS_FS_IOCTL_DEV)
+/* clang-format on */
+
typedef u16 access_mask_t;
/* Makes sure all filesystem access rights can be stored. */
@@ -60,6 +66,23 @@ typedef u16 layer_mask_t;
/* Makes sure all layers can be checked. */
static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
+/*
+ * Tracks domains responsible of a denied access. This is required to avoid
+ * storing in each object the full layer_masks[] required by update_request().
+ */
+typedef u8 deny_masks_t;
+
+/*
+ * Makes sure all optional access rights can be tied to a layer index (cf.
+ * get_deny_mask).
+ */
+static_assert(BITS_PER_TYPE(deny_masks_t) >=
+ (HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) *
+ HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)));
+
+/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */
+static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1);
+
/* Upgrades with all initially denied by default access rights. */
static inline struct access_masks
landlock_upgrade_handled_access_masks(struct access_masks access_masks)
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
new file mode 100644
index 000000000000..7e5e0ed0e4e5
--- /dev/null
+++ b/security/landlock/audit.c
@@ -0,0 +1,522 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Audit helpers
+ *
+ * Copyright © 2023-2025 Microsoft Corporation
+ */
+
+#include <kunit/test.h>
+#include <linux/audit.h>
+#include <linux/bitops.h>
+#include <linux/lsm_audit.h>
+#include <linux/pid.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 "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",
+};
+
+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",
+};
+
+static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
+
+static __attribute_const__ const char *
+get_blocker(const enum landlock_request_type type,
+ const unsigned long access_bit)
+{
+ switch (type) {
+ case LANDLOCK_REQUEST_PTRACE:
+ WARN_ON_ONCE(access_bit != -1);
+ return "ptrace";
+
+ case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
+ WARN_ON_ONCE(access_bit != -1);
+ return "fs.change_topology";
+
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
+ return "unknown";
+ return fs_access_strings[access_bit];
+
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings)))
+ return "unknown";
+ return net_access_strings[access_bit];
+
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ WARN_ON_ONCE(access_bit != -1);
+ return "scope.abstract_unix_socket";
+
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ WARN_ON_ONCE(access_bit != -1);
+ return "scope.signal";
+ }
+
+ WARN_ON_ONCE(1);
+ return "unknown";
+}
+
+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;
+ 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 ? "" : ",",
+ get_blocker(type, access_bit));
+ is_first = false;
+ }
+ if (is_first)
+ audit_log_format(ab, "%s", get_blocker(type, -1));
+}
+
+static void log_domain(struct landlock_hierarchy *const hierarchy)
+{
+ struct audit_buffer *ab;
+
+ /* Ignores already logged domains. */
+ if (READ_ONCE(hierarchy->log_status) == LANDLOCK_LOG_RECORDED)
+ return;
+
+ /* Uses consistent allocation flags wrt common_lsm_audit(). */
+ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ WARN_ON_ONCE(hierarchy->id == 0);
+ audit_log_format(
+ ab,
+ "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
+ hierarchy->id, pid_nr(hierarchy->details->pid),
+ hierarchy->details->uid);
+ audit_log_untrustedstring(ab, hierarchy->details->exe_path);
+ audit_log_format(ab, " comm=");
+ audit_log_untrustedstring(ab, hierarchy->details->comm);
+ audit_log_end(ab);
+
+ /*
+ * There may be race condition leading to logging of the same domain
+ * several times but that is OK.
+ */
+ 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 */
+
+static size_t get_denied_layer(const struct landlock_ruleset *const domain,
+ access_mask_t *const access_request,
+ const layer_mask_t (*const layer_masks)[],
+ const size_t layer_masks_size)
+{
+ const unsigned long access_req = *access_request;
+ unsigned long access_bit;
+ access_mask_t missing = 0;
+ long youngest_layer = -1;
+
+ for_each_set_bit(access_bit, &access_req, layer_masks_size) {
+ const access_mask_t mask = (*layer_masks)[access_bit];
+ long layer;
+
+ if (!mask)
+ continue;
+
+ /* __fls(1) == 0 */
+ layer = __fls(mask);
+ if (layer > youngest_layer) {
+ youngest_layer = layer;
+ missing = BIT(access_bit);
+ } else if (layer == youngest_layer) {
+ missing |= BIT(access_bit);
+ }
+ }
+
+ *access_request = missing;
+ if (youngest_layer == -1)
+ return domain->num_layers - 1;
+
+ return youngest_layer;
+}
+
+#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 layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT(0),
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = BIT(1),
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = BIT(1) | BIT(0),
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = BIT(2),
+ };
+ access_mask_t access;
+
+ access = LANDLOCK_ACCESS_FS_EXECUTE;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_denied_layer(&dom, &access, &layer_masks,
+ sizeof(layer_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, &layer_masks,
+ sizeof(layer_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, &layer_masks,
+ sizeof(layer_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, &layer_masks,
+ sizeof(layer_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, &layer_masks,
+ sizeof(layer_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, &layer_masks,
+ sizeof(layer_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)
+{
+ 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;
+
+ /* 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 * 4)) &
+ (LANDLOCK_MAX_NUM_LAYERS - 1);
+
+ if (layer > youngest_layer) {
+ youngest_layer = layer;
+ missing = BIT(access_bit);
+ } else if (layer == youngest_layer) {
+ missing |= BIT(access_bit);
+ }
+ }
+ access_index++;
+ }
+
+ *access_request = missing;
+ 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;
+
+ /* truncate:0 ioctl_dev:2 */
+ deny_mask = 0x20;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 0,
+ get_layer_from_deny_masks(&access,
+ _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+
+ 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));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+
+ /* truncate:15 ioctl_dev:15 */
+ deny_mask = 0xff;
+
+ access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ KUNIT_EXPECT_EQ(test, 15,
+ get_layer_from_deny_masks(&access,
+ _LANDLOCK_ACCESS_FS_OPTIONAL,
+ deny_mask));
+ KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+
+ 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));
+ KUNIT_EXPECT_EQ(test, access,
+ LANDLOCK_ACCESS_FS_TRUNCATE |
+ LANDLOCK_ACCESS_FS_IOCTL_DEV);
+}
+
+#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 (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size))
+ return false;
+
+ if (request->deny_masks) {
+ if (WARN_ON_ONCE(!request->all_existing_optional_access))
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * landlock_log_denial - Create audit records related to a denial
+ *
+ * @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 audit_buffer *ab;
+ struct landlock_hierarchy *youngest_denied;
+ size_t youngest_layer;
+ access_mask_t missing;
+
+ 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,
+ request->layer_masks_size);
+ } else {
+ youngest_layer = get_layer_from_deny_masks(
+ &missing, request->all_existing_optional_access,
+ request->deny_masks);
+ }
+ 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 & (1 << 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;
+ }
+
+ /* Uses consistent allocation flags wrt common_lsm_audit(). */
+ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
+ AUDIT_LANDLOCK_ACCESS);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id);
+ log_blockers(ab, request->type, missing);
+ audit_log_lsm_data(ab, &request->audit);
+ audit_log_end(ab);
+
+ /* Logs this domain the first time it shows in log. */
+ log_domain(youngest_denied);
+}
+
+/**
+ * landlock_log_drop_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().
+ */
+void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ struct audit_buffer *ab;
+
+ if (WARN_ON_ONCE(!hierarchy))
+ return;
+
+ if (!audit_enabled)
+ return;
+
+ /* Ignores domains that were not logged. */
+ if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
+ return;
+
+ /*
+ * If logging of domain allocation succeeded, warns about failure to log
+ * domain deallocation to highlight unbalanced domain lifetime logs.
+ */
+ ab = audit_log_start(audit_context(), GFP_KERNEL,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
+ 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
new file mode 100644
index 000000000000..92428b7fc4d8
--- /dev/null
+++ b/security/landlock/audit.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Audit helpers
+ *
+ * Copyright © 2023-2025 Microsoft Corporation
+ */
+
+#ifndef _SECURITY_LANDLOCK_AUDIT_H
+#define _SECURITY_LANDLOCK_AUDIT_H
+
+#include <linux/audit.h>
+#include <linux/lsm_audit.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 layer_mask_t (*layer_masks)[];
+ size_t layer_masks_size;
+
+ /* Required fields for requests with deny masks. */
+ const access_mask_t all_existing_optional_access;
+ deny_masks_t deny_masks;
+};
+
+#ifdef CONFIG_AUDIT
+
+void landlock_log_drop_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_AUDIT */
+
+static inline void
+landlock_log_drop_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_AUDIT */
+
+#endif /* _SECURITY_LANDLOCK_AUDIT_H */
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index db9fe7d906ba..0cb3edde4d18 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -1,11 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
- * Landlock LSM - Credential hooks
+ * Landlock - Credential hooks
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2024-2025 Microsoft Corporation
*/
+#include <linux/binfmts.h>
#include <linux/cred.h>
#include <linux/lsm_hooks.h>
@@ -17,11 +19,12 @@
static void hook_cred_transfer(struct cred *const new,
const struct cred *const old)
{
- struct landlock_ruleset *const old_dom = landlock_cred(old)->domain;
+ const struct landlock_cred_security *const old_llcred =
+ landlock_cred(old);
- if (old_dom) {
- landlock_get_ruleset(old_dom);
- landlock_cred(new)->domain = old_dom;
+ if (old_llcred->domain) {
+ landlock_get_ruleset(old_llcred->domain);
+ *landlock_cred(new) = *old_llcred;
}
}
@@ -40,10 +43,25 @@ static void hook_cred_free(struct cred *const cred)
landlock_put_ruleset_deferred(dom);
}
+#ifdef CONFIG_AUDIT
+
+static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
+{
+ /* Resets for each execution. */
+ landlock_cred(bprm->cred)->domain_exec = 0;
+ return 0;
+}
+
+#endif /* CONFIG_AUDIT */
+
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
+ LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
+#endif /* CONFIG_AUDIT */
};
__init void landlock_add_cred_hooks(void)
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index bf755459838a..c82fe63ec598 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -1,24 +1,63 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
- * Landlock LSM - Credential hooks
+ * Landlock - Credential hooks
*
* Copyright © 2019-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
+ * Copyright © 2021-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_CRED_H
#define _SECURITY_LANDLOCK_CRED_H
+#include <linux/container_of.h>
#include <linux/cred.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
+#include "access.h"
+#include "limits.h"
#include "ruleset.h"
#include "setup.h"
+/**
+ * struct landlock_cred_security - Credential security blob
+ *
+ * This structure is packed to minimize the size of struct
+ * landlock_file_security. However, it is always aligned in the LSM cred blob,
+ * see lsm_set_blob_size().
+ */
struct landlock_cred_security {
+ /**
+ * @domain: Immutable ruleset enforced on a task.
+ */
struct landlock_ruleset *domain;
-};
+
+#ifdef CONFIG_AUDIT
+ /**
+ * @domain_exec: Bitmask identifying the domain layers that were enforced by
+ * the current task's executed file (i.e. no new execve(2) since
+ * landlock_restrict_self(2)).
+ */
+ u16 domain_exec;
+ /**
+ * @log_subdomains_off: Set if the domain descendants's log_status should be
+ * set to %LANDLOCK_LOG_DISABLED. This is not a landlock_hierarchy
+ * configuration because it applies to future descendant domains and it does
+ * not require a current domain.
+ */
+ u8 log_subdomains_off : 1;
+#endif /* CONFIG_AUDIT */
+} __packed;
+
+#ifdef CONFIG_AUDIT
+
+/* 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 */
static inline struct landlock_cred_security *
landlock_cred(const struct cred *cred)
@@ -53,6 +92,55 @@ static inline bool landlocked(const struct task_struct *const task)
return has_dom;
}
+/**
+ * landlock_get_applicable_subject - Return the subject's Landlock credential
+ * if its enforced domain applies to (i.e.
+ * handles) at least one of the access rights
+ * specified in @masks
+ *
+ * @cred: credential
+ * @masks: access masks
+ * @handle_layer: returned youngest layer handling a subset of @masks. Not set
+ * if the function returns NULL.
+ *
+ * Returns: landlock_cred(@cred) if any access rights specified in @masks is
+ * handled, or NULL otherwise.
+ */
+static inline const struct landlock_cred_security *
+landlock_get_applicable_subject(const struct cred *const cred,
+ const struct access_masks masks,
+ size_t *const handle_layer)
+{
+ const union access_masks_all masks_all = {
+ .masks = masks,