// SPDX-License-Identifier: GPL-2.0-only /* * Landlock - Audit helpers * * Copyright © 2023-2025 Microsoft Corporation */ #include #include #include #include #include #include #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 ). 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); }