diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-08-21 12:28:35 -0700 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-08-21 12:28:35 -0700 |
| commit | 7199989f3f3194d653b024ce8e79cea6b15e38b9 (patch) | |
| tree | 00801d41a37df6bdd737ede8798f68006be3ca88 /security | |
| parent | 614b9fb585f143d65162f17f1a4b4ec4cbdf5794 (diff) | |
| parent | 172b6a6d8463562b0cbebfd66f770b078f81966b (diff) | |
| download | linux-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/Kconfig | 5 | ||||
| -rw-r--r-- | security/landlock/Makefile | 12 | ||||
| -rw-r--r-- | security/landlock/access.h | 6 | ||||
| -rw-r--r-- | security/landlock/audit.c | 641 | ||||
| -rw-r--r-- | security/landlock/audit.h | 57 | ||||
| -rw-r--r-- | security/landlock/cred.c | 14 | ||||
| -rw-r--r-- | security/landlock/cred.h | 29 | ||||
| -rw-r--r-- | security/landlock/domain.c | 472 | ||||
| -rw-r--r-- | security/landlock/domain.h | 163 | ||||
| -rw-r--r-- | security/landlock/errata/abi-1.h | 23 | ||||
| -rw-r--r-- | security/landlock/fs.c | 259 | ||||
| -rw-r--r-- | security/landlock/fs.h | 40 | ||||
| -rw-r--r-- | security/landlock/id.h | 6 | ||||
| -rw-r--r-- | security/landlock/limits.h | 2 | ||||
| -rw-r--r-- | security/landlock/log.c | 587 | ||||
| -rw-r--r-- | security/landlock/log.h | 86 | ||||
| -rw-r--r-- | security/landlock/net.c | 38 | ||||
| -rw-r--r-- | security/landlock/ruleset.c | 546 | ||||
| -rw-r--r-- | security/landlock/ruleset.h | 250 | ||||
| -rw-r--r-- | security/landlock/syscalls.c | 125 | ||||
| -rw-r--r-- | security/landlock/task.c | 87 | ||||
| -rw-r--r-- | security/landlock/trace.c | 185 | ||||
| -rw-r--r-- | security/landlock/trace.h | 44 | ||||
| -rw-r--r-- | security/landlock/tsync.c | 24 | ||||
| -rw-r--r-- | security/landlock/tsync.h | 4 |
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 */ |
