Skip to content

Commit 07c2572

Browse files
committed
selftests/landlock: Skip stale records in audit_match_record()
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 <gnoack@google.com> Cc: stable@vger.kernel.org Fixes: 6a500b2 ("selftests/landlock: Add tests for audit flags and domain IDs") Link: https://lore.kernel.org/r/20260402192608.1458252-5-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
1 parent 3647a49 commit 07c2572

2 files changed

Lines changed: 77 additions & 39 deletions

File tree

tools/testing/selftests/landlock/audit.h

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -249,31 +249,45 @@ static __maybe_unused char *regex_escape(const char *const src, char *dst,
249249
static int audit_match_record(int audit_fd, const __u16 type,
250250
const char *const pattern, __u64 *domain_id)
251251
{
252-
struct audit_message msg;
252+
struct audit_message msg, last_mismatch = {};
253253
int ret, err = 0;
254-
bool matches_record = !type;
254+
int num_type_match = 0;
255255
regmatch_t matches[2];
256256
regex_t regex;
257257

258258
ret = regcomp(&regex, pattern, 0);
259259
if (ret)
260260
return -EINVAL;
261261

262-
do {
262+
/*
263+
* Reads records until one matches both the expected type and the
264+
* pattern. Type-matching records with non-matching content are
265+
* silently consumed, which handles stale domain deallocation records
266+
* from a previous test emitted asynchronously by kworker threads.
267+
*/
268+
while (true) {
263269
memset(&msg, 0, sizeof(msg));
264270
err = audit_recv(audit_fd, &msg);
265-
if (err)
271+
if (err) {
272+
if (num_type_match) {
273+
printf("DATA: %s\n", last_mismatch.data);
274+
printf("ERROR: %d record(s) matched type %u"
275+
" but not pattern: %s\n",
276+
num_type_match, type, pattern);
277+
}
266278
goto out;
279+
}
267280

268-
if (msg.header.nlmsg_type == type)
269-
matches_record = true;
270-
} while (!matches_record);
281+
if (type && msg.header.nlmsg_type != type)
282+
continue;
271283

272-
ret = regexec(&regex, msg.data, ARRAY_SIZE(matches), matches, 0);
273-
if (ret) {
274-
printf("DATA: %s\n", msg.data);
275-
printf("ERROR: no match for pattern: %s\n", pattern);
276-
err = -ENOENT;
284+
ret = regexec(&regex, msg.data, ARRAY_SIZE(matches), matches,
285+
0);
286+
if (!ret)
287+
break;
288+
289+
num_type_match++;
290+
last_mismatch = msg;
277291
}
278292

279293
if (domain_id) {
@@ -316,21 +330,49 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid,
316330
domain_id);
317331
}
318332

319-
static int __maybe_unused matches_log_domain_deallocated(
320-
int audit_fd, unsigned int num_denials, __u64 *domain_id)
333+
/*
334+
* Matches a domain deallocation record. When expected_domain_id is non-zero,
335+
* the pattern includes the specific domain ID so that stale deallocation
336+
* records from a previous test (with a different domain ID) are skipped by
337+
* audit_match_record(), and the socket timeout is temporarily increased to
338+
* audit_tv_dom_drop to wait for the asynchronous kworker deallocation.
339+
*/
340+
static int __maybe_unused
341+
matches_log_domain_deallocated(int audit_fd, unsigned int num_denials,
342+
__u64 expected_domain_id, __u64 *domain_id)
321343
{
322344
static const char log_template[] = REGEX_LANDLOCK_PREFIX
323345
" status=deallocated denials=%u$";
324-
char log_match[sizeof(log_template) + 10];
325-
int log_match_len;
346+
static const char log_template_with_id[] =
347+
"^audit([0-9.:]\\+): domain=\\(%llx\\)"
348+
" status=deallocated denials=%u$";
349+
char log_match[sizeof(log_template_with_id) + 32];
350+
int log_match_len, err;
351+
352+
if (expected_domain_id)
353+
log_match_len = snprintf(log_match, sizeof(log_match),
354+
log_template_with_id,
355+
(unsigned long long)expected_domain_id,
356+
num_denials);
357+
else
358+
log_match_len = snprintf(log_match, sizeof(log_match),
359+
log_template, num_denials);
326360

327-
log_match_len = snprintf(log_match, sizeof(log_match), log_template,
328-
num_denials);
329361
if (log_match_len >= sizeof(log_match))
330362
return -E2BIG;
331363

332-
return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
333-
domain_id);
364+
if (expected_domain_id)
365+
setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO,
366+
&audit_tv_dom_drop, sizeof(audit_tv_dom_drop));
367+
368+
err = audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
369+
domain_id);
370+
371+
if (expected_domain_id)
372+
setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
373+
sizeof(audit_tv_default));
374+
375+
return err;
334376
}
335377

336378
struct audit_records {

tools/testing/selftests/landlock/audit_test.c

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,24 @@ TEST_F(audit, layers)
139139
WEXITSTATUS(status) != EXIT_SUCCESS)
140140
_metadata->exit_code = KSFT_FAIL;
141141

142-
/* Purges log from deallocated domains. */
143-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
144-
&audit_tv_dom_drop, sizeof(audit_tv_dom_drop)));
142+
/*
143+
* Purges log from deallocated domains. Records arrive in LIFO order
144+
* (innermost domain first) because landlock_put_hierarchy() walks the
145+
* chain sequentially in a single kworker context.
146+
*/
145147
for (i = ARRAY_SIZE(*domain_stack) - 1; i >= 0; i--) {
146148
__u64 deallocated_dom = 2;
147149

148150
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1,
151+
(*domain_stack)[i],
149152
&deallocated_dom));
150153
EXPECT_EQ((*domain_stack)[i], deallocated_dom)
151154
{
152155
TH_LOG("Failed to match domain %llx (#%d)",
153-
(*domain_stack)[i], i);
156+
(unsigned long long)(*domain_stack)[i], i);
154157
}
155158
}
156159
EXPECT_EQ(0, munmap(domain_stack, sizeof(*domain_stack)));
157-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
158-
&audit_tv_default, sizeof(audit_tv_default)));
159160
EXPECT_EQ(0, close(ruleset_fd));
160161
}
161162

@@ -271,13 +272,9 @@ TEST_F(audit, thread)
271272
EXPECT_EQ(0, close(pipe_parent[1]));
272273
ASSERT_EQ(0, pthread_join(thread, NULL));
273274

274-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
275-
&audit_tv_dom_drop, sizeof(audit_tv_dom_drop)));
276-
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1,
277-
&deallocated_dom));
275+
EXPECT_EQ(0, matches_log_domain_deallocated(
276+
self->audit_fd, 1, denial_dom, &deallocated_dom));
278277
EXPECT_EQ(denial_dom, deallocated_dom);
279-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
280-
&audit_tv_default, sizeof(audit_tv_default)));
281278
}
282279

283280
/*
@@ -753,22 +750,21 @@ TEST_F(audit_flags, signal)
753750

754751
if (variant->restrict_flags &
755752
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
753+
/*
754+
* No deallocation record: denials=0 never matches a real
755+
* record.
756+
*/
756757
EXPECT_EQ(-EAGAIN,
757-
matches_log_domain_deallocated(self->audit_fd, 0,
758+
matches_log_domain_deallocated(self->audit_fd, 0, 0,
758759
&deallocated_dom));
759760
EXPECT_EQ(deallocated_dom, 2);
760761
} else {
761-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
762-
&audit_tv_dom_drop,
763-
sizeof(audit_tv_dom_drop)));
764762
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2,
763+
*self->domain_id,
765764
&deallocated_dom));
766765
EXPECT_NE(deallocated_dom, 2);
767766
EXPECT_NE(deallocated_dom, 0);
768767
EXPECT_EQ(deallocated_dom, *self->domain_id);
769-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
770-
&audit_tv_default,
771-
sizeof(audit_tv_default)));
772768
}
773769
}
774770

0 commit comments

Comments
 (0)