landlock: Decouple the per-denial logging decision from CONFIG_AUDIT

Until now, whether a denial is logged was decided inside
landlock_audit_denial(): a per-execution flag check (log_same_exec or
log_new_exec, selected by the credential's domain_exec bitmask),
preceded by a LANDLOCK_LOG_DISABLED early return in
landlock_log_denial() for domains an ancestor fully quieted.

Factor that decision into a single is_denial_logged() helper called once
by landlock_log_denial(), and pass its result to landlock_audit_denial()
as a "logged" boolean.  A following commit passes the same boolean to
the deny tracepoints, so audit and tracing share one decision that stays
correct as new log state is added, and a tracepoints-only build
(CONFIG_AUDIT=n) computes it identically.  Computing the logged verdict
once in the shared helper makes audit and tracing apply identical
filtering, so they cannot report different logged= values for the same
denial as log controls grow.

Move the LANDLOCK_LOG_DISABLED gate out of landlock_log_denial() into
the decision so num_denials counts every denial, including those a
domain quiets.  This was previously masked: the only reader of
num_denials is the audit "domain deallocated" record, emitted only for
domains that reached LANDLOCK_LOG_RECORDED; a fully quieted domain never
records, so its undercount was never observable.  A following commit
adds a free_domain tracepoint that reports num_denials, which needs the
full count.

This is not a functional change for audit: the logged decision and the
audit_enabled gate are preserved, so the emitted records are identical.

Cc: Günther Noack <gnoack@google.com>
Link: https://patch.msgid.link/20260811094338.288094-6-mic@digikod.net
Reviewed-by: Tingmao Wang <m@maowtm.org>
[mic: Update copyright]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
Mickaël Salaün
2026-08-11 11:43:19 +02:00
parent e761a88ced
commit 69ca5782f8
3 changed files with 98 additions and 95 deletions

View File

@@ -136,104 +136,32 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
}
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_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.
* @youngest_layer: The layer index of @youngest_denied.
* @missing: The set of denied access rights.
* @object_quiet_flag: Whether the object denied by @youngest_denied is
* covered by a quiet rule in that layer.
* @logged: Whether the denial is selected for logging, as computed by
* landlock_log_denial() (domain policy and quiet rules).
*
* Called from landlock_log_denial() with the same arguments.
* Emits the record when audit is enabled and the denial is selected for
* logging.
*/
void landlock_audit_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 size_t youngest_layer,
const access_mask_t missing,
const bool object_quiet_flag)
const access_mask_t missing, const bool logged)
{
struct audit_buffer *ab;
bool quiet_applicable_to_access = false;
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(). */

View File

@@ -12,18 +12,14 @@
#include "access.h"
struct landlock_cred_security;
struct landlock_hierarchy;
struct landlock_request;
#ifdef CONFIG_AUDIT
void landlock_audit_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 size_t youngest_layer,
const access_mask_t missing,
const bool object_quiet_flag);
const access_mask_t missing, const bool logged);
void landlock_audit_free_domain(
const struct landlock_hierarchy *const hierarchy);
@@ -31,11 +27,9 @@ void landlock_audit_free_domain(
#else /* CONFIG_AUDIT */
static inline void
landlock_audit_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request,
landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
const size_t youngest_layer, const access_mask_t missing,
const bool object_quiet_flag)
const access_mask_t missing, const bool logged)
{
}

View File

@@ -3,6 +3,7 @@
* Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#include <kunit/test.h>
@@ -405,6 +406,86 @@ static bool is_valid_request(const struct landlock_request *const request)
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
*
@@ -451,8 +532,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
get_hierarchy(subject->domain, youngest_layer);
}
if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
return;
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
@@ -461,8 +543,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
*/
atomic64_inc(&youngest_denied->num_denials);
landlock_audit_denial(subject, request, youngest_denied, youngest_layer,
missing, object_quiet_flag);
landlock_audit_denial(request, youngest_denied, missing, logged);
}
/**