Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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: 6a500b22971c ("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>

+77 -39
+62 -20
tools/testing/selftests/landlock/audit.h
··· 249 249 static int audit_match_record(int audit_fd, const __u16 type, 250 250 const char *const pattern, __u64 *domain_id) 251 251 { 252 - struct audit_message msg; 252 + struct audit_message msg, last_mismatch = {}; 253 253 int ret, err = 0; 254 - bool matches_record = !type; 254 + int num_type_match = 0; 255 255 regmatch_t matches[2]; 256 256 regex_t regex; 257 257 ··· 259 259 if (ret) 260 260 return -EINVAL; 261 261 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) { 263 269 memset(&msg, 0, sizeof(msg)); 264 270 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 + } 266 278 goto out; 279 + } 267 280 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; 271 283 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; 277 291 } 278 292 279 293 if (domain_id) { ··· 330 316 domain_id); 331 317 } 332 318 333 - static int __maybe_unused matches_log_domain_deallocated( 334 - int audit_fd, unsigned int num_denials, __u64 *domain_id) 319 + /* 320 + * Matches a domain deallocation record. When expected_domain_id is non-zero, 321 + * the pattern includes the specific domain ID so that stale deallocation 322 + * records from a previous test (with a different domain ID) are skipped by 323 + * audit_match_record(), and the socket timeout is temporarily increased to 324 + * audit_tv_dom_drop to wait for the asynchronous kworker deallocation. 325 + */ 326 + static int __maybe_unused 327 + matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, 328 + __u64 expected_domain_id, __u64 *domain_id) 335 329 { 336 330 static const char log_template[] = REGEX_LANDLOCK_PREFIX 337 331 " status=deallocated denials=%u$"; 338 - char log_match[sizeof(log_template) + 10]; 339 - int log_match_len; 332 + static const char log_template_with_id[] = 333 + "^audit([0-9.:]\\+): domain=\\(%llx\\)" 334 + " status=deallocated denials=%u$"; 335 + char log_match[sizeof(log_template_with_id) + 32]; 336 + int log_match_len, err; 340 337 341 - log_match_len = snprintf(log_match, sizeof(log_match), log_template, 342 - num_denials); 338 + if (expected_domain_id) 339 + log_match_len = snprintf(log_match, sizeof(log_match), 340 + log_template_with_id, 341 + (unsigned long long)expected_domain_id, 342 + num_denials); 343 + else 344 + log_match_len = snprintf(log_match, sizeof(log_match), 345 + log_template, num_denials); 346 + 343 347 if (log_match_len >= sizeof(log_match)) 344 348 return -E2BIG; 345 349 346 - return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, 347 - domain_id); 350 + if (expected_domain_id) 351 + setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, 352 + &audit_tv_dom_drop, sizeof(audit_tv_dom_drop)); 353 + 354 + err = audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, 355 + domain_id); 356 + 357 + if (expected_domain_id) 358 + setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, 359 + sizeof(audit_tv_default)); 360 + 361 + return err; 348 362 } 349 363 350 364 struct audit_records {
+15 -19
tools/testing/selftests/landlock/audit_test.c
··· 139 139 WEXITSTATUS(status) != EXIT_SUCCESS) 140 140 _metadata->exit_code = KSFT_FAIL; 141 141 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 + */ 145 147 for (i = ARRAY_SIZE(*domain_stack) - 1; i >= 0; i--) { 146 148 __u64 deallocated_dom = 2; 147 149 148 150 EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1, 151 + (*domain_stack)[i], 149 152 &deallocated_dom)); 150 153 EXPECT_EQ((*domain_stack)[i], deallocated_dom) 151 154 { 152 155 TH_LOG("Failed to match domain %llx (#%d)", 153 - (*domain_stack)[i], i); 156 + (unsigned long long)(*domain_stack)[i], i); 154 157 } 155 158 } 156 159 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))); 159 160 EXPECT_EQ(0, close(ruleset_fd)); 160 161 } 161 162 ··· 272 271 EXPECT_EQ(0, close(pipe_parent[1])); 273 272 ASSERT_EQ(0, pthread_join(thread, NULL)); 274 273 275 - EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, 276 - &audit_tv_dom_drop, sizeof(audit_tv_dom_drop))); 277 - EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1, 278 - &deallocated_dom)); 274 + EXPECT_EQ(0, matches_log_domain_deallocated( 275 + self->audit_fd, 1, denial_dom, &deallocated_dom)); 279 276 EXPECT_EQ(denial_dom, deallocated_dom); 280 - EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, 281 - &audit_tv_default, sizeof(audit_tv_default))); 282 277 } 283 278 284 279 /* ··· 750 753 751 754 if (variant->restrict_flags & 752 755 LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { 756 + /* 757 + * No deallocation record: denials=0 never matches a real 758 + * record. 759 + */ 753 760 EXPECT_EQ(-EAGAIN, 754 - matches_log_domain_deallocated(self->audit_fd, 0, 761 + matches_log_domain_deallocated(self->audit_fd, 0, 0, 755 762 &deallocated_dom)); 756 763 EXPECT_EQ(deallocated_dom, 2); 757 764 } else { 758 - EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, 759 - &audit_tv_dom_drop, 760 - sizeof(audit_tv_dom_drop))); 761 765 EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2, 766 + *self->domain_id, 762 767 &deallocated_dom)); 763 768 EXPECT_NE(deallocated_dom, 2); 764 769 EXPECT_NE(deallocated_dom, 0); 765 770 EXPECT_EQ(deallocated_dom, *self->domain_id); 766 - EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, 767 - &audit_tv_default, 768 - sizeof(audit_tv_default))); 769 771 } 770 772 } 771 773