| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Landlock - Audit helpers |
| * |
| * Copyright © 2023-2025 Microsoft Corporation |
| */ |
| |
| #include <kunit/test.h> |
| #include <linux/audit.h> |
| #include <linux/bitops.h> |
| #include <linux/lsm_audit.h> |
| #include <linux/pid.h> |
| #include <uapi/linux/landlock.h> |
| |
| #include "access.h" |
| #include "audit.h" |
| #include "common.h" |
| #include "cred.h" |
| #include "domain.h" |
| #include "limits.h" |
| #include "ruleset.h" |
| |
| static const char *const fs_access_strings[] = { |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate", |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev", |
| }; |
| |
| static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS); |
| |
| static const char *const net_access_strings[] = { |
| [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp", |
| [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp", |
| }; |
| |
| static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET); |
| |
| static __attribute_const__ const char * |
| get_blocker(const enum landlock_request_type type, |
| const unsigned long access_bit) |
| { |
| switch (type) { |
| case LANDLOCK_REQUEST_PTRACE: |
| WARN_ON_ONCE(access_bit != -1); |
| return "ptrace"; |
| |
| case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY: |
| WARN_ON_ONCE(access_bit != -1); |
| return "fs.change_topology"; |
| |
| case LANDLOCK_REQUEST_FS_ACCESS: |
| if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings))) |
| return "unknown"; |
| return fs_access_strings[access_bit]; |
| |
| case LANDLOCK_REQUEST_NET_ACCESS: |
| if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings))) |
| return "unknown"; |
| return net_access_strings[access_bit]; |
| |
| case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: |
| WARN_ON_ONCE(access_bit != -1); |
| return "scope.abstract_unix_socket"; |
| |
| case LANDLOCK_REQUEST_SCOPE_SIGNAL: |
| WARN_ON_ONCE(access_bit != -1); |
| return "scope.signal"; |
| } |
| |
| WARN_ON_ONCE(1); |
| return "unknown"; |
| } |
| |
| static void log_blockers(struct audit_buffer *const ab, |
| const enum landlock_request_type type, |
| const access_mask_t access) |
| { |
| const unsigned long access_mask = access; |
| unsigned long access_bit; |
| bool is_first = true; |
| |
| for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) { |
| audit_log_format(ab, "%s%s", is_first ? "" : ",", |
| get_blocker(type, access_bit)); |
| is_first = false; |
| } |
| if (is_first) |
| audit_log_format(ab, "%s", get_blocker(type, -1)); |
| } |
| |
| static void log_domain(struct landlock_hierarchy *const hierarchy) |
| { |
| struct audit_buffer *ab; |
| |
| /* Ignores already logged domains. */ |
| if (READ_ONCE(hierarchy->log_status) == LANDLOCK_LOG_RECORDED) |
| return; |
| |
| /* Uses consistent allocation flags wrt common_lsm_audit(). */ |
| ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, |
| AUDIT_LANDLOCK_DOMAIN); |
| if (!ab) |
| return; |
| |
| WARN_ON_ONCE(hierarchy->id == 0); |
| audit_log_format( |
| ab, |
| "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=", |
| hierarchy->id, pid_nr(hierarchy->details->pid), |
| hierarchy->details->uid); |
| audit_log_untrustedstring(ab, hierarchy->details->exe_path); |
| audit_log_format(ab, " comm="); |
| audit_log_untrustedstring(ab, hierarchy->details->comm); |
| audit_log_end(ab); |
| |
| /* |
| * There may be race condition leading to logging of the same domain |
| * several times but that is OK. |
| */ |
| WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED); |
| } |
| |
| static struct landlock_hierarchy * |
| get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer) |
| { |
| struct landlock_hierarchy *hierarchy = domain->hierarchy; |
| ssize_t i; |
| |
| if (WARN_ON_ONCE(layer >= domain->num_layers)) |
| return hierarchy; |
| |
| for (i = domain->num_layers - 1; i > layer; i--) { |
| if (WARN_ON_ONCE(!hierarchy->parent)) |
| break; |
| |
| hierarchy = hierarchy->parent; |
| } |
| |
| return hierarchy; |
| } |
| |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST |
| |
| static void test_get_hierarchy(struct kunit *const test) |
| { |
| struct landlock_hierarchy dom0_hierarchy = { |
| .id = 10, |
| }; |
| struct landlock_hierarchy dom1_hierarchy = { |
| .parent = &dom0_hierarchy, |
| .id = 20, |
| }; |
| struct landlock_hierarchy dom2_hierarchy = { |
| .parent = &dom1_hierarchy, |
| .id = 30, |
| }; |
| struct landlock_ruleset dom2 = { |
| .hierarchy = &dom2_hierarchy, |
| .num_layers = 3, |
| }; |
| |
| KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id); |
| KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id); |
| KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id); |
| KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); |
| } |
| |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ |
| |
| static size_t get_denied_layer(const struct landlock_ruleset *const domain, |
| access_mask_t *const access_request, |
| const layer_mask_t (*const layer_masks)[], |
| const size_t layer_masks_size) |
| { |
| const unsigned long access_req = *access_request; |
| unsigned long access_bit; |
| access_mask_t missing = 0; |
| long youngest_layer = -1; |
| |
| for_each_set_bit(access_bit, &access_req, layer_masks_size) { |
| const access_mask_t mask = (*layer_masks)[access_bit]; |
| long layer; |
| |
| if (!mask) |
| continue; |
| |
| /* __fls(1) == 0 */ |
| layer = __fls(mask); |
| if (layer > youngest_layer) { |
| youngest_layer = layer; |
| missing = BIT(access_bit); |
| } else if (layer == youngest_layer) { |
| missing |= BIT(access_bit); |
| } |
| } |
| |
| *access_request = missing; |
| if (youngest_layer == -1) |
| return domain->num_layers - 1; |
| |
| return youngest_layer; |
| } |
| |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST |
| |
| static void test_get_denied_layer(struct kunit *const test) |
| { |
| const struct landlock_ruleset dom = { |
| .num_layers = 5, |
| }; |
| const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT(0), |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = BIT(1), |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = BIT(1) | BIT(0), |
| [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = BIT(2), |
| }; |
| access_mask_t access; |
| |
| access = LANDLOCK_ACCESS_FS_EXECUTE; |
| KUNIT_EXPECT_EQ(test, 0, |
| get_denied_layer(&dom, &access, &layer_masks, |
| sizeof(layer_masks))); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE); |
| |
| access = LANDLOCK_ACCESS_FS_READ_FILE; |
| KUNIT_EXPECT_EQ(test, 1, |
| get_denied_layer(&dom, &access, &layer_masks, |
| sizeof(layer_masks))); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE); |
| |
| access = LANDLOCK_ACCESS_FS_READ_DIR; |
| KUNIT_EXPECT_EQ(test, 1, |
| get_denied_layer(&dom, &access, &layer_masks, |
| sizeof(layer_masks))); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR); |
| |
| access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR; |
| KUNIT_EXPECT_EQ(test, 1, |
| get_denied_layer(&dom, &access, &layer_masks, |
| sizeof(layer_masks))); |
| KUNIT_EXPECT_EQ(test, access, |
| LANDLOCK_ACCESS_FS_READ_FILE | |
| LANDLOCK_ACCESS_FS_READ_DIR); |
| |
| access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR; |
| KUNIT_EXPECT_EQ(test, 1, |
| get_denied_layer(&dom, &access, &layer_masks, |
| sizeof(layer_masks))); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR); |
| |
| access = LANDLOCK_ACCESS_FS_WRITE_FILE; |
| KUNIT_EXPECT_EQ(test, 4, |
| get_denied_layer(&dom, &access, &layer_masks, |
| sizeof(layer_masks))); |
| KUNIT_EXPECT_EQ(test, access, 0); |
| } |
| |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ |
| |
| static size_t |
| get_layer_from_deny_masks(access_mask_t *const access_request, |
| const access_mask_t all_existing_optional_access, |
| const deny_masks_t deny_masks) |
| { |
| const unsigned long access_opt = all_existing_optional_access; |
| const unsigned long access_req = *access_request; |
| access_mask_t missing = 0; |
| size_t youngest_layer = 0; |
| size_t access_index = 0; |
| unsigned long access_bit; |
| |
| /* This will require change with new object types. */ |
| WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL); |
| |
| for_each_set_bit(access_bit, &access_opt, |
| BITS_PER_TYPE(access_mask_t)) { |
| if (access_req & BIT(access_bit)) { |
| const size_t layer = |
| (deny_masks >> (access_index * 4)) & |
| (LANDLOCK_MAX_NUM_LAYERS - 1); |
| |
| if (layer > youngest_layer) { |
| youngest_layer = layer; |
| missing = BIT(access_bit); |
| } else if (layer == youngest_layer) { |
| missing |= BIT(access_bit); |
| } |
| } |
| access_index++; |
| } |
| |
| *access_request = missing; |
| return youngest_layer; |
| } |
| |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST |
| |
| static void test_get_layer_from_deny_masks(struct kunit *const test) |
| { |
| deny_masks_t deny_mask; |
| access_mask_t access; |
| |
| /* truncate:0 ioctl_dev:2 */ |
| deny_mask = 0x20; |
| |
| access = LANDLOCK_ACCESS_FS_TRUNCATE; |
| KUNIT_EXPECT_EQ(test, 0, |
| get_layer_from_deny_masks(&access, |
| _LANDLOCK_ACCESS_FS_OPTIONAL, |
| deny_mask)); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); |
| |
| access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; |
| KUNIT_EXPECT_EQ(test, 2, |
| get_layer_from_deny_masks(&access, |
| _LANDLOCK_ACCESS_FS_OPTIONAL, |
| deny_mask)); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); |
| |
| /* truncate:15 ioctl_dev:15 */ |
| deny_mask = 0xff; |
| |
| access = LANDLOCK_ACCESS_FS_TRUNCATE; |
| KUNIT_EXPECT_EQ(test, 15, |
| get_layer_from_deny_masks(&access, |
| _LANDLOCK_ACCESS_FS_OPTIONAL, |
| deny_mask)); |
| KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); |
| |
| access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; |
| KUNIT_EXPECT_EQ(test, 15, |
| get_layer_from_deny_masks(&access, |
| _LANDLOCK_ACCESS_FS_OPTIONAL, |
| deny_mask)); |
| KUNIT_EXPECT_EQ(test, access, |
| LANDLOCK_ACCESS_FS_TRUNCATE | |
| LANDLOCK_ACCESS_FS_IOCTL_DEV); |
| } |
| |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ |
| |
| static bool is_valid_request(const struct landlock_request *const request) |
| { |
| if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS)) |
| return false; |
| |
| if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access))) |
| return false; |
| |
| if (request->access) { |
| if (WARN_ON_ONCE(!(!!request->layer_masks ^ |
| !!request->all_existing_optional_access))) |
| return false; |
| } else { |
| if (WARN_ON_ONCE(request->layer_masks || |
| request->all_existing_optional_access)) |
| return false; |
| } |
| |
| if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size)) |
| return false; |
| |
| if (request->deny_masks) { |
| if (WARN_ON_ONCE(!request->all_existing_optional_access)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * landlock_log_denial - Create audit records related to a denial |
| * |
| * @subject: The Landlock subject's credential denying an action. |
| * @request: Detail of the user space request. |
| */ |
| void landlock_log_denial(const struct landlock_cred_security *const subject, |
| const struct landlock_request *const request) |
| { |
| struct audit_buffer *ab; |
| struct landlock_hierarchy *youngest_denied; |
| size_t youngest_layer; |
| access_mask_t missing; |
| |
| if (WARN_ON_ONCE(!subject || !subject->domain || |
| !subject->domain->hierarchy || !request)) |
| return; |
| |
| if (!is_valid_request(request)) |
| return; |
| |
| missing = request->access; |
| if (missing) { |
| /* Gets the nearest domain that denies the request. */ |
| if (request->layer_masks) { |
| youngest_layer = get_denied_layer( |
| subject->domain, &missing, request->layer_masks, |
| request->layer_masks_size); |
| } else { |
| youngest_layer = get_layer_from_deny_masks( |
| &missing, request->all_existing_optional_access, |
| request->deny_masks); |
| } |
| youngest_denied = |
| get_hierarchy(subject->domain, youngest_layer); |
| } else { |
| youngest_layer = request->layer_plus_one - 1; |
| youngest_denied = |
| get_hierarchy(subject->domain, youngest_layer); |
| } |
| |
| if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED) |
| return; |
| |
| /* |
| * Consistently keeps track of the number of denied access requests |
| * even if audit is currently disabled, or if audit rules currently |
| * exclude this record type, or if landlock_restrict_self(2)'s flags |
| * quiet logs. |
| */ |
| atomic64_inc(&youngest_denied->num_denials); |
| |
| if (!audit_enabled) |
| return; |
| |
| /* Checks if the current exec was restricting itself. */ |
| if (subject->domain_exec & (1 << youngest_layer)) { |
| /* Ignores denials for the same execution. */ |
| if (!youngest_denied->log_same_exec) |
| return; |
| } else { |
| /* Ignores denials after a new execution. */ |
| if (!youngest_denied->log_new_exec) |
| return; |
| } |
| |
| /* Uses consistent allocation flags wrt common_lsm_audit(). */ |
| ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, |
| AUDIT_LANDLOCK_ACCESS); |
| if (!ab) |
| return; |
| |
| audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id); |
| log_blockers(ab, request->type, missing); |
| audit_log_lsm_data(ab, &request->audit); |
| audit_log_end(ab); |
| |
| /* Logs this domain the first time it shows in log. */ |
| log_domain(youngest_denied); |
| } |
| |
| /** |
| * landlock_log_drop_domain - Create an audit record on domain deallocation |
| * |
| * @hierarchy: The domain's hierarchy being deallocated. |
| * |
| * Only domains which previously appeared in the audit logs are logged again. |
| * This is useful to know when a domain will never show again in the audit log. |
| * |
| * Called in a work queue scheduled by landlock_put_ruleset_deferred() called |
| * by hook_cred_free(). |
| */ |
| void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy) |
| { |
| struct audit_buffer *ab; |
| |
| if (WARN_ON_ONCE(!hierarchy)) |
| return; |
| |
| if (!audit_enabled) |
| return; |
| |
| /* Ignores domains that were not logged. */ |
| if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED) |
| return; |
| |
| /* |
| * If logging of domain allocation succeeded, warns about failure to log |
| * domain deallocation to highlight unbalanced domain lifetime logs. |
| */ |
| ab = audit_log_start(audit_context(), GFP_KERNEL, |
| AUDIT_LANDLOCK_DOMAIN); |
| if (!ab) |
| return; |
| |
| audit_log_format(ab, "domain=%llx status=deallocated denials=%llu", |
| hierarchy->id, atomic64_read(&hierarchy->num_denials)); |
| audit_log_end(ab); |
| } |
| |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST |
| |
| static struct kunit_case test_cases[] = { |
| /* clang-format off */ |
| KUNIT_CASE(test_get_hierarchy), |
| KUNIT_CASE(test_get_denied_layer), |
| KUNIT_CASE(test_get_layer_from_deny_masks), |
| {} |
| /* clang-format on */ |
| }; |
| |
| static struct kunit_suite test_suite = { |
| .name = "landlock_audit", |
| .test_cases = test_cases, |
| }; |
| |
| kunit_test_suite(test_suite); |
| |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ |