* [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records()
@ 2026-05-13 10:51 Mickaël Salaün
2026-05-13 10:51 ` [PATCH v1 2/2] selftests/landlock: Increase default audit socket timeout Mickaël Salaün
2026-05-16 19:21 ` [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() Günther Noack
0 siblings, 2 replies; 4+ messages in thread
From: Mickaël Salaün @ 2026-05-13 10:51 UTC (permalink / raw)
Cc: Mickaël Salaün, Günther Noack, Kees Cook,
Shuah Khan, Thomas Weißschuh, kernel test robot,
linux-kernel, linux-kselftest, linux-security-module, lkp, oe-lkp,
stable
audit_count_records() counts both AUDIT_LANDLOCK_DOMAIN allocation and
deallocation records in records.domain . Domain deallocation is tied to
asynchronous credential freeing via kworker threads
(landlock_put_ruleset_deferred), so the dealloc record can arrive after
the drain in audit_init() and after the preceding audit_match_record()
call. This causes flaky failures in tests that assert an exact
records.domain count: a stale dealloc record from a previous test's
domain inflates the count by one.
Observed on x86_64 under build configurations that delay the kworker
firing the dealloc callback (e.g. coverage instrumentation): the
audit_layout1 tests in fs_test.c intermittently saw records.domain == 2
where 1 was expected. The fix is in the shared helper, so those
existing checks become robust without needing a fs_test.c edit.
Filter audit_count_records() with a regex to skip records containing
deallocation status. The remaining domain records (allocation, emitted
synchronously during landlock_log_denial()) are deterministic.
Deallocation records are already tested explicitly via
matches_log_domain_deallocated() in audit_test.c, which uses its own
domain-ID-based filtering and longer timeout.
With this filter in place, re-add the records.domain == 0 checks that
were removed in commit 3647a4977fb7 ("selftests/landlock: Drain stale
audit records on init") as a workaround for this race.
Cc: Günther Noack <gnoack@google.com>
Cc: stable@vger.kernel.org
Depends-on: 07c2572a8757 ("selftests/landlock: Skip stale records in audit_match_record()")
Fixes: 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs")
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
tools/testing/selftests/landlock/audit.h | 39 ++++++++++++-------
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, 30 insertions(+), 13 deletions(-)
diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index 834005b2b0f0..699aed5ffab4 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h
@@ -381,18 +381,24 @@ struct audit_records {
};
/*
- * 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.
+ * Counts remaining audit records by type, skipping domain deallocation records.
+ * Deallocation records are emitted asynchronously from kworker threads after a
+ * previous test's child has exited, so they can arrive after the drain in
+ * audit_init() and after the preceding audit_match_record() call. Allocation
+ * records are emitted synchronously during landlock_log_denial() in the current
+ * test's syscall context, so only those are counted in records->domain.
*/
static int audit_count_records(int audit_fd, struct audit_records *records)
{
+ static const char dealloc_pattern[] = REGEX_LANDLOCK_PREFIX
+ " status=deallocated ";
struct audit_message msg;
- int err;
+ regex_t dealloc_re;
+ int ret, err = 0;
+
+ ret = regcomp(&dealloc_re, dealloc_pattern, 0);
+ if (ret)
+ return -ENOMEM;
records->access = 0;
records->domain = 0;
@@ -402,9 +408,8 @@ static int audit_count_records(int audit_fd, struct audit_records *records)
err = audit_recv(audit_fd, &msg);
if (err) {
if (err == -EAGAIN)
- return 0;
- else
- return err;
+ err = 0;
+ break;
}
switch (msg.header.nlmsg_type) {
@@ -412,12 +417,20 @@ static int audit_count_records(int audit_fd, struct audit_records *records)
records->access++;
break;
case AUDIT_LANDLOCK_DOMAIN:
- records->domain++;
+ ret = regexec(&dealloc_re, msg.data, 0, NULL, 0);
+ if (ret == REG_NOMATCH) {
+ records->domain++;
+ } else if (ret != 0) {
+ err = -EIO;
+ goto out;
+ }
break;
}
} while (true);
- return 0;
+out:
+ regfree(&dealloc_re);
+ return err;
}
static int audit_init(void)
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index 93ae5bd0dcce..758cf2368281 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -730,6 +730,7 @@ 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);
@@ -917,6 +918,7 @@ 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
diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c
index 1b6c8b53bf33..4f64c90583cd 100644
--- a/tools/testing/selftests/landlock/ptrace_test.c
+++ b/tools/testing/selftests/landlock/ptrace_test.c
@@ -342,6 +342,7 @@ 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);
diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
index c47491d2d1c1..72f97648d4a7 100644
--- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
+++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
@@ -312,6 +312,7 @@ 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));
--
2.54.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH v1 2/2] selftests/landlock: Increase default audit socket timeout 2026-05-13 10:51 [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() Mickaël Salaün @ 2026-05-13 10:51 ` Mickaël Salaün 2026-05-16 19:21 ` Günther Noack 2026-05-16 19:21 ` [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() Günther Noack 1 sibling, 1 reply; 4+ messages in thread From: Mickaël Salaün @ 2026-05-13 10:51 UTC (permalink / raw) Cc: Mickaël Salaün, Günther Noack, Kees Cook, Shuah Khan, Thomas Weißschuh, kernel test robot, linux-kernel, linux-kselftest, linux-security-module, lkp, oe-lkp, stable, Günther Noack matches_log_fs() and other audit_match_record() callers intermittently return -EAGAIN under heavy debug configs (KASAN, lockdep). The audit record delivery pipeline is asynchronous: landlock_log_denial() queues the record to audit_queue, and kauditd_thread dequeues and delivers via netlink. Under debug configs, kauditd scheduling between audit_log_end() and netlink_unicast() can exceed a syscall round trip (more than 1 usec), which was the value of the socket timeout used for the recvfrom() calls. The observed failure [1] is an EAGAIN error code (-11) which means that the access record had not arrived within the 1 usec timeout of recvfrom(). The expected record does arrive, but only after matches_log_fs() has already returned. It is then consumed by a later audit_count_records() call, making records.access == 1 instead of 0. Switch the default socket timeout to the slow value (1 second) so all audit_match_record() callers wait long enough for kauditd delivery, and lower it to the fast value (1 usec) only on the two paths that expect no record: audit_count_records() and the expected_domain_id == 0 probe in matches_log_domain_deallocated(). audit_init() drains stale records with the fast timeout (terminating on -EAGAIN once the backlog is empty) and switches to the patient default before returning. 1 second gives ~10x margin over the observed maximum (~100 ms, while the happy path is ~23 us). Rename the timeval constants to reflect their new roles: - audit_tv_dom_drop (1 second) -> audit_tv_default: default socket timeout, patient enough for asynchronous kauditd delivery. - audit_tv_default (1 usec) -> audit_tv_fast: fast timeout for paths that expect no record (drain, audit_count_records(), probes). Invert the conditional in matches_log_domain_deallocated(). Check setsockopt returns on both the lower and restore paths; preserve the first error via !err when the restore fails after a prior error so the actionable return code is not masked by a bookkeeping failure. Cc: Günther Noack <gnoack@google.com> Cc: Thomas Weißschuh <thomas.weissschuh@linutronix.de> Cc: stable@vger.kernel.org Depends-on: 07c2572a8757 ("selftests/landlock: Skip stale records in audit_match_record()") Fixes: 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs") Reported-by: Günther Noack <gnoack3000@gmail.com> Closes: https://lore.kernel.org/r/20260402.eb5c4e85f472@gnoack.org [1] Reported-by: kernel test robot <oliver.sang@intel.com> Closes: https://lore.kernel.org/oe-lkp/202605111649.a8b30a62-lkp@intel.com Signed-off-by: Mickaël Salaün <mic@digikod.net> --- tools/testing/selftests/landlock/audit.h | 80 +++++++++++++++++++----- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h index 699aed5ffab4..936fe20f020e 100644 --- a/tools/testing/selftests/landlock/audit.h +++ b/tools/testing/selftests/landlock/audit.h @@ -45,17 +45,25 @@ struct audit_message { }; }; -static const struct timeval audit_tv_dom_drop = { +static const struct timeval audit_tv_default = { /* - * Because domain deallocation is tied to asynchronous credential - * freeing, receiving such event may take some time. In practice, - * on a small VM, it should not exceed 100k usec, but let's wait up - * to 1 second to be safe. + * Default socket timeout for audit_match_record() callers that expect a + * record to arrive. Asynchronous kauditd delivery can exceed 1 usec + * under heavy debug configs (KASAN, lockdep), where kauditd_thread + * scheduling between audit_log_end() and netlink_unicast() takes longer + * than the previous 1 usec timeout. 1 second is a generous ceiling: on + * the happy path, kauditd delivers within dozens of usec. */ .tv_sec = 1, }; -static const struct timeval audit_tv_default = { +static const struct timeval audit_tv_fast = { + /* + * Fast timeout for paths that expect no record (audit_init() drain, + * audit_count_records(), probes). Causes audit_recv() to return + * -EAGAIN once the socket buffer is empty, naturally terminating the + * read loop. + */ .tv_usec = 1, }; @@ -334,8 +342,13 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid, * 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. + * audit_match_record(), waiting for the asynchronous kworker deallocation with + * the default patient timeout. + * + * When expected_domain_id is zero, the caller is probing for any dealloc record + * that may or may not arrive. Temporarily lowers the socket timeout to + * audit_tv_fast for this probe so it returns promptly when no record is + * pending; restores audit_tv_default after. */ static int __maybe_unused matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, @@ -361,16 +374,21 @@ matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, if (log_match_len >= sizeof(log_match)) return -E2BIG; - if (expected_domain_id) - setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, - &audit_tv_dom_drop, sizeof(audit_tv_dom_drop)); + if (!expected_domain_id) { + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_fast, sizeof(audit_tv_fast))) + return -errno; + } 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)); + if (!expected_domain_id) { + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_default, sizeof(audit_tv_default)) && + !err) + err = -errno; + } return err; } @@ -387,6 +405,11 @@ struct audit_records { * audit_init() and after the preceding audit_match_record() call. Allocation * records are emitted synchronously during landlock_log_denial() in the current * test's syscall context, so only those are counted in records->domain. + * + * Temporarily lowers SO_RCVTIMEO to audit_tv_fast for the read loop: this is a + * "no record expected" path that should terminate on the first -EAGAIN. The + * default patient timeout is restored on exit for subsequent + * audit_match_record() callers. */ static int audit_count_records(int audit_fd, struct audit_records *records) { @@ -403,6 +426,12 @@ static int audit_count_records(int audit_fd, struct audit_records *records) records->access = 0; records->domain = 0; + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, + sizeof(audit_tv_fast))) { + err = -errno; + goto out; + } + do { memset(&msg, 0, sizeof(msg)); err = audit_recv(audit_fd, &msg); @@ -429,6 +458,10 @@ static int audit_count_records(int audit_fd, struct audit_records *records) } while (true); out: + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)) && + !err) + err = -errno; regfree(&dealloc_re); return err; } @@ -449,9 +482,9 @@ static int audit_init(void) if (err) goto err_close; - /* Sets a timeout for negative tests. */ - err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, - sizeof(audit_tv_default)); + /* Uses the fast timeout to drain stale records below. */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, + sizeof(audit_tv_fast)); if (err) { err = -errno; goto err_close; @@ -467,6 +500,19 @@ static int audit_init(void) while (audit_recv(fd, NULL) == 0) ; + /* + * Restores the default timeout for audit_match_record() callers that + * expect a record to arrive. Paths that expect no record restore the + * fast timeout locally (audit_count_records(), the expected_domain_id + * == 0 probe in matches_log_domain_deallocated()). + */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)); + if (err) { + err = -errno; + goto err_close; + } + return fd; err_close: -- 2.54.0 ^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v1 2/2] selftests/landlock: Increase default audit socket timeout 2026-05-13 10:51 ` [PATCH v1 2/2] selftests/landlock: Increase default audit socket timeout Mickaël Salaün @ 2026-05-16 19:21 ` Günther Noack 0 siblings, 0 replies; 4+ messages in thread From: Günther Noack @ 2026-05-16 19:21 UTC (permalink / raw) To: Mickaël Salaün Cc: Günther Noack, Kees Cook, Shuah Khan, Thomas Weißschuh, kernel test robot, linux-kernel, linux-kselftest, linux-security-module, lkp, oe-lkp, stable On Wed, May 13, 2026 at 12:51:09PM +0200, Mickaël Salaün wrote: > matches_log_fs() and other audit_match_record() callers intermittently > return -EAGAIN under heavy debug configs (KASAN, lockdep). The audit > record delivery pipeline is asynchronous: landlock_log_denial() queues > the record to audit_queue, and kauditd_thread dequeues and delivers via > netlink. Under debug configs, kauditd scheduling between > audit_log_end() and netlink_unicast() can exceed a syscall round trip > (more than 1 usec), which was the value of the socket timeout used for > the recvfrom() calls. > > The observed failure [1] is an EAGAIN error code (-11) which means that > the access record had not arrived within the 1 usec timeout of > recvfrom(). The expected record does arrive, but only after > matches_log_fs() has already returned. It is then consumed by a later > audit_count_records() call, making records.access == 1 instead of 0. > > Switch the default socket timeout to the slow value (1 second) so all > audit_match_record() callers wait long enough for kauditd delivery, and > lower it to the fast value (1 usec) only on the two paths that expect no > record: audit_count_records() and the expected_domain_id == 0 probe in > matches_log_domain_deallocated(). audit_init() drains stale records > with the fast timeout (terminating on -EAGAIN once the backlog is empty) > and switches to the patient default before returning. 1 second gives > ~10x margin over the observed maximum (~100 ms, while the happy path is > ~23 us). > > Rename the timeval constants to reflect their new roles: > - audit_tv_dom_drop (1 second) -> audit_tv_default: default socket > timeout, patient enough for asynchronous kauditd delivery. > - audit_tv_default (1 usec) -> audit_tv_fast: fast timeout for paths > that expect no record (drain, audit_count_records(), probes). > > Invert the conditional in matches_log_domain_deallocated(). Check > setsockopt returns on both the lower and restore paths; preserve the > first error via !err when the restore fails after a prior error so the > actionable return code is not masked by a bookkeeping failure. > > Cc: Günther Noack <gnoack@google.com> > Cc: Thomas Weißschuh <thomas.weissschuh@linutronix.de> > Cc: stable@vger.kernel.org > Depends-on: 07c2572a8757 ("selftests/landlock: Skip stale records in audit_match_record()") > Fixes: 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs") > Reported-by: Günther Noack <gnoack3000@gmail.com> > Closes: https://lore.kernel.org/r/20260402.eb5c4e85f472@gnoack.org [1] > Reported-by: kernel test robot <oliver.sang@intel.com> > Closes: https://lore.kernel.org/oe-lkp/202605111649.a8b30a62-lkp@intel.com > Signed-off-by: Mickaël Salaün <mic@digikod.net> > --- > tools/testing/selftests/landlock/audit.h | 80 +++++++++++++++++++----- > 1 file changed, 63 insertions(+), 17 deletions(-) > > diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h > index 699aed5ffab4..936fe20f020e 100644 > --- a/tools/testing/selftests/landlock/audit.h > +++ b/tools/testing/selftests/landlock/audit.h > @@ -45,17 +45,25 @@ struct audit_message { > }; > }; > > -static const struct timeval audit_tv_dom_drop = { > +static const struct timeval audit_tv_default = { > /* > - * Because domain deallocation is tied to asynchronous credential > - * freeing, receiving such event may take some time. In practice, > - * on a small VM, it should not exceed 100k usec, but let's wait up > - * to 1 second to be safe. > + * Default socket timeout for audit_match_record() callers that expect a > + * record to arrive. Asynchronous kauditd delivery can exceed 1 usec > + * under heavy debug configs (KASAN, lockdep), where kauditd_thread > + * scheduling between audit_log_end() and netlink_unicast() takes longer > + * than the previous 1 usec timeout. 1 second is a generous ceiling: on > + * the happy path, kauditd delivers within dozens of usec. > */ > .tv_sec = 1, > }; > > -static const struct timeval audit_tv_default = { > +static const struct timeval audit_tv_fast = { > + /* > + * Fast timeout for paths that expect no record (audit_init() drain, > + * audit_count_records(), probes). Causes audit_recv() to return > + * -EAGAIN once the socket buffer is empty, naturally terminating the > + * read loop. > + */ > .tv_usec = 1, > }; > > @@ -334,8 +342,13 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid, > * 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. > + * audit_match_record(), waiting for the asynchronous kworker deallocation with > + * the default patient timeout. > + * > + * When expected_domain_id is zero, the caller is probing for any dealloc record > + * that may or may not arrive. Temporarily lowers the socket timeout to > + * audit_tv_fast for this probe so it returns promptly when no record is > + * pending; restores audit_tv_default after. > */ > static int __maybe_unused > matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, > @@ -361,16 +374,21 @@ matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, > if (log_match_len >= sizeof(log_match)) > return -E2BIG; > > - if (expected_domain_id) > - setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, > - &audit_tv_dom_drop, sizeof(audit_tv_dom_drop)); > + if (!expected_domain_id) { > + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, > + &audit_tv_fast, sizeof(audit_tv_fast))) > + return -errno; > + } > > 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)); > + if (!expected_domain_id) { > + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, > + &audit_tv_default, sizeof(audit_tv_default)) && > + !err) > + err = -errno; > + } > > return err; > } > @@ -387,6 +405,11 @@ struct audit_records { > * audit_init() and after the preceding audit_match_record() call. Allocation > * records are emitted synchronously during landlock_log_denial() in the current > * test's syscall context, so only those are counted in records->domain. > + * > + * Temporarily lowers SO_RCVTIMEO to audit_tv_fast for the read loop: this is a > + * "no record expected" path that should terminate on the first -EAGAIN. The > + * default patient timeout is restored on exit for subsequent > + * audit_match_record() callers. > */ > static int audit_count_records(int audit_fd, struct audit_records *records) > { > @@ -403,6 +426,12 @@ static int audit_count_records(int audit_fd, struct audit_records *records) > records->access = 0; > records->domain = 0; > > + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, > + sizeof(audit_tv_fast))) { > + err = -errno; > + goto out; > + } > + > do { > memset(&msg, 0, sizeof(msg)); > err = audit_recv(audit_fd, &msg); > @@ -429,6 +458,10 @@ static int audit_count_records(int audit_fd, struct audit_records *records) > } while (true); > > out: > + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, > + sizeof(audit_tv_default)) && > + !err) > + err = -errno; > regfree(&dealloc_re); > return err; > } > @@ -449,9 +482,9 @@ static int audit_init(void) > if (err) > goto err_close; > > - /* Sets a timeout for negative tests. */ > - err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, > - sizeof(audit_tv_default)); > + /* Uses the fast timeout to drain stale records below. */ > + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, > + sizeof(audit_tv_fast)); > if (err) { > err = -errno; > goto err_close; > @@ -467,6 +500,19 @@ static int audit_init(void) > while (audit_recv(fd, NULL) == 0) > ; > > + /* > + * Restores the default timeout for audit_match_record() callers that > + * expect a record to arrive. Paths that expect no record restore the > + * fast timeout locally (audit_count_records(), the expected_domain_id > + * == 0 probe in matches_log_domain_deallocated()). > + */ > + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, > + sizeof(audit_tv_default)); > + if (err) { > + err = -errno; > + goto err_close; > + } > + > return fd; > > err_close: > -- > 2.54.0 > Tested-by: Günther Noack <gnoack3000@gmail.com> ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() 2026-05-13 10:51 [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() Mickaël Salaün 2026-05-13 10:51 ` [PATCH v1 2/2] selftests/landlock: Increase default audit socket timeout Mickaël Salaün @ 2026-05-16 19:21 ` Günther Noack 1 sibling, 0 replies; 4+ messages in thread From: Günther Noack @ 2026-05-16 19:21 UTC (permalink / raw) To: Mickaël Salaün Cc: Günther Noack, Kees Cook, Shuah Khan, Thomas Weißschuh, kernel test robot, linux-kernel, linux-kselftest, linux-security-module, lkp, oe-lkp, stable On Wed, May 13, 2026 at 12:51:08PM +0200, Mickaël Salaün wrote: > audit_count_records() counts both AUDIT_LANDLOCK_DOMAIN allocation and > deallocation records in records.domain . Domain deallocation is tied to > asynchronous credential freeing via kworker threads > (landlock_put_ruleset_deferred), so the dealloc record can arrive after > the drain in audit_init() and after the preceding audit_match_record() > call. This causes flaky failures in tests that assert an exact > records.domain count: a stale dealloc record from a previous test's > domain inflates the count by one. > > Observed on x86_64 under build configurations that delay the kworker > firing the dealloc callback (e.g. coverage instrumentation): the > audit_layout1 tests in fs_test.c intermittently saw records.domain == 2 > where 1 was expected. The fix is in the shared helper, so those > existing checks become robust without needing a fs_test.c edit. > > Filter audit_count_records() with a regex to skip records containing > deallocation status. The remaining domain records (allocation, emitted > synchronously during landlock_log_denial()) are deterministic. > Deallocation records are already tested explicitly via > matches_log_domain_deallocated() in audit_test.c, which uses its own > domain-ID-based filtering and longer timeout. > > With this filter in place, re-add the records.domain == 0 checks that > were removed in commit 3647a4977fb7 ("selftests/landlock: Drain stale > audit records on init") as a workaround for this race. > > Cc: Günther Noack <gnoack@google.com> > Cc: stable@vger.kernel.org > Depends-on: 07c2572a8757 ("selftests/landlock: Skip stale records in audit_match_record()") > Fixes: 6a500b22971c ("selftests/landlock: Add tests for audit flags and domain IDs") > Signed-off-by: Mickaël Salaün <mic@digikod.net> > --- > tools/testing/selftests/landlock/audit.h | 39 ++++++++++++------- > 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, 30 insertions(+), 13 deletions(-) > > diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h > index 834005b2b0f0..699aed5ffab4 100644 > --- a/tools/testing/selftests/landlock/audit.h > +++ b/tools/testing/selftests/landlock/audit.h > @@ -381,18 +381,24 @@ struct audit_records { > }; > > /* > - * 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. > + * Counts remaining audit records by type, skipping domain deallocation records. > + * Deallocation records are emitted asynchronously from kworker threads after a > + * previous test's child has exited, so they can arrive after the drain in > + * audit_init() and after the preceding audit_match_record() call. Allocation > + * records are emitted synchronously during landlock_log_denial() in the current > + * test's syscall context, so only those are counted in records->domain. > */ > static int audit_count_records(int audit_fd, struct audit_records *records) > { > + static const char dealloc_pattern[] = REGEX_LANDLOCK_PREFIX > + " status=deallocated "; > struct audit_message msg; > - int err; > + regex_t dealloc_re; > + int ret, err = 0; > + > + ret = regcomp(&dealloc_re, dealloc_pattern, 0); > + if (ret) > + return -ENOMEM; > > records->access = 0; > records->domain = 0; > @@ -402,9 +408,8 @@ static int audit_count_records(int audit_fd, struct audit_records *records) > err = audit_recv(audit_fd, &msg); > if (err) { > if (err == -EAGAIN) > - return 0; > - else > - return err; > + err = 0; > + break; > } > > switch (msg.header.nlmsg_type) { > @@ -412,12 +417,20 @@ static int audit_count_records(int audit_fd, struct audit_records *records) > records->access++; > break; > case AUDIT_LANDLOCK_DOMAIN: > - records->domain++; > + ret = regexec(&dealloc_re, msg.data, 0, NULL, 0); > + if (ret == REG_NOMATCH) { > + records->domain++; > + } else if (ret != 0) { > + err = -EIO; > + goto out; > + } > break; > } > } while (true); > > - return 0; > +out: > + regfree(&dealloc_re); > + return err; > } > > static int audit_init(void) > diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c > index 93ae5bd0dcce..758cf2368281 100644 > --- a/tools/testing/selftests/landlock/audit_test.c > +++ b/tools/testing/selftests/landlock/audit_test.c > @@ -730,6 +730,7 @@ 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); > @@ -917,6 +918,7 @@ 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 > diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c > index 1b6c8b53bf33..4f64c90583cd 100644 > --- a/tools/testing/selftests/landlock/ptrace_test.c > +++ b/tools/testing/selftests/landlock/ptrace_test.c > @@ -342,6 +342,7 @@ 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); > diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c > index c47491d2d1c1..72f97648d4a7 100644 > --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c > +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c > @@ -312,6 +312,7 @@ 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)); > -- > 2.54.0 > Tested-by: Günther Noack <gnoack3000@gmail.com> ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-05-16 19:21 UTC | newest] Thread overview: 4+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-05-13 10:51 [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() Mickaël Salaün 2026-05-13 10:51 ` [PATCH v1 2/2] selftests/landlock: Increase default audit socket timeout Mickaël Salaün 2026-05-16 19:21 ` Günther Noack 2026-05-16 19:21 ` [PATCH v1 1/2] selftests/landlock: Filter dealloc records in audit_count_records() Günther Noack
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox