Skip to contentSkip to conversation

Fix Landlock audit test flakiness

PATCH In review Mickaël Salaün 2 participants 13 messages Apr 2 – Apr 3

Subsystems (4)
lore
Share & tools
lore

Mickaël Salaün

author maintainer
#0 lore raw

v3 cover Fix Landlock audit test flakiness

This series fixes two classes of audit selftest failures plus two minor bugs in the audit test helpers.

The main issue is that domain deallocation audit records are emitted asynchronously from kworker threads and can arrive after a previous test's socket has been closed. This causes two distinct failure modes:

- audit_match_record() picks up a stale deallocation record from a previous test instead of the expected one, causing a domain ID mismatch. The audit.layers test (which reads 16 deallocation records in sequence) is particularly vulnerable because the large read window allows stale records to interleave. Patch 4 fixes this by filtering deallocation records by domain ID and skipping type-matching records with wrong content patterns.

- audit_count_records() counts stale deallocation records from a previous test, incrementing records.domain from the expected 0 to 1. Patch 3 fixes this by draining stale records at audit_init() time and removing records.domain == 0 checks that are not preceded by audit_match_record() calls (which would consume stale records).

These races are more likely to manifest when additional instrumentation changes kworker timing in the deallocation path (e.g. with the upcoming Landlock tracepoints work).

The two minor fixes (patches 1-2) correct a snprintf truncation check off-by-one and socket file descriptor leaks on error paths in audit_init(), audit_init_with_exe_filter(), and audit_cleanup(). Patch 5 fixes a __u64 format warning reported by the kbuild bot on powerpc64.

Patch 1 is an exact subset of the v1 combined patch, which is why it carries the Reviewed-by tag. Patches 2 and 3 extend beyond what was in v1, so the Reviewed-by is not carried. Patches 4 and 5 are new.

Changes since v2:
https://lore.kernel.org/r/[email protected]
- Patches 4-5: fix __u64 format warnings on powerpc64 (cast to unsigned long long for %llx). Patch 5 is new.

Changes since v1:
https://lore.kernel.org/r/[email protected]
- Split the combined drain fix into four separate patches.
- Patch 2: extend fd leak fix to audit_init_with_exe_filter() and audit_cleanup().
- Patch 3: also remove domain checks from audit.trace and scoped_audit.connect_to_child, document constraint, explain why a longer drain timeout was rejected.
- Patch 4: new, add domain ID filtering and timeout management to matches_log_domain_deallocated(), skip stale records in audit_match_record().

Mickaël Salaün (5):
selftests/landlock: Fix snprintf truncation checks in audit helpers selftests/landlock: Fix socket file descriptor leaks in audit helpers selftests/landlock: Drain stale audit records on init selftests/landlock: Skip stale records in audit_match_record() selftests/landlock: Fix format warning for __u64 in net_test

 tools/testing/selftests/landlock/audit.h      | 133 ++++++++++++++----
 tools/testing/selftests/landlock/audit_test.c |  36 ++---
 tools/testing/selftests/landlock/net_test.c   |   2 +-
 .../testing/selftests/landlock/ptrace_test.c  |   1 -
 .../landlock/scoped_abstract_unix_test.c      |   1 -
 5 files changed, 119 insertions(+), 54 deletions(-)
Signature · 1 line
2.53.0
author maintainer
#1 lore raw

PATCH v3 1/5 selftests/landlock: Fix snprintf truncation checks in audit helpers

snprintf() returns the number of characters that would have been written, excluding the terminating NUL byte. When the output is truncated, this return value equals or exceeds the buffer size. Fix matches_log_domain_allocated() and matches_log_domain_deallocated() to detect truncation with ">=" instead of ">".

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v1:
https://lore.kernel.org/r/[email protected]
- New patch (split from the drain fix).

---
 tools/testing/selftests/landlock/audit.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
tools/testing/selftests/landlock/audit.h +2 -2
index 44eb433e9666..1049a0582af5 100644
@@ -309,7 +309,7 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid,
 
 	log_match_len =
 		snprintf(log_match, sizeof(log_match), log_template, pid);
-	if (log_match_len > sizeof(log_match))
+	if (log_match_len >= sizeof(log_match))
 		return -E2BIG;
 
 	return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
@@ -326,7 +326,7 @@ static int __maybe_unused matches_log_domain_deallocated(
 
 	log_match_len = snprintf(log_match, sizeof(log_match), log_template,
 				 num_denials);
-	if (log_match_len > sizeof(log_match))
+	if (log_match_len >= sizeof(log_match))
 		return -E2BIG;
 
 	return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
Signature · 1 line
2.53.0
Reviewed-by on v3 1/5
#2 lore raw

Re: [PATCH v3 1/5] selftests/landlock: Fix snprintf truncation checks in audit helpers

Quoted 45 lines · Mickaël Salaün

snprintf() returns the number of characters that would have been written, excluding the terminating NUL byte. When the output is truncated, this return value equals or exceeds the buffer size. Fix matches_log_domain_allocated() and matches_log_domain_deallocated() to detect truncation with ">=" instead of ">".

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v1:
https://lore.kernel.org/r/[email protected]
- New patch (split from the drain fix).

---
 tools/testing/selftests/landlock/audit.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
tools/testing/selftests/landlock/audit.h +2 -2
index 44eb433e9666..1049a0582af5 100644
@@ -309,7 +309,7 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid,
 
 	log_match_len =
 		snprintf(log_match, sizeof(log_match), log_template, pid);
-	if (log_match_len > sizeof(log_match))
+	if (log_match_len >= sizeof(log_match))
 		return -E2BIG;
 
 	return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
@@ -326,7 +326,7 @@ static int __maybe_unused matches_log_domain_deallocated(
 
 	log_match_len = snprintf(log_match, sizeof(log_match), log_template,
 				 num_denials);
-	if (log_match_len > sizeof(log_match))
+	if (log_match_len >= sizeof(log_match))
 		return -E2BIG;
 
 	return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
Signature · 1 line
2.53.0

(I noticed the Reviewed-by tag was already there, re-sending to confirm that this also applies to this subset of the original patch)

–Günther

author maintainer
#3 lore raw

PATCH v3 2/5 selftests/landlock: Fix socket file descriptor leaks in audit helpers

In reply to Mickaël Salaün

audit_init() opens a netlink socket and configures it, but leaks the file descriptor if audit_set_status() or setsockopt() fails. Fix this by jumping to an error path that closes the socket before returning.

Apply the same fix to audit_init_with_exe_filter(), which leaks the file descriptor from audit_init() if audit_init_filter_exe() or audit_filter_exe() fails, and to audit_cleanup(), which leaks it if audit_init_filter_exe() fails in FIXTURE_TEARDOWN_PARENT().

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v1:
https://lore.kernel.org/r/[email protected]
- New patch (split from the drain fix, extended to audit_init_with_exe_filter() and audit_cleanup()).

---
 tools/testing/selftests/landlock/audit.h | 26 +++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)
tools/testing/selftests/landlock/audit.h +19 -7
index 1049a0582af5..6422943fc69e 100644
@@ -379,19 +379,25 @@ static int audit_init(void)
 
 	err = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1);
 	if (err)
-		return err;
+		goto err_close;
 
 	err = audit_set_status(fd, AUDIT_STATUS_PID, getpid());
 	if (err)
-		return err;
+		goto err_close;
 
 	/* Sets a timeout for negative tests. */
 	err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
 			 sizeof(audit_tv_default));
-	if (err)
-		return -errno;
+	if (err) {
+		err = -errno;
+		goto err_close;
+	}
 
 	return fd;
+
+err_close:
+	close(fd);
+	return err;
 }
 
 static int audit_init_filter_exe(struct audit_filter *filter, const char *path)
@@ -441,8 +447,10 @@ static int audit_cleanup(int audit_fd, struct audit_filter *filter)
 
 		filter = &new_filter;
 		err = audit_init_filter_exe(filter, NULL);
-		if (err)
+		if (err) {
+			close(audit_fd);
 			return err;
+		}
 	}
 
 	/* Filters might not be in place. */
@@ -468,11 +476,15 @@ static int audit_init_with_exe_filter(struct audit_filter *filter)
 
 	err = audit_init_filter_exe(filter, NULL);
 	if (err)
-		return err;
+		goto err_close;
 
 	err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE);
 	if (err)
-		return err;
+		goto err_close;
 
 	return fd;
+
+err_close:
+	close(fd);
+	return err;
 }
Signature · 1 line
2.53.0
Reviewed-by on v3 2/5
#4 lore raw

Re: [PATCH v3 2/5] selftests/landlock: Fix socket file descriptor leaks in audit helpers

Quoted 90 lines · Mickaël Salaün

audit_init() opens a netlink socket and configures it, but leaks the file descriptor if audit_set_status() or setsockopt() fails. Fix this by jumping to an error path that closes the socket before returning.

Apply the same fix to audit_init_with_exe_filter(), which leaks the file descriptor from audit_init() if audit_init_filter_exe() or audit_filter_exe() fails, and to audit_cleanup(), which leaks it if audit_init_filter_exe() fails in FIXTURE_TEARDOWN_PARENT().

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v1:
https://lore.kernel.org/r/[email protected]
- New patch (split from the drain fix, extended to audit_init_with_exe_filter() and audit_cleanup()).

---
 tools/testing/selftests/landlock/audit.h | 26 +++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)
tools/testing/selftests/landlock/audit.h +19 -7
index 1049a0582af5..6422943fc69e 100644
@@ -379,19 +379,25 @@ static int audit_init(void)
 
 	err = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1);
 	if (err)
-		return err;
+		goto err_close;
 
 	err = audit_set_status(fd, AUDIT_STATUS_PID, getpid());
 	if (err)
-		return err;
+		goto err_close;
 
 	/* Sets a timeout for negative tests. */
 	err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
 			 sizeof(audit_tv_default));
-	if (err)
-		return -errno;
+	if (err) {
+		err = -errno;
+		goto err_close;
+	}
 
 	return fd;
+
+err_close:
+	close(fd);
+	return err;
 }
 
 static int audit_init_filter_exe(struct audit_filter *filter, const char *path)
@@ -441,8 +447,10 @@ static int audit_cleanup(int audit_fd, struct audit_filter *filter)
 
 		filter = &new_filter;
 		err = audit_init_filter_exe(filter, NULL);
-		if (err)
+		if (err) {
+			close(audit_fd);
 			return err;
+		}
 	}
 
 	/* Filters might not be in place. */
@@ -468,11 +476,15 @@ static int audit_init_with_exe_filter(struct audit_filter *filter)
 
 	err = audit_init_filter_exe(filter, NULL);
 	if (err)
-		return err;
+		goto err_close;
 
 	err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE);
 	if (err)
-		return err;
+		goto err_close;
 
 	return fd;
+
+err_close:
+	close(fd);
+	return err;
 }
Signature · 1 line
2.53.0
author maintainer
#5 lore raw

PATCH v3 3/5 selftests/landlock: Drain stale audit records on init

In reply to Mickaël Salaün

Non-audit Landlock tests generate audit records as side effects when audit_enabled is non-zero (e.g. from boot configuration). These records accumulate in the kernel audit backlog while no audit daemon socket is open. When the next test opens a new netlink socket and registers as the audit daemon, the stale backlog is delivered, causing baseline record count checks to fail spuriously.

Fix this by draining all pending records in audit_init() right after setting the receive timeout. The 1-usec SO_RCVTIMEO causes audit_recv() to return -EAGAIN once the backlog is empty, naturally terminating the drain loop.

Domain deallocation records are emitted asynchronously from a work queue, so they may still arrive after the drain. Remove records.domain == 0 checks that are not preceded by audit_match_record() calls, which would otherwise consume stale records before the count. Document this constraint above audit_count_records().

Increasing the drain timeout to catch in-flight deallocation records was considered but rejected: a longer timeout adds latency to every audit_init() call even when no stale record is pending, and any fixed timeout is still not guaranteed to catch all records under load. Removing the unprotected checks is simpler and avoids the spurious failures.

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v1:
https://lore.kernel.org/r/[email protected]
- Also remove domain checks from audit.trace and scoped_audit.connect_to_child.
- Document records.domain == 0 constraint above audit_count_records().
- Explain why a longer drain timeout was rejected.
- Drop Reviewed-by (new code comment not in v1).
- Split snprintf and fd leak fixes into separate patches.

---
 tools/testing/selftests/landlock/audit.h      | 19 +++++++++++++++++++
 tools/testing/selftests/landlock/audit_test.c |  2 --
 .../testing/selftests/landlock/ptrace_test.c  |  1 -
 .../landlock/scoped_abstract_unix_test.c      |  1 -
 4 files changed, 19 insertions(+), 4 deletions(-)
tools/testing/selftests/landlock/audit.h +19 -0
index 6422943fc69e..74e1c3d763be 100644
@@ -338,6 +338,15 @@ struct audit_records {
 	size_t domain;
 };
 
+/*
+ * WARNING: Do not assert records.domain == 0 without a preceding
+ * audit_match_record() call.  Domain deallocation records are emitted
+ * asynchronously from kworker threads and can arrive after the drain in
+ * audit_init(), corrupting the domain count.  A preceding audit_match_record()
+ * call consumes stale records while scanning, making the assertion safe in
+ * practice because stale deallocation records arrive before the expected access
+ * records.
+ */
 static int audit_count_records(int audit_fd, struct audit_records *records)
 {
 	struct audit_message msg;
@@ -393,6 +402,16 @@ static int audit_init(void)
 		goto err_close;
 	}
 
+	/*
+	 * Drains stale audit records that accumulated in the kernel backlog
+	 * while no audit daemon socket was open.  This happens when non-audit
+	 * Landlock tests generate records while audit_enabled is non-zero (e.g.
+	 * from boot configuration), or when domain deallocation records arrive
+	 * asynchronously after a previous test's socket was closed.
+	 */
+	while (audit_recv(fd, NULL) == 0)
+		;
+
 	return fd;
 
 err_close:
tools/testing/selftests/landlock/audit_test.c +0 -2
index 46d02d49835a..f92ba6774faa 100644
@@ -412,7 +412,6 @@ TEST_F(audit_flags, signal)
 		} else {
 			EXPECT_EQ(1, records.access);
 		}
-		EXPECT_EQ(0, records.domain);
 
 		/* Updates filter rules to match the drop record. */
 		set_cap(_metadata, CAP_AUDIT_CONTROL);
@@ -601,7 +600,6 @@ TEST_F(audit_exec, signal_and_open)
 	/* Tests that there was no denial until now. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(0, records.domain);
 
 	/*
 	 * Wait for the child to do a first denied action by layer1 and
tools/testing/selftests/landlock/ptrace_test.c +0 -1
index 4f64c90583cd..1b6c8b53bf33 100644
@@ -342,7 +342,6 @@ TEST_F(audit, trace)
 	/* Makes sure there is no superfluous logged records. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(0, records.domain);
 
 	yama_ptrace_scope = get_yama_ptrace_scope();
 	ASSERT_LE(0, yama_ptrace_scope);
tools/testing/selftests/landlock/scoped_abstract_unix_test.c +0 -1
index 72f97648d4a7..c47491d2d1c1 100644
@@ -312,7 +312,6 @@ TEST_F(scoped_audit, connect_to_child)
 	/* Makes sure there is no superfluous logged records. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(0, records.domain);
 
 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
Signature · 1 line
2.53.0
Reviewed-by on v3 3/5
#6 lore raw

Re: [PATCH v3 3/5] selftests/landlock: Drain stale audit records on init

Quoted 131 lines · Mickaël Salaün

Non-audit Landlock tests generate audit records as side effects when audit_enabled is non-zero (e.g. from boot configuration). These records accumulate in the kernel audit backlog while no audit daemon socket is open. When the next test opens a new netlink socket and registers as the audit daemon, the stale backlog is delivered, causing baseline record count checks to fail spuriously.

Fix this by draining all pending records in audit_init() right after setting the receive timeout. The 1-usec SO_RCVTIMEO causes audit_recv() to return -EAGAIN once the backlog is empty, naturally terminating the drain loop.

Domain deallocation records are emitted asynchronously from a work queue, so they may still arrive after the drain. Remove records.domain == 0 checks that are not preceded by audit_match_record() calls, which would otherwise consume stale records before the count. Document this constraint above audit_count_records().

Increasing the drain timeout to catch in-flight deallocation records was considered but rejected: a longer timeout adds latency to every audit_init() call even when no stale record is pending, and any fixed timeout is still not guaranteed to catch all records under load. Removing the unprotected checks is simpler and avoids the spurious failures.

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v1:
https://lore.kernel.org/r/[email protected]
- Also remove domain checks from audit.trace and scoped_audit.connect_to_child.
- Document records.domain == 0 constraint above audit_count_records().
- Explain why a longer drain timeout was rejected.
- Drop Reviewed-by (new code comment not in v1).
- Split snprintf and fd leak fixes into separate patches.

---
 tools/testing/selftests/landlock/audit.h      | 19 +++++++++++++++++++
 tools/testing/selftests/landlock/audit_test.c |  2 --
 .../testing/selftests/landlock/ptrace_test.c  |  1 -
 .../landlock/scoped_abstract_unix_test.c      |  1 -
 4 files changed, 19 insertions(+), 4 deletions(-)
tools/testing/selftests/landlock/audit.h +19 -0
index 6422943fc69e..74e1c3d763be 100644
@@ -338,6 +338,15 @@ struct audit_records {
 	size_t domain;
 };
 
+/*
+ * WARNING: Do not assert records.domain == 0 without a preceding
+ * audit_match_record() call.  Domain deallocation records are emitted
+ * asynchronously from kworker threads and can arrive after the drain in
+ * audit_init(), corrupting the domain count.  A preceding audit_match_record()
+ * call consumes stale records while scanning, making the assertion safe in
+ * practice because stale deallocation records arrive before the expected access
+ * records.
+ */
 static int audit_count_records(int audit_fd, struct audit_records *records)
 {
 	struct audit_message msg;
@@ -393,6 +402,16 @@ static int audit_init(void)
 		goto err_close;
 	}
 
+	/*
+	 * Drains stale audit records that accumulated in the kernel backlog
+	 * while no audit daemon socket was open.  This happens when non-audit
+	 * Landlock tests generate records while audit_enabled is non-zero (e.g.
+	 * from boot configuration), or when domain deallocation records arrive
+	 * asynchronously after a previous test's socket was closed.
+	 */
+	while (audit_recv(fd, NULL) == 0)
+		;
+
 	return fd;
 
 err_close:
tools/testing/selftests/landlock/audit_test.c +0 -2
index 46d02d49835a..f92ba6774faa 100644
@@ -412,7 +412,6 @@ TEST_F(audit_flags, signal)
 		} else {
 			EXPECT_EQ(1, records.access);
 		}
-		EXPECT_EQ(0, records.domain);
 
 		/* Updates filter rules to match the drop record. */
 		set_cap(_metadata, CAP_AUDIT_CONTROL);
@@ -601,7 +600,6 @@ TEST_F(audit_exec, signal_and_open)
 	/* Tests that there was no denial until now. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(0, records.domain);
 
 	/*
 	 * Wait for the child to do a first denied action by layer1 and
tools/testing/selftests/landlock/ptrace_test.c +0 -1
index 4f64c90583cd..1b6c8b53bf33 100644
@@ -342,7 +342,6 @@ TEST_F(audit, trace)
 	/* Makes sure there is no superfluous logged records. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(0, records.domain);
 
 	yama_ptrace_scope = get_yama_ptrace_scope();
 	ASSERT_LE(0, yama_ptrace_scope);
tools/testing/selftests/landlock/scoped_abstract_unix_test.c +0 -1
index 72f97648d4a7..c47491d2d1c1 100644
@@ -312,7 +312,6 @@ TEST_F(scoped_audit, connect_to_child)
 	/* Makes sure there is no superfluous logged records. */
 	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
 	EXPECT_EQ(0, records.access);
-	EXPECT_EQ(0, records.domain);
 
 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
Signature · 1 line
2.53.0
author maintainer
#7 lore raw

PATCH v3 4/5 selftests/landlock: Skip stale records in audit_match_record()

In reply to Mickaël Salaün

Domain deallocation records are emitted asynchronously from kworker threads (via free_ruleset_work()). Stale deallocation records from a previous test can arrive during the current test's deallocation read loop and be picked up by audit_match_record() instead of the expected record, causing a domain ID mismatch. The audit.layers test (which creates 16 nested domains) is particularly vulnerable because it reads 16 deallocation records in sequence, providing a large window for stale records to interleave.

The same issue affects audit_flags.signal, where deallocation records from a previous test (audit.layers) can leak into the next test and be picked up by audit_match_record() instead of the expected record.

Fix this by continuing to read records when the type matches but the content pattern does not. Stale records are silently consumed, and the loop only stops when both type and pattern match (or the socket times out with -EAGAIN).

Additionally, extend matches_log_domain_deallocated() with an expected_domain_id parameter. When set, the regex pattern includes the specific domain ID as a literal hex value, so that deallocation records for a different domain do not match the pattern at all. This handles the case where the stale record has the same denial count as the expected one (e.g. both have denials=1), which the type+pattern loop alone cannot distinguish. Callers that already know the expected domain ID (from a prior denial or allocation record) now pass it to filter precisely.

When expected_domain_id is set, matches_log_domain_deallocated() also temporarily increases the socket timeout to audit_tv_dom_drop (1 second) to wait for the asynchronous kworker deallocation, and restores audit_tv_default afterward. This removes the need for callers to manage the timeout switch manually.

Cc Günther Noack <[email protected]>
Fixes 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")

---

Changes since v2:
https://lore.kernel.org/r/[email protected]
- Fix __u64 format warnings on powerpc64 (cast to unsigned long long for %llx).

Changes since v1:
https://lore.kernel.org/r/[email protected]
- New patch.

---
 tools/testing/selftests/landlock/audit.h      | 82 ++++++++++++++-----
 tools/testing/selftests/landlock/audit_test.c | 34 ++++----
 2 files changed, 77 insertions(+), 39 deletions(-)
tools/testing/selftests/landlock/audit.h +62 -20
index 74e1c3d763be..834005b2b0f0 100644
@@ -249,9 +249,9 @@ static __maybe_unused char *regex_escape(const char *const src, char *dst,
 static int audit_match_record(int audit_fd, const __u16 type,
 			      const char *const pattern, __u64 *domain_id)
 {
-	struct audit_message msg;
+	struct audit_message msg, last_mismatch = {};
 	int ret, err = 0;
-	bool matches_record = !type;
+	int num_type_match = 0;
 	regmatch_t matches[2];
 	regex_t regex;
 
@@ -259,21 +259,35 @@ static int audit_match_record(int audit_fd, const __u16 type,
 	if (ret)
 		return -EINVAL;
 
-	do {
+	/*
+	 * Reads records until one matches both the expected type and the
+	 * pattern.  Type-matching records with non-matching content are
+	 * silently consumed, which handles stale domain deallocation records
+	 * from a previous test emitted asynchronously by kworker threads.
+	 */
+	while (true) {
 		memset(&msg, 0, sizeof(msg));
 		err = audit_recv(audit_fd, &msg);
-		if (err)
+		if (err) {
+			if (num_type_match) {
+				printf("DATA: %s\n", last_mismatch.data);
+				printf("ERROR: %d record(s) matched type %u"
+				       " but not pattern: %s\n",
+				       num_type_match, type, pattern);
+			}
 			goto out;
+		}
 
-		if (msg.header.nlmsg_type == type)
-			matches_record = true;
-	} while (!matches_record);
+		if (type && msg.header.nlmsg_type != type)
+			continue;
 
-	ret = regexec(&regex, msg.data, ARRAY_SIZE(matches), matches, 0);
-	if (ret) {
-		printf("DATA: %s\n", msg.data);
-		printf("ERROR: no match for pattern: %s\n", pattern);
-		err = -ENOENT;
+		ret = regexec(&regex, msg.data, ARRAY_SIZE(matches), matches,
+			      0);
+		if (!ret)
+			break;
+
+		num_type_match++;
+		last_mismatch = msg;
 	}
 
 	if (domain_id) {
@@ -316,21 +330,49 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid,
 				  domain_id);
 }
 
-static int __maybe_unused matches_log_domain_deallocated(
-	int audit_fd, unsigned int num_denials, __u64 *domain_id)
+/*
+ * Matches a domain deallocation record.  When expected_domain_id is non-zero,
+ * the pattern includes the specific domain ID so that stale deallocation
+ * records from a previous test (with a different domain ID) are skipped by
+ * audit_match_record(), and the socket timeout is temporarily increased to
+ * audit_tv_dom_drop to wait for the asynchronous kworker deallocation.
+ */
+static int __maybe_unused
+matches_log_domain_deallocated(int audit_fd, unsigned int num_denials,
+			       __u64 expected_domain_id, __u64 *domain_id)
 {
 	static const char log_template[] = REGEX_LANDLOCK_PREFIX
 		" status=deallocated denials=%u$";
-	char log_match[sizeof(log_template) + 10];
-	int log_match_len;
+	static const char log_template_with_id[] =
+		"^audit([0-9.:]\\+): domain=\\(%llx\\)"
+		" status=deallocated denials=%u$";
+	char log_match[sizeof(log_template_with_id) + 32];
+	int log_match_len, err;
+
+	if (expected_domain_id)
+		log_match_len = snprintf(log_match, sizeof(log_match),
+					 log_template_with_id,
+					 (unsigned long long)expected_domain_id,
+					 num_denials);
+	else
+		log_match_len = snprintf(log_match, sizeof(log_match),
+					 log_template, num_denials);
 
-	log_match_len = snprintf(log_match, sizeof(log_match), log_template,
-				 num_denials);
 	if (log_match_len >= sizeof(log_match))
 		return -E2BIG;
 
-	return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
-				  domain_id);
+	if (expected_domain_id)
+		setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO,
+			   &audit_tv_dom_drop, sizeof(audit_tv_dom_drop));
+
+	err = audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
+				 domain_id);
+
+	if (expected_domain_id)
+		setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
+			   sizeof(audit_tv_default));
+
+	return err;
 }
 
 struct audit_records {
tools/testing/selftests/landlock/audit_test.c +15 -19
index f92ba6774faa..60de97bd0153 100644
@@ -139,23 +139,24 @@ TEST_F(audit, layers)
 	    WEXITSTATUS(status) != EXIT_SUCCESS)
 		_metadata->exit_code = KSFT_FAIL;
 
-	/* Purges log from deallocated domains. */
-	EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
-				&audit_tv_dom_drop, sizeof(audit_tv_dom_drop)));
+	/*
+	 * Purges log from deallocated domains.  Records arrive in LIFO order
+	 * (innermost domain first) because landlock_put_hierarchy() walks the
+	 * chain sequentially in a single kworker context.
+	 */
 	for (i = ARRAY_SIZE(*domain_stack) - 1; i >= 0; i--) {
 		__u64 deallocated_dom = 2;
 
 		EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1,
+							    (*domain_stack)[i],
 							    &deallocated_dom));
 		EXPECT_EQ((*domain_stack)[i], deallocated_dom)
 		{
 			TH_LOG("Failed to match domain %llx (#%d)",
-			       (*domain_stack)[i], i);
+			       (unsigned long long)(*domain_stack)[i], i);
 		}
 	}
 	EXPECT_EQ(0, munmap(domain_stack, sizeof(*domain_stack)));
-	EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
-				&audit_tv_default, sizeof(audit_tv_default)));
 	EXPECT_EQ(0, close(ruleset_fd));
 }
 
@@ -270,13 +271,9 @@ TEST_F(audit, thread)
 	EXPECT_EQ(0, close(pipe_parent[1]));
 	ASSERT_EQ(0, pthread_join(thread, NULL));
 
-	EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
-				&audit_tv_dom_drop, sizeof(audit_tv_dom_drop)));
-	EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1,
-						    &deallocated_dom));
+	EXPECT_EQ(0, matches_log_domain_deallocated(
+			     self->audit_fd, 1, denial_dom, &deallocated_dom));
 	EXPECT_EQ(denial_dom, deallocated_dom);
-	EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
-				&audit_tv_default, sizeof(audit_tv_default)));
 }
 
 FIXTURE(audit_flags)
@@ -432,22 +429,21 @@ TEST_F(audit_flags, signal)
 
 	if (variant->restrict_flags &
 	    LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
+		/*
+		 * No deallocation record: denials=0 never matches a real
+		 * record.
+		 */
 		EXPECT_EQ(-EAGAIN,
-			  matches_log_domain_deallocated(self->audit_fd, 0,
+			  matches_log_domain_deallocated(self->audit_fd, 0, 0,
 							 &deallocated_dom));
 		EXPECT_EQ(deallocated_dom, 2);
 	} else {
-		EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
-					&audit_tv_dom_drop,
-					sizeof(audit_tv_dom_drop)));
 		EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2,
+							    *self->domain_id,
 							    &deallocated_dom));
 		EXPECT_NE(deallocated_dom, 2);
 		EXPECT_NE(deallocated_dom, 0);
 		EXPECT_EQ(deallocated_dom, *self->domain_id);
-		EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
-					&audit_tv_default,
-					sizeof(audit_tv_default)));
 	}
 }
 
Signature · 1 line
2.53.0
author maintainer
#8 lore raw

PATCH v3 5/5 selftests/landlock: Fix format warning for __u64 in net_test

In reply to Mickaël Salaün

On architectures where __u64 is unsigned long (e.g. powerpc64), using %llx to format a __u64 triggers a -Wformat warning because %llx expects unsigned long long. Cast the argument to unsigned long long.

Cc Günther Noack <[email protected]>
Fixes a549d055a22e ("selftests/landlock: Add network tests")

---

Changes since v2:
- New patch.

---
 tools/testing/selftests/landlock/net_test.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
tools/testing/selftests/landlock/net_test.c +1 -1
index b34b139b3f89..4c528154ea92 100644
@@ -1356,7 +1356,7 @@ TEST_F(mini, network_access_rights)
 					    &net_port, 0))
 		{
 			TH_LOG("Failed to add rule with access 0x%llx: %s",
-			       access, strerror(errno));
+			       (unsigned long long)access, strerror(errno));
 		}
 	}
 	EXPECT_EQ(0, close(ruleset_fd));
Signature · 1 line
2.53.0
Reviewed-by on v3 5/5
#9 lore raw

Re: [PATCH v3 5/5] selftests/landlock: Fix format warning for __u64 in net_test

Quoted 34 lines · Mickaël Salaün

On architectures where __u64 is unsigned long (e.g. powerpc64), using %llx to format a __u64 triggers a -Wformat warning because %llx expects unsigned long long. Cast the argument to unsigned long long.

Cc Günther Noack <[email protected]>
Fixes a549d055a22e ("selftests/landlock: Add network tests")

---

Changes since v2:
- New patch.

---
 tools/testing/selftests/landlock/net_test.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
tools/testing/selftests/landlock/net_test.c +1 -1
index b34b139b3f89..4c528154ea92 100644
@@ -1356,7 +1356,7 @@ TEST_F(mini, network_access_rights)
 					    &net_port, 0))
 		{
 			TH_LOG("Failed to add rule with access 0x%llx: %s",
-			       access, strerror(errno));
+			       (unsigned long long)access, strerror(errno));
 		}
 	}
 	EXPECT_EQ(0, close(ruleset_fd));
Signature · 1 line
2.53.0
#10 lore raw
In reply to Mickaël Salaün

Hello!

Quoted 59 lines · Mickaël Salaün

This series fixes two classes of audit selftest failures plus two minor bugs in the audit test helpers.

The main issue is that domain deallocation audit records are emitted asynchronously from kworker threads and can arrive after a previous test's socket has been closed. This causes two distinct failure modes:

- audit_match_record() picks up a stale deallocation record from a previous test instead of the expected one, causing a domain ID mismatch. The audit.layers test (which reads 16 deallocation records in sequence) is particularly vulnerable because the large read window allows stale records to interleave. Patch 4 fixes this by filtering deallocation records by domain ID and skipping type-matching records with wrong content patterns.

- audit_count_records() counts stale deallocation records from a previous test, incrementing records.domain from the expected 0 to 1. Patch 3 fixes this by draining stale records at audit_init() time and removing records.domain == 0 checks that are not preceded by audit_match_record() calls (which would consume stale records).

These races are more likely to manifest when additional instrumentation changes kworker timing in the deallocation path (e.g. with the upcoming Landlock tracepoints work).

The two minor fixes (patches 1-2) correct a snprintf truncation check off-by-one and socket file descriptor leaks on error paths in audit_init(), audit_init_with_exe_filter(), and audit_cleanup(). Patch 5 fixes a __u64 format warning reported by the kbuild bot on powerpc64.

Patch 1 is an exact subset of the v1 combined patch, which is why it carries the Reviewed-by tag. Patches 2 and 3 extend beyond what was in v1, so the Reviewed-by is not carried. Patches 4 and 5 are new.

Changes since v2:
https://lore.kernel.org/r/[email protected]
- Patches 4-5: fix __u64 format warnings on powerpc64 (cast to unsigned long long for %llx). Patch 5 is new.

Changes since v1:
https://lore.kernel.org/r/[email protected]
- Split the combined drain fix into four separate patches.
- Patch 2: extend fd leak fix to audit_init_with_exe_filter() and audit_cleanup().
- Patch 3: also remove domain checks from audit.trace and scoped_audit.connect_to_child, document constraint, explain why a longer drain timeout was rejected.
- Patch 4: new, add domain ID filtering and timeout management to matches_log_domain_deallocated(), skip stale records in audit_match_record().

Mickaël Salaün (5):
selftests/landlock: Fix snprintf truncation checks in audit helpers selftests/landlock: Fix socket file descriptor leaks in audit helpers selftests/landlock: Drain stale audit records on init selftests/landlock: Skip stale records in audit_match_record() selftests/landlock: Fix format warning for __u64 in net_test

Quoted 10 lines
 tools/testing/selftests/landlock/audit.h      | 133 ++++++++++++++----
 tools/testing/selftests/landlock/audit_test.c |  36 ++---
 tools/testing/selftests/landlock/net_test.c   |   2 +-
 .../testing/selftests/landlock/ptrace_test.c  |   1 -
 .../landlock/scoped_abstract_unix_test.c      |   1 -
 5 files changed, 119 insertions(+), 54 deletions(-)
Signature · 1 line
2.53.0

I am still getting flaky audit tests even with these patches, I am afraid. It differs which of these tests is flaking, some of them still do, for example:

# RUN audit_layout1.remove_dir ...
# fs_test.c:7281:remove_dir:Expected 0 (0) == matches_log_fs(_metadata, self->audit_fd, "fs\\.remove_dir", dir_s1d2) (-11) # remove_dir: Test failed
# ❌ FAIL audit_layout1.remove_dir
not ok 191 audit_layout1.remove_dir
# RUN audit_layout1.read_dir ...
# ✅ OK audit_layout1.read_dir
ok 192 audit_layout1.read_dir
# RUN audit_layout1.read_file ...
# ✅ OK audit_layout1.read_file
ok 193 audit_layout1.read_file
# RUN audit_layout1.write_file ...
# fs_test.c:7221:write_file:Expected 0 (0) == matches_log_fs(_metadata, self->audit_fd, "fs\\.write_file", file1_s1d1) (-11) # fs_test.c:7224:write_file:Expected 0 (0) == records.access (1)
# write_file: Test failed
# ❌ FAIL audit_layout1.write_file
not ok 194 audit_layout1.write_file

My kernel config is this:

    make defconfig
    make kvm_guest.config
    KCONFIG_CONFIG="${KBUILD_OUTPUT}/.config" ./scripts/kconfig/merge_config.sh "${KBUILD_OUTPUT}/.config" tools/testing/selftests/landlock/config
    make debug.config
    echo "CONFIG_RANDOMIZE_BASE=n" >> "${KBUILD_OUTPUT}/.config"
    make olddefconfig

and then I run the selftests in Qemu with these flags:

qemu-system-x86_64 \
    -nographic \
    -m 4G \
    -enable-kvm \
    -append "console=ttyS0 lsm=landlock no_hash_pointers" \
    -kernel "${KBUILD_OUTPUT}/arch/x86/boot/bzImage" \
    -initrd "${INITRAMFS}"

This is using my own selftest runner scripts which builds an initramfs with the statically linked selftests.

Do you have a hunch what might be missing there? In the test run above, I have applied your V4 patch set on top of the current master, 5619b098e2fbf3a23bf13d91897056a1fe238c6d ("Merge tag 'for-7.0-rc6-tag' of git://git.kernel.org/pub/scm/linux/kernel/git/kdave/linux").

–Günther

#11 lore raw
Quoted 8 lines · Günther Noack

My kernel config is this:

    make defconfig
    make kvm_guest.config
    KCONFIG_CONFIG="${KBUILD_OUTPUT}/.config" ./scripts/kconfig/merge_config.sh "${KBUILD_OUTPUT}/.config" tools/testing/selftests/landlock/config
    make debug.config
    echo "CONFIG_RANDOMIZE_BASE=n" >> "${KBUILD_OUTPUT}/.config"
    make olddefconfig

P.S.: I should point out, everytime that I have observed these flakiness problems with the audit tests, it was in this debug config. I suspect that it adds delays in a way that makes it more likely.

–Günther

author maintainer
#12 lore raw
In reply to Günther Noack
Quoted 86 lines · Günther Noack

Hello!

Quoted 59 lines · Mickaël Salaün

This series fixes two classes of audit selftest failures plus two minor bugs in the audit test helpers.

The main issue is that domain deallocation audit records are emitted asynchronously from kworker threads and can arrive after a previous test's socket has been closed. This causes two distinct failure modes:

- audit_match_record() picks up a stale deallocation record from a previous test instead of the expected one, causing a domain ID mismatch. The audit.layers test (which reads 16 deallocation records in sequence) is particularly vulnerable because the large read window allows stale records to interleave. Patch 4 fixes this by filtering deallocation records by domain ID and skipping type-matching records with wrong content patterns.

- audit_count_records() counts stale deallocation records from a previous test, incrementing records.domain from the expected 0 to 1. Patch 3 fixes this by draining stale records at audit_init() time and removing records.domain == 0 checks that are not preceded by audit_match_record() calls (which would consume stale records).

These races are more likely to manifest when additional instrumentation changes kworker timing in the deallocation path (e.g. with the upcoming Landlock tracepoints work).

The two minor fixes (patches 1-2) correct a snprintf truncation check off-by-one and socket file descriptor leaks on error paths in audit_init(), audit_init_with_exe_filter(), and audit_cleanup(). Patch 5 fixes a __u64 format warning reported by the kbuild bot on powerpc64.

Patch 1 is an exact subset of the v1 combined patch, which is why it carries the Reviewed-by tag. Patches 2 and 3 extend beyond what was in v1, so the Reviewed-by is not carried. Patches 4 and 5 are new.

Changes since v2:
https://lore.kernel.org/r/[email protected]
- Patches 4-5: fix __u64 format warnings on powerpc64 (cast to unsigned long long for %llx). Patch 5 is new.

Changes since v1:
https://lore.kernel.org/r/[email protected]
- Split the combined drain fix into four separate patches.
- Patch 2: extend fd leak fix to audit_init_with_exe_filter() and audit_cleanup().
- Patch 3: also remove domain checks from audit.trace and scoped_audit.connect_to_child, document constraint, explain why a longer drain timeout was rejected.
- Patch 4: new, add domain ID filtering and timeout management to matches_log_domain_deallocated(), skip stale records in audit_match_record().

Mickaël Salaün (5):
selftests/landlock: Fix snprintf truncation checks in audit helpers selftests/landlock: Fix socket file descriptor leaks in audit helpers selftests/landlock: Drain stale audit records on init selftests/landlock: Skip stale records in audit_match_record() selftests/landlock: Fix format warning for __u64 in net_test

Quoted 10 lines
 tools/testing/selftests/landlock/audit.h      | 133 ++++++++++++++----
 tools/testing/selftests/landlock/audit_test.c |  36 ++---
 tools/testing/selftests/landlock/net_test.c   |   2 +-
 .../testing/selftests/landlock/ptrace_test.c  |   1 -
 .../landlock/scoped_abstract_unix_test.c      |   1 -
 5 files changed, 119 insertions(+), 54 deletions(-)
Signature · 1 line
2.53.0

I am still getting flaky audit tests even with these patches, I am afraid. It differs which of these tests is flaking, some of them still do, for example:

# RUN audit_layout1.remove_dir ...
# fs_test.c:7281:remove_dir:Expected 0 (0) == matches_log_fs(_metadata, self->audit_fd, "fs\\.remove_dir", dir_s1d2) (-11) # remove_dir: Test failed
# ❌ FAIL audit_layout1.remove_dir
not ok 191 audit_layout1.remove_dir
# RUN audit_layout1.read_dir ...
# ✅ OK audit_layout1.read_dir
ok 192 audit_layout1.read_dir
# RUN audit_layout1.read_file ...

Quoted 8 lines

# ✅ OK audit_layout1.read_file
ok 193 audit_layout1.read_file
# RUN audit_layout1.write_file ...
# fs_test.c:7221:write_file:Expected 0 (0) == matches_log_fs(_metadata, self->audit_fd, "fs\\.write_file", file1_s1d1) (-11) # fs_test.c:7224:write_file:Expected 0 (0) == records.access (1)
# write_file: Test failed
# ❌ FAIL audit_layout1.write_file
not ok 194 audit_layout1.write_file

I never hit these issues and I cannot reproduce them. This patch fixes the async events (i.e. domain drops).

You can try to increase audit_tv_default.

Quoted 20 lines

My kernel config is this:

    make defconfig
    make kvm_guest.config
    KCONFIG_CONFIG="${KBUILD_OUTPUT}/.config" ./scripts/kconfig/merge_config.sh "${KBUILD_OUTPUT}/.config" tools/testing/selftests/landlock/config
    make debug.config
    echo "CONFIG_RANDOMIZE_BASE=n" >> "${KBUILD_OUTPUT}/.config"
    make olddefconfig

and then I run the selftests in Qemu with these flags:

qemu-system-x86_64 \
    -nographic \
    -m 4G \
    -enable-kvm \
    -append "console=ttyS0 lsm=landlock no_hash_pointers" \
    -kernel "${KBUILD_OUTPUT}/arch/x86/boot/bzImage" \
    -initrd "${INITRAMFS}"
Quoted 2 lines

This is using my own selftest runner scripts which builds an initramfs with the statically linked selftests.

Can you try with the check-linux.sh build kselftest (which also set a lot of debug options)? You can also try with qemu if you set ARCH=x86_64

Quoted 5 lines

Do you have a hunch what might be missing there? In the test run above, I have applied your V4 patch set on top of the current master, 5619b098e2fbf3a23bf13d91897056a1fe238c6d ("Merge tag 'for-7.0-rc6-tag' of git://git.kernel.org/pub/scm/linux/kernel/git/kdave/linux").

This is weird because this is related to FS events, and they should be (almost) synchronous events. Maybe the audit event pipeline is made very slow because of some audit options but still...

Anyway, this is not what this patch fixes, but we should fix your issues as well.