mirror of
https://github.com/torvalds/linux.git
synced 2026-10-07 19:16:02 +02:00
Audit formats denial records with per-right name strings. A following
commit adds trace events that print the same access and scope masks with
__print_flags() and need the same names, but a trace event header cannot
include Landlock-internal headers, so the names cannot be shared from
the logging unit.
Define the filesystem, network, and scope names once, as the
_LANDLOCK_ACCESS_FS_NAMES, _LANDLOCK_ACCESS_NET_NAMES, and
_LANDLOCK_SCOPE_NAMES lists in the public Landlock header. Each entry
is a _LANDLOCK_NAME_ENTRY() the consumer expands: audit maps it to a
"[bit] = name" array slot for an O(1) lookup, the trace events map it to
a __print_flags() { mask, name } pair. The bit value comes only from
the LANDLOCK_* UAPI constant each entry references, so every bit-to-name
mapping has a single source and does not depend on entry order.
The shared names are unprefixed; blocker_prefix() prepends the
fs./net./scope. category for audit records, so the scope names move from
inline literals to the shared table too. Audit records are unchanged.
No functional change.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://patch.msgid.link/20260811094338.288094-7-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
238 lines
6.7 KiB
C
238 lines
6.7 KiB
C
// 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);
|
|
}
|