Linux Trace Kernel
 help / color / mirror / Atom feed
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


  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