| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Landlock - Audit helpers |
| * |
| * Copyright © 2023-2025 Microsoft Corporation |
| */ |
| |
| #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> |
| |
| #include "access.h" |
| #include "audit.h" |
| #include "common.h" |
| #include "cred.h" |
| #include "domain.h" |
| #include "limits.h" |
| #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[] = { _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) |
| { |
| 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 "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_strings[BIT_INDEX( |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)]; |
| |
| case LANDLOCK_REQUEST_SCOPE_SIGNAL: |
| WARN_ON_ONCE(access_bit != -1); |
| 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%s", is_first ? "" : ",", prefix, |
| get_blocker(type, access_bit)); |
| is_first = false; |
| } |
| if (is_first) |
| audit_log_format(ab, "%s%s", prefix, 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); |
| } |
| |
| /** |
| * landlock_audit_denial - Create an audit record 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. |
| * @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_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; |
| |
| if (!audit_enabled) |
| return; |
| |
| /* |
| * Skips denials the domain's policy or a quiet rule excludes from |
| * logging (folded into @logged by landlock_log_denial()). |
| */ |
| if (!logged) |
| 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_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 from landlock_log_free_domain(). |
| */ |
| void landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy) |
| { |
| struct audit_buffer *ab; |
| |
| 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); |
| } |