From: "Mickaël Salaün" <mic@digikod.net>
To: "Günther Noack" <gnoack@google.com>,
"Steven Rostedt" <rostedt@goodmis.org>
Cc: "Mickaël Salaün" <mic@digikod.net>,
"Christian Brauner" <brauner@kernel.org>,
"Jann Horn" <jannh@google.com>, "Jeff Xu" <jeffxu@google.com>,
"Justin Suess" <utilityemal77@gmail.com>,
"Kees Cook" <kees@kernel.org>,
"Masami Hiramatsu" <mhiramat@kernel.org>,
"Mathieu Desnoyers" <mathieu.desnoyers@efficios.com>,
"Matthieu Buffet" <matthieu@buffet.re>,
"Mikhail Ivanov" <ivanov.mikhail1@huawei-partners.com>,
"Tingmao Wang" <m@maowtm.org>,
kernel-team@cloudflare.com,
linux-security-module@vger.kernel.org,
linux-trace-kernel@vger.kernel.org
Subject: [PATCH v3 05/20] landlock: Decouple the per-denial logging decision from CONFIG_AUDIT
Date: Wed, 22 Jul 2026 19:11:37 +0200 [thread overview]
Message-ID: <20260722171159.2776765-6-mic@digikod.net> (raw)
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
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>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
- New patch.
---
security/landlock/audit.c | 90 ++++-----------------------------------
security/landlock/audit.h | 14 ++----
security/landlock/log.c | 88 ++++++++++++++++++++++++++++++++++++--
3 files changed, 97 insertions(+), 95 deletions(-)
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index dfa1e5a64aac..32260e7cbfe9 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -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(). */
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 1b36bb3c6cfd..14a514065e08 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -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)
{
}
diff --git a/security/landlock/log.c b/security/landlock/log.c
index 6dd3373c4ba4..f42e6dc4de5c 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -405,6 +405,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 +531,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 +542,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);
}
/**
--
2.54.0
next prev parent reply other threads:[~2026-07-22 17:12 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-22 17:11 [PATCH v3 00/20] Landlock tracepoints Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 01/20] landlock: Prepare ruleset and domain type split Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 02/20] landlock: Move domain query functions to domain.c Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 03/20] landlock: Split struct landlock_domain from struct landlock_ruleset Mickaël Salaün
2026-07-22 21:12 ` Justin Suess
2026-07-22 17:11 ` [PATCH v3 04/20] landlock: Split denial logging from audit into common framework Mickaël Salaün
2026-07-22 17:11 ` Mickaël Salaün [this message]
2026-07-22 17:11 ` [PATCH v3 06/20] landlock: Consolidate access-right and scope names in a shared header Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 07/20] tracing: Add __print_untrusted_str() Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 08/20] landlock: Add create_ruleset and free_ruleset tracepoints Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 09/20] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 10/20] landlock: Add create_domain and free_domain tracepoints Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 11/20] landlock: Add landlock_enforce_domain tracepoint Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 12/20] landlock: Add tracepoints for rule checking Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 13/20] landlock: Add landlock_deny_access_fs and landlock_deny_access_net Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 14/20] landlock: Add tracepoints for ptrace and scope denials Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 15/20] selftests/landlock: Add trace event test infrastructure and tests Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 16/20] selftests/landlock: Add filesystem tracepoint tests Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 17/20] selftests/landlock: Add network " Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 18/20] selftests/landlock: Add scope and ptrace " Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 19/20] selftests/landlock: Add landlock_enforce_domain trace tests Mickaël Salaün
2026-07-22 17:11 ` [PATCH v3 20/20] landlock: Document tracepoints Mickaël Salaün
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260722171159.2776765-6-mic@digikod.net \
--to=mic@digikod.net \
--cc=brauner@kernel.org \
--cc=gnoack@google.com \
--cc=ivanov.mikhail1@huawei-partners.com \
--cc=jannh@google.com \
--cc=jeffxu@google.com \
--cc=kees@kernel.org \
--cc=kernel-team@cloudflare.com \
--cc=linux-security-module@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=m@maowtm.org \
--cc=mathieu.desnoyers@efficios.com \
--cc=matthieu@buffet.re \
--cc=mhiramat@kernel.org \
--cc=rostedt@goodmis.org \
--cc=utilityemal77@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox