Linux Security Modules development
 help / color / mirror / Atom feed
* [PATCH v2 1/2] landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()
From: Mickaël Salaün @ 2026-04-07 16:41 UTC (permalink / raw)
  To: Günther Noack
  Cc: Mickaël Salaün, linux-security-module, Jann Horn,
	stable, Günther Noack

hook_cred_transfer() only copies the Landlock security blob when the
source credential has a domain.  This is inconsistent with
landlock_restrict_self() which can set LOG_SUBDOMAINS_OFF on a
credential without creating a domain (via the ruleset_fd=-1 path): the
field is committed but not preserved across fork() because the child's
prepare_creds() calls hook_cred_transfer() which skips the copy when
domain is NULL.

This breaks the documented use case where a process mutes subdomain logs
before forking sandboxed children: the children lose the muting and
their domains produce unexpected audit records.

Fix this by unconditionally copying the Landlock credential blob.

Cc: Günther Noack <gnoack@google.com>
Cc: Jann Horn <jannh@google.com>
Cc: stable@vger.kernel.org
Fixes: ead9079f7569 ("landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF")
Reviewed-by: Günther Noack <gnoack3000@gmail.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v1:
https://lore.kernel.org/r/20260404085001.1604405-1-mic@digikod.net
- Improve subject.
- Add Reviewed-by Günther.
---
 security/landlock/cred.c                      |  6 +-
 tools/testing/selftests/landlock/audit_test.c | 88 +++++++++++++++++++
 2 files changed, 90 insertions(+), 4 deletions(-)

diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index 0cb3edde4d18..cc419de75cd6 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -22,10 +22,8 @@ static void hook_cred_transfer(struct cred *const new,
 	const struct landlock_cred_security *const old_llcred =
 		landlock_cred(old);
 
-	if (old_llcred->domain) {
-		landlock_get_ruleset(old_llcred->domain);
-		*landlock_cred(new) = *old_llcred;
-	}
+	landlock_get_ruleset(old_llcred->domain);
+	*landlock_cred(new) = *old_llcred;
 }
 
 static int hook_cred_prepare(struct cred *const new,
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index 46d02d49835a..20099b8667e7 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -279,6 +279,94 @@ TEST_F(audit, thread)
 				&audit_tv_default, sizeof(audit_tv_default)));
 }
 
+/*
+ * Verifies that log_subdomains_off set via the ruleset_fd=-1 path (without
+ * creating a domain) is inherited by children across fork().  This exercises
+ * the hook_cred_transfer() fix: the Landlock credential blob must be copied
+ * even when the source credential has no domain.
+ *
+ * Phase 1 (baseline): a child without muting creates a domain and triggers a
+ * denial that IS logged.
+ *
+ * Phase 2 (after muting): the parent mutes subdomain logs, forks another child
+ * who creates a domain and triggers a denial that is NOT logged.
+ */
+TEST_F(audit, log_subdomains_off_fork)
+{
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.scoped = LANDLOCK_SCOPE_SIGNAL,
+	};
+	struct audit_records records;
+	int ruleset_fd, status;
+	pid_t child;
+
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+	/*
+	 * Phase 1: forks a child that creates a domain and triggers a denial
+	 * before any muting.  This proves the audit path works.
+	 */
+	child = fork();
+	ASSERT_LE(0, child);
+	if (child == 0) {
+		ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
+		ASSERT_EQ(-1, kill(getppid(), 0));
+		ASSERT_EQ(EPERM, errno);
+		_exit(0);
+		return;
+	}
+
+	ASSERT_EQ(child, waitpid(child, &status, 0));
+	ASSERT_EQ(true, WIFEXITED(status));
+	ASSERT_EQ(0, WEXITSTATUS(status));
+
+	/* The denial must be logged (baseline). */
+	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, getpid(),
+					NULL));
+
+	/* Drains any remaining records (e.g. domain allocation). */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+
+	/*
+	 * Mutes subdomain logs without creating a domain.  The parent's
+	 * credential has domain=NULL and log_subdomains_off=1.
+	 */
+	ASSERT_EQ(0, landlock_restrict_self(
+			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
+
+	/*
+	 * Phase 2: forks a child that creates a domain and triggers a denial.
+	 * Because log_subdomains_off was inherited via fork(), the child's
+	 * domain has log_status=LANDLOCK_LOG_DISABLED.
+	 */
+	child = fork();
+	ASSERT_LE(0, child);
+	if (child == 0) {
+		ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
+		ASSERT_EQ(-1, kill(getppid(), 0));
+		ASSERT_EQ(EPERM, errno);
+		_exit(0);
+		return;
+	}
+
+	ASSERT_EQ(child, waitpid(child, &status, 0));
+	ASSERT_EQ(true, WIFEXITED(status));
+	ASSERT_EQ(0, WEXITSTATUS(status));
+
+	/* No denial record should appear. */
+	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
+					      getpid(), NULL));
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	EXPECT_EQ(0, close(ruleset_fd));
+}
+
 FIXTURE(audit_flags)
 {
 	struct audit_filter audit_filter;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 2/2] landlock: Allow TSYNC with LOG_SUBDOMAINS_OFF and fd=-1
From: Mickaël Salaün @ 2026-04-07 16:41 UTC (permalink / raw)
  To: Günther Noack
  Cc: Mickaël Salaün, linux-security-module, Jann Horn,
	stable, Günther Noack
In-Reply-To: <20260407164107.2012589-1-mic@digikod.net>

LANDLOCK_RESTRICT_SELF_TSYNC does not allow
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with ruleset_fd=-1, preventing
a multithreaded process from atomically propagating subdomain log muting
to all threads without creating a domain layer.  Relax the fd=-1
condition to accept TSYNC alongside LOG_SUBDOMAINS_OFF, and update the
documentation accordingly.

Add flag validation tests for all TSYNC combinations with ruleset_fd=-1,
and audit tests verifying both transition directions: muting via TSYNC
(logged to not logged) and override via TSYNC (not logged to logged).

Cc: Günther Noack <gnoack@google.com>
Cc: stable@vger.kernel.org
Fixes: 42fc7e6543f6 ("landlock: Multithreading support for landlock_restrict_self()")
Reviewed-by: Günther Noack <gnoack3000@gmail.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v1:
https://lore.kernel.org/r/20260404085001.1604405-2-mic@digikod.net
- Zero-initialize struct thread_data to avoid flaky uninitialized
  mute_subdomains field (pointed out by Günther Noack).
- Drop capabilities and set no_new_privs in tsync_without_ruleset
  fixture setup instead of relying on ambient CAP_SYS_ADMIN (pointed
  out by Günther Noack).
- Add Reviewed-by Günther.
---
 include/uapi/linux/landlock.h                 |   4 +-
 security/landlock/syscalls.c                  |  14 +-
 tools/testing/selftests/landlock/audit_test.c | 233 ++++++++++++++++++
 tools/testing/selftests/landlock/tsync_test.c |  77 ++++++
 4 files changed, 322 insertions(+), 6 deletions(-)

diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index f88fa1f68b77..d37603efc273 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -116,7 +116,9 @@ struct landlock_ruleset_attr {
  *     ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, this flag only affects
  *     future nested domains, not the one being created. It can also be used
  *     with a @ruleset_fd value of -1 to mute subdomain logs without creating a
- *     domain.
+ *     domain.  When combined with %LANDLOCK_RESTRICT_SELF_TSYNC and a
+ *     @ruleset_fd value of -1, this configuration is propagated to all threads
+ *     of the current process.
  *
  * The following flag supports policy enforcement in multithreaded processes:
  *
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 0d66a68677b7..a0bb664e0d31 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -512,10 +512,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
 	/*
 	 * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
-	 * -1 as ruleset_fd, but no other flag must be set.
+	 * -1 as ruleset_fd, optionally combined with
+	 * LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all
+	 * threads.  No other flag must be set.
 	 */
 	if (!(ruleset_fd == -1 &&
-	      flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
+	      (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
+		      LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
 		/* Gets and checks the ruleset. */
 		ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
 		if (IS_ERR(ruleset))
@@ -537,9 +540,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
 	/*
 	 * The only case when a ruleset may not be set is if
-	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1.
-	 * We could optimize this case by not calling commit_creds() if this flag
-	 * was already set, but it is not worth the complexity.
+	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
+	 * LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1.  We could
+	 * optimize this case by not calling commit_creds() if this flag was
+	 * already set, but it is not worth the complexity.
 	 */
 	if (ruleset) {
 		/*
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index 20099b8667e7..897596cd7c80 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -162,6 +162,7 @@ TEST_F(audit, layers)
 struct thread_data {
 	pid_t parent_pid;
 	int ruleset_fd, pipe_child, pipe_parent;
+	bool mute_subdomains;
 };
 
 static void *thread_audit_test(void *arg)
@@ -367,6 +368,238 @@ TEST_F(audit, log_subdomains_off_fork)
 	EXPECT_EQ(0, close(ruleset_fd));
 }
 
+/*
+ * Thread function: runs two rounds of (create domain, trigger denial, signal
+ * back), waiting for the main thread before each round.  When mute_subdomains
+ * is set, phase 1 also mutes subdomain logs via the fd=-1 path before creating
+ * the domain.  The ruleset_fd is kept open across both rounds so each
+ * restrict_self call stacks a new domain layer.
+ */
+static void *thread_sandbox_deny_twice(void *arg)
+{
+	const struct thread_data *data = (struct thread_data *)arg;
+	uintptr_t err = 0;
+	char buffer;
+
+	/* Phase 1: optionally mutes, creates a domain, and triggers a denial. */
+	if (read(data->pipe_parent, &buffer, 1) != 1) {
+		err = 1;
+		goto out;
+	}
+
+	if (data->mute_subdomains &&
+	    landlock_restrict_self(-1,
+				   LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
+		err = 2;
+		goto out;
+	}
+
+	if (landlock_restrict_self(data->ruleset_fd, 0)) {
+		err = 3;
+		goto out;
+	}
+
+	if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
+		err = 4;
+		goto out;
+	}
+
+	if (write(data->pipe_child, ".", 1) != 1) {
+		err = 5;
+		goto out;
+	}
+
+	/* Phase 2: stacks another domain and triggers a denial. */
+	if (read(data->pipe_parent, &buffer, 1) != 1) {
+		err = 6;
+		goto out;
+	}
+
+	if (landlock_restrict_self(data->ruleset_fd, 0)) {
+		err = 7;
+		goto out;
+	}
+
+	if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
+		err = 8;
+		goto out;
+	}
+
+	if (write(data->pipe_child, ".", 1) != 1) {
+		err = 9;
+		goto out;
+	}
+
+out:
+	close(data->ruleset_fd);
+	close(data->pipe_child);
+	close(data->pipe_parent);
+	return (void *)err;
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
+ * LANDLOCK_RESTRICT_SELF_TSYNC and ruleset_fd=-1 propagates log_subdomains_off
+ * to a sibling thread, suppressing audit logging on domains it subsequently
+ * creates.
+ *
+ * Phase 1 (before TSYNC) acts as an inline baseline: the sibling creates a
+ * domain and triggers a denial that IS logged.
+ *
+ * Phase 2 (after TSYNC) verifies suppression: the sibling stacks another domain
+ * and triggers a denial that is NOT logged.
+ */
+TEST_F(audit, log_subdomains_off_tsync)
+{
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.scoped = LANDLOCK_SCOPE_SIGNAL,
+	};
+	struct audit_records records;
+	struct thread_data child_data = {};
+	int pipe_child[2], pipe_parent[2];
+	char buffer;
+	pthread_t thread;
+	void *thread_ret;
+
+	child_data.parent_pid = getppid();
+	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+	child_data.pipe_child = pipe_child[1];
+	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+	child_data.pipe_parent = pipe_parent[0];
+	child_data.ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, child_data.ruleset_fd);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+	/* Creates the sibling thread. */
+	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
+				    &child_data));
+
+	/*
+	 * Phase 1: the sibling creates a domain and triggers a denial before
+	 * any log muting.  This proves the audit path works.
+	 */
+	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+	/* The denial must be logged. */
+	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
+					child_data.parent_pid, NULL));
+
+	/* Drains any remaining records (e.g. domain allocation). */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+
+	/*
+	 * Mutes subdomain logs and propagates to the sibling thread via TSYNC,
+	 * without creating a domain.
+	 */
+	ASSERT_EQ(0, landlock_restrict_self(
+			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+					 LANDLOCK_RESTRICT_SELF_TSYNC));
+
+	/*
+	 * Phase 2: the sibling stacks another domain and triggers a denial.
+	 * Because log_subdomains_off was propagated via TSYNC, the new domain
+	 * has log_status=LANDLOCK_LOG_DISABLED.
+	 */
+	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+	/* No denial record should appear. */
+	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
+					      child_data.parent_pid, NULL));
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	EXPECT_EQ(0, close(pipe_child[0]));
+	EXPECT_EQ(0, close(pipe_parent[1]));
+	ASSERT_EQ(0, pthread_join(thread, &thread_ret));
+	EXPECT_EQ(NULL, thread_ret);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_TSYNC without
+ * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF overrides a sibling thread's
+ * log_subdomains_off, re-enabling audit logging on domains the sibling
+ * subsequently creates.
+ *
+ * Phase 1: the sibling sets log_subdomains_off, creates a muted domain, and
+ * triggers a denial that is NOT logged.
+ *
+ * Phase 2 (after TSYNC without LOG_SUBDOMAINS_OFF): the sibling stacks another
+ * domain and triggers a denial that IS logged, proving the muting was
+ * overridden.
+ */
+TEST_F(audit, tsync_override_log_subdomains_off)
+{
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.scoped = LANDLOCK_SCOPE_SIGNAL,
+	};
+	struct audit_records records;
+	struct thread_data child_data = {};
+	int pipe_child[2], pipe_parent[2];
+	char buffer;
+	pthread_t thread;
+	void *thread_ret;
+
+	child_data.parent_pid = getppid();
+	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+	child_data.pipe_child = pipe_child[1];
+	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+	child_data.pipe_parent = pipe_parent[0];
+	child_data.ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, child_data.ruleset_fd);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+	child_data.mute_subdomains = true;
+
+	/* Creates the sibling thread. */
+	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
+				    &child_data));
+
+	/*
+	 * Phase 1: the sibling mutes subdomain logs, creates a domain, and
+	 * triggers a denial.  The denial must not be logged.
+	 */
+	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
+					      child_data.parent_pid, NULL));
+
+	/* Drains any remaining records. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	/*
+	 * Overrides the sibling's log_subdomains_off by calling TSYNC without
+	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.
+	 */
+	ASSERT_EQ(0, landlock_restrict_self(child_data.ruleset_fd,
+					    LANDLOCK_RESTRICT_SELF_TSYNC));
+
+	/*
+	 * Phase 2: the sibling stacks another domain and triggers a denial.
+	 * Because TSYNC replaced its log_subdomains_off with 0, the new domain
+	 * has log_status=LANDLOCK_LOG_PENDING.
+	 */
+	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+	/* The denial must be logged. */
+	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
+					child_data.parent_pid, NULL));
+
+	EXPECT_EQ(0, close(pipe_child[0]));
+	EXPECT_EQ(0, close(pipe_parent[1]));
+	ASSERT_EQ(0, pthread_join(thread, &thread_ret));
+	EXPECT_EQ(NULL, thread_ret);
+}
+
 FIXTURE(audit_flags)
 {
 	struct audit_filter audit_filter;
diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
index 2b9ad4f154f4..9cf1491bbaaf 100644
--- a/tools/testing/selftests/landlock/tsync_test.c
+++ b/tools/testing/selftests/landlock/tsync_test.c
@@ -247,4 +247,81 @@ TEST(tsync_interrupt)
 	EXPECT_EQ(0, close(ruleset_fd));
 }
 
+/* clang-format off */
+FIXTURE(tsync_without_ruleset) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(tsync_without_ruleset)
+{
+	const __u32 flags;
+	const int expected_errno;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) {
+	/* clang-format on */
+	.flags = LANDLOCK_RESTRICT_SELF_TSYNC,
+	.expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) {
+	/* clang-format on */
+	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+		 LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
+		 LANDLOCK_RESTRICT_SELF_TSYNC,
+	.expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) {
+	/* clang-format on */
+	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+		 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
+		 LANDLOCK_RESTRICT_SELF_TSYNC,
+	.expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) {
+	/* clang-format on */
+	.flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
+		 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
+		 LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+		 LANDLOCK_RESTRICT_SELF_TSYNC,
+	.expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) {
+	/* clang-format on */
+	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+		 LANDLOCK_RESTRICT_SELF_TSYNC,
+	.expected_errno = 0,
+};
+
+FIXTURE_SETUP(tsync_without_ruleset)
+{
+	disable_caps(_metadata);
+}
+
+FIXTURE_TEARDOWN(tsync_without_ruleset)
+{
+}
+
+TEST_F(tsync_without_ruleset, check)
+{
+	int ret;
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+	ret = landlock_restrict_self(-1, variant->flags);
+	if (variant->expected_errno) {
+		EXPECT_EQ(-1, ret);
+		EXPECT_EQ(variant->expected_errno, errno);
+	} else {
+		EXPECT_EQ(0, ret);
+	}
+}
+
 TEST_HARNESS_MAIN
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v5 2/3] ima: trim N IMA event log records
From: Roberto Sassu @ 2026-04-07 16:19 UTC (permalink / raw)
  To: steven chen, linux-integrity
  Cc: zohar, roberto.sassu, dmitry.kasatkin, eric.snowberg, corbet,
	serge, paul, jmorris, linux-security-module, anirudhve,
	gregorylumen, nramas, sushring, linux-doc
In-Reply-To: <20260401172956.4581-3-chenste@linux.microsoft.com>

On Wed, 2026-04-01 at 10:29 -0700, steven chen wrote:
> Trim N entries of the IMA event logs. Do not clean the hash table.

The very first change of this patch is the kernel option
ima_flush_htable option that I introduced for my use case.

At the bottom of this patch you actually check the ima_flush_htable
boolean, and delete the measurements entries without disconnecting them
from the hash table, so the digest lookup is done on freed memory.

Next, you duplicated my changes regarding the measurements list
counter. But instead of removing the old counter from the hash table,
you keep incrementing both, but use the new one.

In ima_log_trim_open(), you use again my duplicated code to manage
exclusive write/concurrent read scheme for the measurement interfaces.
However, for read, if the process does not have CAP_SYS_ADMIN it falls
back calling _ima_measurements_open(). Not sure it was intended.

And, in ima_log_trim_release(), you check again CAP_SYS_ADMIN which is
redundant, you would not reach this code if the same requirements were
not met at open time. You also return an error on close().

In ima_log_trim_write(), you do manual string to number conversion for
your first number and use kstrtoul() for the second.

The measurements lists and the associated counter are atomically
updated in ima_add_digest_entry(), but not atomically accessed in
ima_delete_event_log(). Also, the measurements list is traversed
without _rcu variant or lock.

While this trimming scheme aims at minimizing the kernel space and user
space delay, it also introduces the following problem. If two agents
perform a TPM quote that include a different number of entries, there
is no guarantee that the one willing to trim less entries wins. Which
means that, one agent could end up not seeing the most recent entries,
as they were already trimmed by the other agent.

My solution is not affected by this problem, since there will be only
one process collecting all the measurements in user space and exposing
them to the agents.

Also, I didn't understand why T and ima_measure_users have to be
preserved on soft reboots. Especially ima_measure_users reflects the
state of open files for a particular kernel, but on soft reboot a new
kernel is booted.

I personally will not endorse a solution based on the ima_trim_log
interface. I could accept trimming N even more efficiently than we
currently do with a lockless walk to determine the cutting position in
ima_queue_stage(), so that we don't need to splice back entries to the
measurement list. This would be a replacement of patch 11 in my patch
set, but this would be as far as I would like to go.

Roberto

> The values saved in hash table were already used.
> 
> Provide a userspace interface ima_trim_log:
> When read this interface, it returns total number T of entries trimmed
> since system boot up.
> When write to this interface need to provide two numbers T:N to let
> kernel to trim N entries of IMA event logs.
> 
> Kernel measurement list lock time performance improvement by not
> clean the hash table.
> 
> when kernel get log trim request T:N
>  - Get the T, compare with the total trimmed number
>  - if equal, then do trim N and change T to T+N
>  - else return error
> 
> Signed-off-by: steven chen <chenste@linux.microsoft.com>
> ---
>  .../admin-guide/kernel-parameters.txt         |   4 +
>  security/integrity/ima/ima.h                  |   4 +-
>  security/integrity/ima/ima_fs.c               | 198 +++++++++++++++++-
>  security/integrity/ima/ima_kexec.c            |   2 +-
>  security/integrity/ima/ima_queue.c            |  96 +++++++++
>  5 files changed, 296 insertions(+), 8 deletions(-)
> 
> diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
> index e92c0056e4e0..cd1a1d0bf0e2 100644
> --- a/Documentation/admin-guide/kernel-parameters.txt
> +++ b/Documentation/admin-guide/kernel-parameters.txt
> @@ -2197,6 +2197,10 @@
>  			Use the canonical format for the binary runtime
>  			measurements, instead of host native format.
>  
> +	ima_flush_htable  [IMA]
> +			Flush the measurement list hash table when trim all
> +			or a part of it for deletion.
> +
>  	ima_hash=	[IMA]
>  			Format: { md5 | sha1 | rmd160 | sha256 | sha384
>  				   | sha512 | ... }
> diff --git a/security/integrity/ima/ima.h b/security/integrity/ima/ima.h
> index e3d71d8d56e3..5cbee3a295a0 100644
> --- a/security/integrity/ima/ima.h
> +++ b/security/integrity/ima/ima.h
> @@ -243,11 +243,13 @@ void ima_post_key_create_or_update(struct key *keyring, struct key *key,
>  				   const void *payload, size_t plen,
>  				   unsigned long flags, bool create);
>  #endif
> -
> +extern atomic_long_t ima_number_entries;
>  #ifdef CONFIG_IMA_KEXEC
>  void ima_measure_kexec_event(const char *event_name);
> +long ima_delete_event_log(long req_val);
>  #else
>  static inline void ima_measure_kexec_event(const char *event_name) {}
> +static inline long ima_delete_event_log(long req_val) { return 0; }
>  #endif
>  
>  /*
> diff --git a/security/integrity/ima/ima_fs.c b/security/integrity/ima/ima_fs.c
> index 87045b09f120..8e26e0f34311 100644
> --- a/security/integrity/ima/ima_fs.c
> +++ b/security/integrity/ima/ima_fs.c
> @@ -21,6 +21,9 @@
>  #include <linux/rcupdate.h>
>  #include <linux/parser.h>
>  #include <linux/vmalloc.h>
> +#include <linux/ktime.h>
> +#include <linux/timekeeping.h>
> +#include <linux/ima.h>
>  
>  #include "ima.h"
>  
> @@ -38,6 +41,17 @@ __setup("ima_canonical_fmt", default_canonical_fmt_setup);
>  
>  static int valid_policy = 1;
>  
> +#define IMA_LOG_TRIM_REQ_NUM_LENGTH 15
> +#define IMA_LOG_TRIM_REQ_TOTAL_LENGTH 32
> +atomic_long_t ima_number_entries = ATOMIC_LONG_INIT(0);
> +static long trimcount;
> +/* mutex protects atomicity of trimming measurement list
> + * and also protects atomicity the measurement list read
> + * write operation.
> + */
> +static DEFINE_MUTEX(ima_measure_lock);
> +static long ima_measure_users;
> +
>  static ssize_t ima_show_htable_value(char __user *buf, size_t count,
>  				     loff_t *ppos, atomic_long_t *val)
>  {
> @@ -64,8 +78,7 @@ static ssize_t ima_show_measurements_count(struct file *filp,
>  					   char __user *buf,
>  					   size_t count, loff_t *ppos)
>  {
> -	return ima_show_htable_value(buf, count, ppos, &ima_htable.len);
> -
> +	return ima_show_htable_value(buf, count, ppos, &ima_number_entries);
>  }
>  
>  static const struct file_operations ima_measurements_count_ops = {
> @@ -202,16 +215,77 @@ static const struct seq_operations ima_measurments_seqops = {
>  	.show = ima_measurements_show
>  };
>  
> +/*
> + * _ima_measurements_open - open the IMA measurements file
> + * @inode: inode of the file being opened
> + * @file: file being opened
> + * @seq_ops: sequence operations for the file
> + *
> + * Returns 0 on success, or negative error code.
> + * Implements mutual exclusion between readers and writer
> + * of the measurements file. Multiple readers are allowed,
> + * but writer get exclusive access only no other readers/writers.
> + * Readers is not allowed when there is a writer.
> + */
> +static int _ima_measurements_open(struct inode *inode, struct file *file,
> +				  const struct seq_operations *seq_ops)
> +{
> +	bool write = !!(file->f_mode & FMODE_WRITE);
> +	int ret;
> +
> +	if (write && !capable(CAP_SYS_ADMIN))
> +		return -EPERM;
> +
> +	mutex_lock(&ima_measure_lock);
> +	if ((write && ima_measure_users != 0) ||
> +	    (!write && ima_measure_users < 0)) {
> +		mutex_unlock(&ima_measure_lock);
> +		return -EBUSY;
> +	}
> +
> +	ret = seq_open(file, seq_ops);
> +	if (ret < 0) {
> +		mutex_unlock(&ima_measure_lock);
> +		return ret;
> +	}
> +
> +	if (write)
> +		ima_measure_users--;
> +	else
> +		ima_measure_users++;
> +
> +	mutex_unlock(&ima_measure_lock);
> +	return ret;
> +}
> +
>  static int ima_measurements_open(struct inode *inode, struct file *file)
>  {
> -	return seq_open(file, &ima_measurments_seqops);
> +	return _ima_measurements_open(inode, file, &ima_measurments_seqops);
> +}
> +
> +static int ima_measurements_release(struct inode *inode, struct file *file)
> +{
> +	bool write = !!(file->f_mode & FMODE_WRITE);
> +	int ret;
> +
> +	mutex_lock(&ima_measure_lock);
> +	ret = seq_release(inode, file);
> +	if (!ret) {
> +		if (!write)
> +			ima_measure_users--;
> +		else
> +			ima_measure_users++;
> +	}
> +
> +	mutex_unlock(&ima_measure_lock);
> +	return ret;
>  }
>  
>  static const struct file_operations ima_measurements_ops = {
>  	.open = ima_measurements_open,
>  	.read = seq_read,
>  	.llseek = seq_lseek,
> -	.release = seq_release,
> +	.release = ima_measurements_release,
>  };
>  
>  void ima_print_digest(struct seq_file *m, u8 *digest, u32 size)
> @@ -279,14 +353,114 @@ static const struct seq_operations ima_ascii_measurements_seqops = {
>  
>  static int ima_ascii_measurements_open(struct inode *inode, struct file *file)
>  {
> -	return seq_open(file, &ima_ascii_measurements_seqops);
> +	return _ima_measurements_open(inode, file, &ima_ascii_measurements_seqops);
>  }
>  
>  static const struct file_operations ima_ascii_measurements_ops = {
>  	.open = ima_ascii_measurements_open,
>  	.read = seq_read,
>  	.llseek = seq_lseek,
> -	.release = seq_release,
> +	.release = ima_measurements_release,
> +};
> +
> +static int ima_log_trim_open(struct inode *inode, struct file *file)
> +{
> +	bool write = !!(file->f_mode & FMODE_WRITE);
> +
> +	if (!write && capable(CAP_SYS_ADMIN))
> +		return 0;
> +	else if (!capable(CAP_SYS_ADMIN))
> +		return -EPERM;
> +
> +	return _ima_measurements_open(inode, file, &ima_measurments_seqops);
> +}
> +
> +static ssize_t ima_log_trim_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
> +{
> +	char tmpbuf[IMA_LOG_TRIM_REQ_NUM_LENGTH];
> +	ssize_t len;
> +
> +	len = scnprintf(tmpbuf, sizeof(tmpbuf), "%li\n", trimcount);
> +	return simple_read_from_buffer(buf, size, ppos, tmpbuf, len);
> +}
> +
> +static ssize_t ima_log_trim_write(struct file *file,
> +				  const char __user *buf, size_t datalen, loff_t *ppos)
> +{
> +	char tmpbuf[IMA_LOG_TRIM_REQ_TOTAL_LENGTH];
> +	char *p = tmpbuf;
> +	long count, ret, val = 0, max = LONG_MAX;
> +
> +	if (*ppos > 0 || datalen > IMA_LOG_TRIM_REQ_TOTAL_LENGTH || datalen < 2) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +
> +	if (copy_from_user(tmpbuf, buf, datalen) != 0) {
> +		ret = -EFAULT;
> +		goto out;
> +	}
> +
> +	p = tmpbuf;
> +
> +	while (*p && *p != ':') {
> +		if (!isdigit((unsigned char)*p))
> +			return -EINVAL;
> +
> +		/* digit value */
> +		int d = *p - '0';
> +
> +		/* overflow check: val * 10 + d > max -> (val > (max - d) / 10) */
> +		if (val > (max - d) / 10)
> +			return -ERANGE;
> +
> +		val = val * 10 + d;
> +		p++;
> +	}
> +
> +	if (*p != ':')
> +		return -EINVAL;
> +
> +	/* verify trim count matches */
> +	if (val != trimcount)
> +		return -EINVAL;
> +
> +	p++; /* skip ':' */
> +	ret = kstrtoul(p, 0, &count);
> +
> +	if (ret < 0)
> +		goto out;
> +
> +	ret = ima_delete_event_log(count);
> +
> +	if (ret < 0)
> +		goto out;
> +
> +	trimcount += ret;
> +
> +	ret = datalen;
> +out:
> +	return ret;
> +}
> +
> +static int ima_log_trim_release(struct inode *inode, struct file *file)
> +{
> +	bool write = !!(file->f_mode & FMODE_WRITE);
> +
> +	if (!write && capable(CAP_SYS_ADMIN))
> +		return 0;
> +	else if (!capable(CAP_SYS_ADMIN))
> +		return -EPERM;
> +
> +	return ima_measurements_release(inode, file);
> +}
> +
> +static const struct file_operations ima_log_trim_ops = {
> +	.open = ima_log_trim_open,
> +	.read = ima_log_trim_read,
> +	.write = ima_log_trim_write,
> +	.llseek = generic_file_llseek,
> +	.release = ima_log_trim_release
>  };
>  
>  static ssize_t ima_read_policy(char *path)
> @@ -528,6 +702,18 @@ int __init ima_fs_init(void)
>  		goto out;
>  	}
>  
> +	if (IS_ENABLED(CONFIG_IMA_LOG_TRIMMING)) {
> +		dentry = securityfs_create_file("ima_trim_log",
> +						S_IRUSR | S_IRGRP | S_IWUSR | S_IWGRP,
> +						ima_dir, NULL, &ima_log_trim_ops);
> +		if (IS_ERR(dentry)) {
> +			ret = PTR_ERR(dentry);
> +			goto out;
> +		}
> +	}
> +
> +	trimcount = 0;
> +
>  	dentry = securityfs_create_file("runtime_measurements_count",
>  				   S_IRUSR | S_IRGRP, ima_dir, NULL,
>  				   &ima_measurements_count_ops);
> diff --git a/security/integrity/ima/ima_kexec.c b/security/integrity/ima/ima_kexec.c
> index 7362f68f2d8b..bee997683e03 100644
> --- a/security/integrity/ima/ima_kexec.c
> +++ b/security/integrity/ima/ima_kexec.c
> @@ -41,7 +41,7 @@ void ima_measure_kexec_event(const char *event_name)
>  	int n;
>  
>  	buf_size = ima_get_binary_runtime_size();
> -	len = atomic_long_read(&ima_htable.len);
> +	len = atomic_long_read(&ima_number_entries);
>  
>  	n = scnprintf(ima_kexec_event, IMA_KEXEC_EVENT_LEN,
>  		      "kexec_segment_size=%lu;ima_binary_runtime_size=%lu;"
> diff --git a/security/integrity/ima/ima_queue.c b/security/integrity/ima/ima_queue.c
> index 590637e81ad1..07225e19b9b5 100644
> --- a/security/integrity/ima/ima_queue.c
> +++ b/security/integrity/ima/ima_queue.c
> @@ -22,6 +22,14 @@
>  
>  #define AUDIT_CAUSE_LEN_MAX 32
>  
> +bool ima_flush_htable;
> +static int __init ima_flush_htable_setup(char *str)
> +{
> +	ima_flush_htable = true;
> +	return 1;
> +}
> +__setup("ima_flush_htable", ima_flush_htable_setup);
> +
>  /* pre-allocated array of tpm_digest structures to extend a PCR */
>  static struct tpm_digest *digests;
>  
> @@ -114,6 +122,7 @@ static int ima_add_digest_entry(struct ima_template_entry *entry,
>  	list_add_tail_rcu(&qe->later, &ima_measurements);
>  
>  	atomic_long_inc(&ima_htable.len);
> +	atomic_long_inc(&ima_number_entries);
>  	if (update_htable) {
>  		key = ima_hash_key(entry->digests[ima_hash_algo_idx].digest);
>  		hlist_add_head_rcu(&qe->hnext, &ima_htable.queue[key]);
> @@ -220,6 +229,93 @@ int ima_add_template_entry(struct ima_template_entry *entry, int violation,
>  	return result;
>  }
>  
> +/**
> + * ima_delete_event_log - delete IMA event entry
> + * @num_records: number of records to delete
> + *
> + * delete num_records entries off the measurement list.
> + * Returns num_records, or negative error code.
> + */
> +long ima_delete_event_log(long num_records)
> +{
> +	long len, cur = num_records, tmp_len = 0;
> +	struct ima_queue_entry *qe, *qe_tmp;
> +	LIST_HEAD(ima_measurements_to_delete);
> +	struct list_head *list_ptr;
> +
> +	if (!IS_ENABLED(CONFIG_IMA_LOG_TRIMMING))
> +		return -EOPNOTSUPP;
> +
> +	if (num_records <= 0)
> +		return num_records;
> +
> +	list_ptr = &ima_measurements;
> +
> +	len = atomic_long_read(&ima_number_entries);
> +
> +	if (num_records <= len) {
> +		list_for_each_entry(qe, list_ptr, later) {
> +			if (cur > 0) {
> +				tmp_len += get_binary_runtime_size(qe->entry);
> +				--cur;
> +			}
> +			if (cur == 0) {
> +				qe_tmp = qe;
> +				break;
> +			}
> +		}
> +	}
> +	else {
> +		return -ENOENT;
> +	}
> +
> +
> +	mutex_lock(&ima_extend_list_mutex);
> +	len = atomic_long_read(&ima_number_entries);
> +
> +	if (num_records == len) {
> +		list_replace(&ima_measurements, &ima_measurements_to_delete);
> +		INIT_LIST_HEAD(&ima_measurements);
> +		atomic_long_set(&ima_number_entries, 0);
> +		list_ptr = &ima_measurements_to_delete;
> +	}
> +	else {
> +		__list_cut_position(&ima_measurements_to_delete, &ima_measurements,
> +				    &qe_tmp->later);
> +		atomic_long_sub(num_records, &ima_number_entries);
> +		if (IS_ENABLED(CONFIG_IMA_KEXEC))
> +			binary_runtime_size -= tmp_len;
> +	}
> +
> +	mutex_unlock(&ima_extend_list_mutex);
> +
> +	if (ima_flush_htable)
> +		synchronize_rcu();
> +
> +	list_for_each_entry_safe(qe, qe_tmp, &ima_measurements_to_delete, later) {
> +		/*
> +		 * Ok because after list delete qe is only accessed by
> +		 * ima_lookup_digest_entry().
> +		 */
> +		for (int i = 0; i < qe->entry->template_desc->num_fields; i++) {
> +			kfree(qe->entry->template_data[i].data);
> +			qe->entry->template_data[i].data = NULL;
> +			qe->entry->template_data[i].len = 0;
> +		}
> +
> +		list_del(&qe->later);
> +
> +		/* No leak if !ima_flush_htable, referenced by ima_htable. */
> +		if (ima_flush_htable) {
> +			kfree(qe->entry->digests);
> +			kfree(qe->entry);
> +			kfree(qe);
> +		}
> +	}
> +
> +	return num_records;
> +}
> +
>  int ima_restore_measurement_entry(struct ima_template_entry *entry)
>  {
>  	int result = 0;


^ permalink raw reply

* Re: [PATCH v1 2/2] landlock: Allow TSYNC with LOG_SUBDOMAINS_OFF and fd=-1
From: Mickaël Salaün @ 2026-04-07 16:06 UTC (permalink / raw)
  To: Günther Noack; +Cc: Günther Noack, linux-security-module, stable
In-Reply-To: <20260407.7d922b20e863@gnoack.org>

On Tue, Apr 07, 2026 at 10:25:30AM +0200, Günther Noack wrote:
> Hello!
> 
> On Sat, Apr 04, 2026 at 10:49:58AM +0200, Mickaël Salaün wrote:
> > LANDLOCK_RESTRICT_SELF_TSYNC does not allow
> > LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with ruleset_fd=-1, preventing
> > a multithreaded process from atomically propagating subdomain log muting
> > to all threads without creating a domain layer.  Relax the fd=-1
> > condition to accept TSYNC alongside LOG_SUBDOMAINS_OFF, and update the
> > documentation accordingly.
> > 
> > Add flag validation tests for all TSYNC combinations with ruleset_fd=-1,
> > and audit tests verifying both transition directions: muting via TSYNC
> > (logged to not logged) and override via TSYNC (not logged to logged).
> > 
> > Cc: Günther Noack <gnoack@google.com>
> > Cc: stable@vger.kernel.org
> > Fixes: 42fc7e6543f6 ("landlock: Multithreading support for landlock_restrict_self()")
> > Signed-off-by: Mickaël Salaün <mic@digikod.net>
> > ---
> >  include/uapi/linux/landlock.h                 |   4 +-
> >  security/landlock/syscalls.c                  |  14 +-
> >  tools/testing/selftests/landlock/audit_test.c | 233 ++++++++++++++++++
> >  tools/testing/selftests/landlock/tsync_test.c |  74 ++++++
> >  4 files changed, 319 insertions(+), 6 deletions(-)
> > 
> > diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> > index f88fa1f68b77..d37603efc273 100644
> > --- a/include/uapi/linux/landlock.h
> > +++ b/include/uapi/linux/landlock.h
> > @@ -116,7 +116,9 @@ struct landlock_ruleset_attr {
> >   *     ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, this flag only affects
> >   *     future nested domains, not the one being created. It can also be used
> >   *     with a @ruleset_fd value of -1 to mute subdomain logs without creating a
> > - *     domain.
> > + *     domain.  When combined with %LANDLOCK_RESTRICT_SELF_TSYNC and a
> > + *     @ruleset_fd value of -1, this configuration is propagated to all threads
> > + *     of the current process.
> >   *
> >   * The following flag supports policy enforcement in multithreaded processes:
> >   *
> > diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
> > index 0d66a68677b7..a0bb664e0d31 100644
> > --- a/security/landlock/syscalls.c
> > +++ b/security/landlock/syscalls.c
> > @@ -512,10 +512,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
> >  
> >  	/*
> >  	 * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
> > -	 * -1 as ruleset_fd, but no other flag must be set.
> > +	 * -1 as ruleset_fd, optionally combined with
> > +	 * LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all
> > +	 * threads.  No other flag must be set.
> >  	 */
> >  	if (!(ruleset_fd == -1 &&
> > -	      flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
> > +	      (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
> > +		      LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
> 
> Well spotted, thanks!
> 
> 
> >  		/* Gets and checks the ruleset. */
> >  		ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
> >  		if (IS_ERR(ruleset))
> > @@ -537,9 +540,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
> >  
> >  	/*
> >  	 * The only case when a ruleset may not be set is if
> > -	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1.
> > -	 * We could optimize this case by not calling commit_creds() if this flag
> > -	 * was already set, but it is not worth the complexity.
> > +	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
> > +	 * LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1.  We could
> > +	 * optimize this case by not calling commit_creds() if this flag was
> > +	 * already set, but it is not worth the complexity.
> >  	 */
> >  	if (ruleset) {
> >  		/*
> > diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
> > index 20099b8667e7..a193d8a97560 100644
> > --- a/tools/testing/selftests/landlock/audit_test.c
> > +++ b/tools/testing/selftests/landlock/audit_test.c
> > @@ -162,6 +162,7 @@ TEST_F(audit, layers)
> >  struct thread_data {
> >  	pid_t parent_pid;
> >  	int ruleset_fd, pipe_child, pipe_parent;
> > +	bool mute_subdomains;
> >  };
> >  
> >  static void *thread_audit_test(void *arg)
> > @@ -367,6 +368,238 @@ TEST_F(audit, log_subdomains_off_fork)
> >  	EXPECT_EQ(0, close(ruleset_fd));
> >  }
> >  
> > +/*
> > + * Thread function: runs two rounds of (create domain, trigger denial, signal
> > + * back), waiting for the main thread before each round.  When mute_subdomains
> > + * is set, phase 1 also mutes subdomain logs via the fd=-1 path before creating
> > + * the domain.  The ruleset_fd is kept open across both rounds so each
> > + * restrict_self call stacks a new domain layer.
> > + */
> > +static void *thread_sandbox_deny_twice(void *arg)
> > +{
> > +	const struct thread_data *data = (struct thread_data *)arg;
> > +	uintptr_t err = 0;
> > +	char buffer;
> > +
> > +	/* Phase 1: optionally mutes, creates a domain, and triggers a denial. */
> > +	if (read(data->pipe_parent, &buffer, 1) != 1) {
> > +		err = 1;
> > +		goto out;
> > +	}
> > +
> > +	if (data->mute_subdomains &&
> > +	    landlock_restrict_self(-1,
> > +				   LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
> > +		err = 2;
> > +		goto out;
> > +	}
> > +
> > +	if (landlock_restrict_self(data->ruleset_fd, 0)) {
> > +		err = 3;
> > +		goto out;
> > +	}
> > +
> > +	if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
> > +		err = 4;
> > +		goto out;
> > +	}
> > +
> > +	if (write(data->pipe_child, ".", 1) != 1) {
> > +		err = 5;
> > +		goto out;
> > +	}
> > +
> > +	/* Phase 2: stacks another domain and triggers a denial. */
> > +	if (read(data->pipe_parent, &buffer, 1) != 1) {
> > +		err = 6;
> > +		goto out;
> > +	}
> > +
> > +	if (landlock_restrict_self(data->ruleset_fd, 0)) {
> > +		err = 7;
> > +		goto out;
> > +	}
> > +
> > +	if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
> > +		err = 8;
> > +		goto out;
> > +	}
> > +
> > +	if (write(data->pipe_child, ".", 1) != 1) {
> > +		err = 9;
> > +		goto out;
> > +	}
> > +
> > +out:
> > +	close(data->ruleset_fd);
> > +	close(data->pipe_child);
> > +	close(data->pipe_parent);
> > +	return (void *)err;
> > +}
> > +
> > +/*
> > + * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
> > + * LANDLOCK_RESTRICT_SELF_TSYNC and ruleset_fd=-1 propagates log_subdomains_off
> > + * to a sibling thread, suppressing audit logging on domains it subsequently
> > + * creates.
> > + *
> > + * Phase 1 (before TSYNC) acts as an inline baseline: the sibling creates a
> > + * domain and triggers a denial that IS logged.
> > + *
> > + * Phase 2 (after TSYNC) verifies suppression: the sibling stacks another domain
> > + * and triggers a denial that is NOT logged.
> > + */
> > +TEST_F(audit, log_subdomains_off_tsync)
> > +{
> > +	const struct landlock_ruleset_attr ruleset_attr = {
> > +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> > +	};
> > +	struct audit_records records;
> > +	struct thread_data child_data;
> 
> The child_data.mute_subdomains field stays uninitialized in this
> function (and maybe others).  Please fix.
> 
>    struct thread_data child_data = {};

Well spotted!

> 
> 
> > +	int pipe_child[2], pipe_parent[2];
> > +	char buffer;
> > +	pthread_t thread;
> > +	void *thread_ret;
> > +
> > +	child_data.parent_pid = getppid();
> > +	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> > +	child_data.pipe_child = pipe_child[1];
> > +	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> > +	child_data.pipe_parent = pipe_parent[0];
> > +	child_data.ruleset_fd =
> > +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> > +	ASSERT_LE(0, child_data.ruleset_fd);
> > +
> > +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> > +
> > +	/* Creates the sibling thread. */
> > +	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
> > +				    &child_data));
> > +
> > +	/*
> > +	 * Phase 1: the sibling creates a domain and triggers a denial before
> > +	 * any log muting.  This proves the audit path works.
> > +	 */
> > +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> > +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> > +
> > +	/* The denial must be logged. */
> > +	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
> > +					child_data.parent_pid, NULL));
> > +
> > +	/* Drains any remaining records (e.g. domain allocation). */
> > +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> > +
> > +	/*
> > +	 * Mutes subdomain logs and propagates to the sibling thread via TSYNC,
> > +	 * without creating a domain.
> > +	 */
> > +	ASSERT_EQ(0, landlock_restrict_self(
> > +			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> > +					 LANDLOCK_RESTRICT_SELF_TSYNC));
> > +
> > +	/*
> > +	 * Phase 2: the sibling stacks another domain and triggers a denial.
> > +	 * Because log_subdomains_off was propagated via TSYNC, the new domain
> > +	 * has log_status=LANDLOCK_LOG_DISABLED.
> > +	 */
> > +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> > +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> > +
> > +	/* No denial record should appear. */
> > +	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
> > +					      child_data.parent_pid, NULL));
> > +
> > +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> > +	EXPECT_EQ(0, records.access);
> > +
> > +	EXPECT_EQ(0, close(pipe_child[0]));
> > +	EXPECT_EQ(0, close(pipe_parent[1]));
> > +	ASSERT_EQ(0, pthread_join(thread, &thread_ret));
> > +	EXPECT_EQ(NULL, thread_ret);
> > +}
> > +
> > +/*
> > + * Verifies that LANDLOCK_RESTRICT_SELF_TSYNC without
> > + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF overrides a sibling thread's
> > + * log_subdomains_off, re-enabling audit logging on domains the sibling
> > + * subsequently creates.
> > + *
> > + * Phase 1: the sibling sets log_subdomains_off, creates a muted domain, and
> > + * triggers a denial that is NOT logged.
> > + *
> > + * Phase 2 (after TSYNC without LOG_SUBDOMAINS_OFF): the sibling stacks another
> > + * domain and triggers a denial that IS logged, proving the muting was
> > + * overridden.
> > + */
> > +TEST_F(audit, tsync_override_log_subdomains_off)
> > +{
> > +	const struct landlock_ruleset_attr ruleset_attr = {
> > +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> > +	};
> > +	struct audit_records records;
> > +	struct thread_data child_data;
> > +	int pipe_child[2], pipe_parent[2];
> > +	char buffer;
> > +	pthread_t thread;
> > +	void *thread_ret;
> > +
> > +	child_data.parent_pid = getppid();
> > +	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> > +	child_data.pipe_child = pipe_child[1];
> > +	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> > +	child_data.pipe_parent = pipe_parent[0];
> > +	child_data.ruleset_fd =
> > +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> > +	ASSERT_LE(0, child_data.ruleset_fd);
> > +
> > +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> > +
> > +	child_data.mute_subdomains = true;
> > +
> > +	/* Creates the sibling thread. */
> > +	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
> > +				    &child_data));
> > +
> > +	/*
> > +	 * Phase 1: the sibling mutes subdomain logs, creates a domain, and
> > +	 * triggers a denial.  The denial must not be logged.
> > +	 */
> > +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> > +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> > +
> > +	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
> > +					      child_data.parent_pid, NULL));
> > +
> > +	/* Drains any remaining records. */
> > +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> > +	EXPECT_EQ(0, records.access);
> > +
> > +	/*
> > +	 * Overrides the sibling's log_subdomains_off by calling TSYNC without
> > +	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.
> > +	 */
> > +	ASSERT_EQ(0, landlock_restrict_self(child_data.ruleset_fd,
> > +					    LANDLOCK_RESTRICT_SELF_TSYNC));
> > +
> > +	/*
> > +	 * Phase 2: the sibling stacks another domain and triggers a denial.
> > +	 * Because TSYNC replaced its log_subdomains_off with 0, the new domain
> > +	 * has log_status=LANDLOCK_LOG_PENDING.
> > +	 */
> > +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> > +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> > +
> > +	/* The denial must be logged. */
> > +	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
> > +					child_data.parent_pid, NULL));
> > +
> > +	EXPECT_EQ(0, close(pipe_child[0]));
> > +	EXPECT_EQ(0, close(pipe_parent[1]));
> > +	ASSERT_EQ(0, pthread_join(thread, &thread_ret));
> > +	EXPECT_EQ(NULL, thread_ret);
> > +}
> > +
> >  FIXTURE(audit_flags)
> >  {
> >  	struct audit_filter audit_filter;
> > diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
> > index 2b9ad4f154f4..abc290271a1a 100644
> > --- a/tools/testing/selftests/landlock/tsync_test.c
> > +++ b/tools/testing/selftests/landlock/tsync_test.c
> > @@ -247,4 +247,78 @@ TEST(tsync_interrupt)
> >  	EXPECT_EQ(0, close(ruleset_fd));
> >  }
> >  
> > +/* clang-format off */
> > +FIXTURE(tsync_without_ruleset) {};
> > +/* clang-format on */
> > +
> > +FIXTURE_VARIANT(tsync_without_ruleset)
> > +{
> > +	const __u32 flags;
> > +	const int expected_errno;
> > +};
> > +
> > +/* clang-format off */
> > +FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) {
> > +	/* clang-format on */
> > +	.flags = LANDLOCK_RESTRICT_SELF_TSYNC,
> > +	.expected_errno = EBADF,
> > +};
> > +
> > +/* clang-format off */
> > +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) {
> > +	/* clang-format on */
> > +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> > +		 LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
> > +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> > +	.expected_errno = EBADF,
> > +};
> > +
> > +/* clang-format off */
> > +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) {
> > +	/* clang-format on */
> > +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> > +		 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
> > +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> > +	.expected_errno = EBADF,
> > +};
> > +
> > +/* clang-format off */
> > +FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) {
> > +	/* clang-format on */
> > +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
> > +		 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
> > +		 LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> > +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> > +	.expected_errno = EBADF,
> > +};
> > +
> > +/* clang-format off */
> > +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) {
> > +	/* clang-format on */
> > +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> > +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> > +	.expected_errno = 0,
> > +};
> > +
> > +FIXTURE_SETUP(tsync_without_ruleset)
> > +{
> > +}
> > +
> > +FIXTURE_TEARDOWN(tsync_without_ruleset)
> > +{
> > +}
> > +
> > +TEST_F(tsync_without_ruleset, check)
> > +{
> > +	int ret;
> > +

I'll set NNP here.

> > +	ret = landlock_restrict_self(-1, variant->flags);
> > +	if (variant->expected_errno) {
> > +		EXPECT_EQ(-1, ret);
> > +		EXPECT_EQ(variant->expected_errno, errno);
> > +	} else {
> > +		EXPECT_EQ(0, ret);
> > +	}
> > +}
> 
> We are not setting the no_new_privs flag in this test, as we do in the
> others.
> 
> no_new_privs or CAP_SYS_ADMIN are required in the implementation, even
> when ruleset_fd == -1 and passing
> LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.

Sure.

> 
> > +
> >  TEST_HARNESS_MAIN
> > -- 
> > 2.53.0
> > 
> 
> Reviewed-by: Günther Noack <gnoack3000@gmail.com>
> 
> But please fix the flaky test.
> 
> –Günther
> 

^ permalink raw reply

* Re: [PATCH v1 1/2] landlock: Fix log_subdomains_off inheritance across fork()
From: Mickaël Salaün @ 2026-04-07 16:03 UTC (permalink / raw)
  To: Günther Noack, Jann Horn
  Cc: Günther Noack, linux-security-module, stable
In-Reply-To: <20260407.844e42deb531@gnoack.org>

On Tue, Apr 07, 2026 at 09:30:40AM +0200, Günther Noack wrote:
> Hello!
> 
> On Sat, Apr 04, 2026 at 10:49:57AM +0200, Mickaël Salaün wrote:
> > hook_cred_transfer() only copies the Landlock security blob when the
> > source credential has a domain.  This is inconsistent with
> > landlock_restrict_self() which can set log_subdomains_off on a
> > credential without creating a domain (via the ruleset_fd=-1 path): the
> > field is committed but not preserved across fork() because the child's
> > prepare_creds() calls hook_cred_transfer() which skips the copy when
> > domain is NULL.
> > 
> > This breaks the documented use case where a process mutes subdomain logs
> > before forking sandboxed children: the children lose the muting and
> > their domains produce unexpected audit records.
> > 
> > Fix this by unconditionally copying the Landlock credential blob.
> > landlock_get_ruleset(NULL) is already a safe no-op.
> > 
> > Cc: Günther Noack <gnoack@google.com>
> > Cc: stable@vger.kernel.org
> > Fixes: ead9079f7569 ("landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF")
> > Signed-off-by: Mickaël Salaün <mic@digikod.net>
> > ---
> >  security/landlock/cred.c                      |  6 +-
> >  tools/testing/selftests/landlock/audit_test.c | 88 +++++++++++++++++++
> >  2 files changed, 90 insertions(+), 4 deletions(-)
> > 
> > diff --git a/security/landlock/cred.c b/security/landlock/cred.c
> > index 0cb3edde4d18..cc419de75cd6 100644
> > --- a/security/landlock/cred.c
> > +++ b/security/landlock/cred.c
> > @@ -22,10 +22,8 @@ static void hook_cred_transfer(struct cred *const new,
> >  	const struct landlock_cred_security *const old_llcred =
> >  		landlock_cred(old);
> >  
> > -	if (old_llcred->domain) {
> > -		landlock_get_ruleset(old_llcred->domain);
> > -		*landlock_cred(new) = *old_llcred;
> > -	}
> > +	landlock_get_ruleset(old_llcred->domain);
> > +	*landlock_cred(new) = *old_llcred;
> 
> This fix looks correct for the hook_cred_prepare() case (and of
> course, hook_cred_prepare() calls hook_cred_transfer() in Landlock).
> 
> 
> But I'm afraid I might have spotted another issue here:
> 
> If I look at the code in security/keys/process_keys.c, where
> security_tranfer_creds() is called, the "old" object is actually
> already initialized, and if we are not checking for that, I think we
> are leaking memory.

old is only a partially initialized credential, and the Landlock
part is not set yet, which is the goal of hook_transfer_creds(), so
there is no leak.

> 
> I would suggest to use the helper landlock_cred_copy() from cred.h for

This is not required but if we would like to do it anyway, that would
not be backportable and would introduce a (minimal) performance penalty.

> that.  This one is anyway supposed to be the central place for this
> copying logic, and it is safe to use with zeroed-out target objects
> (because the put is safe for the NULL-pointer).
> 
> Maybe this is worth updating while we are at it?
> 
> 
> >  }
> >  
> >  static int hook_cred_prepare(struct cred *const new,
> > diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
> > index 46d02d49835a..20099b8667e7 100644
> > --- a/tools/testing/selftests/landlock/audit_test.c
> > +++ b/tools/testing/selftests/landlock/audit_test.c
> > @@ -279,6 +279,94 @@ TEST_F(audit, thread)
> >  				&audit_tv_default, sizeof(audit_tv_default)));
> >  }
> >  
> > +/*
> > + * Verifies that log_subdomains_off set via the ruleset_fd=-1 path (without
> > + * creating a domain) is inherited by children across fork().  This exercises
> > + * the hook_cred_transfer() fix: the Landlock credential blob must be copied
> > + * even when the source credential has no domain.
> > + *
> > + * Phase 1 (baseline): a child without muting creates a domain and triggers a
> > + * denial that IS logged.
> > + *
> > + * Phase 2 (after muting): the parent mutes subdomain logs, forks another child
> > + * who creates a domain and triggers a denial that is NOT logged.
> > + */
> > +TEST_F(audit, log_subdomains_off_fork)
> > +{
> > +	const struct landlock_ruleset_attr ruleset_attr = {
> > +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> > +	};
> > +	struct audit_records records;
> > +	int ruleset_fd, status;
> > +	pid_t child;
> > +
> > +	ruleset_fd =
> > +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> > +	ASSERT_LE(0, ruleset_fd);
> > +
> > +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> > +
> > +	/*
> > +	 * Phase 1: forks a child that creates a domain and triggers a denial
> > +	 * before any muting.  This proves the audit path works.
> > +	 */
> > +	child = fork();
> > +	ASSERT_LE(0, child);
> > +	if (child == 0) {
> > +		ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
> > +		ASSERT_EQ(-1, kill(getppid(), 0));
> > +		ASSERT_EQ(EPERM, errno);
> > +		_exit(0);
> > +		return;
> > +	}
> > +
> > +	ASSERT_EQ(child, waitpid(child, &status, 0));
> > +	ASSERT_EQ(true, WIFEXITED(status));
> > +	ASSERT_EQ(0, WEXITSTATUS(status));
> > +
> > +	/* The denial must be logged (baseline). */
> > +	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, getpid(),
> > +					NULL));
> > +
> > +	/* Drains any remaining records (e.g. domain allocation). */
> > +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> > +
> > +	/*
> > +	 * Mutes subdomain logs without creating a domain.  The parent's
> > +	 * credential has domain=NULL and log_subdomains_off=1.
> > +	 */
> > +	ASSERT_EQ(0, landlock_restrict_self(
> > +			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
> > +
> > +	/*
> > +	 * Phase 2: forks a child that creates a domain and triggers a denial.
> > +	 * Because log_subdomains_off was inherited via fork(), the child's
> > +	 * domain has log_status=LANDLOCK_LOG_DISABLED.
> > +	 */
> > +	child = fork();
> > +	ASSERT_LE(0, child);
> > +	if (child == 0) {
> > +		ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
> > +		ASSERT_EQ(-1, kill(getppid(), 0));
> > +		ASSERT_EQ(EPERM, errno);
> > +		_exit(0);
> > +		return;
> > +	}
> > +
> > +	ASSERT_EQ(child, waitpid(child, &status, 0));
> > +	ASSERT_EQ(true, WIFEXITED(status));
> > +	ASSERT_EQ(0, WEXITSTATUS(status));
> > +
> > +	/* No denial record should appear. */
> > +	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
> > +					      getpid(), NULL));
> > +
> > +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> > +	EXPECT_EQ(0, records.access);
> > +
> > +	EXPECT_EQ(0, close(ruleset_fd));
> > +}
> > +
> >  FIXTURE(audit_flags)
> >  {
> >  	struct audit_filter audit_filter;
> > -- 
> > 2.53.0
> > 
> 
> Test looks fine.
> 
> While I do still think we should investigate the memory leak, this
> commit is, as it is, already a strict improvement over what we had
> before, so:
> 
> Reviewed-by: Günther Noack <gnoack3000@gmail.com>

I'll keep your tag if this patch is ok with you as-is.

> 
> –Günther
> 

^ permalink raw reply

* Re: [PATCH v4 3/3] selinux: fix overlayfs mmap() and mprotect() access checks
From: Paul Moore @ 2026-04-07 14:35 UTC (permalink / raw)
  To: Stephen Smalley
  Cc: Ondrej Mosnacek, linux-security-module, selinux, linux-fsdevel,
	linux-unionfs, linux-erofs, Amir Goldstein, Gao Xiang,
	Christian Brauner
In-Reply-To: <CAEjxPJ4nqeuPhve3Fe-tFuNW9R5grnWwfYJv7q2cRu+UPQ5c4A@mail.gmail.com>

On Tue, Apr 7, 2026 at 8:14 AM Stephen Smalley
<stephen.smalley.work@gmail.com> wrote:
> On Thu, Apr 2, 2026 at 11:09 PM Paul Moore <paul@paul-moore.com> wrote:
> >
> > The existing SELinux security model for overlayfs is to allow access if
> > the current task is able to access the top level file (the "user" file)
> > and the mounter's credentials are sufficient to access the lower
> > level file (the "backing" file).  Unfortunately, the current code does
> > not properly enforce these access controls for both mmap() and mprotect()
> > operations on overlayfs filesystems.
> >
> > This patch makes use of the newly created security_mmap_backing_file()
> > LSM hook to provide the missing backing file enforcement for mmap()
> > operations, and leverages the backing file API and new LSM blob to
> > provide the necessary information to properly enforce the mprotect()
> > access controls.
> >
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Paul Moore <paul@paul-moore.com>
>
> Do you have tests for these changes showing the before and after (i.e.
> failing without your patches, passing with them)? I tried running an
> earlier set from Ondrej but they failed.

A few months ago I sent you and Ondrej some feedback on those early
tests from Ondrej, but yes, I also had problems with Ondrej's tests.
I've been using a hacked up combination of the existing tests, some of
Ondrej's additions, and an additional debug/test patch to ensure the
labeling is correct.  It's far from ideal, but I didn't invest time in
test development as I assumed Ondrej would continue his efforts there
(unfortunately it doesn't appear that he has?), and I wanted to focus
on getting a solution as soon as possible for obvious reasons.

-- 
paul-moore.com

^ permalink raw reply

* LSM: Whiteout chardev creation sidesteps mknod hook
From: Günther Noack @ 2026-04-07 13:05 UTC (permalink / raw)
  To: Christian Brauner, Mickaël Salaün, Paul Moore
  Cc: linux-security-module

Hello Christian, Paul, Mickaël and LSM maintainers!

I discovered the following bug in Landlock, which potentially also
affects other LSMs:

With renameat2(2)'s RENAME_WHITEOUT flag, it is possible to create a
"whiteout object" at the source of the rename.  Whiteout objects are
character devices with major/minor (0, 0) -- these devices are not
bound to any driver, so they are harmless, but still, the creation of
these files can sidestep the LANDLOCK_ACCESS_FS_MAKE_CHAR access right
in Landlock.


I am unconvinced which is the right fix here -- do you have an opinion
on this from the VFS/LSM side?


Option 1: Make filesystems call security_path_mknod() during RENAME_WHITEOUT?

Do it in the VFS rename hook.

* Pro: Fixes it for all LSMs
* Con: Call would have to be done in multiple filesystems


Option 2: Handle it in security_{path,inode}_rename()

Make Landlock handle it in security_inode_rename() by looking for the
RENAME_WHITEOUT flag.

* Con: Operation should only be denied if the file system even
  implements RENAME_WHITEOUT, and we would have to maintain a list of
  affected filesystems for that.  (That feels like solving it at the
  wrong layer of abstraction.)
* Con: Unclear whether other LSMs need a similar fix


Option 3: Declare that this is working as intended?

* Pro: (0, 0) is not a "real" character device


In cases 1 and 2, we'd likely need to double check that we are not
breaking existing scenarios involving OverlayFS, by suddenly requiring
a more lax policy for creating character devices on these directories.

Please let me know what you think.  I'm specifically interested in:

1. Christian: What is the appropriate way to do this VFS wise?
2. LSM maintainers: Is this a bug that affects other LSMs as well?

Thanks,
—Günther

P.S.: For full transparency, I found this bug by pointing Google
Gemini at the Landlock codebase.

^ permalink raw reply

* Re: [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials
From: Mickaël Salaün @ 2026-04-07 13:00 UTC (permalink / raw)
  To: Steven Rostedt
  Cc: Christian Brauner, Günther Noack, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-fsdevel, linux-security-module, linux-trace-kernel
In-Reply-To: <20260406110123.4072a765@gandalf.local.home>

On Mon, Apr 06, 2026 at 11:01:23AM -0400, Steven Rostedt wrote:
> On Mon,  6 Apr 2026 16:37:10 +0200
> Mickaël Salaün <mic@digikod.net> wrote:
> 
> > ---
> >  include/trace/events/landlock.h | 135 ++++++++++++++++++++++++++++++++
> >  security/landlock/log.c         |  20 +++++
> >  2 files changed, 155 insertions(+)
> > 
> > diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
> > index 1afab091efba..9f96c9897f44 100644
> > --- a/include/trace/events/landlock.h
> > +++ b/include/trace/events/landlock.h
> > @@ -11,6 +11,7 @@
> >  #define _TRACE_LANDLOCK_H
> >  
> >  #include <linux/tracepoint.h>
> > +#include <net/af_unix.h>
> >  
> >  struct dentry;
> >  struct landlock_domain;
> > @@ -19,6 +20,7 @@ struct landlock_rule;
> >  struct landlock_ruleset;
> >  struct path;
> >  struct sock;
> > +struct task_struct;
> >  
> >  /**
> >   * DOC: Landlock trace events
> > @@ -433,6 +435,139 @@ TRACE_EVENT(
> >  		__entry->log_new_exec, __entry->blockers, __entry->sport,
> >  		__entry->dport));
> >  
> > +/**
> > + * landlock_deny_ptrace - ptrace access denied
> > + * @hierarchy: Hierarchy node that blocked the access (never NULL)
> > + * @same_exec: Whether the current task is the same executable that called
> > + *             landlock_restrict_self() for the denying hierarchy node
> > + * @tracee: Target task (never NULL); eBPF can read pid, comm, cred,
> > + *          namespaces, and cgroup via BTF
> > + */
> > +TRACE_EVENT(
> > +	landlock_deny_ptrace,
> > +
> > +	TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
> > +		 const struct task_struct *tracee),
> > +
> > +	TP_ARGS(hierarchy, same_exec, tracee),
> > +
> > +	TP_STRUCT__entry(
> > +		__field(__u64, domain_id) __field(bool, same_exec)
> > +			__field(u32, log_same_exec) __field(u32, log_new_exec)
> > +				__field(pid_t, tracee_pid)
> > +					__string(tracee_comm, tracee->comm)),
> 
> Event formats are different than normal macro formatting. Please use the
> event formatting. The above is a defined structure that is being created
> for use. Keep it looking like a structure:
> 
> 	TP_STRUCT__entry(
> 		__field(	__u64,		domain_id)
> 		__field(	bool,		same_exec)
> 		__field(	u32,		log_same_exec)
> 		__field(	u32,		log_new_exec)
> 		__field(	pid_t,		tracee_pid)
> 		__string(	tracee_comm,	tracee->comm)
> 	),

I was using clang-format, but it doesn't make sense here, I'll fix it.

> 
> See how the above resembles:
> 
> struct entry {
> 	__u64		domain_id;
> 	bool		same_exec;
> 	u32		log_same_exec;
> 	u32		log_new_exec;
> 	pid_t		tracee_pid;
> 	string		tracee_comm;
> };
> 
> Because that's pretty much what the trace event TP_STRUCT__entry() is going
> to do with it. (The string will obviously be something else).
> 
> This way it's also easy to spot wholes in the structure that is written
> into the ring buffer. The "same_exec" being a bool followed by two u32
> types, is going to cause a hole. Move it to between tracee_pid and
> tracee_comm.

Actually, the log_* field should be bool too.  Anyway, is it a concern
that the ring buffer leaks (previous event) kernel memory or is the
concern mostly about avoiding wasted space and making easy to spot holes
even if it's OK?

> 
> Please fix the other events too.

Sure. Thanks!

> 
> -- Steve
> 
> 
> > +
> > +	TP_fast_assign(__entry->domain_id = hierarchy->id;
> > +		       __entry->same_exec = same_exec;
> > +		       __entry->log_same_exec = hierarchy->log_same_exec;
> > +		       __entry->log_new_exec = hierarchy->log_new_exec;
> > +		       __entry->tracee_pid =
> > +			       task_tgid_nr((struct task_struct *)tracee);
> > +		       __assign_str(tracee_comm);),
> > +
> > +	TP_printk(
> > +		"domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u tracee_pid=%d comm=%s",
> > +		__entry->domain_id, __entry->same_exec, __entry->log_same_exec,
> > +		__entry->log_new_exec, __entry->tracee_pid,
> > +		__print_untrusted_str(tracee_comm)));

Are you OK with this new helper?

> > +
> >
> 

^ permalink raw reply

* Re: [PATCH v4 3/3] selinux: fix overlayfs mmap() and mprotect() access checks
From: Stephen Smalley @ 2026-04-07 12:14 UTC (permalink / raw)
  To: Paul Moore, Ondrej Mosnacek
  Cc: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs, Amir Goldstein, Gao Xiang, Christian Brauner
In-Reply-To: <20260403030848.731867-8-paul@paul-moore.com>

On Thu, Apr 2, 2026 at 11:09 PM Paul Moore <paul@paul-moore.com> wrote:
>
> The existing SELinux security model for overlayfs is to allow access if
> the current task is able to access the top level file (the "user" file)
> and the mounter's credentials are sufficient to access the lower
> level file (the "backing" file).  Unfortunately, the current code does
> not properly enforce these access controls for both mmap() and mprotect()
> operations on overlayfs filesystems.
>
> This patch makes use of the newly created security_mmap_backing_file()
> LSM hook to provide the missing backing file enforcement for mmap()
> operations, and leverages the backing file API and new LSM blob to
> provide the necessary information to properly enforce the mprotect()
> access controls.
>
> Cc: stable@vger.kernel.org
> Signed-off-by: Paul Moore <paul@paul-moore.com>

Do you have tests for these changes showing the before and after (i.e.
failing without your patches, passing with them)? I tried running an
earlier set from Ondrej but they failed.

^ permalink raw reply

* Re: [PATCH v1 2/2] landlock: Allow TSYNC with LOG_SUBDOMAINS_OFF and fd=-1
From: Günther Noack @ 2026-04-07  8:25 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Günther Noack, linux-security-module, stable
In-Reply-To: <20260404085001.1604405-2-mic@digikod.net>

Hello!

On Sat, Apr 04, 2026 at 10:49:58AM +0200, Mickaël Salaün wrote:
> LANDLOCK_RESTRICT_SELF_TSYNC does not allow
> LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with ruleset_fd=-1, preventing
> a multithreaded process from atomically propagating subdomain log muting
> to all threads without creating a domain layer.  Relax the fd=-1
> condition to accept TSYNC alongside LOG_SUBDOMAINS_OFF, and update the
> documentation accordingly.
> 
> Add flag validation tests for all TSYNC combinations with ruleset_fd=-1,
> and audit tests verifying both transition directions: muting via TSYNC
> (logged to not logged) and override via TSYNC (not logged to logged).
> 
> Cc: Günther Noack <gnoack@google.com>
> Cc: stable@vger.kernel.org
> Fixes: 42fc7e6543f6 ("landlock: Multithreading support for landlock_restrict_self()")
> Signed-off-by: Mickaël Salaün <mic@digikod.net>
> ---
>  include/uapi/linux/landlock.h                 |   4 +-
>  security/landlock/syscalls.c                  |  14 +-
>  tools/testing/selftests/landlock/audit_test.c | 233 ++++++++++++++++++
>  tools/testing/selftests/landlock/tsync_test.c |  74 ++++++
>  4 files changed, 319 insertions(+), 6 deletions(-)
> 
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index f88fa1f68b77..d37603efc273 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -116,7 +116,9 @@ struct landlock_ruleset_attr {
>   *     ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, this flag only affects
>   *     future nested domains, not the one being created. It can also be used
>   *     with a @ruleset_fd value of -1 to mute subdomain logs without creating a
> - *     domain.
> + *     domain.  When combined with %LANDLOCK_RESTRICT_SELF_TSYNC and a
> + *     @ruleset_fd value of -1, this configuration is propagated to all threads
> + *     of the current process.
>   *
>   * The following flag supports policy enforcement in multithreaded processes:
>   *
> diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
> index 0d66a68677b7..a0bb664e0d31 100644
> --- a/security/landlock/syscalls.c
> +++ b/security/landlock/syscalls.c
> @@ -512,10 +512,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
>  
>  	/*
>  	 * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
> -	 * -1 as ruleset_fd, but no other flag must be set.
> +	 * -1 as ruleset_fd, optionally combined with
> +	 * LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all
> +	 * threads.  No other flag must be set.
>  	 */
>  	if (!(ruleset_fd == -1 &&
> -	      flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
> +	      (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
> +		      LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {

Well spotted, thanks!


>  		/* Gets and checks the ruleset. */
>  		ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
>  		if (IS_ERR(ruleset))
> @@ -537,9 +540,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
>  
>  	/*
>  	 * The only case when a ruleset may not be set is if
> -	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1.
> -	 * We could optimize this case by not calling commit_creds() if this flag
> -	 * was already set, but it is not worth the complexity.
> +	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
> +	 * LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1.  We could
> +	 * optimize this case by not calling commit_creds() if this flag was
> +	 * already set, but it is not worth the complexity.
>  	 */
>  	if (ruleset) {
>  		/*
> diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
> index 20099b8667e7..a193d8a97560 100644
> --- a/tools/testing/selftests/landlock/audit_test.c
> +++ b/tools/testing/selftests/landlock/audit_test.c
> @@ -162,6 +162,7 @@ TEST_F(audit, layers)
>  struct thread_data {
>  	pid_t parent_pid;
>  	int ruleset_fd, pipe_child, pipe_parent;
> +	bool mute_subdomains;
>  };
>  
>  static void *thread_audit_test(void *arg)
> @@ -367,6 +368,238 @@ TEST_F(audit, log_subdomains_off_fork)
>  	EXPECT_EQ(0, close(ruleset_fd));
>  }
>  
> +/*
> + * Thread function: runs two rounds of (create domain, trigger denial, signal
> + * back), waiting for the main thread before each round.  When mute_subdomains
> + * is set, phase 1 also mutes subdomain logs via the fd=-1 path before creating
> + * the domain.  The ruleset_fd is kept open across both rounds so each
> + * restrict_self call stacks a new domain layer.
> + */
> +static void *thread_sandbox_deny_twice(void *arg)
> +{
> +	const struct thread_data *data = (struct thread_data *)arg;
> +	uintptr_t err = 0;
> +	char buffer;
> +
> +	/* Phase 1: optionally mutes, creates a domain, and triggers a denial. */
> +	if (read(data->pipe_parent, &buffer, 1) != 1) {
> +		err = 1;
> +		goto out;
> +	}
> +
> +	if (data->mute_subdomains &&
> +	    landlock_restrict_self(-1,
> +				   LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
> +		err = 2;
> +		goto out;
> +	}
> +
> +	if (landlock_restrict_self(data->ruleset_fd, 0)) {
> +		err = 3;
> +		goto out;
> +	}
> +
> +	if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
> +		err = 4;
> +		goto out;
> +	}
> +
> +	if (write(data->pipe_child, ".", 1) != 1) {
> +		err = 5;
> +		goto out;
> +	}
> +
> +	/* Phase 2: stacks another domain and triggers a denial. */
> +	if (read(data->pipe_parent, &buffer, 1) != 1) {
> +		err = 6;
> +		goto out;
> +	}
> +
> +	if (landlock_restrict_self(data->ruleset_fd, 0)) {
> +		err = 7;
> +		goto out;
> +	}
> +
> +	if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
> +		err = 8;
> +		goto out;
> +	}
> +
> +	if (write(data->pipe_child, ".", 1) != 1) {
> +		err = 9;
> +		goto out;
> +	}
> +
> +out:
> +	close(data->ruleset_fd);
> +	close(data->pipe_child);
> +	close(data->pipe_parent);
> +	return (void *)err;
> +}
> +
> +/*
> + * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
> + * LANDLOCK_RESTRICT_SELF_TSYNC and ruleset_fd=-1 propagates log_subdomains_off
> + * to a sibling thread, suppressing audit logging on domains it subsequently
> + * creates.
> + *
> + * Phase 1 (before TSYNC) acts as an inline baseline: the sibling creates a
> + * domain and triggers a denial that IS logged.
> + *
> + * Phase 2 (after TSYNC) verifies suppression: the sibling stacks another domain
> + * and triggers a denial that is NOT logged.
> + */
> +TEST_F(audit, log_subdomains_off_tsync)
> +{
> +	const struct landlock_ruleset_attr ruleset_attr = {
> +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> +	};
> +	struct audit_records records;
> +	struct thread_data child_data;

The child_data.mute_subdomains field stays uninitialized in this
function (and maybe others).  Please fix.

   struct thread_data child_data = {};


> +	int pipe_child[2], pipe_parent[2];
> +	char buffer;
> +	pthread_t thread;
> +	void *thread_ret;
> +
> +	child_data.parent_pid = getppid();
> +	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> +	child_data.pipe_child = pipe_child[1];
> +	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> +	child_data.pipe_parent = pipe_parent[0];
> +	child_data.ruleset_fd =
> +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> +	ASSERT_LE(0, child_data.ruleset_fd);
> +
> +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> +
> +	/* Creates the sibling thread. */
> +	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
> +				    &child_data));
> +
> +	/*
> +	 * Phase 1: the sibling creates a domain and triggers a denial before
> +	 * any log muting.  This proves the audit path works.
> +	 */
> +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> +
> +	/* The denial must be logged. */
> +	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
> +					child_data.parent_pid, NULL));
> +
> +	/* Drains any remaining records (e.g. domain allocation). */
> +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> +
> +	/*
> +	 * Mutes subdomain logs and propagates to the sibling thread via TSYNC,
> +	 * without creating a domain.
> +	 */
> +	ASSERT_EQ(0, landlock_restrict_self(
> +			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> +					 LANDLOCK_RESTRICT_SELF_TSYNC));
> +
> +	/*
> +	 * Phase 2: the sibling stacks another domain and triggers a denial.
> +	 * Because log_subdomains_off was propagated via TSYNC, the new domain
> +	 * has log_status=LANDLOCK_LOG_DISABLED.
> +	 */
> +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> +
> +	/* No denial record should appear. */
> +	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
> +					      child_data.parent_pid, NULL));
> +
> +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> +	EXPECT_EQ(0, records.access);
> +
> +	EXPECT_EQ(0, close(pipe_child[0]));
> +	EXPECT_EQ(0, close(pipe_parent[1]));
> +	ASSERT_EQ(0, pthread_join(thread, &thread_ret));
> +	EXPECT_EQ(NULL, thread_ret);
> +}
> +
> +/*
> + * Verifies that LANDLOCK_RESTRICT_SELF_TSYNC without
> + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF overrides a sibling thread's
> + * log_subdomains_off, re-enabling audit logging on domains the sibling
> + * subsequently creates.
> + *
> + * Phase 1: the sibling sets log_subdomains_off, creates a muted domain, and
> + * triggers a denial that is NOT logged.
> + *
> + * Phase 2 (after TSYNC without LOG_SUBDOMAINS_OFF): the sibling stacks another
> + * domain and triggers a denial that IS logged, proving the muting was
> + * overridden.
> + */
> +TEST_F(audit, tsync_override_log_subdomains_off)
> +{
> +	const struct landlock_ruleset_attr ruleset_attr = {
> +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> +	};
> +	struct audit_records records;
> +	struct thread_data child_data;
> +	int pipe_child[2], pipe_parent[2];
> +	char buffer;
> +	pthread_t thread;
> +	void *thread_ret;
> +
> +	child_data.parent_pid = getppid();
> +	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
> +	child_data.pipe_child = pipe_child[1];
> +	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
> +	child_data.pipe_parent = pipe_parent[0];
> +	child_data.ruleset_fd =
> +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> +	ASSERT_LE(0, child_data.ruleset_fd);
> +
> +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> +
> +	child_data.mute_subdomains = true;
> +
> +	/* Creates the sibling thread. */
> +	ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
> +				    &child_data));
> +
> +	/*
> +	 * Phase 1: the sibling mutes subdomain logs, creates a domain, and
> +	 * triggers a denial.  The denial must not be logged.
> +	 */
> +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> +
> +	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
> +					      child_data.parent_pid, NULL));
> +
> +	/* Drains any remaining records. */
> +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> +	EXPECT_EQ(0, records.access);
> +
> +	/*
> +	 * Overrides the sibling's log_subdomains_off by calling TSYNC without
> +	 * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.
> +	 */
> +	ASSERT_EQ(0, landlock_restrict_self(child_data.ruleset_fd,
> +					    LANDLOCK_RESTRICT_SELF_TSYNC));
> +
> +	/*
> +	 * Phase 2: the sibling stacks another domain and triggers a denial.
> +	 * Because TSYNC replaced its log_subdomains_off with 0, the new domain
> +	 * has log_status=LANDLOCK_LOG_PENDING.
> +	 */
> +	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
> +	ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
> +
> +	/* The denial must be logged. */
> +	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
> +					child_data.parent_pid, NULL));
> +
> +	EXPECT_EQ(0, close(pipe_child[0]));
> +	EXPECT_EQ(0, close(pipe_parent[1]));
> +	ASSERT_EQ(0, pthread_join(thread, &thread_ret));
> +	EXPECT_EQ(NULL, thread_ret);
> +}
> +
>  FIXTURE(audit_flags)
>  {
>  	struct audit_filter audit_filter;
> diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
> index 2b9ad4f154f4..abc290271a1a 100644
> --- a/tools/testing/selftests/landlock/tsync_test.c
> +++ b/tools/testing/selftests/landlock/tsync_test.c
> @@ -247,4 +247,78 @@ TEST(tsync_interrupt)
>  	EXPECT_EQ(0, close(ruleset_fd));
>  }
>  
> +/* clang-format off */
> +FIXTURE(tsync_without_ruleset) {};
> +/* clang-format on */
> +
> +FIXTURE_VARIANT(tsync_without_ruleset)
> +{
> +	const __u32 flags;
> +	const int expected_errno;
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) {
> +	/* clang-format on */
> +	.flags = LANDLOCK_RESTRICT_SELF_TSYNC,
> +	.expected_errno = EBADF,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) {
> +	/* clang-format on */
> +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> +		 LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
> +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> +	.expected_errno = EBADF,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) {
> +	/* clang-format on */
> +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> +		 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
> +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> +	.expected_errno = EBADF,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) {
> +	/* clang-format on */
> +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
> +		 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
> +		 LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> +	.expected_errno = EBADF,
> +};
> +
> +/* clang-format off */
> +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) {
> +	/* clang-format on */
> +	.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> +		 LANDLOCK_RESTRICT_SELF_TSYNC,
> +	.expected_errno = 0,
> +};
> +
> +FIXTURE_SETUP(tsync_without_ruleset)
> +{
> +}
> +
> +FIXTURE_TEARDOWN(tsync_without_ruleset)
> +{
> +}
> +
> +TEST_F(tsync_without_ruleset, check)
> +{
> +	int ret;
> +
> +	ret = landlock_restrict_self(-1, variant->flags);
> +	if (variant->expected_errno) {
> +		EXPECT_EQ(-1, ret);
> +		EXPECT_EQ(variant->expected_errno, errno);
> +	} else {
> +		EXPECT_EQ(0, ret);
> +	}
> +}

We are not setting the no_new_privs flag in this test, as we do in the
others.

no_new_privs or CAP_SYS_ADMIN are required in the implementation, even
when ruleset_fd == -1 and passing
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.

> +
>  TEST_HARNESS_MAIN
> -- 
> 2.53.0
> 

Reviewed-by: Günther Noack <gnoack3000@gmail.com>

But please fix the flaky test.

–Günther

^ permalink raw reply

* Re: [PATCH v1 1/2] landlock: Fix log_subdomains_off inheritance across fork()
From: Günther Noack @ 2026-04-07  7:30 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Günther Noack, linux-security-module, stable
In-Reply-To: <20260404085001.1604405-1-mic@digikod.net>

Hello!

On Sat, Apr 04, 2026 at 10:49:57AM +0200, Mickaël Salaün wrote:
> hook_cred_transfer() only copies the Landlock security blob when the
> source credential has a domain.  This is inconsistent with
> landlock_restrict_self() which can set log_subdomains_off on a
> credential without creating a domain (via the ruleset_fd=-1 path): the
> field is committed but not preserved across fork() because the child's
> prepare_creds() calls hook_cred_transfer() which skips the copy when
> domain is NULL.
> 
> This breaks the documented use case where a process mutes subdomain logs
> before forking sandboxed children: the children lose the muting and
> their domains produce unexpected audit records.
> 
> Fix this by unconditionally copying the Landlock credential blob.
> landlock_get_ruleset(NULL) is already a safe no-op.
> 
> Cc: Günther Noack <gnoack@google.com>
> Cc: stable@vger.kernel.org
> Fixes: ead9079f7569 ("landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF")
> Signed-off-by: Mickaël Salaün <mic@digikod.net>
> ---
>  security/landlock/cred.c                      |  6 +-
>  tools/testing/selftests/landlock/audit_test.c | 88 +++++++++++++++++++
>  2 files changed, 90 insertions(+), 4 deletions(-)
> 
> diff --git a/security/landlock/cred.c b/security/landlock/cred.c
> index 0cb3edde4d18..cc419de75cd6 100644
> --- a/security/landlock/cred.c
> +++ b/security/landlock/cred.c
> @@ -22,10 +22,8 @@ static void hook_cred_transfer(struct cred *const new,
>  	const struct landlock_cred_security *const old_llcred =
>  		landlock_cred(old);
>  
> -	if (old_llcred->domain) {
> -		landlock_get_ruleset(old_llcred->domain);
> -		*landlock_cred(new) = *old_llcred;
> -	}
> +	landlock_get_ruleset(old_llcred->domain);
> +	*landlock_cred(new) = *old_llcred;

This fix looks correct for the hook_cred_prepare() case (and of
course, hook_cred_prepare() calls hook_cred_transfer() in Landlock).


But I'm afraid I might have spotted another issue here:

If I look at the code in security/keys/process_keys.c, where
security_tranfer_creds() is called, the "old" object is actually
already initialized, and if we are not checking for that, I think we
are leaking memory.

I would suggest to use the helper landlock_cred_copy() from cred.h for
that.  This one is anyway supposed to be the central place for this
copying logic, and it is safe to use with zeroed-out target objects
(because the put is safe for the NULL-pointer).

Maybe this is worth updating while we are at it?


>  }
>  
>  static int hook_cred_prepare(struct cred *const new,
> diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
> index 46d02d49835a..20099b8667e7 100644
> --- a/tools/testing/selftests/landlock/audit_test.c
> +++ b/tools/testing/selftests/landlock/audit_test.c
> @@ -279,6 +279,94 @@ TEST_F(audit, thread)
>  				&audit_tv_default, sizeof(audit_tv_default)));
>  }
>  
> +/*
> + * Verifies that log_subdomains_off set via the ruleset_fd=-1 path (without
> + * creating a domain) is inherited by children across fork().  This exercises
> + * the hook_cred_transfer() fix: the Landlock credential blob must be copied
> + * even when the source credential has no domain.
> + *
> + * Phase 1 (baseline): a child without muting creates a domain and triggers a
> + * denial that IS logged.
> + *
> + * Phase 2 (after muting): the parent mutes subdomain logs, forks another child
> + * who creates a domain and triggers a denial that is NOT logged.
> + */
> +TEST_F(audit, log_subdomains_off_fork)
> +{
> +	const struct landlock_ruleset_attr ruleset_attr = {
> +		.scoped = LANDLOCK_SCOPE_SIGNAL,
> +	};
> +	struct audit_records records;
> +	int ruleset_fd, status;
> +	pid_t child;
> +
> +	ruleset_fd =
> +		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> +	ASSERT_LE(0, ruleset_fd);
> +
> +	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
> +
> +	/*
> +	 * Phase 1: forks a child that creates a domain and triggers a denial
> +	 * before any muting.  This proves the audit path works.
> +	 */
> +	child = fork();
> +	ASSERT_LE(0, child);
> +	if (child == 0) {
> +		ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
> +		ASSERT_EQ(-1, kill(getppid(), 0));
> +		ASSERT_EQ(EPERM, errno);
> +		_exit(0);
> +		return;
> +	}
> +
> +	ASSERT_EQ(child, waitpid(child, &status, 0));
> +	ASSERT_EQ(true, WIFEXITED(status));
> +	ASSERT_EQ(0, WEXITSTATUS(status));
> +
> +	/* The denial must be logged (baseline). */
> +	EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, getpid(),
> +					NULL));
> +
> +	/* Drains any remaining records (e.g. domain allocation). */
> +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> +
> +	/*
> +	 * Mutes subdomain logs without creating a domain.  The parent's
> +	 * credential has domain=NULL and log_subdomains_off=1.
> +	 */
> +	ASSERT_EQ(0, landlock_restrict_self(
> +			     -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
> +
> +	/*
> +	 * Phase 2: forks a child that creates a domain and triggers a denial.
> +	 * Because log_subdomains_off was inherited via fork(), the child's
> +	 * domain has log_status=LANDLOCK_LOG_DISABLED.
> +	 */
> +	child = fork();
> +	ASSERT_LE(0, child);
> +	if (child == 0) {
> +		ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
> +		ASSERT_EQ(-1, kill(getppid(), 0));
> +		ASSERT_EQ(EPERM, errno);
> +		_exit(0);
> +		return;
> +	}
> +
> +	ASSERT_EQ(child, waitpid(child, &status, 0));
> +	ASSERT_EQ(true, WIFEXITED(status));
> +	ASSERT_EQ(0, WEXITSTATUS(status));
> +
> +	/* No denial record should appear. */
> +	EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
> +					      getpid(), NULL));
> +
> +	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
> +	EXPECT_EQ(0, records.access);
> +
> +	EXPECT_EQ(0, close(ruleset_fd));
> +}
> +
>  FIXTURE(audit_flags)
>  {
>  	struct audit_filter audit_filter;
> -- 
> 2.53.0
> 

Test looks fine.

While I do still think we should investigate the memory leak, this
commit is, as it is, already a strict improvement over what we had
before, so:

Reviewed-by: Günther Noack <gnoack3000@gmail.com>

–Günther

^ permalink raw reply

* [PATCH] evm: zero-initialize the evm_xattrs read buffer
From: Pengpeng Hou @ 2026-04-07  6:09 UTC (permalink / raw)
  To: Mimi Zohar, Roberto Sassu
  Cc: Dmitry Kasatkin, Eric Snowberg, Paul Moore, James Morris,
	Serge Hallyn, linux-integrity, linux-security-module,
	linux-kernel, pengpeng

evm_read_xattrs() allocates size + 1 bytes, fills them from the list of
enabled xattrs and then passes strlen(temp) to simple_read_from_buffer().
When no configured xattrs are enabled, the fill loop stores nothing and
temp[0] remains uninitialized, so strlen() reads beyond initialized
memory.

Use kzalloc() so the empty-list case stays a valid empty C string.

Signed-off-by: Pengpeng Hou <pengpeng@iscas.ac.cn>
---
 security/integrity/evm/evm_secfs.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/security/integrity/evm/evm_secfs.c b/security/integrity/evm/evm_secfs.c
index acd840461902..03d376fa36c2 100644
--- a/security/integrity/evm/evm_secfs.c
+++ b/security/integrity/evm/evm_secfs.c
@@ -145,7 +145,7 @@ static ssize_t evm_read_xattrs(struct file *filp, char __user *buf,
 		size += strlen(xattr->name) + 1;
 	}
 
-	temp = kmalloc(size + 1, GFP_KERNEL);
+	temp = kzalloc(size + 1, GFP_KERNEL);
 	if (!temp) {
 		mutex_unlock(&xattr_list_mutex);
 		return -ENOMEM;
-- 
2.50.1 (Apple Git-155)



^ permalink raw reply related

* Re: [PATCH v2] KEYS: trusted: Debugging as a feature
From: Nayna Jain @ 2026-04-07  2:42 UTC (permalink / raw)
  To: Jarkko Sakkinen, linux-integrity
  Cc: keyrings, Srish Srinivasan, James Bottomley, Mimi Zohar,
	David Howells, Paul Moore, James Morris, Serge E. Hallyn,
	Ahmad Fatoum, Pengutronix Kernel Team, open list,
	open list:SECURITY SUBSYSTEM
In-Reply-To: <20260324110043.67248-1-jarkko@kernel.org>


On 3/24/26 7:00 AM, Jarkko Sakkinen wrote:
> TPM_DEBUG, and other similar flags, are a non-standard way to specify a
> feature in Linux kernel.  Introduce CONFIG_TRUSTED_KEYS_DEBUG for
> trusted keys, and use it to replace these ad-hoc feature flags.
>
> Given that trusted keys debug dumps can contain sensitive data, harden
> the feature as follows:
>
> 1. In the Kconfig description postulate that pr_debug() statements must be
>     used.
> 2. Use pr_debug() statements in TPM 1.x driver to print the protocol dump.
>
> Traces, when actually needed, can be easily enabled by providing
> trusted.dyndbg='+p' in the kernel command-line.
>
> Cc: Srish Srinivasan <ssrish@linux.ibm.com>
> Reported-by: Nayna Jain <nayna@linux.ibm.com>
> Closes: https://lore.kernel.org/all/7f8b8478-5cd8-4d97-bfd0-341fd5cf10f9@linux.ibm.com/
> Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> ---
> v2:
> - Implement for all trusted keys backends.
> - Add HAVE_TRUSTED_KEYS_DEBUG as it is a good practice despite full
>    coverage.
> ---
>   include/keys/trusted-type.h               | 18 +++++-------
>   security/keys/trusted-keys/Kconfig        | 19 ++++++++++++
>   security/keys/trusted-keys/trusted_caam.c |  4 +--
>   security/keys/trusted-keys/trusted_tpm1.c | 36 +++++++++++------------
>   4 files changed, 46 insertions(+), 31 deletions(-)
>
> diff --git a/include/keys/trusted-type.h b/include/keys/trusted-type.h
> index 03527162613f..620a1f890b6b 100644
> --- a/include/keys/trusted-type.h
> +++ b/include/keys/trusted-type.h
> @@ -83,18 +83,16 @@ struct trusted_key_source {
>   
>   extern struct key_type key_type_trusted;
>   
> -#define TRUSTED_DEBUG 0
> -
> -#if TRUSTED_DEBUG
> +#ifdef CONFIG_TRUSTED_KEYS_DEBUG
>   static inline void dump_payload(struct trusted_key_payload *p)
>   {
> -	pr_info("key_len %d\n", p->key_len);
> -	print_hex_dump(KERN_INFO, "key ", DUMP_PREFIX_NONE,
> -		       16, 1, p->key, p->key_len, 0);
> -	pr_info("bloblen %d\n", p->blob_len);
> -	print_hex_dump(KERN_INFO, "blob ", DUMP_PREFIX_NONE,
> -		       16, 1, p->blob, p->blob_len, 0);
> -	pr_info("migratable %d\n", p->migratable);
> +	pr_debug("key_len %d\n", p->key_len);
> +	print_hex_dump_debug("key ", DUMP_PREFIX_NONE,
> +			     16, 1, p->key, p->key_len, 0);
> +	pr_debug("bloblen %d\n", p->blob_len);
> +	print_hex_dump_debug("blob ", DUMP_PREFIX_NONE,
> +			     16, 1, p->blob, p->blob_len, 0);
> +	pr_debug("migratable %d\n", p->migratable);
>   }
>   #else
>   static inline void dump_payload(struct trusted_key_payload *p)
> diff --git a/security/keys/trusted-keys/Kconfig b/security/keys/trusted-keys/Kconfig
> index 9e00482d886a..2ad9ba0e03f1 100644
> --- a/security/keys/trusted-keys/Kconfig
> +++ b/security/keys/trusted-keys/Kconfig
> @@ -1,10 +1,25 @@
>   config HAVE_TRUSTED_KEYS
>   	bool
>   
> +config HAVE_TRUSTED_KEYS_DEBUG
> +	bool
> +
> +config TRUSTED_KEYS_DEBUG
> +	bool "Debug trusted keys"
> +	depends on HAVE_TRUSTED_KEYS_DEBUG
> +	default n
> +	help
> +	  Trusted keys backends and core code that support debug dumps
> +	  can opt-in that feature here. Dumps must only use DEBUG
> +	  level output, as sensitive data may pass by. In the
> +	  kernel-command line traces can be enabled via
> +	  trusted.dyndbg='+p'.

Would it be good idea to add an explicit note/warning:


NOTE: This option is intended for debugging purposes only. Do not enable 
on production systems as debug output may expose sensitive cryptographic 
material.
If you are unsure, say N.

Apart from this, looks good to me.

Reviewed-by: Nayna Jain <nayna@linux.ibm.com>


^ permalink raw reply

* Re: [PATCH 1/3] crypto: public_key: Remove check for valid hash_algo for ML-DSA keys
From: Eric Biggers @ 2026-04-06 16:53 UTC (permalink / raw)
  To: Stefan Berger
  Cc: linux-integrity, linux-security-module, linux-kernel, zohar,
	roberto.sassu, David Howells, Lukas Wunner, Ignat Korchagin,
	keyrings, linux-crypto
In-Reply-To: <20260405231224.4008298-2-stefanb@linux.ibm.com>

On Sun, Apr 05, 2026 at 07:12:22PM -0400, Stefan Berger wrote:
> Remove the check for the hash_algo since ML-DSA is only used in pure mode
> and there is no relevance of a hash_algo for the input data.
> 
> Cc: David Howells <dhowells@redhat.com>
> Cc: Lukas Wunner <lukas@wunner.de>
> Cc: Ignat Korchagin <ignat@linux.win>
> Cc: keyrings@vger.kernel.org
> Cc: linux-crypto@vger.kernel.org
> Signed-off-by: Stefan Berger <stefanb@linux.ibm.com>
> ---
>  crypto/asymmetric_keys/public_key.c | 5 -----
>  1 file changed, 5 deletions(-)
> 
> diff --git a/crypto/asymmetric_keys/public_key.c b/crypto/asymmetric_keys/public_key.c
> index 09a0b83d5d77..df6918a77ab8 100644
> --- a/crypto/asymmetric_keys/public_key.c
> +++ b/crypto/asymmetric_keys/public_key.c
> @@ -147,11 +147,6 @@ software_key_determine_akcipher(const struct public_key *pkey,
>  		   strcmp(pkey->pkey_algo, "mldsa87") == 0) {
>  		if (strcmp(encoding, "raw") != 0)
>  			return -EINVAL;
> -		if (!hash_algo)
> -			return -EINVAL;
> -		if (strcmp(hash_algo, "none") != 0 &&
> -		    strcmp(hash_algo, "sha512") != 0)
> -			return -EINVAL;

Does this broaden which hash algorithms are accepted for CMS signatures
that use ML-DSA and contain signed attributes?

- Eric

^ permalink raw reply

* [PATCH v8 9/9] selftests/landlock: Add tests for invalid use of quiet flag
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v4:
- New patch

 tools/testing/selftests/landlock/base_test.c | 57 ++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index 84e91fcaa1b2..af9ad822a444 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -526,4 +526,61 @@ TEST(cred_transfer)
 	EXPECT_EQ(EACCES, errno);
 }
 
+TEST(useless_quiet_rule)
+{
+	struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		.quiet_access_fs = 0,
+	};
+	struct landlock_path_beneath_attr path_beneath_attr = {
+		.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+	};
+	int ruleset_fd, root_fd;
+
+	drop_caps(_metadata);
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+
+	root_fd = open("/", O_PATH | O_CLOEXEC);
+	ASSERT_LE(0, root_fd);
+	path_beneath_attr.parent_fd = root_fd;
+	ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+					&path_beneath_attr,
+					LANDLOCK_ADD_RULE_QUIET));
+	ASSERT_EQ(EINVAL, errno);
+
+	/* Check that the rule had not been added. */
+	ASSERT_EQ(0, close(root_fd));
+	enforce_ruleset(_metadata, ruleset_fd);
+	ASSERT_EQ(0, close(ruleset_fd));
+
+	ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+}
+
+TEST(invalid_quiet_bits_1)
+{
+	struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		.quiet_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE,
+	};
+
+	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr,
+					      sizeof(ruleset_attr), 0));
+	ASSERT_EQ(EINVAL, errno);
+}
+
+TEST(invalid_quiet_bits_2)
+{
+	struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		.quiet_access_fs = 1ULL << 63,
+	};
+
+	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr,
+					      sizeof(ruleset_attr), 0));
+	ASSERT_EQ(EINVAL, errno);
+}
+
 TEST_HARNESS_MAIN
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 8/9] selftests/landlock: Add tests for quiet flag with scope
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

Enhance scoped_audit.connect_to_child and audit_flags.signal to test
interaction with various quiet flag settings.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v4:
- New patch

 tools/testing/selftests/landlock/audit_test.c | 25 ++++--
 .../landlock/scoped_abstract_unix_test.c      | 77 ++++++++++++++++---
 2 files changed, 87 insertions(+), 15 deletions(-)

diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index 36b0e750e889..a0f51e3e93b1 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -607,30 +607,42 @@ FIXTURE(audit_flags)
 FIXTURE_VARIANT(audit_flags)
 {
 	const int restrict_flags;
+	const __u64 quiet_scoped;
 };
 
 /* clang-format off */
 FIXTURE_VARIANT_ADD(audit_flags, default) {
 	/* clang-format on */
 	.restrict_flags = 0,
+	.quiet_scoped = 0,
 };
 
 /* clang-format off */
 FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) {
 	/* clang-format on */
 	.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
+	.quiet_scoped = 0,
 };
 
 /* clang-format off */
 FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) {
 	/* clang-format on */
 	.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
+	.quiet_scoped = 0,
 };
 
 /* clang-format off */
 FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) {
 	/* clang-format on */
 	.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
+	.quiet_scoped = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_flags, signal_quieted) {
+	/* clang-format on */
+	.restrict_flags = 0,
+	.quiet_scoped = LANDLOCK_SCOPE_SIGNAL,
 };
 
 FIXTURE_SETUP(audit_flags)
@@ -674,12 +686,16 @@ TEST_F(audit_flags, signal)
 	pid_t child;
 	struct audit_records records;
 	__u64 deallocated_dom = 2;
+	bool expect_audit = !(variant->restrict_flags &
+			      LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) &&
+			    !(variant->quiet_scoped & LANDLOCK_SCOPE_SIGNAL);
 
 	child = fork();
 	ASSERT_LE(0, child);
 	if (child == 0) {
 		const struct landlock_ruleset_attr ruleset_attr = {
 			.scoped = LANDLOCK_SCOPE_SIGNAL,
+			.quiet_scoped = variant->quiet_scoped,
 		};
 		int ruleset_fd;
 
@@ -696,8 +712,7 @@ TEST_F(audit_flags, signal)
 		EXPECT_EQ(-1, kill(getppid(), 0));
 		EXPECT_EQ(EPERM, errno);
 
-		if (variant->restrict_flags &
-		    LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+		if (!expect_audit) {
 			EXPECT_EQ(-EAGAIN, matches_log_signal(
 						   _metadata, self->audit_fd,
 						   getppid(), self->domain_id));
@@ -724,8 +739,7 @@ TEST_F(audit_flags, signal)
 
 		/* Makes sure there is no superfluous logged records. */
 		EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
-		if (variant->restrict_flags &
-		    LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+		if (!expect_audit) {
 			EXPECT_EQ(0, records.access);
 		} else {
 			EXPECT_EQ(1, records.access);
@@ -748,8 +762,7 @@ TEST_F(audit_flags, signal)
 	    WEXITSTATUS(status) != EXIT_SUCCESS)
 		_metadata->exit_code = KSFT_FAIL;
 
-	if (variant->restrict_flags &
-	    LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+	if (!expect_audit) {
 		/*
 		 * No deallocation record: denials=0 never matches a real
 		 * record.
diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
index c47491d2d1c1..ac456185b835 100644
--- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
+++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
@@ -293,6 +293,45 @@ FIXTURE_TEARDOWN_PARENT(scoped_audit)
 	EXPECT_EQ(0, audit_cleanup(-1, NULL));
 }
 
+FIXTURE_VARIANT(scoped_audit)
+{
+	const __u64 scoped;
+	const __u64 quiet_scoped;
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, no_quiet)
+{
+	// clang-format on
+	.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+	.quiet_scoped = 0,
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket)
+{
+	// clang-format on
+	.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+	.quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket_2)
+{
+	// clang-format on
+	.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL,
+	.quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
+			LANDLOCK_SCOPE_SIGNAL,
+};
+
+// clang-format off
+FIXTURE_VARIANT_ADD(scoped_audit, quiet_unrelated)
+{
+	// clang-format on
+	.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL,
+	.quiet_scoped = LANDLOCK_SCOPE_SIGNAL,
+};
+
 /* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */
 #define ABSTRACT_SOCKET_PATH_PREFIX \
 	"0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D"
@@ -308,6 +347,13 @@ TEST_F(scoped_audit, connect_to_child)
 	char buf;
 	int dgram_client;
 	struct audit_records records;
+	int ruleset_fd;
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.scoped = variant->scoped,
+		.quiet_scoped = variant->quiet_scoped,
+	};
+	bool should_audit =
+		!(variant->quiet_scoped & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
 
 	/* Makes sure there is no superfluous logged records. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
@@ -344,7 +390,14 @@ TEST_F(scoped_audit, connect_to_child)
 	EXPECT_EQ(0, close(pipe_child[1]));
 	EXPECT_EQ(0, close(pipe_parent[0]));
 
-	create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd)
+	{
+		TH_LOG("Failed to create a ruleset: %s", strerror(errno));
+	}
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
 
 	/* Signals that the parent is in a domain, if any. */
 	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
@@ -359,14 +412,20 @@ TEST_F(scoped_audit, connect_to_child)
 	EXPECT_EQ(-1, err_dgram);
 	EXPECT_EQ(EPERM, errno);
 
-	EXPECT_EQ(
-		0,
-		audit_match_record(
-			self->audit_fd, AUDIT_LANDLOCK_ACCESS,
-			REGEX_LANDLOCK_PREFIX
-			" blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
-			"[0-9A-F]\\+$",
-			NULL));
+	if (should_audit) {
+		EXPECT_EQ(
+			0,
+			audit_match_record(
+				self->audit_fd, AUDIT_LANDLOCK_ACCESS,
+				REGEX_LANDLOCK_PREFIX
+				" blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX
+				"[0-9A-F]\\+$",
+				NULL));
+	}
+
+	/* No other logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
 
 	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
 	EXPECT_EQ(0, close(dgram_client));
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 7/9] selftests/landlock: add tests for quiet flag with net rules
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

Tests that:
- Quiet flag works on network rules
- Quiet flag applied to unrelated ports has no effect
- Denied access not in quiet_access_net is still logged

This is not as thorough as the fs tests, but given the shared logic it
should be sufficient.  There is also no "optional" access for network
rules.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v3:
- New patch

 tools/testing/selftests/landlock/net_test.c | 121 ++++++++++++++++++--
 1 file changed, 111 insertions(+), 10 deletions(-)

diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 4c528154ea92..729cb2f76517 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -1897,21 +1897,22 @@ TEST_F(port_specific, bind_connect_1023)
 
 static int matches_log_tcp(const int audit_fd, const char *const blockers,
 			   const char *const dir_addr, const char *const addr,
-			   const char *const dir_port)
+			   const char *const dir_port, const __u16 port)
 {
 	static const char log_template[] = REGEX_LANDLOCK_PREFIX
-		" blockers=%s %s=%s %s=1024$";
+		" blockers=%s %s=%s %s=%u$";
 	/*
 	 * Max strlen(blockers): 16
 	 * Max strlen(dir_addr): 5
 	 * Max strlen(addr): 12
 	 * Max strlen(dir_port): 4
+	 * Max strlen(%d port): 5
 	 */
-	char log_match[sizeof(log_template) + 37];
+	char log_match[sizeof(log_template) + 42];
 	int log_match_len;
 
 	log_match_len = snprintf(log_match, sizeof(log_match), log_template,
-				 blockers, dir_addr, addr, dir_port);
+				 blockers, dir_addr, addr, dir_port, port);
 	if (log_match_len > sizeof(log_match))
 		return -E2BIG;
 
@@ -1921,7 +1922,8 @@ static int matches_log_tcp(const int audit_fd, const char *const blockers,
 
 FIXTURE(audit)
 {
-	struct service_fixture srv0;
+	/* srv1 has a rule with no access but quiet bit set, srv0 does not. */
+	struct service_fixture srv0, srv1;
 	struct audit_filter audit_filter;
 	int audit_fd;
 };
@@ -1955,6 +1957,7 @@ FIXTURE_VARIANT_ADD(audit, ipv6) {
 FIXTURE_SETUP(audit)
 {
 	ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
+	ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1));
 	setup_loopback(_metadata);
 
 	set_cap(_metadata, CAP_AUDIT_CONTROL);
@@ -1975,6 +1978,12 @@ TEST_F(audit, bind)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
+		.quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP |
+				    LANDLOCK_ACCESS_NET_BIND_TCP,
+	};
+	const struct landlock_net_port_attr quiet_rule = {
+		.allowed_access = 0,
+		.port = self->srv1.port,
 	};
 	struct audit_records records;
 	int ruleset_fd, sock_fd;
@@ -1982,6 +1991,8 @@ TEST_F(audit, bind)
 	ruleset_fd =
 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
 	ASSERT_LE(0, ruleset_fd);
+	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+				       &quiet_rule, LANDLOCK_ADD_RULE_QUIET));
 	enforce_ruleset(_metadata, ruleset_fd);
 	EXPECT_EQ(0, close(ruleset_fd));
 
@@ -1989,11 +2000,21 @@ TEST_F(audit, bind)
 	ASSERT_LE(0, sock_fd);
 	EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0));
 	EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.bind_tcp", "saddr",
-				     variant->addr, "src"));
+				     variant->addr, "src", self->srv0.port));
 
+	/* No other logs expected. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	EXPECT_EQ(0, close(sock_fd));
+
+	sock_fd = socket_variant(&self->srv1);
+	ASSERT_LE(0, sock_fd);
+	EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv1));
+
+	/* No log expected due to quiet rule. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(1, records.domain);
 
 	EXPECT_EQ(0, close(sock_fd));
 }
@@ -2003,6 +2024,12 @@ TEST_F(audit, connect)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
+		.quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP |
+				    LANDLOCK_ACCESS_NET_BIND_TCP,
+	};
+	const struct landlock_net_port_attr quiet_rule = {
+		.allowed_access = 0,
+		.port = self->srv1.port,
 	};
 	struct audit_records records;
 	int ruleset_fd, sock_fd;
@@ -2010,18 +2037,92 @@ TEST_F(audit, connect)
 	ruleset_fd =
 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
 	ASSERT_LE(0, ruleset_fd);
+	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+				       &quiet_rule, LANDLOCK_ADD_RULE_QUIET));
 	enforce_ruleset(_metadata, ruleset_fd);
 	EXPECT_EQ(0, close(ruleset_fd));
 
 	sock_fd = socket_variant(&self->srv0);
 	ASSERT_LE(0, sock_fd);
 	EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv0));
-	EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.connect_tcp",
-				     "daddr", variant->addr, "dest"));
+	EXPECT_EQ(0,
+		  matches_log_tcp(self->audit_fd, "net\\.connect_tcp", "daddr",
+				  variant->addr, "dest", self->srv0.port));
+
+	/* No other logs expected. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	EXPECT_EQ(0, close(sock_fd));
+
+	sock_fd = socket_variant(&self->srv1);
+	ASSERT_LE(0, sock_fd);
+	EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1));
+
+	/* Quieted - no logs expected. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	EXPECT_EQ(0, close(sock_fd));
+}
+
+/* Quieting bind access has no effect on connect. */
+TEST_F(audit, connect_quiet_bind)
+{
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
+				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
+		.quiet_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+	};
+	const struct landlock_ruleset_attr ruleset_attr_2 = {
+		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
+				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
+		.quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+	};
+	const struct landlock_net_port_attr quiet_rule = {
+		.allowed_access = 0,
+		.port = self->srv1.port,
+	};
+	struct audit_records records;
+	int ruleset_fd, sock_fd;
+
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+				       &quiet_rule, LANDLOCK_ADD_RULE_QUIET));
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+
+	sock_fd = socket_variant(&self->srv1);
+	ASSERT_LE(0, sock_fd);
+	EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1));
+	EXPECT_EQ(0,
+		  matches_log_tcp(self->audit_fd, "net\\.connect_tcp", "daddr",
+				  variant->addr, "dest", self->srv1.port));
+
+	/* No other logs expected. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+
+	EXPECT_EQ(0, close(sock_fd));
+
+	/* New layer that also denies connect but has the correct quiet bit. */
+	ruleset_fd = landlock_create_ruleset(&ruleset_attr_2,
+					     sizeof(ruleset_attr_2), 0);
+	ASSERT_LE(0, ruleset_fd);
+	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+				       &quiet_rule, LANDLOCK_ADD_RULE_QUIET));
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+
+	sock_fd = socket_variant(&self->srv1);
+	ASSERT_LE(0, sock_fd);
+	EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1));
 
+	/* Quieted - no logs expected. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(1, records.domain);
 
 	EXPECT_EQ(0, close(sock_fd));
 }
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 6/9] selftests/landlock: add tests for quiet flag with fs rules
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

Test various interactions of the quiet flag with filesystem rules:
- Non-optional access (tested with open and rename).
- Optional access (tested with truncate and ioctl).
- Behaviour around mounts matches with normal Landlock rules.
- Behaviour around disconnected directories matches with normal Landlock
  rules (test expected behaviour of 9a868cdbe66a ("landlock: Fix handling of
  disconnected directories") applied to the collected quiet flag).
- Multiple layers works as expected.

Assisted-by: GitHub Copilot:claude-opus-4.6 copilot-review
Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v8:
- Rebase, resolve conflict, then clang-format
- Remove previously added comment about domain allocation record leakage -
  this is now documented properly by 239fd9a6f948 ("selftests/landlock:
  Drain stale audit records on init")
- Fix missing EXPECT_EQ on audit_count_records() return value

Changes in v6:
- Change quiet bool argument of add_path_beneath into a __u32 flags
  (suggested by Justin Suess)
- Rename quiet_behind_mountpoint_ignored_disconnected to
  quiet_behind_mountpoint_disconnected and fix test due to disconnected
  directory handling changes

Changes in v5:
- Add quiet_two_layers_different_handled_{1,2,3} variants.

Changes in v3:
- New patch

 tools/testing/selftests/landlock/fs_test.c | 2448 +++++++++++++++++++-
 1 file changed, 2439 insertions(+), 9 deletions(-)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 10d9355ade5f..2e32295258f9 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -720,7 +720,7 @@ TEST_F_FORK(layout1, rule_with_unhandled_access)
 
 static void add_path_beneath(struct __test_metadata *const _metadata,
 			     const int ruleset_fd, const __u64 allowed_access,
-			     const char *const path)
+			     const char *const path, __u32 flags)
 {
 	struct landlock_path_beneath_attr path_beneath = {
 		.allowed_access = allowed_access,
@@ -733,7 +733,7 @@ static void add_path_beneath(struct __test_metadata *const _metadata,
 		       strerror(errno));
 	}
 	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
-				       &path_beneath, 0))
+				       &path_beneath, flags))
 	{
 		TH_LOG("Failed to update the ruleset with \"%s\": %s", path,
 		       strerror(errno));
@@ -780,7 +780,7 @@ static int create_ruleset(struct __test_metadata *const _metadata,
 				continue;
 
 			add_path_beneath(_metadata, ruleset_fd, rules[i].access,
-					 rules[i].path);
+					 rules[i].path, 0);
 		}
 	return ruleset_fd;
 }
@@ -1310,7 +1310,7 @@ TEST_F_FORK(layout1, inherit_subset)
 	 * ANDed with the previous ones.
 	 */
 	add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE,
-			 dir_s1d2);
+			 dir_s1d2, 0);
 	/*
 	 * According to ruleset_fd, dir_s1d2 should now have the
 	 * LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE
@@ -1342,7 +1342,7 @@ TEST_F_FORK(layout1, inherit_subset)
 	 * Try to get more privileges by adding new access rights to the parent
 	 * directory: dir_s1d1.
 	 */
-	add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1);
+	add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1, 0);
 	enforce_ruleset(_metadata, ruleset_fd);
 
 	/* Same tests and results as above. */
@@ -1365,7 +1365,7 @@ TEST_F_FORK(layout1, inherit_subset)
 	 * that there was no rule tied to it before.
 	 */
 	add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE,
-			 dir_s1d3);
+			 dir_s1d3, 0);
 	enforce_ruleset(_metadata, ruleset_fd);
 	ASSERT_EQ(0, close(ruleset_fd));
 
@@ -1417,7 +1417,7 @@ TEST_F_FORK(layout1, inherit_superset)
 	add_path_beneath(_metadata, ruleset_fd,
 			 LANDLOCK_ACCESS_FS_READ_FILE |
 				 LANDLOCK_ACCESS_FS_READ_DIR,
-			 dir_s1d2);
+			 dir_s1d2, 0);
 	enforce_ruleset(_metadata, ruleset_fd);
 	EXPECT_EQ(0, close(ruleset_fd));
 
@@ -3970,7 +3970,7 @@ static int ioctl_error(struct __test_metadata *const _metadata, int fd,
 		       unsigned int cmd)
 {
 	char buf[128]; /* sufficiently large */
-	int res, stdinbak_fd;
+	int res, stdinbak_fd, err;
 
 	/*
 	 * Depending on the IOCTL command, parts of the zeroed-out buffer might
@@ -3985,13 +3985,14 @@ static int ioctl_error(struct __test_metadata *const _metadata, int fd,
 	/* Invokes the IOCTL with a zeroed-out buffer. */
 	bzero(&buf, sizeof(buf));
 	res = ioctl(fd, cmd, &buf);
+	err = errno;
 
 	/* Restores the old FD 0 and closes the backup FD. */
 	ASSERT_EQ(0, dup2(stdinbak_fd, 0));
 	ASSERT_EQ(0, close(stdinbak_fd));
 
 	if (res < 0)
-		return errno;
+		return err;
 
 	return 0;
 }
@@ -4789,6 +4790,7 @@ FIXTURE(layout1_bind) {};
 
 static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3";
 static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1";
+static const char bind_file2_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f2";
 
 /* Move targets for disconnected path tests. */
 static const char dir_s4d1[] = TMP_DIR "/s4d1";
@@ -7764,4 +7766,2432 @@ TEST_F(audit_layout1, mount)
 	EXPECT_EQ(1, records.domain);
 }
 
+static bool debug_quiet_tests;
+
+FIXTURE(audit_quiet_layout1)
+{
+	struct audit_filter audit_filter;
+	int audit_fd;
+};
+
+FIXTURE_SETUP(audit_quiet_layout1)
+{
+	prepare_layout(_metadata);
+	create_layout1(_metadata);
+
+	set_cap(_metadata, CAP_AUDIT_CONTROL);
+	self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
+	EXPECT_LE(0, self->audit_fd);
+	clear_cap(_metadata, CAP_AUDIT_CONTROL);
+
+	if (getenv("DEBUG_QUIET_TESTS"))
+		debug_quiet_tests = true;
+}
+
+FIXTURE_TEARDOWN_PARENT(audit_quiet_layout1)
+{
+	remove_layout1(_metadata);
+	cleanup_layout(_metadata);
+
+	set_cap(_metadata, CAP_AUDIT_CONTROL);
+	EXPECT_EQ(0, audit_cleanup(-1, NULL));
+	clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+struct a_rule {
+	const char *path;
+	__u64 access;
+	bool quiet;
+};
+
+struct a_layer {
+	__u64 handled_access_fs;
+	__u64 quiet_access_fs;
+	struct a_rule rules[6];
+	__u64 restrict_flags;
+};
+
+struct a_target {
+	/* File/dir to try open. */
+	const char *target;
+	/* Open mode (one of O_RDONLY, O_WRONLY, or O_RDWR). */
+	int open_mode;
+	/* Should open succeed? */
+	bool expect_open_success;
+	/* If open fails, whether to expect an audit log for read. */
+	bool audit_read_blocked;
+	/* If open fails, whether to expect an audit log for write. */
+	bool audit_write_blocked;
+	/* If ftruncate() is expected to be allowed. */
+	bool expect_truncate_success;
+	/* If ftruncate fails, whether to expect an audit log. */
+	bool audit_truncate;
+	/*
+	 * If ioctl() is expected to be allowed (ioctl not attempted if
+	 * neither this nor expect_ioctl_denied is set).
+	 */
+	bool expect_ioctl_allowed;
+	/* If ioctl() is expected to be denied. */
+	bool expect_ioctl_denied;
+	/* If ioctl fails, whether to expect an audit log. */
+	bool audit_ioctl;
+};
+
+#define AUDIT_QUIET_MAX_TARGETS 10
+
+FIXTURE_VARIANT(audit_quiet_layout1)
+{
+	struct a_layer layers[3];
+	struct a_target targets[AUDIT_QUIET_MAX_TARGETS];
+};
+
+#define FS_R LANDLOCK_ACCESS_FS_READ_FILE
+#define FS_W LANDLOCK_ACCESS_FS_WRITE_FILE
+#define FS_TRUNC LANDLOCK_ACCESS_FS_TRUNCATE
+#define FS_IOCTL LANDLOCK_ACCESS_FS_IOCTL_DEV
+
+static int sprint_access_bits(char *buf, size_t buflen, __u64 access)
+{
+	size_t offset = 0;
+
+	if (buflen < strlen("rwti make_reg remove_file refer") + 1)
+		abort();
+
+	buf[0] = '\0';
+	if (access & FS_R)
+		offset += snprintf(buf + offset, buflen - offset, "r");
+	if (access & FS_W)
+		offset += snprintf(buf + offset, buflen - offset, "w");
+	if (access & FS_TRUNC)
+		offset += snprintf(buf + offset, buflen - offset, "t");
+	if (access & FS_IOCTL)
+		offset += snprintf(buf + offset, buflen - offset, "i");
+	if (access & LANDLOCK_ACCESS_FS_MAKE_REG)
+		offset += snprintf(buf + offset, buflen - offset, ",make_reg");
+	if (access & LANDLOCK_ACCESS_FS_REMOVE_FILE)
+		offset +=
+			snprintf(buf + offset, buflen - offset, ",remove_file");
+	if (access & LANDLOCK_ACCESS_FS_REFER)
+		offset += snprintf(buf + offset, buflen - offset, ",refer");
+
+	if (buf[0] == ',') {
+		offset--;
+		memmove(buf, buf + 1, offset);
+		buf[offset] = '\0';
+	}
+
+	return offset;
+}
+
+static int apply_a_layer(struct __test_metadata *const _metadata,
+			 const struct a_layer *l)
+{
+	struct landlock_ruleset_attr rs_attr = {
+		.handled_access_fs = l->handled_access_fs,
+		.quiet_access_fs = l->quiet_access_fs,
+	};
+	int rs_fd;
+	int i;
+	const struct a_rule *r;
+	char handled_access_s[33], quiet_access_s[33], rule_access_s[33];
+
+	if (!l->handled_access_fs)
+		return 0;
+
+	rs_fd = landlock_create_ruleset(&rs_attr, sizeof(rs_attr), 0);
+	ASSERT_LE(0, rs_fd);
+
+	for (i = 0; i < ARRAY_SIZE(l->rules); i++) {
+		r = &l->rules[i];
+		if (!r->path)
+			continue;
+
+		add_path_beneath(_metadata, rs_fd, r->access, r->path,
+				 r->quiet ? LANDLOCK_ADD_RULE_QUIET : 0);
+	}
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	ASSERT_EQ(0, landlock_restrict_self(rs_fd, l->restrict_flags))
+	{
+		TH_LOG("Failed to enforce ruleset: %s", strerror(errno));
+	}
+	ASSERT_EQ(0, close(rs_fd));
+
+	if (debug_quiet_tests) {
+		sprint_access_bits(handled_access_s, sizeof(handled_access_s),
+				   l->handled_access_fs);
+		sprint_access_bits(quiet_access_s, sizeof(quiet_access_s),
+				   l->quiet_access_fs);
+		TH_LOG("applied layer: handled=%s quiet=%s restrict_flags=0x%llx",
+		       handled_access_s, quiet_access_s,
+		       (unsigned long long)l->restrict_flags);
+		for (i = 0; i < ARRAY_SIZE(l->rules); i++) {
+			r = &l->rules[i];
+			if (!r->path)
+				continue;
+
+			sprint_access_bits(rule_access_s, sizeof(rule_access_s),
+					   r->access);
+			TH_LOG("  rule[%d]: path=%s access=%s quiet=%d", i,
+			       r->path, rule_access_s, r->quiet);
+		}
+	}
+	return 0;
+}
+
+void audit_quiet_layout1_test_body(struct __test_metadata *const _metadata,
+				   FIXTURE_DATA(audit_quiet_layout1) * self,
+				   const struct a_target *targets)
+{
+	struct audit_records records = {};
+	int i;
+	const struct a_target *target;
+	int fd = -1;
+	int open_mode;
+	int ret;
+	bool expect_audit;
+	const char *blocker;
+
+	for (i = 0; i < AUDIT_QUIET_MAX_TARGETS; i++) {
+		target = &targets[i];
+		if (!target->target)
+			continue;
+
+		open_mode = target->open_mode & (O_RDONLY | O_WRONLY | O_RDWR);
+
+		EXPECT_TRUE(open_mode == O_RDONLY || open_mode == O_WRONLY ||
+			    open_mode == O_RDWR);
+
+		if (target->expect_open_success) {
+			EXPECT_FALSE(target->audit_read_blocked);
+			EXPECT_FALSE(target->audit_write_blocked);
+		}
+		if (target->expect_truncate_success)
+			EXPECT_TRUE(target->expect_open_success &&
+				    !target->audit_truncate);
+
+		if (debug_quiet_tests)
+			TH_LOG("Try open \"%s\" with %s%s", target->target,
+			       open_mode != O_WRONLY ? "r" : "",
+			       open_mode != O_RDONLY ? "w" : "");
+
+		fd = openat(AT_FDCWD, target->target, open_mode | O_CLOEXEC);
+		if (target->expect_open_success) {
+			ASSERT_LE(0, fd)
+			{
+				TH_LOG("Failed to open \"%s\": %s",
+				       target->target, strerror(errno));
+			};
+		} else {
+			ASSERT_EQ(-1, fd);
+			ASSERT_EQ(EACCES, errno);
+		}
+
+		expect_audit = true;
+
+		if (target->audit_read_blocked && target->audit_write_blocked)
+			blocker = "fs\\.write_file,fs\\.read_file";
+		else if (target->audit_read_blocked)
+			blocker = "fs\\.read_file";
+		else if (target->audit_write_blocked)
+			blocker = "fs\\.write_file";
+		else
+			expect_audit = false;
+
+		if (expect_audit)
+			ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+						    blocker, target->target));
+
+		/* Check that we see no (other) logs. */
+		EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+		ASSERT_EQ(0, records.access);
+
+		if (target->expect_open_success && fd >= 0) {
+			if (debug_quiet_tests)
+				TH_LOG("Try ftruncate \"%s\"", target->target);
+
+			ret = ftruncate(fd, 0);
+			if (target->expect_truncate_success) {
+				ASSERT_EQ(0, ret);
+			} else {
+				ASSERT_EQ(-1, ret);
+				if (open_mode != O_RDONLY)
+					ASSERT_EQ(EACCES, errno);
+			}
+
+			if (target->audit_truncate)
+				ASSERT_EQ(0, matches_log_fs(_metadata,
+							    self->audit_fd,
+							    "fs\\.truncate",
+							    target->target));
+
+			if (target->expect_ioctl_allowed ||
+			    target->expect_ioctl_denied) {
+				if (debug_quiet_tests)
+					TH_LOG("Try ioctl FIONREAD on \"%s\"",
+					       target->target);
+
+				ret = ioctl_error(_metadata, fd, FIONREAD);
+				if (target->expect_ioctl_allowed) {
+					ASSERT_NE(EACCES, ret);
+				} else {
+					ASSERT_EQ(EACCES, ret);
+				}
+			}
+
+			if (target->audit_ioctl)
+				ASSERT_EQ(0, matches_log_fs_extra(
+						     _metadata, self->audit_fd,
+						     "fs\\.ioctl_dev",
+						     target->target,
+						     " ioctlcmd=0x541b\\+"));
+
+			/* Check that we see no other logs. */
+			EXPECT_EQ(0, audit_count_records(self->audit_fd,
+							 &records));
+			ASSERT_EQ(0, records.access);
+			ASSERT_EQ(0, close(fd));
+		}
+	}
+}
+
+TEST_F(audit_quiet_layout1, base)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++)
+		ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i]));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant->targets);
+}
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_simple) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{ .path = dir_s1d1, .access = 0, .quiet = true },
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+		},
+		/* Not covered by quiet */
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		/* Access not quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.audit_write_blocked = true,
+		},
+		/*
+		 * Quiet flag only takes effect if all blocked access bits are
+		 * quieted, otherwise audit log emitted as normal (with all blockers)
+		 */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+			.audit_write_blocked = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_read) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+			.quiet_access_fs = FS_W,
+			.rules = {
+				{ .path = dir_s1d1, .access = FS_R, .quiet = true },
+				/* Quiet flags inherit down and is not overridden */
+				{ .path = file1_s1d1, .access = FS_R, .quiet = false },
+				{ .path = file1_s2d3, .access = 0, .quiet = true },
+			},
+		},
+	},
+	.targets = {
+		/* Read ok */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+		},
+		/* Write quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+		},
+		/* Read allowed, write quieted so no audit */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+		},
+		/* Not covered by quiet */
+		{
+			.target = file1_s2d2,
+			.open_mode = O_WRONLY,
+			.audit_write_blocked = true,
+		},
+		{
+			.target = file1_s2d2,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+			.audit_write_blocked = true,
+		},
+		/* Single file quiet */
+		{
+			.target = file1_s2d3,
+			.open_mode = O_WRONLY,
+		},
+		/* Wrong file */
+		{
+			.target = file2_s2d3,
+			.open_mode = O_WRONLY,
+			.audit_write_blocked = true,
+		},
+		/* Access not quieted */
+		{
+			.target = file1_s2d3,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		/* Some access not quieted */
+		{
+			.target = file1_s2d3,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+			.audit_write_blocked = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_write) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{ .path = dir_s1d1, .access = FS_W, .quiet = true },
+			},
+		},
+	},
+	.targets = {
+		/* Read quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+		},
+		/* Truncate not quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate = true,
+		},
+		/* Not covered by quiet */
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		/* Write allowed, read quieted so no audit */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_write_quiet_trunc) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+			.quiet_access_fs = FS_TRUNC,
+			.rules = {
+				{ .path = dir_s1d1, .access = FS_W, .quiet = true },
+				{ .path = dir_s2d1, .access = FS_W, .quiet = false },
+			},
+		},
+	},
+	.targets = {
+		/* Read not allowed and not quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		/* Truncate quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+		},
+		/* Not covered by quiet (truncate) */
+		{
+			.target = file1_s2d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate = true,
+		},
+		/* Not covered by quiet (read/write) */
+		{
+			.target = file1_s3d1,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+			.audit_write_blocked = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_rw_quiet_trunc) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+			.quiet_access_fs = FS_TRUNC,
+			.rules = {
+				{ .path = dir_s1d1, .access = FS_R | FS_W, .quiet = true },
+				{ .path = dir_s2d1, .access = FS_R | FS_W, .quiet = false },
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			.audit_truncate = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_all) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{ .path = dir_s1d1, .access = 0, .quiet = true },
+				{ .path = file1_s2d1, .access = FS_R | FS_W, .quiet = true },
+				{ .path = file1_s2d3, .access = 0, .quiet = true },
+				{ .path = dir_s3d1, .access = FS_W, .quiet = false },
+				{ .path = "/dev/zero", .access = FS_R, .quiet = false },
+				{ .path = "/dev/null", .access = FS_R, .quiet = true },
+			},
+		},
+	},
+	.targets = {
+		/* No logs */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+		},
+		/* Truncate quieted - no log */
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+		},
+		/* Truncate not covered by quiet */
+		{
+			.target = file1_s3d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate = true,
+		},
+		/* Not covered by quiet */
+		{
+			.target = file1_s3d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		/* Single file quiet */
+		{
+			.target = file1_s2d3,
+			.open_mode = O_RDWR,
+		},
+		/* Wrong file */
+		{
+			.target = file2_s2d3,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+			.audit_write_blocked = true,
+		},
+		/* Ioctl quieted */
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+		},
+		/* Ioctl not quieted */
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			.audit_ioctl = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_across_mountpoint) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{ .path = dir_s3d1, .access = 0, .quiet = true },
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s3d3,
+			.open_mode = O_RDONLY,
+		},
+		/* Not covered by quiet */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+			.audit_write_blocked = true,
+		},
+		/* Access not quieted */
+		{
+			.target = file1_s3d3,
+			.open_mode = O_WRONLY,
+			.audit_write_blocked = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_all_quiet) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = true
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = true
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			.expect_truncate_success = true,
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_allowed = true,
+		},
+	},
+};
+
+/*
+ * With LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, it doesn't matter what
+ * the quiet flags below the layer says
+ */
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, subdomains_off) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R,
+			.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
+			.rules = {
+				{ .path = "/", .access = FS_R, .quiet = false },
+			}
+		},
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{ .path = dir_s1d1, .access = 0, .quiet = true },
+				{ .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true },
+				{ .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false },
+				{ .path = "/dev/null", .access = FS_R | FS_W, .quiet = true },
+				{ .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false },
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDWR,
+		},
+		{
+			.target = file1_s2d2,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			/* No audit_truncate */
+		},
+		{
+			.target = file1_s2d3,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			/* No audit_truncate */
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			/* No audit_ioctl */
+		},
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			/* No audit_ioctl */
+		},
+	},
+};
+
+/*
+ * With LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, it doesn't matter what
+ * the quiet flags on the layer says
+ */
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, same_exec_off) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R,
+			.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
+			.rules = {
+				{ .path = dir_s1d1, .access = 0, .quiet = true },
+				{ .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true },
+				{ .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false },
+				{ .path = "/dev/null", .access = FS_R | FS_W, .quiet = true },
+				{ .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false },
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDWR,
+		},
+		{
+			.target = file1_s2d2,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			/* No audit_truncate */
+		},
+		{
+			.target = file1_s2d3,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			/* No audit_truncate */
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			/* No audit_ioctl */
+		},
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			/* No audit_ioctl */
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_1) {
+	/* Here, rules that deny access is always quiet. */
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R,
+					.quiet = true,
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = false,
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = false,
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R,
+					.quiet = true,
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+		},
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_2) {
+	/* Here, rules that deny access is never quiet. */
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_W,
+					.quiet = false
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = true
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R,
+					.quiet = false
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = true
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = true
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_W,
+					.quiet = false
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = true
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R,
+					.quiet = false
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate	= true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate	= true,
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			.audit_ioctl = true,
+		},
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			.audit_ioctl = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_3) {
+	/* This time only the second layer quiets things. */
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_W,
+					.quiet = false,
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = false,
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = false,
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R,
+					.quiet = true,
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate = true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+			.audit_ioctl = true,
+		},
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_quiet_access) {
+	/* Here, rules that deny access is always quiet. */
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R,
+					.quiet = true,
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = false,
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+			.quiet_access_fs = FS_IOCTL,
+			.rules = {
+				{
+					.path = dir_s1d1,
+					.access = FS_R | FS_W | FS_TRUNC,
+					.quiet = false,
+				},
+				{
+					.path = dir_s2d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = "/dev/null",
+					.access = FS_R | FS_W | FS_IOCTL,
+					.quiet = false,
+				},
+				{
+					.path = "/dev/zero",
+					.access = FS_R,
+					.quiet = true,
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+		},
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+		{
+			.target = file1_s2d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.audit_truncate	= true,
+		},
+		{
+			.target = "/dev/null",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+		},
+		{
+			.target = "/dev/zero",
+			.open_mode = O_RDONLY,
+			.expect_open_success = true,
+			.expect_ioctl_denied = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_1) {
+	/* Quiet from layer 1 */
+	.layers = {
+		{
+			.handled_access_fs = FS_R,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{
+					.path = file1_s1d1,
+					.access = FS_R,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d1,
+					.access = 0,
+					.quiet = true,
+				},
+				{
+					.path = file1_s1d2,
+					.access = 0,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d2,
+					.access = FS_R,
+					.quiet = true,
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_W,
+			.quiet_access_fs = FS_W,
+			.rules = {
+				{
+					.path = file1_s1d1,
+					.access = FS_W,
+					.quiet = false,
+				},
+				/* Nothing for file2_s1d1 */
+				{
+					.path = file1_s1d2,
+					.access = FS_W,
+					.quiet = false,
+				},
+				/* Nothing for file2_s1d2 */
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			.expect_truncate_success = true,
+		},
+		/* Missing both, youngest layer denies write, not quiet */
+		{
+			.target = file2_s1d1,
+			.open_mode = O_RDWR,
+			.audit_write_blocked = true,
+		},
+		/* Missing read, denied and quieted by layer 1 */
+		{
+			.target = file1_s1d2,
+			.open_mode = O_RDWR,
+		},
+		/* Missing write, denied and not quieted by layer 2 */
+		{
+			.target = file2_s1d2,
+			.open_mode = O_RDWR,
+			.audit_write_blocked = true,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_2) {
+	/* Quiet from layer 2 */
+	.layers = {
+		{
+			.handled_access_fs = FS_R,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{
+					.path = file1_s1d1,
+					.access = FS_R,
+					.quiet = false,
+				},
+				/* Nothing for file2_s1d1 and file1_s1d2 */
+				{
+					.path = file2_s1d2,
+					.access = FS_R,
+					.quiet = false,
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_W,
+			.quiet_access_fs = FS_W,
+			.rules = {
+				{
+					.path = file1_s1d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d1,
+					.access = 0,
+					.quiet = true,
+				},
+				{
+					.path = file1_s1d2,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d2,
+					.access = 0,
+					.quiet = true,
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			.expect_truncate_success = true,
+		},
+		/* Missing both, youngest layer denies write, quiet */
+		{
+			.target = file2_s1d1,
+			.open_mode = O_RDWR,
+		},
+		/* Missing read, denied and not quieted by layer 1 */
+		{
+			.target = file1_s1d2,
+			.open_mode = O_RDWR,
+			.audit_read_blocked = true,
+		},
+		/* Missing write, denied and quieted by layer 2 */
+		{
+			.target = file2_s1d2,
+			.open_mode = O_RDWR,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_3) {
+	/* Quiet from both layers */
+	.layers = {
+		{
+			.handled_access_fs = FS_R,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{
+					.path = file1_s1d1,
+					.access = FS_R,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d1,
+					.access = 0,
+					.quiet = true,
+				},
+				{
+					.path = file1_s1d2,
+					.access = 0,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d2,
+					.access = FS_R,
+					.quiet = true,
+				},
+			},
+		},
+		{
+			.handled_access_fs = FS_W,
+			.quiet_access_fs = FS_W,
+			.rules = {
+				{
+					.path = file1_s1d1,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d1,
+					.access = 0,
+					.quiet = true,
+				},
+				{
+					.path = file1_s1d2,
+					.access = FS_W,
+					.quiet = true,
+				},
+				{
+					.path = file2_s1d2,
+					.access = 0,
+					.quiet = true,
+				},
+			},
+		},
+	},
+	.targets = {
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+			.expect_open_success = true,
+			.expect_truncate_success = true,
+		},
+		{
+			.target = file2_s1d1,
+			.open_mode = O_RDWR,
+		},
+		{
+			.target = file1_s1d2,
+			.open_mode = O_RDWR,
+		},
+		{
+			.target = file2_s1d2,
+			.open_mode = O_RDWR,
+		},
+	},
+};
+
+FIXTURE_VARIANT_ADD(audit_quiet_layout1, without_quiet_then_with_quiet) {
+	.layers = {
+		{
+			.handled_access_fs = FS_R | FS_W,
+			.quiet_access_fs = FS_R,
+			.rules = {
+				{ .path = dir_s1d1, .access = FS_W, .quiet = false },
+				{ .path = dir_s1d1, .access = 0, .quiet = true },
+			},
+		},
+	},
+	.targets = {
+		/* Read denied and quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDONLY,
+		},
+		/* Write ok */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_WRONLY,
+			.expect_open_success = true,
+			.expect_truncate_success = true,
+		},
+		/* Write ok, read denied and quieted */
+		{
+			.target = file1_s1d1,
+			.open_mode = O_RDWR,
+		},
+		/* Not covered by quiet */
+		{
+			.target = file1_s2d1,
+			.open_mode = O_RDONLY,
+			.audit_read_blocked = true,
+		},
+	},
+};
+
+/*
+ * The following TEST_F extend the above test cases to test more layers,
+ * with the inserted layers having varying configurations.
+ */
+
+/* Extra allow all layers, quiet or not, does not change any behaviour. */
+TEST_F(audit_quiet_layout1, allow_all_layer)
+{
+	struct a_layer allow_all_layer = {
+		.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+		.quiet_access_fs = 0,
+		.rules = {
+			{
+				.path = "/",
+				.access = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+				.quiet = false,
+			},
+		},
+	};
+	int i;
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer));
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++)
+		ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i]));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant->targets);
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant->targets);
+
+	/*
+	 * SELF_LOG flags or quiet bits from inner allowing layers should not
+	 * affect behaviour.
+	 */
+	allow_all_layer.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL;
+	allow_all_layer.rules[0].quiet = true;
+	/*
+	 * Note: this only works because we're not checking counts of domain
+	 * alloc/dealloc logs
+	 */
+	allow_all_layer.restrict_flags =
+		LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
+		LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF;
+	ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant->targets);
+}
+
+/*
+ * Add useless outer layers until we reach the layer limit.  Should not
+ * change anything.
+ */
+TEST_F(audit_quiet_layout1, many_outer_layers)
+{
+	struct a_layer useless_layer = {
+		.handled_access_fs = FS_R | FS_W | FS_TRUNC,
+		.quiet_access_fs = FS_R | FS_W | FS_TRUNC,
+		.rules = {
+			{ .path = "/", .access = FS_R | FS_W | FS_TRUNC, .quiet = true },
+		},
+	};
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++) {
+		if (variant->layers[i].handled_access_fs == 0)
+			break;
+	}
+
+	for (; i < LANDLOCK_MAX_NUM_LAYERS; i++)
+		ASSERT_EQ(0, apply_a_layer(_metadata, &useless_layer));
+
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++)
+		ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i]));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant->targets);
+}
+
+/*
+ * An inner layer that denies and quiets everything should result in no
+ * logs.
+ */
+TEST_F(audit_quiet_layout1, deny_all_quiet_layer)
+{
+	struct a_layer deny_all_layer = {
+		.handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+		.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL,
+		.rules = {
+			{ .path = "/", .access = 0, .quiet = true },
+		},
+	};
+	int i;
+	FIXTURE_VARIANT(audit_quiet_layout1) variant_2 = {};
+
+	/* Any open should fail with no logs. */
+	for (i = 0; i < ARRAY_SIZE(variant->targets); i++) {
+		const struct a_target *target = &variant->targets[i];
+
+		variant_2.targets[i] = (struct a_target){
+			.target = target->target,
+			.open_mode = target->open_mode,
+			/* We denied everything, open should always fail. */
+			.expect_open_success = false,
+		};
+	}
+
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++)
+		ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i]));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &deny_all_layer));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant_2.targets);
+}
+
+/*
+ * An inner layer that denies everything without quiet should produce logs
+ * for all access.
+ */
+TEST_F(audit_quiet_layout1, deny_all_layer)
+{
+	struct a_layer deny_all_layer = {
+		.handled_access_fs = FS_R | FS_W,
+		.quiet_access_fs = FS_R | FS_W,
+	};
+	int i;
+	FIXTURE_VARIANT(audit_quiet_layout1) variant_2 = {};
+	bool test_has_subdomains_off = false;
+
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++) {
+		if (variant->layers[i].restrict_flags &
+		    LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF) {
+			test_has_subdomains_off = true;
+			break;
+		}
+	}
+
+	for (i = 0; i < ARRAY_SIZE(variant->targets); i++) {
+		const struct a_target *target = &variant->targets[i];
+
+		variant_2.targets[i] = (struct a_target){
+			.target = target->target,
+			.open_mode = target->open_mode,
+
+			/* We denied everything, open should always fail. */
+			.expect_open_success = false,
+			/* Audit should always happen as long as open request contains read. */
+			.audit_read_blocked = !test_has_subdomains_off &&
+					      target->open_mode != O_WRONLY,
+			/* Audit should always happen as long as open request contains write. */
+			.audit_write_blocked = !test_has_subdomains_off &&
+					       target->open_mode != O_RDONLY,
+		};
+	}
+
+	for (i = 0; i < ARRAY_SIZE(variant->layers); i++)
+		ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i]));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &deny_all_layer));
+
+	audit_quiet_layout1_test_body(_metadata, self, variant_2.targets);
+}
+
+/* Uses layout1_bind hierarchy */
+FIXTURE(audit_quiet_rename)
+{
+	struct audit_filter audit_filter;
+	int audit_fd;
+};
+
+FIXTURE_SETUP(audit_quiet_rename)
+{
+	prepare_layout(_metadata);
+	create_layout1(_metadata);
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, mount(dir_s1d2, dir_s2d2, NULL, MS_BIND, NULL));
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	set_cap(_metadata, CAP_AUDIT_CONTROL);
+	self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
+	EXPECT_LE(0, self->audit_fd);
+	clear_cap(_metadata, CAP_AUDIT_CONTROL);
+
+	if (getenv("DEBUG_QUIET_TESTS"))
+		debug_quiet_tests = true;
+}
+
+FIXTURE_TEARDOWN_PARENT(audit_quiet_rename)
+{
+	remove_layout1(_metadata);
+	cleanup_layout(_metadata);
+
+	/* umount(dir_s2d2)) is handled by namespace lifetime. */
+
+	remove_path(file1_s4d1);
+	remove_path(file2_s4d1);
+
+	set_cap(_metadata, CAP_AUDIT_CONTROL);
+	EXPECT_EQ(0, audit_cleanup(-1, NULL));
+	clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+static void simple_quiet_rename(struct __test_metadata *const _metadata,
+				FIXTURE_DATA(audit_quiet_rename) *const self,
+				__u64 handled_access, __u64 quiet_access,
+				bool source_allow, bool dest_allow,
+				bool source_quiet, bool dest_quiet,
+				const char *source_blockers,
+				const char *dest_blockers)
+{
+	/* We will move file1_s1d1 to file1_s2d1 */
+	struct a_layer layer = {
+		.handled_access_fs = handled_access,
+		.quiet_access_fs = quiet_access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = source_allow ? handled_access : 0,
+				.quiet = source_quiet,
+			},
+			{
+				.path = dir_s2d1,
+				.access = dest_allow ? handled_access : 0,
+				.quiet = dest_quiet,
+			},
+		},
+	};
+	struct audit_records records = {};
+	int ret, err;
+
+	/* Skip landlock_add_rule for useless rules. */
+	if (!source_allow && !source_quiet)
+		layer.rules[0].path = NULL;
+	if (!dest_allow && !dest_quiet)
+		layer.rules[1].path = NULL;
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+	EXPECT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	if (debug_quiet_tests)
+		TH_LOG("Try renameat \"%s\" to \"%s\"", file1_s1d1, file1_s2d1);
+	ret = renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1);
+	err = errno;
+	if (ret != 0 && debug_quiet_tests) {
+		TH_LOG("renameat error: %s", err == EXDEV  ? "EXDEV" :
+					     err == EACCES ? "EACCES" :
+							     strerror(err));
+	}
+	if (source_allow && dest_allow) {
+		ASSERT_EQ(0, ret);
+	} else {
+		ASSERT_EQ(-1, ret);
+		if (handled_access & (LANDLOCK_ACCESS_FS_MAKE_REG |
+				      LANDLOCK_ACCESS_FS_REMOVE_FILE)) {
+			ASSERT_EQ(EACCES, err);
+		} else {
+			ASSERT_EQ(EXDEV, err);
+		}
+
+		if (source_blockers)
+			ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+						    source_blockers, dir_s1d1));
+		if (dest_blockers)
+			ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+						    dest_blockers, dir_s2d1));
+	}
+	/*
+	 * No other logs. records.domain not checked per reasoning in
+	 * audit_quiet_layout1_test_body.
+	 */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, rename_ok)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, true, true, false,
+			    false, NULL, NULL);
+}
+
+TEST_F(audit_quiet_rename, no_quiet)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, false, false,
+			    false, false, "fs\\.remove_file,fs\\.refer",
+			    "fs\\.make_reg,fs\\.refer");
+}
+
+TEST_F(audit_quiet_rename, quiet)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, false, false, true,
+			    true, NULL, NULL);
+}
+
+TEST_F(audit_quiet_rename, source_no_quiet_dest_quiet)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, false, false,
+			    false, true, "fs\\.remove_file,fs\\.refer", NULL);
+}
+
+TEST_F(audit_quiet_rename, source_quiet_dest_no_quiet)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, false, false, true,
+			    false, NULL, "fs\\.make_reg,fs\\.refer");
+}
+
+TEST_F(audit_quiet_rename, only_quiet_refer)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, LANDLOCK_ACCESS_FS_REFER,
+			    false, false, true, true,
+			    "fs\\.remove_file,fs\\.refer",
+			    "fs\\.make_reg,fs\\.refer");
+}
+
+TEST_F(audit_quiet_rename, source_allow_dest_quiet)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, true, false, false,
+			    true, NULL, NULL);
+}
+
+TEST_F(audit_quiet_rename, source_quiet_dest_allow)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+
+	simple_quiet_rename(_metadata, self, access, access, false, true, true,
+			    false, NULL, NULL);
+}
+
+TEST_F(audit_quiet_rename, handle_all_deny_quiet_refer)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = LANDLOCK_ACCESS_FS_REFER,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					LANDLOCK_ACCESS_FS_REMOVE_FILE,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					LANDLOCK_ACCESS_FS_REMOVE_FILE,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EXDEV, errno);
+
+	/* No logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, handle_all_deny_not_quiet_refer)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = 0,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					LANDLOCK_ACCESS_FS_REMOVE_FILE,
+				.quiet = false,
+			},
+			{
+				.path = dir_s2d1,
+				.access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					LANDLOCK_ACCESS_FS_REMOVE_FILE,
+				.quiet = false,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EXDEV, errno);
+
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
+				    dir_s1d1));
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
+				    dir_s2d1));
+
+	/* No other logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, handle_all_deny_refer_quiet_source_not_quiet_dest)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = LANDLOCK_ACCESS_FS_REFER,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					LANDLOCK_ACCESS_FS_REMOVE_FILE,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					LANDLOCK_ACCESS_FS_REMOVE_FILE,
+				.quiet = false,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EXDEV, errno);
+
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer",
+				    dir_s2d1));
+
+	/* No other logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_same_dir)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = file1_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = file1_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EACCES, errno);
+
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.remove_file,fs\\.refer", dir_s1d1));
+	/* We didn't unlink destination file */
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.remove_file,fs\\.make_reg,fs\\.refer",
+				    dir_s2d1));
+
+	/* No other logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored_same_dir)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = file1_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = file2_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1));
+	ASSERT_EQ(EACCES, errno);
+
+	ASSERT_EQ(0,
+		  matches_log_fs(_metadata, self->audit_fd,
+				 "fs\\.remove_file,fs\\.make_reg", dir_s1d1));
+
+	/* No other logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, two_layers_different_quiet1)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer1 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = access,
+				.quiet = false,
+			},
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct a_layer layer2 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = LANDLOCK_ACCESS_FS_REFER,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = access,
+				.quiet = false,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer2));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EACCES, errno);
+
+	/*
+	 * The youngest denial will be layer 2.  Refer is quieted but we are
+	 * also missing remove_file on source.
+	 */
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.remove_file,fs\\.refer", dir_s1d1));
+	/* No other logs */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, two_layers_different_quiet2)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer1 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = access,
+				.quiet = false,
+			},
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct a_layer layer2 = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_REFER,
+		.quiet_access_fs = LANDLOCK_ACCESS_FS_REFER,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = LANDLOCK_ACCESS_FS_REFER,
+				.quiet = false,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer2));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EACCES, errno);
+
+	/*
+	 * The youngest denial will be layer 2, but refer is quieted (and that
+	 * layer does not handle any other accesses).
+	 */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, two_layers_different_quiet3)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer1 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = access,
+				.quiet = false,
+			},
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct a_layer layer2 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = access,
+				.quiet = false,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer2));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EACCES, errno);
+
+	/*
+	 * The youngest denial will be layer 2, in which everything is
+	 * quieted.
+	 */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename,
+       first_layer_quiet_deny_all_second_layer_not_quiet_deny_all)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer1 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct a_layer layer2 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer2));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EACCES, errno);
+
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.remove_file,fs\\.refer", dir_s1d1));
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.make_reg,fs\\.refer", dir_s2d1));
+	/* No other logs. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename,
+       first_layer_quiet_deny_all_second_layer_dest_not_quiet)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer1 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct a_layer layer2 = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file1_s2d1));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer1));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer2));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1));
+	ASSERT_EQ(EACCES, errno);
+
+	/*
+	 * Source is quieted but destination is not.
+	 */
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.make_reg,fs\\.refer", dir_s2d1));
+	/* No other logs. */
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, rename_xchg)
+{
+	struct a_layer layer = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_REG |
+				     LANDLOCK_ACCESS_FS_REMOVE_FILE |
+				     LANDLOCK_ACCESS_FS_REFER,
+		.quiet_access_fs = LANDLOCK_ACCESS_FS_MAKE_REG,
+		.rules = { {
+				   .path = dir_s1d1,
+				   .access = LANDLOCK_ACCESS_FS_REMOVE_FILE |
+					     LANDLOCK_ACCESS_FS_REFER,
+				   .quiet = true,
+			   },
+			   {
+				   .path = dir_s2d1,
+				   .access = LANDLOCK_ACCESS_FS_MAKE_REG |
+					     LANDLOCK_ACCESS_FS_REMOVE_FILE |
+					     LANDLOCK_ACCESS_FS_REFER,
+				   .quiet = false,
+			   } },
+	};
+	struct audit_records records = {};
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1,
+				RENAME_EXCHANGE));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_on_parent_mount)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file2_s1d3));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, bind_file1_s1d3, AT_FDCWD,
+			       bind_file2_s1d3));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_behind_mountpoint_ignored)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s1d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+
+	EXPECT_EQ(0, unlink(file2_s1d3));
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1, renameat(AT_FDCWD, bind_file1_s1d3, AT_FDCWD,
+			       bind_file2_s1d3));
+	ASSERT_EQ(EACCES, errno);
+	ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+				    "fs\\.remove_file,fs\\.make_reg",
+				    bind_dir_s1d3));
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_on_parent_mount_disconnected)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s2d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+	int bind_s1d3_fd;
+
+	EXPECT_EQ(0, unlink(file2_s1d3));
+
+	bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_DIRECTORY);
+	ASSERT_GE(bind_s1d3_fd, 0);
+
+	/* Make s1d3 disconnected. */
+	create_directory(_metadata, dir_s4d1);
+	ASSERT_EQ(0, renameat(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s4d2));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1,
+		  renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
+TEST_F(audit_quiet_rename, quiet_behind_mountpoint_disconnected)
+{
+	__u64 access = LANDLOCK_ACCESS_FS_MAKE_REG |
+		       LANDLOCK_ACCESS_FS_REMOVE_FILE |
+		       LANDLOCK_ACCESS_FS_REFER;
+	struct a_layer layer = {
+		.handled_access_fs = access,
+		.quiet_access_fs = access,
+		.rules = {
+			{
+				.path = dir_s4d1,
+				.access = 0,
+				.quiet = true,
+			},
+		},
+	};
+	struct audit_records records = {};
+	int bind_s1d3_fd;
+
+	EXPECT_EQ(0, unlink(file2_s1d3));
+
+	bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_DIRECTORY);
+	ASSERT_GE(bind_s1d3_fd, 0);
+
+	/* Make s1d3 disconnected. */
+	create_directory(_metadata, dir_s4d1);
+	ASSERT_EQ(0, renameat(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s4d2));
+
+	ASSERT_EQ(0, apply_a_layer(_metadata, &layer));
+
+	ASSERT_EQ(-1,
+		  renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	ASSERT_EQ(0, records.access);
+}
+
 TEST_HARNESS_MAIN
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 5/9] selftests/landlock: Replace hard-coded 16 with a constant
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

The next commit will reuse this number.  Make it a shared constant to
future-proof changes.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v3:
- New patch

 tools/testing/selftests/landlock/audit_test.c | 2 +-
 tools/testing/selftests/landlock/common.h     | 2 ++
 tools/testing/selftests/landlock/fs_test.c    | 2 +-
 3 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index da0bfd06391e..36b0e750e889 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -76,7 +76,7 @@ TEST_F(audit, layers)
 		.scoped = LANDLOCK_SCOPE_SIGNAL,
 	};
 	int status, ruleset_fd, i;
-	__u64(*domain_stack)[16];
+	__u64(*domain_stack)[LANDLOCK_MAX_NUM_LAYERS];
 	__u64 prev_dom = 3;
 	pid_t child;
 
diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 90551650299c..7206d5105d66 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -25,6 +25,8 @@
 /* TEST_F_FORK() should not be used for new tests. */
 #define TEST_F_FORK(fixture_name, test_name) TEST_F(fixture_name, test_name)
 
+#define LANDLOCK_MAX_NUM_LAYERS 16
+
 static const char bin_sandbox_and_launch[] = "./sandbox-and-launch";
 static const char bin_wait_pipe[] = "./wait-pipe";
 static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox";
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index cdb47fc1fc0a..10d9355ade5f 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -1441,7 +1441,7 @@ TEST_F_FORK(layout0, max_layers)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	for (i = 0; i < 16; i++)
+	for (i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
 		enforce_ruleset(_metadata, ruleset_fd);
 
 	for (i = 0; i < 2; i++) {
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 4/9] samples/landlock: Add quiet flag support to sandboxer
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

Adds ability to set which access bits to quiet via LL_*_QUIET_ACCESS (FS,
NET or SCOPED), and attach quiet flags to individual objects via
LL_*_QUIET for FS and NET.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v8:
- Rebase on top of mic/next
- populate_ruleset_net() already does not require the env var to be
  present, so remove redundant comment and check above
  populate_ruleset_net(ENV_NET_QUIET_NAME, ...).

Changes in v6:
- Make populate_ruleset_{fs,net} take a flags argument instead of a bool
  quiet (suggested by Justin Suess)
- Fix if braces style

Changes in v3:
- Minor change to the above commit message.

Changes in v2:
- Added new environment variables to control which quiet access bits to
  set on the rule, and populate quiet_access_* from it.
- Added support for quieting net rules and scoped access.  Renamed patch
  title.
- Increment ABI version

 samples/landlock/sandboxer.c | 128 ++++++++++++++++++++++++++++++++---
 1 file changed, 118 insertions(+), 10 deletions(-)

diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 66e56ae275c6..daba6da2fb74 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -58,9 +58,14 @@ static inline int landlock_restrict_self(const int ruleset_fd,
 
 #define ENV_FS_RO_NAME "LL_FS_RO"
 #define ENV_FS_RW_NAME "LL_FS_RW"
+#define ENV_FS_QUIET_NAME "LL_FS_QUIET"
+#define ENV_FS_QUIET_ACCESS_NAME "LL_FS_QUIET_ACCESS"
 #define ENV_TCP_BIND_NAME "LL_TCP_BIND"
 #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
+#define ENV_NET_QUIET_NAME "LL_NET_QUIET"
+#define ENV_NET_QUIET_ACCESS_NAME "LL_NET_QUIET_ACCESS"
 #define ENV_SCOPED_NAME "LL_SCOPED"
+#define ENV_SCOPED_QUIET_ACCESS_NAME "LL_SCOPED_QUIET_ACCESS"
 #define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
 #define ENV_DELIMITER ":"
 
@@ -117,7 +122,7 @@ static int parse_path(char *env_path, const char ***const path_list)
 /* clang-format on */
 
 static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,
-			       const __u64 allowed_access)
+			       const __u64 allowed_access, __u32 flags)
 {
 	int num_paths, i, ret = 1;
 	char *env_path_name;
@@ -167,7 +172,7 @@ static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,
 		if (!S_ISDIR(statbuf.st_mode))
 			path_beneath.allowed_access &= ACCESS_FILE;
 		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
-				      &path_beneath, 0)) {
+				      &path_beneath, flags)) {
 			fprintf(stderr,
 				"Failed to update the ruleset with \"%s\": %s\n",
 				path_list[i], strerror(errno));
@@ -185,7 +190,7 @@ static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,
 }
 
 static int populate_ruleset_net(const char *const env_var, const int ruleset_fd,
-				const __u64 allowed_access)
+				const __u64 allowed_access, __u32 flags)
 {
 	int ret = 1;
 	char *env_port_name, *env_port_name_next, *strport;
@@ -213,7 +218,7 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd,
 		}
 		net_port.port = port;
 		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
-				      &net_port, 0)) {
+				      &net_port, flags)) {
 			fprintf(stderr,
 				"Failed to update the ruleset with port \"%llu\": %s\n",
 				net_port.port, strerror(errno));
@@ -301,7 +306,55 @@ static bool check_ruleset_scope(const char *const env_var,
 
 /* clang-format on */
 
-#define LANDLOCK_ABI_LAST 9
+static int add_quiet_access(__u64 *const quiet_access,
+			    const __u64 handled_access,
+			    const char *const env_var, const bool default_all)
+{
+	char *env_quiet_access, *env_quiet_access_next, *str_access;
+
+	if (default_all)
+		*quiet_access = handled_access;
+	else
+		*quiet_access = 0;
+
+	env_quiet_access = getenv(env_var);
+	if (!env_quiet_access)
+		return 0;
+
+	env_quiet_access = strdup(env_quiet_access);
+	env_quiet_access_next = env_quiet_access;
+	unsetenv(env_var);
+	*quiet_access = 0;
+
+	while ((str_access = strsep(&env_quiet_access_next, ENV_DELIMITER))) {
+		if (strcmp(str_access, "") == 0)
+			continue;
+		else if (strcmp(str_access, "r") == 0)
+			*quiet_access |= ACCESS_FS_ROUGHLY_READ;
+		else if (strcmp(str_access, "w") == 0)
+			*quiet_access |= ACCESS_FS_ROUGHLY_WRITE;
+		else if (strcmp(str_access, "b") == 0)
+			*quiet_access |= LANDLOCK_ACCESS_NET_BIND_TCP;
+		else if (strcmp(str_access, "c") == 0)
+			*quiet_access |= LANDLOCK_ACCESS_NET_CONNECT_TCP;
+		else if (strcmp(str_access, "a") == 0)
+			*quiet_access |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
+		else if (strcmp(str_access, "s") == 0)
+			*quiet_access |= LANDLOCK_SCOPE_SIGNAL;
+		else {
+			fprintf(stderr, "Unknown quiet access \"%s\"\n",
+				str_access);
+			free(env_quiet_access);
+			return -1;
+		}
+	}
+
+	free(env_quiet_access);
+	*quiet_access &= handled_access;
+	return 0;
+}
+
+#define LANDLOCK_ABI_LAST 10
 
 #define XSTR(s) #s
 #define STR(s) XSTR(s)
@@ -330,6 +383,20 @@ static const char help[] =
 	"\n"
 	"A sandboxer should not log denied access requests to avoid spamming logs, "
 	"but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
+	ENV_FS_QUIET_NAME " and " ENV_NET_QUIET_NAME ", both optional, can then be used "
+	"to make access to some denied paths or network ports not trigger audit logging.\n"
+	ENV_FS_QUIET_ACCESS_NAME " and " ENV_NET_QUIET_ACCESS_NAME " can be used to specify "
+	"which accesses should be quieted (defaults to all):\n"
+	"* " ENV_FS_QUIET_ACCESS_NAME ": file system accesses to quiet\n"
+	"  - \"r\" to quiet all file/dir read accesses\n"
+	"  - \"w\" to quiet all file/dir write accesses\n"
+	"* " ENV_NET_QUIET_ACCESS_NAME ": network accesses to quiet\n"
+	"  - \"b\" to quiet bind denials\n"
+	"  - \"c\" to quiet connect denials\n"
+	"In addition, " ENV_SCOPED_QUIET_ACCESS_NAME " can be set to quiet all denials for "
+	"scoped actions (defaults to none).\n"
+	"  - \"a\" to quiet abstract unix socket denials\n"
+	"  - \"s\" to quiet signal denials\n"
 	"\n"
 	"Example:\n"
 	ENV_FS_RO_NAME "=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
@@ -359,7 +426,12 @@ int main(const int argc, char *const argv[], char *const *const envp)
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
 		.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
 			  LANDLOCK_SCOPE_SIGNAL,
+		.quiet_access_fs = 0,
+		.quiet_access_net = 0,
+		.quiet_scoped = 0,
 	};
+
+	bool quiet_supported = true;
 	int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
 	int set_restrict_flags = 0;
 
@@ -451,6 +523,11 @@ int main(const int argc, char *const argv[], char *const *const envp)
 			"provided by ABI version %d (instead of %d).\n",
 			LANDLOCK_ABI_LAST, abi);
 		__attribute__((fallthrough));
+	case 9:
+		/* Don't add quiet flags for ABI < 10 later on. */
+		quiet_supported = false;
+
+		__attribute__((fallthrough));
 	case LANDLOCK_ABI_LAST:
 		break;
 	default:
@@ -497,6 +574,25 @@ int main(const int argc, char *const argv[], char *const *const envp)
 		unsetenv(ENV_FORCE_LOG_NAME);
 	}
 
+	/*
+	 * Add quiet for fs/net handled access bits.  Doing this alone has no
+	 * effect unless we later add quiet rules per FS_QUIET/NET_QUIET.
+	 */
+	if (quiet_supported) {
+		if (add_quiet_access(&ruleset_attr.quiet_access_fs,
+				     ruleset_attr.handled_access_fs,
+				     ENV_FS_QUIET_ACCESS_NAME, true))
+			return 1;
+		if (add_quiet_access(&ruleset_attr.quiet_access_net,
+				     ruleset_attr.handled_access_net,
+				     ENV_NET_QUIET_ACCESS_NAME, true))
+			return 1;
+		if (add_quiet_access(&ruleset_attr.quiet_scoped,
+				     ruleset_attr.scoped,
+				     ENV_SCOPED_QUIET_ACCESS_NAME, false))
+			return 1;
+	}
+
 	ruleset_fd =
 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
 	if (ruleset_fd < 0) {
@@ -504,22 +600,34 @@ int main(const int argc, char *const argv[], char *const *const envp)
 		return 1;
 	}
 
-	if (populate_ruleset_fs(ENV_FS_RO_NAME, ruleset_fd, access_fs_ro)) {
+	if (populate_ruleset_fs(ENV_FS_RO_NAME, ruleset_fd, access_fs_ro, 0))
 		goto err_close_ruleset;
-	}
-	if (populate_ruleset_fs(ENV_FS_RW_NAME, ruleset_fd, access_fs_rw)) {
+	if (populate_ruleset_fs(ENV_FS_RW_NAME, ruleset_fd, access_fs_rw, 0))
 		goto err_close_ruleset;
+
+	/* Don't require this env to be present. */
+	if (quiet_supported && getenv(ENV_FS_QUIET_NAME)) {
+		if (populate_ruleset_fs(ENV_FS_QUIET_NAME, ruleset_fd, 0,
+					LANDLOCK_ADD_RULE_QUIET))
+			goto err_close_ruleset;
 	}
 
 	if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd,
-				 LANDLOCK_ACCESS_NET_BIND_TCP)) {
+				 LANDLOCK_ACCESS_NET_BIND_TCP, 0)) {
 		goto err_close_ruleset;
 	}
 	if (populate_ruleset_net(ENV_TCP_CONNECT_NAME, ruleset_fd,
-				 LANDLOCK_ACCESS_NET_CONNECT_TCP)) {
+				 LANDLOCK_ACCESS_NET_CONNECT_TCP, 0)) {
 		goto err_close_ruleset;
 	}
 
+	if (quiet_supported) {
+		if (populate_ruleset_net(ENV_NET_QUIET_NAME, ruleset_fd, 0,
+					 LANDLOCK_ADD_RULE_QUIET)) {
+			goto err_close_ruleset;
+		}
+	}
+
 	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
 		perror("Failed to restrict privileges");
 		goto err_close_ruleset;
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 3/9] landlock: Suppress logging when quiet flag is present
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

The quietness behaviour is as documented in the previous patch.

For optional accesses, since the existing deny_masks can only store 2x4bit
of layer index, with no way to represent "no layer", we need to either
expand it or have another field to correctly handle quieting of those.
This commit uses the latter approach - we add another field to store which
optional access (of the 2) are covered by quiet rules in their respective
layers as stored in deny_masks.

We can avoid making struct landlock_file_security larger by converting the
existing fown_layer to a 4bit field.  This commit does that, and adds test
to ensure that it is large enough for LANDLOCK_MAX_NUM_LAYERS-1.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v8:
- Rebase on top of mic/next
- Populate request.rule_flags in hook_unix_find()

Changes in v7:
- Following change in commit 1, now we need to copy rule_flags into
  landlock_request before calling landlock_log_denial for relevant fs
  denials
- Remove left over param comment

Changes in v5:
- Update code style and comment in get_layer_from_deny_masks() and
  landlock_log_denial()
- Now that rule_flags is moved into landlock_request, this version removes
  the extra parameter for landlock_log_denial and gets rid of
  no_rule_flags, simplifying some code.
- Fix build failure without CONFIG_AUDIT (reported by Justin Suess)

Changes in v3:
- Renamed patch title from "Check for quiet flag in landlock_log_denial"
  to this given the growth.
- Moved quiet bit check after domain_exec check
- Rename, style and comment fixes suggested by Mickaël.
- Squashed patch 6/6 from v2 "Implement quiet for optional accesses" into
  this one.  Changes to that below:
- Refactor the quiet flag setting in get_layer_from_deny_masks() to be
  more clear.
- Add KUnit tests
- Fix comments, add WARN_ON_ONCE, use __const_hweight64() as suggested by
  review
- Move build_check_file_security to fs.c
- Use a typedef for quiet_optional_accesses, add static_assert, and
  improve docs on landlock_get_quiet_optional_accesses.

Changes in v2:
- Supports the new quiet access masks.
- Support quieting scope requests (but not ptrace and attempted mounting
  for now)

 security/landlock/access.h |   5 +
 security/landlock/audit.c  | 255 +++++++++++++++++++++++++++++++++++--
 security/landlock/audit.h  |   3 +
 security/landlock/domain.c |  33 +++++
 security/landlock/domain.h |   5 +
 security/landlock/fs.c     |  35 +++++
 security/landlock/fs.h     |  17 ++-
 security/landlock/net.c    |  16 +--
 8 files changed, 340 insertions(+), 29 deletions(-)

diff --git a/security/landlock/access.h b/security/landlock/access.h
index c19d5bc13944..2775df80c7da 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -120,4 +120,9 @@ static inline bool access_mask_subset(access_mask_t subset,
 	return (subset | superset) == superset;
 }
 
+/* A bitmask that is large enough to hold set of optional accesses. */
+typedef u8 optional_access_t;
+static_assert(BITS_PER_TYPE(optional_access_t) >=
+	      HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL));
+
 #endif /* _SECURITY_LANDLOCK_ACCESS_H */
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 8d0edf94037d..2941b6d88688 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -246,7 +246,8 @@ static void test_get_denied_layer(struct kunit *const test)
 static size_t
 get_layer_from_deny_masks(access_mask_t *const access_request,
 			  const access_mask_t all_existing_optional_access,
-			  const deny_masks_t deny_masks)
+			  const deny_masks_t deny_masks,
+			  u8 quiet_optional_accesses, bool *quiet)
 {
 	const unsigned long access_opt = all_existing_optional_access;
 	const unsigned long access_req = *access_request;
@@ -254,6 +255,7 @@ get_layer_from_deny_masks(access_mask_t *const access_request,
 	size_t youngest_layer = 0;
 	size_t access_index = 0;
 	unsigned long access_bit;
+	bool should_quiet = false;
 
 	/* This will require change with new object types. */
 	WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
@@ -264,18 +266,29 @@ get_layer_from_deny_masks(access_mask_t *const access_request,
 			const size_t layer =
 				(deny_masks >> (access_index * 4)) &
 				(LANDLOCK_MAX_NUM_LAYERS - 1);
+			const bool layer_has_quiet =
+				!!(quiet_optional_accesses & BIT(access_index));
 
 			if (layer > youngest_layer) {
 				youngest_layer = layer;
 				missing = BIT(access_bit);
+				should_quiet = layer_has_quiet;
 			} else if (layer == youngest_layer) {
 				missing |= BIT(access_bit);
+				/*
+				 * Whether the layer has rules with quiet flag covering
+				 * the file accessed does not depend on the access, and so
+				 * the following WARN_ON_ONCE() should not fail.
+				 */
+				WARN_ON_ONCE(should_quiet && !layer_has_quiet);
+				should_quiet = layer_has_quiet;
 			}
 		}
 		access_index++;
 	}
 
 	*access_request = missing;
+	*quiet = should_quiet;
 	return youngest_layer;
 }
 
@@ -285,42 +298,188 @@ static void test_get_layer_from_deny_masks(struct kunit *const test)
 {
 	deny_masks_t deny_mask;
 	access_mask_t access;
+	u8 quiet_optional_accesses;
+	bool quiet;
 
 	/* truncate:0 ioctl_dev:2 */
 	deny_mask = 0x20;
+	quiet_optional_accesses = 0;
 
 	access = LANDLOCK_ACCESS_FS_TRUNCATE;
 	KUNIT_EXPECT_EQ(test, 0,
-			get_layer_from_deny_masks(&access,
-						  _LANDLOCK_ACCESS_FS_OPTIONAL,
-						  deny_mask));
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
 	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* layer denying truncate: quiet, ioctl: not quiet */
+	quiet_optional_accesses = 0b01;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* Reverse order - truncate:2 ioctl_dev:0 */
+	deny_mask = 0x02;
+	quiet_optional_accesses = 0;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* layer denying truncate: quiet, ioctl: not quiet */
+	quiet_optional_accesses = 0b01;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
 
 	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
 	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(&access,
-						  _LANDLOCK_ACCESS_FS_OPTIONAL,
-						  deny_mask));
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	/* layer denying truncate: not quiet, ioctl: quiet */
+	quiet_optional_accesses = 0b10;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
 	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
 
 	/* truncate:15 ioctl_dev:15 */
 	deny_mask = 0xff;
+	quiet_optional_accesses = 0;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 15,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 15,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access,
+			LANDLOCK_ACCESS_FS_TRUNCATE |
+				LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* Both quiet (same layer so quietness must be the same) */
+	quiet_optional_accesses = 0b11;
 
 	access = LANDLOCK_ACCESS_FS_TRUNCATE;
 	KUNIT_EXPECT_EQ(test, 15,
-			get_layer_from_deny_masks(&access,
-						  _LANDLOCK_ACCESS_FS_OPTIONAL,
-						  deny_mask));
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
 	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
 
 	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
 	KUNIT_EXPECT_EQ(test, 15,
-			get_layer_from_deny_masks(&access,
-						  _LANDLOCK_ACCESS_FS_OPTIONAL,
-						  deny_mask));
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
 	KUNIT_EXPECT_EQ(test, access,
 			LANDLOCK_ACCESS_FS_TRUNCATE |
 				LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, true);
 }
 
 #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
@@ -351,6 +510,22 @@ 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;
+	}
+}
+
 /**
  * landlock_log_denial - Create audit records related to a denial
  *
@@ -364,6 +539,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
 	struct landlock_hierarchy *youngest_denied;
 	size_t youngest_layer;
 	access_mask_t missing;
+	bool object_quiet_flag = false, quiet_applicable_to_access = false;
 
 	if (WARN_ON_ONCE(!subject || !subject->domain ||
 			 !subject->domain->hierarchy || !request))
@@ -379,10 +555,14 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
 			youngest_layer = get_denied_layer(subject->domain,
 							  &missing,
 							  request->layer_masks);
+			object_quiet_flag = !!(request->rule_flags.quiet_masks &
+					       BIT(youngest_layer));
 		} else {
 			youngest_layer = get_layer_from_deny_masks(
 				&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				request->deny_masks);
+				request->deny_masks,
+				request->quiet_optional_accesses,
+				&object_quiet_flag);
 		}
 		youngest_denied =
 			get_hierarchy(subject->domain, youngest_layer);
@@ -417,6 +597,53 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
 			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.
+	 */
+	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)
+		return;
+
 	/* Uses consistent allocation flags wrt common_lsm_audit(). */
 	ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
 			     AUDIT_LANDLOCK_ACCESS);
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 56778331b58c..c2da854d4405 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -48,6 +48,9 @@ struct landlock_request {
 	/* Required fields for requests with deny masks. */
 	const access_mask_t all_existing_optional_access;
 	deny_masks_t deny_masks;
+	u8 quiet_optional_accesses;
+
+	struct collected_rule_flags rule_flags;
 };
 
 #ifdef CONFIG_AUDIT
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 06b6bd845060..f365721050b7 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -156,6 +156,39 @@ get_layer_deny_mask(const access_mask_t all_existing_optional_access,
 	       << ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1));
 }
 
+/**
+ * landlock_get_quiet_optional_accesses - Get optional accesses which are
+ * "covered" by quiet rule flags.
+ *
+ * Returns a bitmask of which optional access are denied by layers for
+ * which rule_flags.quiet_masks has the corresponding bit set.
+ */
+optional_access_t landlock_get_quiet_optional_accesses(
+	const access_mask_t all_existing_optional_access,
+	const deny_masks_t deny_masks,
+	const struct collected_rule_flags rule_flags)
+{
+	const unsigned long access_opt = all_existing_optional_access;
+	size_t access_index = 0;
+	unsigned long access_bit;
+	optional_access_t quiet_optional_accesses = 0;
+
+	/* This will require change with new object types. */
+	WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
+
+	for_each_set_bit(access_bit, &access_opt,
+			 BITS_PER_TYPE(access_mask_t)) {
+		const u8 layer = (deny_masks >> (access_index * 4)) &
+				 (LANDLOCK_MAX_NUM_LAYERS - 1);
+		const bool is_quiet = !!(rule_flags.quiet_masks & BIT(layer));
+
+		if (is_quiet)
+			quiet_optional_accesses |= BIT(access_index);
+		access_index++;
+	}
+	return quiet_optional_accesses;
+}
+
 #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
 
 static void test_get_layer_deny_mask(struct kunit *const test)
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 9b8aeac8ebd2..c74730097a33 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -129,6 +129,11 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
 			const access_mask_t optional_access,
 			const struct layer_access_masks *const masks);
 
+optional_access_t landlock_get_quiet_optional_accesses(
+	const access_mask_t all_existing_optional_access,
+	const deny_masks_t deny_masks,
+	const struct collected_rule_flags rule_flags);
+
 int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
 
 static inline void
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 06a8d2258558..bd7554d0b65a 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -983,6 +983,7 @@ static int current_check_access_path(const struct path *const path,
 				       NULL, 0, NULL, NULL, NULL, NULL))
 		return 0;
 
+	request.rule_flags = rule_flags;
 	landlock_log_denial(subject, &request);
 	return -EACCES;
 }
@@ -1195,6 +1196,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 			    &request1, NULL, 0, NULL, NULL, NULL, NULL))
 			return 0;
 
+		request1.rule_flags = rule_flags_parent1;
 		landlock_log_denial(subject, &request1);
 		return -EACCES;
 	}
@@ -1243,10 +1245,12 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 
 	if (request1.access) {
 		request1.audit.u.path.dentry = old_parent;
+		request1.rule_flags = rule_flags_parent1;
 		landlock_log_denial(subject, &request1);
 	}
 	if (request2.access) {
 		request2.audit.u.path.dentry = new_dir->dentry;
+		request2.rule_flags = rule_flags_parent2;
 		landlock_log_denial(subject, &request2);
 	}
 
@@ -1706,6 +1710,7 @@ static int hook_unix_find(const struct path *const path, struct sock *other,
 		    &rule_flags, &request, NULL, 0, NULL, NULL, NULL, NULL))
 		return 0;
 
+	request.rule_flags = rule_flags;
 	landlock_log_denial(subject, &request);
 	return -EACCES;
 }
@@ -1739,8 +1744,31 @@ get_required_file_open_access(const struct file *const file)
 	return access;
 }
 
+static void build_check_file_security(void)
+{
+#ifdef CONFIG_AUDIT
+	const struct landlock_file_security file_sec = {
+		.quiet_optional_accesses = ~0,
+		.fown_layer = ~0,
+	};
+
+	/*
+	 * Make sure quiet_optional_accesses has enough bits to cover all
+	 * optional accesses.  The use of __const_hweight64() rather than
+	 * HWEIGHT() is due to GCC erroring about non-constants in
+	 * BUILD_BUG_ON call when using the latter, and the use of the 64bit
+	 * version is for future-proofing.
+	 */
+	BUILD_BUG_ON(__const_hweight64((u64)file_sec.quiet_optional_accesses) <
+		     __const_hweight64(_LANDLOCK_ACCESS_FS_OPTIONAL));
+	/* Makes sure all layers can be identified. */
+	BUILD_BUG_ON(file_sec.fown_layer < LANDLOCK_MAX_NUM_LAYERS - 1);
+#endif /* CONFIG_AUDIT */
+}
+
 static int hook_file_alloc_security(struct file *const file)
 {
+	build_check_file_security();
 	/*
 	 * Grants all access rights, even if most of them are not checked later
 	 * on. It is more consistent.
@@ -1819,6 +1847,10 @@ static int hook_file_open(struct file *const file)
 #ifdef CONFIG_AUDIT
 	landlock_file(file)->deny_masks = landlock_get_deny_masks(
 		_LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks);
+	landlock_file(file)->quiet_optional_accesses =
+		landlock_get_quiet_optional_accesses(
+			_LANDLOCK_ACCESS_FS_OPTIONAL,
+			landlock_file(file)->deny_masks, rule_flags);
 #endif /* CONFIG_AUDIT */
 
 	if (access_mask_subset(open_access_request, allowed_access))
@@ -1826,6 +1858,7 @@ static int hook_file_open(struct file *const file)
 
 	/* Sets access to reflect the actual request. */
 	request.access = open_access_request;
+	request.rule_flags = rule_flags;
 	landlock_log_denial(subject, &request);
 	return -EACCES;
 }
@@ -1855,6 +1888,7 @@ static int hook_file_truncate(struct file *const file)
 		.access = LANDLOCK_ACCESS_FS_TRUNCATE,
 #ifdef CONFIG_AUDIT
 		.deny_masks = landlock_file(file)->deny_masks,
+		.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
 #endif /* CONFIG_AUDIT */
 	});
 	return -EACCES;
@@ -1894,6 +1928,7 @@ static int hook_file_ioctl_common(const struct file *const file,
 		.access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
 #ifdef CONFIG_AUDIT
 		.deny_masks = landlock_file(file)->deny_masks,
+		.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
 #endif /* CONFIG_AUDIT */
 	});
 	return -EACCES;
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index cb7e654933ac..ac6e50216f87 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -63,11 +63,20 @@ struct landlock_file_security {
 	 * _LANDLOCK_ACCESS_FS_OPTIONAL).
 	 */
 	deny_masks_t deny_masks;
+	/**
+	 * @quiet_optional_accesses: Stores which optional accesses are
+	 * covered by quiet rules within the layer referred to in deny_masks,
+	 * one access per bit.  Does not take into account whether the quiet
+	 * access bits are actually set in the layer's corresponding
+	 * landlock_hierarchy.
+	 */
+	optional_access_t quiet_optional_accesses
+		: HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL);
 	/**
 	 * @fown_layer: Layer level of @fown_subject->domain with
 	 * LANDLOCK_SCOPE_SIGNAL.
 	 */
-	u8 fown_layer;
+	u8 fown_layer:4;
 #endif /* CONFIG_AUDIT */
 
 	/**
@@ -82,12 +91,6 @@ struct landlock_file_security {
 
 #ifdef CONFIG_AUDIT
 
-/* Makes sure all layers can be identified. */
-/* clang-format off */
-static_assert((typeof_member(struct landlock_file_security, fown_layer))~0 >=
-	      LANDLOCK_MAX_NUM_LAYERS);
-/* clang-format off */
-
 #endif /* CONFIG_AUDIT */
 
 /**
diff --git a/security/landlock/net.c b/security/landlock/net.c
index ade2b1750042..4349bd42e533 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -200,14 +200,14 @@ static int current_check_access_socket(struct socket *const sock,
 		return 0;
 
 	audit_net.family = address->sa_family;
-	landlock_log_denial(subject,
-			    &(struct landlock_request){
-				    .type = LANDLOCK_REQUEST_NET_ACCESS,
-				    .audit.type = LSM_AUDIT_DATA_NET,
-				    .audit.u.net = &audit_net,
-				    .access = access_request,
-				    .layer_masks = &layer_masks,
-			    });
+	landlock_log_denial(
+		subject,
+		&(struct landlock_request){ .type = LANDLOCK_REQUEST_NET_ACCESS,
+					    .audit.type = LSM_AUDIT_DATA_NET,
+					    .audit.u.net = &audit_net,
+					    .access = access_request,
+					    .layer_masks = &layer_masks,
+					    .rule_flags = rule_flags });
 	return -EACCES;
 }
 
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 2/9] landlock: Add API support and docs for the quiet flags
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

Adds the UAPI for the quiet flags feature (but not the implementation
yet).

According to pahole, even after adding the struct access_masks quiet_masks
in struct landlock_hierarchy, the u32 log_* bitfield still only has a size
of 2 bytes, so there's minimal wasted space.

Signed-off-by: Tingmao Wang <m@maowtm.org>
---

Changes in v8:
- The new Landlock ABI version is now v10 as a result of rebase.
- Allocate a rule_flags in hook_unix_find() and pass to
  is_access_to_paths_allowed().

Changes in v6:
- Fix typo in doc

Changes in v5:
- Doc fixes.
- Fix build failure without CONFIG_AUDIT / CONFIG_INET (reported by Justin
  Suess)

Changes in v4:
- Minor update to this commit message.
- Fix minor formatting

Changes in v3:
- Updated docs from Mickaël's suggestions.

Changes in v2:
- Per suggestion, added support for quieting only certain access bits,
  controlled by extra quiet_access_* fields in the ruleset_attr.
- Added docs for the extra fields and made updates to doc changes in v1.
  In particular, call out that the effect of LANDLOCK_ADD_RULE_QUIET is
  independent from the access bits passed in rule_attr
- landlock_add_rule will return -EINVAL when LANDLOCK_ADD_RULE_QUIET is
  used but the ruleset does not have any quiet access bits set for the
  given rule type.
- ABI version bump to v8
- Syntactic and comment changes per suggestion.

 include/uapi/linux/landlock.h                | 64 +++++++++++++++++
 security/landlock/domain.h                   |  5 ++
 security/landlock/fs.c                       | 11 +--
 security/landlock/fs.h                       |  2 +-
 security/landlock/net.c                      |  5 +-
 security/landlock/net.h                      |  5 +-
 security/landlock/ruleset.c                  | 12 +++-
 security/landlock/ruleset.h                  | 12 +++-
 security/landlock/syscalls.c                 | 72 +++++++++++++++-----
 tools/testing/selftests/landlock/base_test.c |  6 +-
 10 files changed, 160 insertions(+), 34 deletions(-)

diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 10a346e55e95..9a41c65623a1 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -32,6 +32,19 @@
  * *handle* a wide range or all access rights that they know about at build time
  * (and that they have tested with a kernel that supported them all).
  *
+ * @quiet_access_fs and @quiet_access_net are bitmasks of actions for
+ * which a denial by this layer will not trigger an audit log if the
+ * corresponding object (or its children, for filesystem rules) is marked
+ * with the "quiet" bit via %LANDLOCK_ADD_RULE_QUIET, even if logging
+ * would normally take place per landlock_restrict_self() flags.
+ * quiet_scoped is similar, except that it does not require marking any
+ * objects as quiet - if the ruleset is created with any bits set in
+ * quiet_scoped, then denial of such scoped resources will not trigger any
+ * log.  These 3 fields are available since Landlock ABI version 10.
+ *
+ * @quiet_access_fs, @quiet_access_net and @quiet_scoped must be a subset
+ * of @handled_access_fs, @handled_access_net and @scoped respectively.
+ *
  * This structure can grow in future Landlock versions.
  */
 struct landlock_ruleset_attr {
@@ -51,6 +64,24 @@ struct landlock_ruleset_attr {
 	 * resources (e.g. IPCs).
 	 */
 	__u64 scoped;
+
+	/* Since ABI 10: */
+
+	/**
+	 * @quiet_access_fs: Bitmask of filesystem actions which should not be
+	 * audit logged if per-object quiet flag is set.
+	 */
+	__u64 quiet_access_fs;
+	/**
+	 * @quiet_access_net: Bitmask of network actions which should not be
+	 * audit logged if per-object quiet flag is set.
+	 */
+	__u64 quiet_access_net;
+	/**
+	 * @quiet_scoped: Bitmask of scoped actions which should not be audit
+	 * logged.
+	 */
+	__u64 quiet_scoped;
 };
 
 /**
@@ -69,6 +100,39 @@ struct landlock_ruleset_attr {
 #define LANDLOCK_CREATE_RULESET_ERRATA			(1U << 1)
 /* clang-format on */
 
+/**
+ * DOC: landlock_add_rule_flags
+ *
+ * **Flags**
+ *
+ * %LANDLOCK_ADD_RULE_QUIET
+ *     Together with the quiet_* fields in struct landlock_ruleset_attr,
+ *     this flag controls whether Landlock will log audit messages when
+ *     access to the objects covered by this rule is denied by this layer.
+ *
+ *     If audit logging is enabled, when Landlock denies an access, it will
+ *     suppress the audit log if all of the following are true:
+ *
+ *     - this layer is the innermost layer that denied the access;
+ *     - all accesses denied by this layer are part of the quiet_* fields
+ *       in the related struct landlock_ruleset_attr;
+ *     - the object (or one of its parents, for filesystem rules) is
+ *       marked as "quiet" via %LANDLOCK_ADD_RULE_QUIET.
+ *
+ *     Because logging is only suppressed by a layer if the layer denies
+ *     access, a sandboxed program cannot use this flag to "hide" access
+ *     denials, without denying itself the access in the first place.
+ *
+ *     The effect of this flag does not depend on the value of
+ *     allowed_access in the passed in rule_attr.  When this flag is
+ *     present, the caller is also allowed to pass in an empty
+ *     allowed_access.
+ */
+
+/* clang-format off */
+#define LANDLOCK_ADD_RULE_QUIET			(1U << 0)
+/* clang-format on */
+
 /**
  * DOC: landlock_restrict_self_flags
  *
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index a9d57db0120d..9b8aeac8ebd2 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -114,6 +114,11 @@ struct landlock_hierarchy {
 		 * %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON.  Set to false by default.
 		 */
 		log_new_exec : 1;
+	/**
+	 * @quiet_masks: Bitmasks of access that should be quieted (i.e. not
+	 * logged) if the related object is marked as quiet.
+	 */
+	struct access_masks quiet_masks;
 #endif /* CONFIG_AUDIT */
 };
 
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 6f63e0182ef0..06a8d2258558 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -325,7 +325,7 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
  */
 int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 			    const struct path *const path,
-			    access_mask_t access_rights)
+			    access_mask_t access_rights, const int flags)
 {
 	int err;
 	struct landlock_id id = {
@@ -346,7 +346,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 	if (IS_ERR(id.key.object))
 		return PTR_ERR(id.key.object);
 	mutex_lock(&ruleset->lock);
-	err = landlock_insert_rule(ruleset, id, access_rights);
+	err = landlock_insert_rule(ruleset, id, access_rights, flags);
 	mutex_unlock(&ruleset->lock);
 	/*
 	 * No need to check for an error because landlock_insert_rule()
@@ -1662,6 +1662,7 @@ static int hook_unix_find(const struct path *const path, struct sock *other,
 	static const struct access_masks fs_resolve_unix = {
 		.fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
 	};
+	struct collected_rule_flags rule_flags = {};
 
 	/* Lookup for the purpose of saving coredumps is OK. */
 	if (unlikely(flags & SOCK_COREDUMP))
@@ -1700,9 +1701,9 @@ static int hook_unix_find(const struct path *const path, struct sock *other,
 	unix_state_unlock(other);
 
 	/* Checks the connections to allow-listed paths. */
-	if (is_access_to_paths_allowed(subject->domain, path,
-				       fs_resolve_unix.fs, &layer_masks,
-				       &request, NULL, 0, NULL, NULL, NULL))
+	if (is_access_to_paths_allowed(
+		    subject->domain, path, fs_resolve_unix.fs, &layer_masks,
+		    &rule_flags, &request, NULL, 0, NULL, NULL, NULL, NULL))
 		return 0;
 
 	landlock_log_denial(subject, &request);
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index bf9948941f2f..cb7e654933ac 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -126,6 +126,6 @@ __init void landlock_add_fs_hooks(void);
 
 int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 			    const struct path *const path,
-			    access_mask_t access_hierarchy);
+			    access_mask_t access_hierarchy, const int flags);
 
 #endif /* _SECURITY_LANDLOCK_FS_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index dc82ce4a2bd4..ade2b1750042 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -20,7 +20,8 @@
 #include "ruleset.h"
 
 int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
-			     const u16 port, access_mask_t access_rights)
+			     const u16 port, access_mask_t access_rights,
+			     const int flags)
 {
 	int err;
 	const struct landlock_id id = {
@@ -35,7 +36,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
 			 ~landlock_get_net_access_mask(ruleset, 0);
 
 	mutex_lock(&ruleset->lock);
-	err = landlock_insert_rule(ruleset, id, access_rights);
+	err = landlock_insert_rule(ruleset, id, access_rights, flags);
 	mutex_unlock(&ruleset->lock);
 
 	return err;
diff --git a/security/landlock/net.h b/security/landlock/net.h
index 09960c237a13..72c47f4d6803 100644
--- a/security/landlock/net.h
+++ b/security/landlock/net.h
@@ -16,7 +16,8 @@
 __init void landlock_add_net_hooks(void);
 
 int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
-			     const u16 port, access_mask_t access_rights);
+			     const u16 port, access_mask_t access_rights,
+			     const int flags);
 #else /* IS_ENABLED(CONFIG_INET) */
 static inline void landlock_add_net_hooks(void)
 {
@@ -24,7 +25,7 @@ static inline void landlock_add_net_hooks(void)
 
 static inline int
 landlock_append_net_rule(struct landlock_ruleset *const ruleset, const u16 port,
-			 access_mask_t access_rights)
+			 access_mask_t access_rights, const int flags)
 {
 	return -EAFNOSUPPORT;
 }
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index e4e6b730b581..d2d1e3fb6cf2 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -21,6 +21,7 @@
 #include <linux/slab.h>
 #include <linux/spinlock.h>
 #include <linux/workqueue.h>
+#include <uapi/linux/landlock.h>
 
 #include "access.h"
 #include "domain.h"
@@ -255,6 +256,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
 			if (WARN_ON_ONCE(this->layers[0].level != 0))
 				return -EINVAL;
 			this->layers[0].access |= (*layers)[0].access;
+			this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
 			return 0;
 		}
 
@@ -305,12 +307,15 @@ static void build_check_layer(void)
 /* @ruleset must be locked by the caller. */
 int landlock_insert_rule(struct landlock_ruleset *const ruleset,
 			 const struct landlock_id id,
-			 const access_mask_t access)
+			 const access_mask_t access, const int flags)
 {
 	struct landlock_layer layers[] = { {
 		.access = access,
 		/* When @level is zero, insert_rule() extends @ruleset. */
 		.level = 0,
+		.flags = {
+			.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
+		},
 	} };
 
 	build_check_layer();
@@ -351,6 +356,7 @@ static int merge_tree(struct landlock_ruleset *const dst,
 			return -EINVAL;
 
 		layers[0].access = walker_rule->layers[0].access;
+		layers[0].flags = walker_rule->layers[0].flags;
 
 		err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
 		if (err)
@@ -581,6 +587,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
 	if (err)
 		return ERR_PTR(err);
 
+#ifdef CONFIG_AUDIT
+	new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
+#endif /* CONFIG_AUDIT */
+
 	return no_free_ptr(new_dom);
 }
 
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 3b31552f0c95..e369f15ae885 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -172,8 +172,8 @@ struct landlock_ruleset {
 		 * @work_free: Enables to free a ruleset within a lockless
 		 * section.  This is only used by
 		 * landlock_put_ruleset_deferred() when @usage reaches zero.
-		 * The fields @lock, @usage, @num_rules, @num_layers and
-		 * @access_masks are then unused.
+		 * The fields @lock, @usage, @num_rules, @num_layers, @quiet_masks
+		 * and @access_masks are then unused.
 		 */
 		struct work_struct work_free;
 		struct {
@@ -199,6 +199,12 @@ struct landlock_ruleset {
 			 * non-merged ruleset (i.e. not a domain).
 			 */
 			u32 num_layers;
+			/**
+			 * @quiet_masks: Stores the quiet flags for an unmerged
+			 * ruleset.  For a merged domain, this is stored in each
+			 * layer's struct landlock_hierarchy instead.
+			 */
+			struct access_masks quiet_masks;
 			/**
 			 * @access_masks: Contains the subset of filesystem and
 			 * network actions that are restricted by a ruleset.
@@ -229,7 +235,7 @@ DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
 
 int landlock_insert_rule(struct landlock_ruleset *const ruleset,
 			 const struct landlock_id id,
-			 const access_mask_t access);
+			 const access_mask_t access, const int flags);
 
 struct landlock_ruleset *
 landlock_merge_ruleset(struct landlock_ruleset *const parent,
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index accfd2e5a0cd..a71068c41f76 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -105,8 +105,11 @@ static void build_check_abi(void)
 	ruleset_size = sizeof(ruleset_attr.handled_access_fs);
 	ruleset_size += sizeof(ruleset_attr.handled_access_net);
 	ruleset_size += sizeof(ruleset_attr.scoped);
+	ruleset_size += sizeof(ruleset_attr.quiet_access_fs);
+	ruleset_size += sizeof(ruleset_attr.quiet_access_net);
+	ruleset_size += sizeof(ruleset_attr.quiet_scoped);
 	BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size);
-	BUILD_BUG_ON(sizeof(ruleset_attr) != 24);
+	BUILD_BUG_ON(sizeof(ruleset_attr) != 48);
 
 	path_beneath_size = sizeof(path_beneath_attr.allowed_access);
 	path_beneath_size += sizeof(path_beneath_attr.parent_fd);
@@ -166,7 +169,7 @@ static const struct file_operations ruleset_fops = {
  * If the change involves a fix that requires userspace awareness, also update
  * the errata documentation in Documentation/userspace-api/landlock.rst .
  */
-const int landlock_abi_version = 9;
+const int landlock_abi_version = 10;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
@@ -193,6 +196,8 @@ const int landlock_abi_version = 9;
  * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
  * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small
  *   @size;
+ * - %EINVAL: quiet_access_fs or quiet_access_net is not a subset of the
+ *   corresponding handled_access_fs or handled_access_net;
  * - %E2BIG: @attr or @size inconsistencies;
  * - %EFAULT: @attr or @size inconsistencies;
  * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs.
@@ -249,6 +254,21 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
 	if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE)
 		return -EINVAL;
 
+	/*
+	 * Check that quiet masks are subsets of the respective handled masks.
+	 * Because of the checks above this is sufficient to also ensure that
+	 * the quiet masks are valid access masks.
+	 */
+	if ((ruleset_attr.quiet_access_fs | ruleset_attr.handled_access_fs) !=
+	    ruleset_attr.handled_access_fs)
+		return -EINVAL;
+	if ((ruleset_attr.quiet_access_net | ruleset_attr.handled_access_net) !=
+	    ruleset_attr.handled_access_net)
+		return -EINVAL;
+	if ((ruleset_attr.quiet_scoped | ruleset_attr.scoped) !=
+	    ruleset_attr.scoped)
+		return -EINVAL;
+
 	/* Checks arguments and transforms to kernel struct. */
 	ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs,
 					  ruleset_attr.handled_access_net,
@@ -256,6 +276,10 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
 	if (IS_ERR(ruleset))
 		return PTR_ERR(ruleset);
 
+	ruleset->quiet_masks.fs = ruleset_attr.quiet_access_fs;
+	ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
+	ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
+
 	/* Creates anonymous FD referring to the ruleset. */
 	ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
 				      ruleset, O_RDWR | O_CLOEXEC);
@@ -320,7 +344,7 @@ static int get_path_from_fd(const s32 fd, struct path *const path)
 }
 
 static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
-				 const void __user *const rule_attr)
+				 const void __user *const rule_attr, int flags)
 {
 	struct landlock_path_beneath_attr path_beneath_attr;
 	struct path path;
@@ -335,9 +359,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
 
 	/*
 	 * Informs about useless rule: empty allowed_access (i.e. deny rules)
-	 * are ignored in path walks.
+	 * are ignored in path walks.  However, the rule is not useless if it
+	 * is there to hold a quiet flag
 	 */
-	if (!path_beneath_attr.allowed_access)
+	if (!flags && !path_beneath_attr.allowed_access)
 		return -ENOMSG;
 
 	/* Checks that allowed_access matches the @ruleset constraints. */
@@ -345,6 +370,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
 	if ((path_beneath_attr.allowed_access | mask) != mask)
 		return -EINVAL;
 
+	/* Check for useless quiet flag. */
+	if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.fs)
+		return -EINVAL;
+
 	/* Gets and checks the new rule. */
 	err = get_path_from_fd(path_beneath_attr.parent_fd, &path);
 	if (err)
@@ -352,13 +381,13 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
 
 	/* Imports the new rule. */
 	err = landlock_append_fs_rule(ruleset, &path,
-				      path_beneath_attr.allowed_access);
+				      path_beneath_attr.allowed_access, flags);
 	path_put(&path);
 	return err;
 }
 
 static int add_rule_net_port(struct landlock_ruleset *ruleset,
-			     const void __user *const rule_attr)
+			     const void __user *const rule_attr, int flags)
 {
 	struct landlock_net_port_attr net_port_attr;
 	int res;
@@ -371,9 +400,10 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
 
 	/*
 	 * Informs about useless rule: empty allowed_access (i.e. deny rules)
-	 * are ignored by network actions.
+	 * are ignored by network actions.  However, the rule is not useless
+	 * if it is there to hold a quiet flag
 	 */
-	if (!net_port_attr.allowed_access)
+	if (!flags && !net_port_attr.allowed_access)
 		return -ENOMSG;
 
 	/* Checks that allowed_access matches the @ruleset constraints. */
@@ -381,13 +411,17 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
 	if ((net_port_attr.allowed_access | mask) != mask)
 		return -EINVAL;
 
+	/* Check for useless quiet flag. */
+	if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.net)
+		return -EINVAL;
+
 	/* Denies inserting a rule with port greater than 65535. */
 	if (net_port_attr.port > U16_MAX)
 		return -EINVAL;
 
 	/* Imports the new rule. */
 	return landlock_append_net_rule(ruleset, net_port_attr.port,
-					net_port_attr.allowed_access);
+					net_port_attr.allowed_access, flags);
 }
 
 /**
@@ -398,7 +432,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
  * @rule_type: Identify the structure type pointed to by @rule_attr:
  *             %LANDLOCK_RULE_PATH_BENEATH or %LANDLOCK_RULE_NET_PORT.
  * @rule_attr: Pointer to a rule (matching the @rule_type).
- * @flags: Must be 0.
+ * @flags: Must be 0 or %LANDLOCK_ADD_RULE_QUIET.
  *
  * This system call enables to define a new rule and add it to an existing
  * ruleset.
@@ -408,20 +442,25 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
  * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
  * - %EAFNOSUPPORT: @rule_type is %LANDLOCK_RULE_NET_PORT but TCP/IP is not
  *   supported by the running kernel;
- * - %EINVAL: @flags is not 0;
+ * - %EINVAL: @flags is not valid;
  * - %EINVAL: The rule accesses are inconsistent (i.e.
  *   &landlock_path_beneath_attr.allowed_access or
  *   &landlock_net_port_attr.allowed_access is not a subset of the ruleset
  *   handled accesses)
  * - %EINVAL: &landlock_net_port_attr.port is greater than 65535;
+ * - %EINVAL: LANDLOCK_ADD_RULE_QUIET is passed but the ruleset has no
+ *   quiet access bits set for the corresponding rule type.
  * - %ENOMSG: Empty accesses (e.g. &landlock_path_beneath_attr.allowed_access is
- *   0);
+ *   0) and no flags;
  * - %EBADF: @ruleset_fd is not a file descriptor for the current thread, or a
  *   member of @rule_attr is not a file descriptor as expected;
  * - %EBADFD: @ruleset_fd is not a ruleset file descriptor, or a member of
  *   @rule_attr is not the expected file descriptor type;
  * - %EPERM: @ruleset_fd has no write access to the underlying ruleset;
  * - %EFAULT: @rule_attr was not a valid address.
+ *
+ * .. kernel-doc:: include/uapi/linux/landlock.h
+ *     :identifiers: landlock_add_rule_flags
  */
 SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
 		const enum landlock_rule_type, rule_type,
@@ -432,8 +471,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
 	if (!is_initialized())
 		return -EOPNOTSUPP;
 
-	/* No flag for now. */
-	if (flags)
+	if (flags && flags != LANDLOCK_ADD_RULE_QUIET)
 		return -EINVAL;
 
 	/* Gets and checks the ruleset. */
@@ -443,9 +481,9 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
 
 	switch (rule_type) {
 	case LANDLOCK_RULE_PATH_BENEATH:
-		return add_rule_path_beneath(ruleset, rule_attr);
+		return add_rule_path_beneath(ruleset, rule_attr, flags);
 	case LANDLOCK_RULE_NET_PORT:
-		return add_rule_net_port(ruleset, rule_attr);
+		return add_rule_net_port(ruleset, rule_attr, flags);
 	default:
 		return -EINVAL;
 	}
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index 30d37234086c..84e91fcaa1b2 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,8 +76,8 @@ TEST(abi_version)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
 	};
-	ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
-					     LANDLOCK_CREATE_RULESET_VERSION));
+	ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
+					      LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
 					      LANDLOCK_CREATE_RULESET_VERSION));
@@ -201,7 +201,7 @@ TEST(add_rule_checks_ordering)
 	ASSERT_LE(0, ruleset_fd);
 
 	/* Checks invalid flags. */
-	ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 1));
+	ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 100));
 	ASSERT_EQ(EINVAL, errno);
 
 	/* Checks invalid ruleset FD. */
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module
In-Reply-To: <cover.1775490344.git.m@maowtm.org>

To avoid unnecessarily increasing the size of struct landlock_layer, we
make the layer level a u8 and use the space to store the flags struct.

Cc: Justin Suess <utilityemal77@gmail.com>
Signed-off-by: Tingmao Wang <m@maowtm.org>
Co-developed-by: Justin Suess <utilityemal77@gmail.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---

Because the no inherit patchset [2] from Justin Suess will depend on
this rule flags mechanism, I and Justin discussed a bit whether this
patch should in fact be a standalone thing separate from quiet flags
(i.e. add the infrastructure for rule flags, but don't actually add any
rule flags in this commit), so that the two series can be developed and
merged independently.  However in the end I decided to not do this and
send this patch as-is.

Changes in v8:
- Rebase on top of mic/next
- Add Co-developed-by: Justin Suess for handling this rebase initially
- layer_mask_t was removed in [1] but we still need it for the
  collected_rule_flags.  Rather than using raw u16, I've chosen to
  re-define it back in ruleset.h (it was in access.h).

Changes in v7:
- Take rule_flags separately from landlock_request in
  is_access_to_paths_allowed to avoid writing to the landlock_request
  variable if CONFIG_AUDIT is disabled (to enable compiler elision).
- Due to the above change, we don't need rule_flags in landlock_request in
  this commit anymore (will be added later).

Changes in v6:
- Rebased to include the revised disconnected directory handling changes
  (without the "reverting" behaviour)

Changes in v5:
- Move rule_flags into landlock_request.  This lets us get rid of the
  extra parameters to is_access_to_paths_allowed (and later on,
  landlock_log_denial), and thus less code changes.

Changes in v3:
- Comment changes, move local variables, simplify if branch

Changes in v2:
- Comment changes
- Rebased to include disconnected directory handling changes on mic/next
  and add backing up of collected_rule_flags.

[1]: https://lore.kernel.org/all/20260125195853.109967-1-gnoack3000@gmail.com/
[2]: https://lore.kernel.org/all/20251221194301.247484-1-utilityemal77@gmail.com/

 security/landlock/fs.c      | 96 +++++++++++++++++++++++--------------
 security/landlock/net.c     |  3 +-
 security/landlock/ruleset.c |  8 +++-
 security/landlock/ruleset.h | 32 ++++++++++++-
 4 files changed, 99 insertions(+), 40 deletions(-)

diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index c1ecfe239032..6f63e0182ef0 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -717,6 +717,9 @@ static void test_is_eacces_with_write(struct kunit *const test)
  *     those identified by @access_request_parent1).  This matrix can
  *     initially refer to domain layer masks and, when the accesses for the
  *     destination and source are the same, to requested layer masks.
+ * @rule_flags_parent1: Pointer to a collected_rule_flags struct
+ *     corresponding to the accumulated rule flags for parent1 to be read from
+ *     and filled as we traverse the path.
  * @log_request_parent1: Audit request to fill if the related access is denied.
  * @dentry_child1: Dentry to the initial child of the parent1 path.  This
  *     pointer must be NULL for non-refer actions (i.e. not link nor rename).
@@ -726,6 +729,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
  *     the source.  Must be set to 0 when using a simple path request.
  * @layer_masks_parent2: Similar to @layer_masks_parent1 but for a refer
  *     action.  This must be NULL otherwise.
+ * @rule_flags_parent2: Similar to @rule_flags_parent1 but for parent2.
  * @log_request_parent2: Audit request to fill if the related access is denied.
  * @dentry_child2: Dentry to the initial child of the parent2 path.  This
  *     pointer is only set for RENAME_EXCHANGE actions and must be NULL
@@ -739,17 +743,19 @@ static void test_is_eacces_with_write(struct kunit *const test)
  *
  * Return: True if the access request is granted, false otherwise.
  */
-static bool
-is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
-			   const struct path *const path,
-			   const access_mask_t access_request_parent1,
-			   struct layer_access_masks *layer_masks_parent1,
-			   struct landlock_request *const log_request_parent1,
-			   struct dentry *const dentry_child1,
-			   const access_mask_t access_request_parent2,
-			   struct layer_access_masks *layer_masks_parent2,
-			   struct landlock_request *const log_request_parent2,
-			   struct dentry *const dentry_child2)
+static bool is_access_to_paths_allowed(
+	const struct landlock_ruleset *const domain,
+	const struct path *const path,
+	const access_mask_t access_request_parent1,
+	struct layer_access_masks *layer_masks_parent1,
+	struct collected_rule_flags *const rule_flags_parent1,
+	struct landlock_request *const log_request_parent1,
+	struct dentry *const dentry_child1,
+	const access_mask_t access_request_parent2,
+	struct layer_access_masks *layer_masks_parent2,
+	struct collected_rule_flags *const rule_flags_parent2,
+	struct landlock_request *const log_request_parent2,
+	struct dentry *const dentry_child2)
 {
 	bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
 	     child1_is_directory = true, child2_is_directory = true;
@@ -797,20 +803,28 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
 	}
 
 	if (unlikely(dentry_child1)) {
+		/*
+		 * Get the layer masks for the child dentries for use by domain
+		 * check later.  The rule_flags for child1 should have been
+		 * included in rule_flags_parent1 already (cf.
+		 * collect_domain_accesses), and is not relevant for domain check,
+		 * so we don't have to pass it to landlock_unmask_layers.
+		 */
 		if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
 					      &_layer_masks_child1,
 					      LANDLOCK_KEY_INODE))
 			landlock_unmask_layers(find_rule(domain, dentry_child1),
-					       &_layer_masks_child1);
+					       &_layer_masks_child1, NULL);
 		layer_masks_child1 = &_layer_masks_child1;
 		child1_is_directory = d_is_dir(dentry_child1);
 	}
 	if (unlikely(dentry_child2)) {
+		/* See above comment for why NULL is passed as rule_flags_masks. */
 		if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
 					      &_layer_masks_child2,
 					      LANDLOCK_KEY_INODE))
 			landlock_unmask_layers(find_rule(domain, dentry_child2),
-					       &_layer_masks_child2);
+					       &_layer_masks_child2, NULL);
 		layer_masks_child2 = &_layer_masks_child2;
 		child2_is_directory = d_is_dir(dentry_child2);
 	}
@@ -865,12 +879,14 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
 		}
 
 		rule = find_rule(domain, walker_path.dentry);
-		allowed_parent1 =
-			allowed_parent1 ||
-			landlock_unmask_layers(rule, layer_masks_parent1);
-		allowed_parent2 =
-			allowed_parent2 ||
-			landlock_unmask_layers(rule, layer_masks_parent2);
+		allowed_parent1 = allowed_parent1 ||
+				  landlock_unmask_layers(rule,
+							 layer_masks_parent1,
+							 rule_flags_parent1);
+		allowed_parent2 = allowed_parent2 ||
+				  landlock_unmask_layers(rule,
+							 layer_masks_parent2,
+							 rule_flags_parent2);
 
 		/* Stops when a rule from each layer grants access. */
 		if (allowed_parent1 && allowed_parent2)
@@ -954,6 +970,7 @@ static int current_check_access_path(const struct path *const path,
 		landlock_get_applicable_subject(current_cred(), masks, NULL);
 	struct layer_access_masks layer_masks;
 	struct landlock_request request = {};
+	struct collected_rule_flags rule_flags = {};
 
 	if (!subject)
 		return 0;
@@ -962,8 +979,8 @@ static int current_check_access_path(const struct path *const path,
 						   access_request, &layer_masks,
 						   LANDLOCK_KEY_INODE);
 	if (is_access_to_paths_allowed(subject->domain, path, access_request,
-				       &layer_masks, &request, NULL, 0, NULL,
-				       NULL, NULL))
+				       &layer_masks, &rule_flags, &request,
+				       NULL, 0, NULL, NULL, NULL, NULL))
 		return 0;
 
 	landlock_log_denial(subject, &request);
@@ -1026,10 +1043,11 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
  * Return: True if all the domain access rights are allowed for @dir, false if
  * the walk reached @mnt_root.
  */
-static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
-				    const struct dentry *const mnt_root,
-				    struct dentry *dir,
-				    struct layer_access_masks *layer_masks_dom)
+static bool
+collect_domain_accesses(const struct landlock_ruleset *const domain,
+			const struct dentry *const mnt_root, struct dentry *dir,
+			struct layer_access_masks *layer_masks_dom,
+			struct collected_rule_flags *const rule_flags)
 {
 	bool ret = false;
 
@@ -1048,7 +1066,7 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
 
 		/* Gets all layers allowing all domain accesses. */
 		if (landlock_unmask_layers(find_rule(domain, dir),
-					   layer_masks_dom)) {
+					   layer_masks_dom, rule_flags)) {
 			/*
 			 * Stops when all handled accesses are allowed by at
 			 * least one rule in each layer.
@@ -1138,6 +1156,8 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 	struct layer_access_masks layer_masks_parent1 = {},
 				  layer_masks_parent2 = {};
 	struct landlock_request request1 = {}, request2 = {};
+	struct collected_rule_flags rule_flags_parent1 = {},
+				    rule_flags_parent2 = {};
 
 	if (!subject)
 		return 0;
@@ -1169,10 +1189,10 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 			subject->domain,
 			access_request_parent1 | access_request_parent2,
 			&layer_masks_parent1, LANDLOCK_KEY_INODE);
-		if (is_access_to_paths_allowed(subject->domain, new_dir,
-					       access_request_parent1,
-					       &layer_masks_parent1, &request1,
-					       NULL, 0, NULL, NULL, NULL))
+		if (is_access_to_paths_allowed(
+			    subject->domain, new_dir, access_request_parent1,
+			    &layer_masks_parent1, &rule_flags_parent1,
+			    &request1, NULL, 0, NULL, NULL, NULL, NULL))
 			return 0;
 
 		landlock_log_denial(subject, &request1);
@@ -1198,11 +1218,12 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 	/* new_dir->dentry is equal to new_dentry->d_parent */
 	allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
 						old_parent,
-						&layer_masks_parent1);
+						&layer_masks_parent1,
+						&rule_flags_parent1);
 	allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
 						new_dir->dentry,
-						&layer_masks_parent2);
-
+						&layer_masks_parent2,
+						&rule_flags_parent2);
 	if (allow_parent1 && allow_parent2)
 		return 0;
 
@@ -1214,8 +1235,9 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 	 */
 	if (is_access_to_paths_allowed(
 		    subject->domain, &mnt_dir, access_request_parent1,
-		    &layer_masks_parent1, &request1, old_dentry,
-		    access_request_parent2, &layer_masks_parent2, &request2,
+		    &layer_masks_parent1, &rule_flags_parent1, &request1,
+		    old_dentry, access_request_parent2, &layer_masks_parent2,
+		    &rule_flags_parent2, &request2,
 		    exchange ? new_dentry : NULL))
 		return 0;
 
@@ -1745,6 +1767,7 @@ static int hook_file_open(struct file *const file)
 	const struct landlock_cred_security *const subject =
 		landlock_get_applicable_subject(file->f_cred, any_fs, NULL);
 	struct landlock_request request = {};
+	struct collected_rule_flags rule_flags = {};
 
 	if (!subject)
 		return 0;
@@ -1771,7 +1794,8 @@ static int hook_file_open(struct file *const file)
 		    landlock_init_layer_masks(subject->domain,
 					      full_access_request, &layer_masks,
 					      LANDLOCK_KEY_INODE),
-		    &layer_masks, &request, NULL, 0, NULL, NULL, NULL)) {
+		    &layer_masks, &rule_flags, &request, NULL, 0, NULL, NULL,
+		    NULL, NULL)) {
 		allowed_access = full_access_request;
 	} else {
 		/*
diff --git a/security/landlock/net.c b/security/landlock/net.c
index c368649985c5..dc82ce4a2bd4 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -48,6 +48,7 @@ static int current_check_access_socket(struct socket *const sock,
 {
 	__be16 port;
 	struct layer_access_masks layer_masks = {};
+	struct collected_rule_flags rule_flags = {};
 	const struct landlock_rule *rule;
 	struct landlock_id id = {
 		.type = LANDLOCK_KEY_NET_PORT,
@@ -194,7 +195,7 @@ static int current_check_access_socket(struct socket *const sock,
 	if (!access_request)
 		return 0;
 
-	if (landlock_unmask_layers(rule, &layer_masks))
+	if (landlock_unmask_layers(rule, &layer_masks, &rule_flags))
 		return 0;
 
 	audit_net.family = address->sa_family;
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 181df7736bb9..e4e6b730b581 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -628,7 +628,8 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,
  * remaining unfulfilled access rights and masks has no leftover set bits).
  */
 bool landlock_unmask_layers(const struct landlock_rule *const rule,
-			    struct layer_access_masks *masks)
+			    struct layer_access_masks *masks,
+			    struct collected_rule_flags *const rule_flags)
 {
 	if (!masks)
 		return true;
@@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
 	 */
 	for (size_t i = 0; i < rule->num_layers; i++) {
 		const struct landlock_layer *const layer = &rule->layers[i];
+		const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
 
 		/* Clear the bits where the layer in the rule grants access. */
 		masks->access[layer->level - 1] &= ~layer->access;
+
+		/* Collect rule flags for each layer. */
+		if (rule_flags && layer->flags.quiet)
+			rule_flags->quiet_masks |= layer_bit;
 	}
 
 	for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 889f4b30301a..3b31552f0c95 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -29,7 +29,18 @@ struct landlock_layer {
 	/**
 	 * @level: Position of this layer in the layer stack.  Starts from 1.
 	 */
-	u16 level;
+	u8 level;
+	/**
+	 * @flags: Bitfield for special flags attached to this rule.
+	 */
+	struct {
+		/**
+		 * @quiet: Suppresses denial audit logs for the object covered by
+		 * this rule in this domain.  For filesystem rules, this inherits
+		 * down the file hierarchy.
+		 */
+		bool quiet:1;
+	} flags;
 	/**
 	 * @access: Bitfield of allowed actions on the kernel object.  They are
 	 * relative to the object type (e.g. %LANDLOCK_ACTION_FS_READ).
@@ -37,6 +48,22 @@ struct landlock_layer {
 	access_mask_t access;
 };
 
+typedef u16 layer_mask_t;
+
+/* Makes sure this is enough to include all layers. */
+static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
+
+
+/**
+ * struct collected_rule_flags - Hold accumulated flags for each layer.
+ */
+struct collected_rule_flags {
+	/**
+	 * @quiet_masks: Layers for which the quiet flag is effective.
+	 */
+	layer_mask_t quiet_masks;
+};
+
 /**
  * union landlock_key - Key of a ruleset's red-black tree
  */
@@ -302,7 +329,8 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
 }
 
 bool landlock_unmask_layers(const struct landlock_rule *const rule,
-			    struct layer_access_masks *masks);
+			    struct layer_access_masks *masks,
+			    struct collected_rule_flags *const rule_flags);
 
 access_mask_t
 landlock_init_layer_masks(const struct landlock_ruleset *const domain,
-- 
2.53.0

^ permalink raw reply related

* [PATCH v8 0/9] Implement LANDLOCK_ADD_RULE_QUIET
From: Tingmao Wang @ 2026-04-06 15:52 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Tingmao Wang, Günther Noack, Justin Suess, Jan Kara,
	Abhinav Saxena, linux-security-module

Hi,

This is the v8 of the "quiet flag" series, implementing the feature as
proposed in [1].

v7: https://lore.kernel.org/all/cover.1766330134.git.m@maowtm.org/
v6: https://lore.kernel.org/all/cover.1765040503.git.m@maowtm.org/
v5: https://lore.kernel.org/all/cover.1763931318.git.m@maowtm.org/
v4: https://lore.kernel.org/all/cover.1763330228.git.m@maowtm.org/
v3: https://lore.kernel.org/all/cover.1761511023.git.m@maowtm.org/
v2: https://lore.kernel.org/all/cover.1759686613.git.m@maowtm.org/
v1: https://lore.kernel.org/all/cover.1757376311.git.m@maowtm.org/

v7..v8:

- Rebase to mic/next
- Re-introduced layer_mask_t due to need in first patch
- Plumb through rule flags in hook_unix_find()
- Some selftests patches were not properly clang-format'd, fixed now.
- Minor env var handling change in sandboxer
- Fix selftests use of audit_count_records() without EXPECT_EQ


All text following this line is unchanged
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

v6..v7:

- Remove "landlock: Fix wrong type usage" (merged)
- Revert back to taking rule_flags separately from landlock_request until
  we call landlock_log_denial (https://lore.kernel.org/all/20251219.ahn3aiJuKahb@digikod.net/)
- Rebase to mic/next

v5..v6 rebases on top of the new simpler disconnected directory handling,
change some bools into u32, and fix some typo and style.

v4..v5 addresses review feedbacks, most significantly:
  - reduces code changes by pushing rule_flags into landlock_request.
  - adding test cases for two layers handling different access bits.

v3..v4 is a one-character formatting change, plus more tests.

We now have 5 patches for the selftest - I'm happy to squash it into one
depending on preference (and happy for Mickaël to do the squash if no
other feedback):
- selftests/landlock: Replace hard-coded 16 with a constant
- selftests/landlock: add tests for quiet flag with fs rules
- selftests/landlock: add tests for quiet flag with net rules
- selftests/landlock: Add tests for quiet flag with scope
- selftests/landlock: Add tests for invalid use of quiet flag

v2..v3:
Not much has changed in the actual functionality except various comment,
typing, asserts and general style fixes based on feedback.  The major new
thing here is tests (a bit of KUnit squashed into the optional access
commit, a lot of selftests especially in fs_tests.c).

The added fs_tests should exercise code path for optional and non-optional
access, renames, and mountpoint and disconnected directory handling.  I
will add the above missing bits to v4.

Removed:
- "Implement quiet for optional accesses"
    (squashed into "landlock: Suppress logging when quiet flag is present")


Old feature summary below:

The quiet flag allows a sandboxer to suppress audit logs for uninteresting
denials.  The flag can be set on objects and inherits downward in the
filesystem hierarchy.  On a denial, the youngest denying layer's quiet
flag setting decides whether to audit.  The motivation for this feature is
to reduce audit noise, and also prepare for a future supervisor feature
which will use this bit to suppress supervisor notifications.

This patch introduces a new quiet access mask in the ruleset_attr, which
gets eventually stored in the hierarchy. This allows the user to specify
which access should be affected by quiet bits.  One can then, for example,
make it such that read accesses to certain files are not audited (but
still denied), but all writes are still audited, regardless of location.

The sandboxer is extended to show example usage of this feature,
supporting quieting filesystem, network and scope accesses.

Demo:

    /# LL_FS_RO=/usr LL_FS_RW= LL_FORCE_LOG=1 LL_FS_QUIET=/dev:/tmp:/etc LL_FS_QUIET_ACCESS=r ./sandboxer bash
    ...
    audit: type=1423 audit(1759680175.562:195): domain=15bb25f6b blockers=fs.write_file,fs.read_file path="/dev/tty" dev="devtmpfs" ino=11
    ^^^^^^^^
    # note: because write is not quieted, we see the above line. blockers
    # contains read as well since that's the originally requested access.
    audit: type=1424 audit(1759680175.562:195): domain=15bb25f6b status=allocated mode=enforcing pid=616 uid=0 exe="/sandboxer" comm="sandboxer"
    audit: type=1300 audit(1759680175.562:195): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c86113d1 a2=802 a3=0 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
    audit: type=1327 audit(1759680175.562:195): proctitle="bash"
    bash: cannot set terminal process group (605): Inappropriate ioctl for device
    bash: no job control in this shell
    bash: /etc/bash.bashrc: Permission denied
    audit: type=1423 audit(1759680175.570:196): domain=15bb25f6b blockers=fs.read_file path="/.bash_history" dev="virtiofs" ino=36963
    ^^^^^^^^
    # read outside /dev:/tmp:/etc - not quieted
    audit: type=1300 audit(1759680175.570:196): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c868e400 a2=0 a3=0 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
    audit: type=1327 audit(1759680175.570:196): proctitle="bash"
    audit: type=1423 audit(1759680175.570:197): domain=15bb25f6b blockers=fs.read_file path="/.bash_history" dev="virtiofs" ino=36963
    audit: type=1300 audit(1759680175.570:197): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c868e400 a2=0 a3=0 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
    audit: type=1327 audit(1759680175.570:197): proctitle="bash"

    bash-5.2# head /etc/passwd
    head: cannot open '/etc/passwd' for reading: Permission denied
    ^^^^^^^^
    # reads to /etc are quieted

    bash-5.2# echo evil >> /etc/passwd
    bash: /etc/passwd: Permission denied
    audit: type=1423 audit(1759680227.030:198): domain=15bb25f6b blockers=fs.write_file path="/etc/passwd" dev="virtiofs" ino=790
    ^^^^^^^^
    # writes are not quieted
    audit: type=1300 audit(1759680227.030:198): arch=c000003e syscall=257 success=no exit=-13 a0=ffffffffffffff9c a1=5565c86ab030 a2=441 a3=1b6 items=0 ppid=605 pid=616 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bash" exe="/usr/bin/bash" key=(null)
    audit: type=1327 audit(1759680227.030:198): proctitle="bash"

Design:

- The user can set the quiet flag for a layer on any part of the fs
  hierarchy (whether it allows any access on it or not), and the flag
  inherits down (no support for "cancelling" the inheritance of the flag
  in specific subdirectories).

- The youngest layer that denies a request gets to decide whether the
  denial is audited or not.  This means that a compromised binary, for
  example, cannot "turn off" Landlock auditing when it tries to access
  files, unless it denies access to the files itself.  There is some
  debate to be had on whether, if a parent layer sets the quiet flag, but
  the request is denied by a deeper layer, whether Landlock should still
  audit anyway (since the rule author of the child layer likely did not
  expect the denial, so it would be good diagnostic).  The current
  approach is to ignore the quiet on the parent layer and audit anyway.

[1]: https://github.com/landlock-lsm/linux/issues/44#issuecomment-2876500918

Kind regards,
Tingmao

Tingmao Wang (9):
  landlock: Add a place for flags to layer rules
  landlock: Add API support and docs for the quiet flags
  landlock: Suppress logging when quiet flag is present
  samples/landlock: Add quiet flag support to sandboxer
  selftests/landlock: Replace hard-coded 16 with a constant
  selftests/landlock: add tests for quiet flag with fs rules
  selftests/landlock: add tests for quiet flag with net rules
  selftests/landlock: Add tests for quiet flag with scope
  selftests/landlock: Add tests for invalid use of quiet flag

 include/uapi/linux/landlock.h                 |   64 +
 samples/landlock/sandboxer.c                  |  128 +-
 security/landlock/access.h                    |    5 +
 security/landlock/audit.c                     |  255 +-
 security/landlock/audit.h                     |    3 +
 security/landlock/domain.c                    |   33 +
 security/landlock/domain.h                    |   10 +
 security/landlock/fs.c                        |  142 +-
 security/landlock/fs.h                        |   19 +-
 security/landlock/net.c                       |   24 +-
 security/landlock/net.h                       |    5 +-
 security/landlock/ruleset.c                   |   20 +-
 security/landlock/ruleset.h                   |   44 +-
 security/landlock/syscalls.c                  |   72 +-
 tools/testing/selftests/landlock/audit_test.c |   27 +-
 tools/testing/selftests/landlock/base_test.c  |   63 +-
 tools/testing/selftests/landlock/common.h     |    2 +
 tools/testing/selftests/landlock/fs_test.c    | 2450 ++++++++++++++++-
 tools/testing/selftests/landlock/net_test.c   |  121 +-
 .../landlock/scoped_abstract_unix_test.c      |   77 +-
 20 files changed, 3415 insertions(+), 149 deletions(-)


base-commit: 8c6a27e02bc55ab110d1828610048b19f903aaec
-- 
2.53.0


^ permalink raw reply

* Re: [PATCH v3 3/3] landlock: transpose the layer masks data structure
From: Tingmao Wang @ 2026-04-06 15:14 UTC (permalink / raw)
  To: Günther Noack, Mickaël Salaün
  Cc: linux-security-module, Justin Suess, Samasth Norway Ananda,
	Matthieu Buffet, Mikhail Ivanov, konstantin.meskhidze,
	Randy Dunlap
In-Reply-To: <20260206151154.97915-5-gnoack3000@gmail.com>

On 2/6/26 15:11, Günther Noack wrote:
> [...]
> @@ -406,12 +375,12 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
>  	if (missing) {
>  		/* Gets the nearest domain that denies the request. */
>  		if (request->layer_masks) {
> -			youngest_layer = get_denied_layer(
> -				subject->domain, &missing, request->layer_masks,
> -				request->layer_masks_size);
> +			youngest_layer = get_denied_layer(subject->domain,
> +							  &missing,
> +							  request->layer_masks);
>  		} else {
>  			youngest_layer = get_layer_from_deny_masks(
> -				&missing, request->all_existing_optional_access,
> +				&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,

Apologies for the post-merge review, but is this intentional?
request->all_existing_optional_access is only ever set to
_LANDLOCK_ACCESS_FS_OPTIONAL tho so this is not a bug, but I guess the
original code was intended to be generic.

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox