Skip to content

Commit 4d79d0c

Browse files
l0kodgregkh
authored andcommitted
selftests/landlock: Skip stale records in audit_match_record()
commit 07c2572 upstream. 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> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1 parent 127ae2e commit 4d79d0c

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

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

273-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
274-
&audit_tv_dom_drop, sizeof(audit_tv_dom_drop)));
275-
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1,
276-
&deallocated_dom));
274+
EXPECT_EQ(0, matches_log_domain_deallocated(
275+
self->audit_fd, 1, denial_dom, &deallocated_dom));
277276
EXPECT_EQ(denial_dom, deallocated_dom);
278-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
279-
&audit_tv_default, sizeof(audit_tv_default)));
280277
}
281278

282279
/*
@@ -520,22 +517,21 @@ TEST_F(audit_flags, signal)
520517

521518
if (variant->restrict_flags &
522519
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) {
520+
/*
521+
* No deallocation record: denials=0 never matches a real
522+
* record.
523+
*/
523524
EXPECT_EQ(-EAGAIN,
524-
matches_log_domain_deallocated(self->audit_fd, 0,
525+
matches_log_domain_deallocated(self->audit_fd, 0, 0,
525526
&deallocated_dom));
526527
EXPECT_EQ(deallocated_dom, 2);
527528
} else {
528-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
529-
&audit_tv_dom_drop,
530-
sizeof(audit_tv_dom_drop)));
531529
EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2,
530+
*self->domain_id,
532531
&deallocated_dom));
533532
EXPECT_NE(deallocated_dom, 2);
534533
EXPECT_NE(deallocated_dom, 0);
535534
EXPECT_EQ(deallocated_dom, *self->domain_id);
536-
EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO,
537-
&audit_tv_default,
538-
sizeof(audit_tv_default)));
539535
}
540536
}
541537

0 commit comments

Comments
 (0)