1 // SPDX-License-Identifier: GPL-2.0
2 /*
3 * Landlock tests - Tracepoints
4 *
5 * Copyright © 2026 Cloudflare, Inc.
6 */
7
8 #define _GNU_SOURCE
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <linux/landlock.h>
12 #include <pthread.h>
13 #include <sched.h>
14 #include <stdio.h>
15 #include <string.h>
16 #include <sys/mount.h>
17 #include <sys/stat.h>
18 #include <sys/types.h>
19 #include <sys/wait.h>
20 #include <unistd.h>
21
22 #include "common.h"
23 #include "trace.h"
24
25 #define TRACE_TASK "trace_test"
26
27 /* clang-format off */
FIXTURE(trace)28 FIXTURE(trace) {
29 /* clang-format on */
30 int tracefs_ok;
31 };
32
FIXTURE_SETUP(trace)33 FIXTURE_SETUP(trace)
34 {
35 int ret;
36
37 set_cap(_metadata, CAP_SYS_ADMIN);
38 ASSERT_EQ(0, unshare(CLONE_NEWNS));
39 ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
40
41 ret = tracefs_fixture_setup();
42 if (ret) {
43 clear_cap(_metadata, CAP_SYS_ADMIN);
44 self->tracefs_ok = 0;
45 SKIP(return, "tracefs not available");
46 }
47 self->tracefs_ok = 1;
48
49 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
50 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true));
51 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, true));
52 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE,
53 true));
54 ASSERT_EQ(0,
55 tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, true));
56 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
57 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true));
58 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
59 ASSERT_EQ(0,
60 tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
61 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, true));
62 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, true));
63 ASSERT_EQ(0, tracefs_clear());
64 clear_cap(_metadata, CAP_SYS_ADMIN);
65 }
66
FIXTURE_TEARDOWN(trace)67 FIXTURE_TEARDOWN(trace)
68 {
69 if (!self->tracefs_ok)
70 return;
71
72 /* Disables landlock events and clears PID filter. */
73 set_cap(_metadata, CAP_SYS_ADMIN);
74 tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
75 tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false);
76 tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false);
77 tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false);
78 tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, false);
79 tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
80 tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false);
81 tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
82 tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
83 tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false);
84 tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false);
85 tracefs_clear_pid_filter();
86 clear_cap(_metadata, CAP_SYS_ADMIN);
87
88 /*
89 * The mount namespace is cleaned up automatically when the test process
90 * (harness child) exits.
91 */
92 }
93
94 /*
95 * Verifies that no trace events are emitted when the tracepoints are disabled.
96 */
TEST_F(trace,no_trace_when_disabled)97 TEST_F(trace, no_trace_when_disabled)
98 {
99 char *buf;
100
101 /* Disable all landlock events. */
102 set_cap(_metadata, CAP_SYS_ADMIN);
103 ASSERT_EQ(0,
104 tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false));
105 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false));
106 ASSERT_EQ(0,
107 tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false));
108 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE,
109 false));
110 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE,
111 false));
112 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false));
113 ASSERT_EQ(0,
114 tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false));
115 ASSERT_EQ(0,
116 tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false));
117 ASSERT_EQ(0,
118 tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false));
119 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false));
120 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE,
121 false));
122 ASSERT_EQ(0, tracefs_enable_event(
123 TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
124 false));
125 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false));
126 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false));
127 ASSERT_EQ(0, tracefs_clear());
128 clear_cap(_metadata, CAP_SYS_ADMIN);
129
130 /*
131 * Trigger both allowed and denied accesses to verify neither check_rule
132 * nor check_access events fire when disabled.
133 */
134 sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
135 LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
136
137 /* Read trace buffer and verify no landlock events at all. */
138 buf = tracefs_read_buf();
139 ASSERT_NE(NULL, buf);
140
141 EXPECT_EQ(0, tracefs_count_matches(buf, "landlock_"))
142 {
143 TH_LOG("Expected 0 landlock events when disabled\n%s", buf);
144 }
145
146 free(buf);
147 }
148
149 /*
150 * Verifies that landlock_create_ruleset emits a trace event with the correct
151 * handled access masks.
152 */
TEST_F(trace,create_ruleset)153 TEST_F(trace, create_ruleset)
154 {
155 struct landlock_ruleset_attr ruleset_attr = {
156 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
157 .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
158 };
159 int ruleset_fd;
160 char *buf, *dot;
161 char field[64];
162
163 ruleset_fd =
164 landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
165 ASSERT_LE(0, ruleset_fd);
166 ASSERT_EQ(0, close(ruleset_fd));
167
168 buf = tracefs_read_buf();
169 ASSERT_NE(NULL, buf);
170
171 EXPECT_EQ(1,
172 tracefs_count_matches(buf, REGEX_CREATE_RULESET(TRACE_TASK)))
173 {
174 TH_LOG("Expected 1 create_ruleset event\n%s", buf);
175 }
176
177 /* Verify handled_fs matches what we requested. */
178 EXPECT_EQ(0,
179 tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
180 "handled_fs", field, sizeof(field)));
181 EXPECT_STREQ("read_file", field);
182
183 /* Verify handled_net matches. */
184 EXPECT_EQ(0,
185 tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
186 "handled_net", field, sizeof(field)));
187 EXPECT_STREQ("bind_tcp", field);
188
189 /* Verify version is 0 at creation (no rules added yet). */
190 EXPECT_EQ(0,
191 tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
192 "ruleset", field, sizeof(field)));
193 /* Format is <hex>.<dec>; version is after the dot. */
194 dot = strchr(field, '.');
195 ASSERT_NE(0, !!dot);
196 EXPECT_STREQ("0", dot + 1);
197
198 free(buf);
199 }
200
201 /*
202 * Verifies that the ruleset version increments with each add_rule call and that
203 * create_domain records the correct version.
204 */
TEST_F(trace,ruleset_version)205 TEST_F(trace, ruleset_version)
206 {
207 pid_t pid;
208 int status;
209 char *buf;
210 const char *dot;
211 char field[64];
212
213 ASSERT_EQ(0, tracefs_clear_buf());
214
215 pid = fork();
216 ASSERT_LE(0, pid);
217
218 if (pid == 0) {
219 struct landlock_ruleset_attr ruleset_attr = {
220 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
221 };
222 struct landlock_path_beneath_attr path_beneath = {
223 .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
224 };
225 int ruleset_fd;
226
227 ruleset_fd = landlock_create_ruleset(&ruleset_attr,
228 sizeof(ruleset_attr), 0);
229 if (ruleset_fd < 0)
230 _exit(1);
231
232 /* First rule: version becomes 1. */
233 path_beneath.parent_fd =
234 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
235 if (path_beneath.parent_fd < 0)
236 _exit(1);
237 landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
238 &path_beneath, 0);
239 close(path_beneath.parent_fd);
240
241 /* Second rule: version becomes 2. */
242 path_beneath.parent_fd =
243 open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
244 if (path_beneath.parent_fd < 0)
245 _exit(1);
246 landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
247 &path_beneath, 0);
248 close(path_beneath.parent_fd);
249
250 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
251 if (landlock_restrict_self(ruleset_fd, 0))
252 _exit(1);
253 close(ruleset_fd);
254 _exit(0);
255 }
256
257 ASSERT_EQ(pid, waitpid(pid, &status, 0));
258 ASSERT_TRUE(WIFEXITED(status));
259 EXPECT_EQ(0, WEXITSTATUS(status));
260
261 buf = tracefs_read_buf();
262 ASSERT_NE(NULL, buf);
263
264 /* Verify create_ruleset has version=0. */
265 ASSERT_EQ(0,
266 tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
267 "ruleset", field, sizeof(field)));
268 dot = strchr(field, '.');
269 ASSERT_NE(0, !!dot);
270 EXPECT_STREQ("0", dot + 1);
271
272 /* Verify two add_rule_path_beneath events were emitted. */
273 EXPECT_EQ(2, tracefs_count_matches(
274 buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)))
275 {
276 TH_LOG("Expected 2 add_rule_path_beneath events\n%s", buf);
277 }
278
279 /*
280 * Verify create_domain records version=2 (after 2 add_rule calls). The
281 * ruleset field format is <hex_id>.<dec_version>.
282 */
283 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
284 "ruleset", field, sizeof(field)));
285 dot = strchr(field, '.');
286 ASSERT_NE(0, !!dot);
287 EXPECT_STREQ("2", dot + 1);
288
289 free(buf);
290 }
291
292 /*
293 * Verifies that landlock_create_domain emits a trace event linking the ruleset
294 * ID to the new domain ID.
295 */
TEST_F(trace,create_domain)296 TEST_F(trace, create_domain)
297 {
298 pid_t pid;
299 int status, check_count;
300 char *buf;
301 char parent_id[64], domain_id[64], check_domain[64];
302
303 /* Clear before the sandboxed child. */
304 ASSERT_EQ(0, tracefs_clear_buf());
305
306 pid = fork();
307 ASSERT_LE(0, pid);
308
309 if (pid == 0) {
310 struct landlock_ruleset_attr ruleset_attr = {
311 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
312 };
313 struct landlock_path_beneath_attr path_beneath = {
314 .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
315 };
316 int ruleset_fd, fd;
317
318 ruleset_fd = landlock_create_ruleset(&ruleset_attr,
319 sizeof(ruleset_attr), 0);
320 if (ruleset_fd < 0)
321 _exit(1);
322
323 path_beneath.parent_fd =
324 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
325 if (path_beneath.parent_fd < 0)
326 _exit(1);
327
328 landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
329 &path_beneath, 0);
330 close(path_beneath.parent_fd);
331
332 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
333 if (landlock_restrict_self(ruleset_fd, 0))
334 _exit(1);
335 close(ruleset_fd);
336
337 /* Trigger a check_rule to verify domain_id correlation. */
338 fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
339 if (fd >= 0)
340 close(fd);
341
342 _exit(0);
343 }
344
345 ASSERT_EQ(pid, waitpid(pid, &status, 0));
346 ASSERT_TRUE(WIFEXITED(status));
347 EXPECT_EQ(0, WEXITSTATUS(status));
348
349 buf = tracefs_read_buf();
350 ASSERT_NE(NULL, buf);
351
352 /* Verify create_domain event exists. */
353 EXPECT_EQ(1,
354 tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
355 {
356 TH_LOG("Expected 1 create_domain event\n%s", buf);
357 }
358
359 /* Extract the domain ID from create_domain. */
360 EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
361 "domain", domain_id,
362 sizeof(domain_id)));
363
364 /* Verify domain ID is non-zero. */
365 EXPECT_NE(0, strcmp(domain_id, "0"));
366
367 /* Verify parent=0 (first restriction, no prior domain). */
368 EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
369 "parent", parent_id,
370 sizeof(parent_id)));
371 EXPECT_STREQ("0", parent_id);
372
373 /*
374 * Verify the same domain ID appears in the check_rule event, confirming
375 * end-to-end correlation.
376 */
377 check_count =
378 tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
379 ASSERT_LE(1, check_count)
380 {
381 TH_LOG("Expected check_rule_inode events\n%s", buf);
382 }
383
384 EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
385 "domain", check_domain,
386 sizeof(check_domain)));
387 EXPECT_STREQ(domain_id, check_domain);
388
389 free(buf);
390 }
391
392 /* Builds a rule-less scope-based ruleset; returns the fd or -1. */
build_enforce_ruleset(void)393 static int build_enforce_ruleset(void)
394 {
395 const struct landlock_ruleset_attr attr = {
396 .scoped = LANDLOCK_SCOPE_SIGNAL,
397 };
398
399 return landlock_create_ruleset(&attr, sizeof(attr), 0);
400 }
401
402 /*
403 * Verifies that nested landlock_restrict_self calls produce trace events with
404 * correct parent domain IDs: the second create_domain's parent should be the
405 * first domain's ID.
406 */
TEST_F(trace,create_domain_nested)407 TEST_F(trace, create_domain_nested)
408 {
409 pid_t pid;
410 int status;
411 char *buf;
412 const char *after_first;
413 char first_domain[64], first_parent[64], second_parent[64];
414
415 ASSERT_EQ(0, tracefs_clear_buf());
416
417 pid = fork();
418 ASSERT_LE(0, pid);
419
420 if (pid == 0) {
421 int ruleset_fd;
422
423 /* First restriction. */
424 ruleset_fd = build_enforce_ruleset();
425 if (ruleset_fd < 0)
426 _exit(1);
427 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
428 if (landlock_restrict_self(ruleset_fd, 0))
429 _exit(1);
430 close(ruleset_fd);
431
432 /* Second restriction (nested). */
433 ruleset_fd = build_enforce_ruleset();
434 if (ruleset_fd < 0)
435 _exit(1);
436 if (landlock_restrict_self(ruleset_fd, 0))
437 _exit(1);
438 close(ruleset_fd);
439
440 _exit(0);
441 }
442
443 ASSERT_EQ(pid, waitpid(pid, &status, 0));
444 ASSERT_TRUE(WIFEXITED(status));
445 EXPECT_EQ(0, WEXITSTATUS(status));
446
447 buf = tracefs_read_buf();
448 ASSERT_NE(NULL, buf);
449
450 /* Should have 2 create_domain events. */
451 EXPECT_EQ(2,
452 tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
453 {
454 TH_LOG("Expected 2 create_domain events\n%s", buf);
455 }
456
457 /*
458 * Extract domain and parent from each create_domain event. The first
459 * event (parent=0) is the outer domain; the second (parent!=0) is the
460 * nested domain whose parent should match the first domain's ID.
461 */
462 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
463 "domain", first_domain,
464 sizeof(first_domain)));
465 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
466 "parent", first_parent,
467 sizeof(first_parent)));
468 EXPECT_STREQ("0", first_parent);
469
470 /*
471 * Find the second create_domain by scanning past the first.
472 * tracefs_extract_field returns the first match, so search in the
473 * buffer after the first event.
474 *
475 * Skip past the first create_domain line. tracefs_extract_field matches
476 * the first line that matches the regex, so passing the buffer after
477 * the first matching line gives us the second event.
478 */
479 after_first = strstr(buf, "landlock_create_domain:");
480 ASSERT_NE(NULL, after_first);
481 after_first = strchr(after_first, '\n');
482 ASSERT_NE(NULL, after_first);
483
484 ASSERT_EQ(0, tracefs_extract_field(
485 after_first + 1, REGEX_CREATE_DOMAIN(TRACE_TASK),
486 "parent", second_parent, sizeof(second_parent)));
487
488 /* The second domain's parent should be the first domain's ID. */
489 EXPECT_STREQ(first_domain, second_parent);
490
491 free(buf);
492 }
493
494 /*
495 * Verifies that landlock_add_rule does not emit a trace event when the syscall
496 * fails (e.g., invalid ruleset fd).
497 */
TEST_F(trace,add_rule_invalid_fd)498 TEST_F(trace, add_rule_invalid_fd)
499 {
500 struct landlock_path_beneath_attr path_beneath = {
501 .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
502 };
503 char *buf;
504
505 path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
506 ASSERT_LE(0, path_beneath.parent_fd);
507
508 /* Invalid ruleset fd (-1). */
509 ASSERT_EQ(-1, landlock_add_rule(-1, LANDLOCK_RULE_PATH_BENEATH,
510 &path_beneath, 0));
511 ASSERT_EQ(0, close(path_beneath.parent_fd));
512
513 buf = tracefs_read_buf();
514 ASSERT_NE(NULL, buf);
515
516 EXPECT_EQ(0, tracefs_count_matches(
517 buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)))
518 {
519 TH_LOG("No add_rule_path_beneath event expected on invalid fd\n%s",
520 buf);
521 }
522
523 free(buf);
524 }
525
526 /*
527 * Verifies that landlock_create_domain does not emit a trace event when the
528 * syscall fails (e.g., invalid ruleset fd or unknown flags).
529 */
TEST_F(trace,create_domain_invalid)530 TEST_F(trace, create_domain_invalid)
531 {
532 int ruleset_fd;
533 char *buf;
534
535 ruleset_fd = build_enforce_ruleset();
536 ASSERT_LE(0, ruleset_fd);
537
538 /* Clear the trace buffer after create_ruleset event. */
539 ASSERT_EQ(0, tracefs_clear_buf());
540
541 /* Invalid fd. */
542 ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
543
544 /* Unknown flags. */
545 ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, -1));
546
547 ASSERT_EQ(0, close(ruleset_fd));
548
549 buf = tracefs_read_buf();
550 ASSERT_NE(NULL, buf);
551
552 EXPECT_EQ(0,
553 tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
554 {
555 TH_LOG("No create_domain event expected on error\n%s", buf);
556 }
557
558 free(buf);
559 }
560
561 /*
562 * Verifies that trace_landlock_free_domain fires when a domain is deallocated,
563 * with the correct denials count.
564 */
TEST_F(trace,free_domain)565 TEST_F(trace, free_domain)
566 {
567 char *buf;
568 int count;
569 char denials_field[32];
570
571 ASSERT_EQ(0, tracefs_clear_buf());
572
573 /*
574 * The domain is freed via a work queue (kworker), so the free_domain
575 * trace event is emitted from a different PID. Clear the PID filter
576 * BEFORE the child exits, so the kworker event passes the filter when
577 * it fires.
578 */
579 set_cap(_metadata, CAP_SYS_ADMIN);
580 tracefs_clear_pid_filter();
581 clear_cap(_metadata, CAP_SYS_ADMIN);
582
583 sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
584 LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
585
586 /*
587 * Wait for the deferred deallocation work to run. The domain is freed
588 * asynchronously from a kworker; poll until the event appears or a
589 * timeout is reached.
590 */
591 for (int retry = 0; retry < 10; retry++) {
592 usleep(100000);
593
594 set_cap(_metadata, CAP_SYS_ADMIN);
595 buf = tracefs_read_trace();
596 clear_cap(_metadata, CAP_SYS_ADMIN);
597 ASSERT_NE(NULL, buf);
598
599 count = tracefs_count_matches(buf,
600 REGEX_FREE_DOMAIN(KWORKER_TASK));
601 if (count >= 1)
602 break;
603 free(buf);
604 buf = NULL;
605 }
606
607 set_cap(_metadata, CAP_SYS_ADMIN);
608 ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
609 clear_cap(_metadata, CAP_SYS_ADMIN);
610
611 ASSERT_NE(NULL, buf);
612 EXPECT_LE(1, count)
613 {
614 TH_LOG("Expected free_domain event, got %d\n%s", count, buf);
615 }
616
617 /* Verify denials count matches the single denial we triggered. */
618 EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
619 "denials", denials_field,
620 sizeof(denials_field)));
621 EXPECT_STREQ("1", denials_field);
622
623 free(buf);
624 }
625
626 /*
627 * Verifies that deny_access_fs includes the enriched fields: same_exec and
628 * logged.
629 */
TEST_F(trace,deny_access_fs_fields)630 TEST_F(trace, deny_access_fs_fields)
631 {
632 char *buf;
633 char field_buf[64];
634
635 ASSERT_EQ(0, tracefs_clear_buf());
636
637 /* Trigger a denial: rule for /usr, access /tmp. */
638 sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
639 LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
640
641 buf = tracefs_read_buf();
642 ASSERT_NE(NULL, buf);
643
644 /* Verify the enriched fields are present and have valid values. */
645 ASSERT_EQ(0, tracefs_extract_field(
646 buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "same_exec",
647 field_buf, sizeof(field_buf)));
648 /* Child is the same exec that restricted itself. */
649 EXPECT_STREQ("1", field_buf);
650
651 /* Same exec with default flags: audit would log this denial. */
652 ASSERT_EQ(0, tracefs_extract_field(
653 buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "logged",
654 field_buf, sizeof(field_buf)));
655 EXPECT_STREQ("1", field_buf);
656
657 free(buf);
658 }
659
660 /*
661 * Verifies that same_exec is 1 (true) for denials from the same executable that
662 * called landlock_restrict_self().
663 */
TEST_F(trace,same_exec_before_exec)664 TEST_F(trace, same_exec_before_exec)
665 {
666 pid_t pid;
667 int status;
668 char *buf;
669 char field[64];
670
671 ASSERT_EQ(0, tracefs_clear_buf());
672
673 pid = fork();
674 ASSERT_LE(0, pid);
675
676 if (pid == 0) {
677 struct landlock_ruleset_attr attr = {
678 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
679 };
680 int ruleset_fd, dir_fd;
681
682 ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
683 if (ruleset_fd < 0)
684 _exit(1);
685
686 /* No rules: all read_dir access is denied. */
687 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
688 if (landlock_restrict_self(ruleset_fd, 0))
689 _exit(1);
690 close(ruleset_fd);
691
692 /* Trigger denial without exec (same executable). */
693 dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
694 if (dir_fd >= 0)
695 close(dir_fd);
696 _exit(0);
697 }
698
699 ASSERT_EQ(pid, waitpid(pid, &status, 0));
700 ASSERT_TRUE(WIFEXITED(status));
701 EXPECT_EQ(0, WEXITSTATUS(status));
702
703 buf = tracefs_read_buf();
704 ASSERT_NE(NULL, buf);
705
706 /* Should have at least one deny_access_fs denial. */
707 EXPECT_LE(1,
708 tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
709
710 /* Verify same_exec=1 (same executable, no exec). */
711 ASSERT_EQ(0,
712 tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
713 "same_exec", field, sizeof(field)));
714 EXPECT_STREQ("1", field);
715
716 /* Same exec with default flags: audit would log this denial. */
717 ASSERT_EQ(0,
718 tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
719 "logged", field, sizeof(field)));
720 EXPECT_STREQ("1", field);
721
722 free(buf);
723 }
724
725 /*
726 * Verifies that same_exec is 0 (false) for denials from a process that has
727 * exec'd a new binary after landlock_restrict_self(). The sandboxed child
728 * exec's true which opens "." and triggers a read_dir denial. Covers the
729 * "trace-only" visibility condition: with same_exec=0 and the default
730 * log_new_exec=0, audit suppresses the denial (logged=0) but the trace event
731 * still fires.
732 */
TEST_F(trace,same_exec_after_exec)733 TEST_F(trace, same_exec_after_exec)
734 {
735 char *buf;
736 char field[64];
737
738 ASSERT_EQ(0, tracefs_clear_buf());
739
740 sandbox_child_exec_true(_metadata, 0);
741
742 buf = tracefs_read_buf();
743 ASSERT_NE(NULL, buf);
744
745 EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
746
747 /* Verify same_exec=0 (different executable after exec). */
748 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
749 "same_exec", field, sizeof(field)));
750 EXPECT_STREQ("0", field);
751
752 /*
753 * same_exec=0 with default log_new_exec=0: audit suppresses (logged=0).
754 */
755 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
756 "logged", field, sizeof(field)));
757 EXPECT_STREQ("0", field);
758
759 free(buf);
760 }
761
762 /*
763 * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF suppresses logging
764 * (logged=0) for a denial from the same executable.
765 */
TEST_F(trace,log_flags_same_exec_off)766 TEST_F(trace, log_flags_same_exec_off)
767 {
768 pid_t pid;
769 int status;
770 char *buf;
771 char field[64];
772
773 ASSERT_EQ(0, tracefs_clear_buf());
774
775 pid = fork();
776 ASSERT_LE(0, pid);
777
778 if (pid == 0) {
779 struct landlock_ruleset_attr attr = {
780 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
781 };
782 int ruleset_fd, dir_fd;
783
784 ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
785 if (ruleset_fd < 0)
786 _exit(1);
787
788 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
789 if (landlock_restrict_self(
790 ruleset_fd,
791 LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF))
792 _exit(1);
793 close(ruleset_fd);
794
795 dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
796 if (dir_fd >= 0)
797 close(dir_fd);
798 _exit(0);
799 }
800
801 ASSERT_EQ(pid, waitpid(pid, &status, 0));
802 ASSERT_TRUE(WIFEXITED(status));
803 EXPECT_EQ(0, WEXITSTATUS(status));
804
805 buf = tracefs_read_buf();
806 ASSERT_NE(NULL, buf);
807
808 EXPECT_LE(1,
809 tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
810
811 /* Same-exec denial with LOG_SAME_EXEC_OFF: audit suppresses it. */
812 ASSERT_EQ(0,
813 tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
814 "logged", field, sizeof(field)));
815 EXPECT_STREQ("0", field);
816
817 free(buf);
818 }
819
820 /*
821 * Verifies that LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON causes a post-exec
822 * denial to be logged (logged=1). The child exec's true so that the denial
823 * comes from a new executable (same_exec=0).
824 */
TEST_F(trace,log_flags_new_exec_on)825 TEST_F(trace, log_flags_new_exec_on)
826 {
827 char *buf;
828 char field[64];
829
830 ASSERT_EQ(0, tracefs_clear_buf());
831
832 sandbox_child_exec_true(_metadata,
833 LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
834
835 buf = tracefs_read_buf();
836 ASSERT_NE(NULL, buf);
837
838 EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
839
840 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
841 "same_exec", field, sizeof(field)));
842 EXPECT_STREQ("0", field);
843
844 /* LOG_NEW_EXEC_ON: the post-exec denial (same_exec=0) is logged. */
845 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
846 "logged", field, sizeof(field)));
847 EXPECT_STREQ("1", field);
848
849 free(buf);
850 }
851
852 /*
853 * Verifies that denials suppressed by audit log flags are still counted in
854 * num_denials. The child restricts itself with default flags (log_same_exec=1,
855 * log_new_exec=0), then execs true which attempts to read a denied directory.
856 * After exec, same_exec=0 and log_new_exec=0, so audit suppresses the denial.
857 * But the trace event fires unconditionally and free_domain must report the
858 * correct denials count.
859 */
TEST_F(trace,non_audit_visible_denial_counting)860 TEST_F(trace, non_audit_visible_denial_counting)
861 {
862 char *buf = NULL;
863 char denials_field[32];
864 int count;
865
866 set_cap(_metadata, CAP_SYS_ADMIN);
867 ASSERT_EQ(0, tracefs_clear());
868 tracefs_clear_pid_filter();
869 clear_cap(_metadata, CAP_SYS_ADMIN);
870
871 sandbox_child_exec_true(_metadata, 0);
872
873 /* Wait for free_domain event with retry. */
874 for (int retry = 0; retry < 10; retry++) {
875 usleep(100000);
876
877 set_cap(_metadata, CAP_SYS_ADMIN);
878 buf = tracefs_read_trace();
879 clear_cap(_metadata, CAP_SYS_ADMIN);
880 if (!buf)
881 break;
882
883 count = tracefs_count_matches(buf,
884 REGEX_FREE_DOMAIN(KWORKER_TASK));
885 if (count >= 1)
886 break;
887 free(buf);
888 buf = NULL;
889 }
890
891 set_cap(_metadata, CAP_SYS_ADMIN);
892 ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
893 clear_cap(_metadata, CAP_SYS_ADMIN);
894
895 /*
896 * The denial happened after exec (same_exec=0), so audit would suppress
897 * it. But num_denials counts all denials regardless.
898 */
899 ASSERT_NE(NULL, buf)
900 {
901 TH_LOG("free_domain event not found after 10 retries");
902 }
903 EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
904 "denials", denials_field,
905 sizeof(denials_field)));
906 EXPECT_STREQ("1", denials_field);
907
908 free(buf);
909 }
910
911 /*
912 * Verifies that landlock_add_rule_net_port emits a trace event with the correct
913 * port and allowed access mask fields.
914 */
TEST_F(trace,add_rule_net_port_fields)915 TEST_F(trace, add_rule_net_port_fields)
916 {
917 struct landlock_ruleset_attr ruleset_attr = {
918 .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
919 };
920 struct landlock_net_port_attr net_port = {
921 .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
922 .port = 8080,
923 };
924 int ruleset_fd;
925 char *buf;
926 char field[64];
927
928 ruleset_fd =
929 landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
930 ASSERT_LE(0, ruleset_fd);
931
932 ASSERT_EQ(0, tracefs_clear_buf());
933
934 ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
935 &net_port, 0));
936 close(ruleset_fd);
937
938 buf = tracefs_read_buf();
939 ASSERT_NE(NULL, buf);
940
941 EXPECT_EQ(1, tracefs_count_matches(buf,
942 REGEX_ADD_RULE_NET_PORT(TRACE_TASK)))
943 {
944 TH_LOG("Expected 1 add_rule_net_port event\n%s", buf);
945 }
946
947 /*
948 * Verify the port is in host endianness, matching the UAPI convention
949 * (landlock_net_port_attr.port). On little-endian, htons(8080) is
950 * 36895, so this comparison catches byte-order bugs.
951 */
952 EXPECT_EQ(0, tracefs_extract_field(buf,
953 REGEX_ADD_RULE_NET_PORT(TRACE_TASK),
954 "port", field, sizeof(field)));
955 EXPECT_STREQ("8080", field);
956 /*
957 * The allowed mask is the absolute value after transformation: the
958 * user-requested BIND_TCP plus all unhandled access rights (the other
959 * net access bits are unhandled because the ruleset only handles
960 * BIND_TCP).
961 */
962 EXPECT_EQ(0, tracefs_extract_field(
963 buf, REGEX_ADD_RULE_NET_PORT(TRACE_TASK),
964 "access_rights", field, sizeof(field)));
965 EXPECT_STREQ("bind_tcp|connect_tcp|bind_udp|connect_send_udp", field);
966
967 free(buf);
968 }
969
970 /*
971 * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF suppresses audit
972 * logging for child domains (logged=0) even though the child's own
973 * per-execution flags are the defaults, while the trace event still fires
974 * (tracing is unconditional). The parent creates a domain with
975 * LOG_SUBDOMAINS_OFF, then the child creates a sub-domain and triggers a
976 * denial.
977 */
TEST_F(trace,log_flags_subdomains_off)978 TEST_F(trace, log_flags_subdomains_off)
979 {
980 pid_t pid;
981 int status;
982 char *buf;
983 char field[64];
984
985 ASSERT_EQ(0, tracefs_clear_buf());
986
987 pid = fork();
988 ASSERT_LE(0, pid);
989
990 if (pid == 0) {
991 struct landlock_ruleset_attr attr = {
992 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
993 };
994 int parent_fd, child_fd, dir_fd;
995
996 /* Parent domain with LOG_SUBDOMAINS_OFF. */
997 parent_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
998 if (parent_fd < 0)
999 _exit(1);
1000
1001 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
1002 if (landlock_restrict_self(
1003 parent_fd,
1004 LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
1005 _exit(1);
1006 close(parent_fd);
1007
1008 /* Child sub-domain with default flags. */
1009 child_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
1010 if (child_fd < 0)
1011 _exit(1);
1012
1013 if (landlock_restrict_self(child_fd, 0))
1014 _exit(1);
1015 close(child_fd);
1016
1017 /* Trigger a denial from the child domain. */
1018 dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
1019 if (dir_fd >= 0)
1020 close(dir_fd);
1021 _exit(0);
1022 }
1023
1024 ASSERT_EQ(pid, waitpid(pid, &status, 0));
1025 ASSERT_TRUE(WIFEXITED(status));
1026 EXPECT_EQ(0, WEXITSTATUS(status));
1027
1028 buf = tracefs_read_buf();
1029 ASSERT_NE(NULL, buf);
1030
1031 /*
1032 * Trace fires unconditionally even though audit is disabled for the
1033 * child domain (parent had LOG_SUBDOMAINS_OFF).
1034 */
1035 EXPECT_LE(1,
1036 tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)))
1037 {
1038 TH_LOG("Expected deny_access_fs event despite "
1039 "LOG_SUBDOMAINS_OFF\n%s",
1040 buf);
1041 }
1042
1043 /*
1044 * The child's per-execution flags default to logging, but the
1045 * ancestor's LOG_SUBDOMAINS_OFF disables it, so audit suppresses this
1046 * denial (logged=0). This is exactly the case the single logged field
1047 * captures and the raw per-execution flags could not.
1048 */
1049 ASSERT_EQ(0,
1050 tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
1051 "logged", field, sizeof(field)));
1052 EXPECT_STREQ("0", field);
1053
1054 free(buf);
1055 }
1056
1057 /* Verifies that landlock_free_ruleset fires when a ruleset FD is closed. */
TEST_F(trace,free_ruleset_on_close)1058 TEST_F(trace, free_ruleset_on_close)
1059 {
1060 struct landlock_ruleset_attr ruleset_attr = {
1061 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
1062 };
1063 int ruleset_fd;
1064 char *buf;
1065
1066 ruleset_fd =
1067 landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
1068 ASSERT_LE(0, ruleset_fd);
1069
1070 ASSERT_EQ(0, tracefs_clear_buf());
1071
1072 /* Closing the FD should trigger free_ruleset. */
1073 close(ruleset_fd);
1074
1075 buf = tracefs_read_buf();
1076 ASSERT_NE(NULL, buf);
1077
1078 EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_FREE_RULESET(TRACE_TASK)))
1079 {
1080 TH_LOG("Expected 1 free_ruleset event\n%s", buf);
1081 }
1082
1083 free(buf);
1084 }
1085
1086 /*
1087 * Counts landlock_enforce_domain lines, filtered by @domain (NULL matches any),
1088 * @complete and @process_wide (a negative value matches any). Builds the
1089 * anchored regex dynamically so a single helper covers every field assertion.
1090 */
count_enforce_matches(const char * buf,const char * domain,int complete,int process_wide,int no_new_privs)1091 static int count_enforce_matches(const char *buf, const char *domain,
1092 int complete, int process_wide,
1093 int no_new_privs)
1094 {
1095 char pattern[512], dom[80], comp[8], pw[8], nnp[8];
1096
1097 if (domain)
1098 snprintf(dom, sizeof(dom), "%s", domain);
1099 else
1100 snprintf(dom, sizeof(dom), "[0-9a-f]\\+");
1101 if (complete < 0)
1102 snprintf(comp, sizeof(comp), "[01]");
1103 else
1104 snprintf(comp, sizeof(comp), "%d", complete);
1105 if (process_wide < 0)
1106 snprintf(pw, sizeof(pw), "[01]");
1107 else
1108 snprintf(pw, sizeof(pw), "%d", process_wide);
1109 if (no_new_privs < 0)
1110 snprintf(nnp, sizeof(nnp), "[01]");
1111 else
1112 snprintf(nnp, sizeof(nnp), "%d", no_new_privs);
1113
1114 snprintf(pattern, sizeof(pattern),
1115 TRACE_PREFIX(TRACE_TASK) "landlock_enforce_domain: "
1116 "domain=%s "
1117 "complete=%s process_wide=%s "
1118 "no_new_privs=%s$",
1119 dom, comp, pw, nnp);
1120 return tracefs_count_matches(buf, pattern);
1121 }
1122
1123 /* Idle sibling: waits on the barrier so it is a live thread, then sleeps. */
enforce_idle(void * arg)1124 static void *enforce_idle(void *arg)
1125 {
1126 pthread_barrier_t *barrier = arg;
1127
1128 pthread_barrier_wait(barrier);
1129 while (true)
1130 sleep(1);
1131 return NULL;
1132 }
1133
1134 /*
1135 * Child body: spawns @nthreads idle siblings (barrier-synchronized so they are
1136 * live when the syscall runs), then enforces a domain with @flags. Returns 0
1137 * on success; the process exits afterwards, reaping the siblings.
1138 */
child_enforce(int nthreads,__u32 flags)1139 static int child_enforce(int nthreads, __u32 flags)
1140 {
1141 pthread_t threads[8];
1142 pthread_barrier_t barrier;
1143 int ruleset_fd, i;
1144
1145 if (nthreads > 0) {
1146 if (pthread_barrier_init(&barrier, NULL, nthreads + 1))
1147 return 1;
1148 for (i = 0; i < nthreads; i++)
1149 if (pthread_create(&threads[i], NULL, enforce_idle,
1150 &barrier))
1151 return 1;
1152 pthread_barrier_wait(&barrier);
1153 }
1154
1155 ruleset_fd = build_enforce_ruleset();
1156 if (ruleset_fd < 0)
1157 return 1;
1158
1159 /*
1160 * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS sets no_new_privs itself, so skip
1161 * the prctl() to exercise that path; otherwise Landlock requires
1162 * no_new_privs up front.
1163 */
1164 if (!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS))
1165 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
1166 if (landlock_restrict_self(ruleset_fd, flags))
1167 return 1;
1168 close(ruleset_fd);
1169 return 0;
1170 }
1171
1172 /*
1173 * Runs in a spawned thread after the group leader called pthread_exit(). The
1174 * leader lingers as an un-reaped zombie, so get_nr_threads() still counts it
1175 * and this non-leader is not the only thread; enforcing here therefore reports
1176 * process_wide=0.
1177 */
enforce_nonleader(void * arg)1178 static void *enforce_nonleader(void *arg)
1179 {
1180 int ruleset_fd;
1181
1182 ruleset_fd = build_enforce_ruleset();
1183 if (ruleset_fd < 0)
1184 _exit(1);
1185 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
1186 if (landlock_restrict_self(ruleset_fd, 0))
1187 _exit(1);
1188 _exit(0);
1189 }
1190
1191 /*
1192 * Collapses the enforce_domain field cases into one parametrized test. Each
1193 * variant runs child_enforce(nthreads, flags) and checks the resulting
1194 * enforce_domain events. The flags column also selects how no_new_privs is
1195 * set: with LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS child_enforce() skips the
1196 * prctl() so the flag sets it (and, with TSYNC, propagates to the siblings);
1197 * otherwise a prior prctl() sets it on the caller (and TSYNC propagates that).
1198 */
1199
1200 /* clang-format off */
FIXTURE(trace_enforce)1201 FIXTURE(trace_enforce) {
1202 /* clang-format on */
1203 int tracefs_ok;
1204 };
1205
FIXTURE_SETUP(trace_enforce)1206 FIXTURE_SETUP(trace_enforce)
1207 {
1208 int ret;
1209
1210 set_cap(_metadata, CAP_SYS_ADMIN);
1211 ASSERT_EQ(0, unshare(CLONE_NEWNS));
1212 ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
1213
1214 ret = tracefs_fixture_setup();
1215 if (ret) {
1216 clear_cap(_metadata, CAP_SYS_ADMIN);
1217 self->tracefs_ok = 0;
1218 SKIP(return, "tracefs not available");
1219 }
1220 self->tracefs_ok = 1;
1221
1222 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
1223 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true));
1224 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, true));
1225 ASSERT_EQ(0, tracefs_clear());
1226 clear_cap(_metadata, CAP_SYS_ADMIN);
1227 }
1228
FIXTURE_TEARDOWN(trace_enforce)1229 FIXTURE_TEARDOWN(trace_enforce)
1230 {
1231 if (!self->tracefs_ok)
1232 return;
1233
1234 set_cap(_metadata, CAP_SYS_ADMIN);
1235 tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
1236 tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false);
1237 tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false);
1238 tracefs_fixture_teardown();
1239 clear_cap(_metadata, CAP_SYS_ADMIN);
1240 }
1241
1242 /* clang-format off */
FIXTURE_VARIANT(trace_enforce)1243 FIXTURE_VARIANT(trace_enforce) {
1244 /* clang-format on */
1245 /* Inputs to child_enforce(). */
1246 int nthreads;
1247 __u32 flags;
1248 /* Expected enforce_domain event counts. */
1249 int total;
1250 int complete;
1251 int process_wide;
1252 int no_new_privs;
1253 };
1254
1255 /* clang-format off */
1256
1257 /* Single thread, no flags: prctl-backed no_new_privs. */
FIXTURE_VARIANT_ADD(trace_enforce,single)1258 FIXTURE_VARIANT_ADD(trace_enforce, single) {
1259 .nthreads = 0, .flags = 0,
1260 .total = 1, .complete = 1, .process_wide = 1, .no_new_privs = 1,
1261 };
1262
1263 /* Single thread: the NO_NEW_PRIVS flag sets no_new_privs (no prctl). */
FIXTURE_VARIANT_ADD(trace_enforce,no_new_privs)1264 FIXTURE_VARIANT_ADD(trace_enforce, no_new_privs) {
1265 .nthreads = 0, .flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
1266 .total = 1, .complete = 1, .process_wide = 1, .no_new_privs = 1,
1267 };
1268
1269 /* TSYNC on a lone thread still concludes, process-wide. */
FIXTURE_VARIANT_ADD(trace_enforce,tsync_single)1270 FIXTURE_VARIANT_ADD(trace_enforce, tsync_single) {
1271 .nthreads = 0, .flags = LANDLOCK_RESTRICT_SELF_TSYNC,
1272 .total = 1, .complete = 1, .process_wide = 1, .no_new_privs = 1,
1273 };
1274
1275 /* TSYNC sweeps N siblings; the caller's prctl-backed nnp propagates to all. */
FIXTURE_VARIANT_ADD(trace_enforce,tsync_multithread)1276 FIXTURE_VARIANT_ADD(trace_enforce, tsync_multithread) {
1277 .nthreads = 3, .flags = LANDLOCK_RESTRICT_SELF_TSYNC,
1278 .total = 4, .complete = 1, .process_wide = 4, .no_new_privs = 4,
1279 };
1280
1281 /* TSYNC + NO_NEW_PRIVS flag sets nnp on the caller and every swept sibling. */
FIXTURE_VARIANT_ADD(trace_enforce,tsync_no_new_privs)1282 FIXTURE_VARIANT_ADD(trace_enforce, tsync_no_new_privs) {
1283 .nthreads = 3,
1284 .flags = LANDLOCK_RESTRICT_SELF_TSYNC | LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
1285 .total = 4, .complete = 1, .process_wide = 4, .no_new_privs = 4,
1286 };
1287
1288 /* Non-TSYNC on a multi-threaded process enforces only the caller. */
FIXTURE_VARIANT_ADD(trace_enforce,multithread_non_tsync)1289 FIXTURE_VARIANT_ADD(trace_enforce, multithread_non_tsync) {
1290 .nthreads = 3, .flags = 0,
1291 .total = 1, .complete = 1, .process_wide = 0, .no_new_privs = 1,
1292 };
1293
1294 /* clang-format on */
1295
1296 /*
1297 * One create_domain and variant->total enforce_domain events sharing that
1298 * domain ID; complete=1 marks the single concluding event, and the process_wide
1299 * / no_new_privs counts match the variant. Counts are order-independent,
1300 * evaluated after the syscall returns.
1301 */
TEST_F(trace_enforce,enforce)1302 TEST_F(trace_enforce, enforce)
1303 {
1304 pid_t pid;
1305 int status;
1306 char *buf;
1307 char domain[64];
1308
1309 ASSERT_EQ(0, tracefs_clear_buf());
1310
1311 pid = fork();
1312 ASSERT_LE(0, pid);
1313 if (pid == 0)
1314 _exit(child_enforce(variant->nthreads, variant->flags));
1315
1316 ASSERT_EQ(pid, waitpid(pid, &status, 0));
1317 ASSERT_TRUE(WIFEXITED(status));
1318 EXPECT_EQ(0, WEXITSTATUS(status));
1319
1320 buf = tracefs_read_buf();
1321 ASSERT_NE(NULL, buf);
1322
1323 EXPECT_EQ(1,
1324 tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
1325 EXPECT_EQ(variant->total, count_enforce_matches(buf, NULL, -1, -1, -1))
1326 {
1327 TH_LOG("Expected %d enforce_domain events\n%s", variant->total,
1328 buf);
1329 }
1330 EXPECT_EQ(variant->complete,
1331 count_enforce_matches(buf, NULL, 1, -1, -1));
1332 EXPECT_EQ(variant->total - variant->complete,
1333 count_enforce_matches(buf, NULL, 0, -1, -1));
1334 EXPECT_EQ(variant->process_wide,
1335 count_enforce_matches(buf, NULL, -1, 1, -1));
1336 EXPECT_EQ(variant->total - variant->process_wide,
1337 count_enforce_matches(buf, NULL, -1, 0, -1));
1338 EXPECT_EQ(variant->no_new_privs,
1339 count_enforce_matches(buf, NULL, -1, -1, 1));
1340
1341 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
1342 "domain", domain, sizeof(domain)));
1343 EXPECT_EQ(variant->total,
1344 count_enforce_matches(buf, domain, -1, -1, -1));
1345
1346 free(buf);
1347 }
1348
1349 /*
1350 * A non-leader thread enforcing a domain while the group leader lingers as an
1351 * un-reaped zombie reports process_wide=0: get_nr_threads() counts the zombie
1352 * leader, so the group is not single-threaded. This is the reachable half of
1353 * the caveat that process_wide==0 never proves the process is multi-threaded
1354 * (get_nr_threads(), unlike the leader-relative thread_group_empty(), counts
1355 * the zombie leader).
1356 */
TEST_F(trace,enforce_single_non_leader)1357 TEST_F(trace, enforce_single_non_leader)
1358 {
1359 pid_t pid;
1360 int status;
1361 char *buf;
1362
1363 ASSERT_EQ(0, tracefs_clear_buf());
1364
1365 pid = fork();
1366 ASSERT_LE(0, pid);
1367 if (pid == 0) {
1368 pthread_t worker;
1369
1370 if (pthread_create(&worker, NULL, enforce_nonleader, NULL))
1371 _exit(1);
1372 /* Leader leaves; the worker enforces as a non-leader. */
1373 pthread_exit(NULL);
1374 }
1375
1376 ASSERT_EQ(pid, waitpid(pid, &status, 0));
1377 ASSERT_TRUE(WIFEXITED(status));
1378 EXPECT_EQ(0, WEXITSTATUS(status));
1379
1380 buf = tracefs_read_buf();
1381 ASSERT_NE(NULL, buf);
1382
1383 EXPECT_EQ(1,
1384 tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
1385 EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, 0, -1))
1386 {
1387 TH_LOG("Expected complete=1 process_wide=0 for non-leader\n%s",
1388 buf);
1389 }
1390
1391 free(buf);
1392 }
1393
1394 /*
1395 * Verifies the flags-only path (ruleset_fd == -1) creates no domain and emits
1396 * neither create_domain nor enforce_domain, with and without TSYNC.
1397 */
TEST_F(trace,enforce_flags_only)1398 TEST_F(trace, enforce_flags_only)
1399 {
1400 pid_t pid;
1401 int status;
1402 char *buf;
1403
1404 ASSERT_EQ(0, tracefs_clear_buf());
1405
1406 pid = fork();
1407 ASSERT_LE(0, pid);
1408 if (pid == 0) {
1409 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
1410 if (landlock_restrict_self(
1411 -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
1412 _exit(1);
1413 if (landlock_restrict_self(
1414 -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
1415 LANDLOCK_RESTRICT_SELF_TSYNC))
1416 _exit(1);
1417 _exit(0);
1418 }
1419
1420 ASSERT_EQ(pid, waitpid(pid, &status, 0));
1421 ASSERT_TRUE(WIFEXITED(status));
1422 EXPECT_EQ(0, WEXITSTATUS(status));
1423
1424 buf = tracefs_read_buf();
1425 ASSERT_NE(NULL, buf);
1426
1427 EXPECT_EQ(0,
1428 tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
1429 EXPECT_EQ(0, count_enforce_matches(buf, NULL, -1, -1, -1))
1430 {
1431 TH_LOG("No enforce_domain expected on flags-only path\n%s",
1432 buf);
1433 }
1434
1435 free(buf);
1436 }
1437
enforce_nop_handler(int sig)1438 static void enforce_nop_handler(int sig)
1439 {
1440 }
1441
1442 struct abort_signaler_data {
1443 pthread_t target;
1444 volatile bool stop;
1445 };
1446
1447 /*
1448 * Hammers the target thread with SIGUSR1 to interrupt the TSYNC prepare wait.
1449 */
abort_signaler(void * arg)1450 static void *abort_signaler(void *arg)
1451 {
1452 struct abort_signaler_data *data = arg;
1453
1454 while (!data->stop)
1455 pthread_kill(data->target, SIGUSR1);
1456 return NULL;
1457 }
1458
1459 /*
1460 * Child body for the abort test: with idle siblings and a signaler interrupting
1461 * it, repeatedly enforces under TSYNC. An interrupted attempt aborts its
1462 * just-created domain (create_domain + free_domain, zero enforce_domain) while
1463 * -ERESTARTNOINTR transparently restarts the syscall, so a successful retry may
1464 * add its own full lifecycle.
1465 */
child_abort(int nsiblings,int attempts)1466 static int child_abort(int nsiblings, int attempts)
1467 {
1468 pthread_t threads[200];
1469 pthread_t signaler;
1470 pthread_barrier_t barrier;
1471 struct abort_signaler_data data = {};
1472 struct sigaction sa = {};
1473 int i;
1474
1475 sa.sa_handler = enforce_nop_handler;
1476 if (sigaction(SIGUSR1, &sa, NULL))
1477 return 1;
1478
1479 if (pthread_barrier_init(&barrier, NULL, nsiblings + 1))
1480 return 1;
1481 for (i = 0; i < nsiblings; i++)
1482 if (pthread_create(&threads[i], NULL, enforce_idle, &barrier))
1483 return 1;
1484 pthread_barrier_wait(&barrier);
1485
1486 data.target = pthread_self();
1487 if (pthread_create(&signaler, NULL, abort_signaler, &data))
1488 return 1;
1489
1490 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
1491 for (i = 0; i < attempts; i++) {
1492 int ruleset_fd = build_enforce_ruleset();
1493
1494 if (ruleset_fd < 0)
1495 break;
1496 /*
1497 * Ignore the result: an abort returns an error, that is fine.
1498 */
1499 landlock_restrict_self(ruleset_fd,
1500 LANDLOCK_RESTRICT_SELF_TSYNC);
1501 close(ruleset_fd);
1502 }
1503
1504 data.stop = true;
1505 pthread_join(signaler, NULL);
1506 return 0;
1507 }
1508
1509 /*
1510 * Verifies the abort contract: a domain aborted by a thread-sync failure emits
1511 * create_domain and free_domain but zero enforce_domain. The signal race is
1512 * probabilistic and -ERESTARTNOINTR may add a successful retry's lifecycle, so
1513 * events are grouped by domain ID and the test SKIPs if no abort occurred.
1514 */
TEST_F(trace,enforce_abort)1515 TEST_F(trace, enforce_abort)
1516 {
1517 pid_t pid;
1518 int status, retry;
1519 char *buf = NULL;
1520 const char *cursor;
1521 char domain[64];
1522 bool abort_found = false;
1523
1524 ASSERT_EQ(0, tracefs_clear_buf());
1525
1526 /* free_domain fires from a kworker, so widen the filter first. */
1527 set_cap(_metadata, CAP_SYS_ADMIN);
1528 tracefs_clear_pid_filter();
1529 clear_cap(_metadata, CAP_SYS_ADMIN);
1530
1531 pid = fork();
1532 ASSERT_LE(0, pid);
1533 if (pid == 0)
1534 /*
1535 * Match tsync_test's NUM_IDLE_THREADS: enough siblings that
1536 * credential preparation runs in several serialized waves,
1537 * giving the signaler a window to interrupt the thread-sync
1538 * wait and abort the operation. A handful of threads finishes
1539 * in a single wave, leaving no window (the abort never fires).
1540 */
1541 _exit(child_abort(200, 8));
1542
1543 ASSERT_EQ(pid, waitpid(pid, &status, 0));
1544 ASSERT_TRUE(WIFEXITED(status));
1545 EXPECT_EQ(0, WEXITSTATUS(status));
1546
1547 /* Poll for the asynchronous free_domain events. */
1548 for (retry = 0; retry < 10; retry++) {
1549 usleep(100000);
1550 set_cap(_metadata, CAP_SYS_ADMIN);
1551 free(buf);
1552 buf = tracefs_read_trace();
1553 clear_cap(_metadata, CAP_SYS_ADMIN);
1554 ASSERT_NE(NULL, buf);
1555 }
1556
1557 set_cap(_metadata, CAP_SYS_ADMIN);
1558 ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
1559 clear_cap(_metadata, CAP_SYS_ADMIN);
1560
1561 /*
1562 * Walk every create_domain and look for one whose domain ID has zero
1563 * enforce_domain events but a matching free_domain: that is an aborted
1564 * domain (created, never enforced, freed).
1565 */
1566 cursor = buf;
1567 while (tracefs_extract_field(cursor, REGEX_CREATE_DOMAIN(TRACE_TASK),
1568 "domain", domain, sizeof(domain)) == 0) {
1569 const char *cd, *nl;
1570 char free_pattern[256];
1571
1572 if (count_enforce_matches(buf, domain, -1, -1, -1) == 0) {
1573 snprintf(
1574 free_pattern, sizeof(free_pattern),
1575 TRACE_PREFIX(
1576 KWORKER_TASK) "landlock_free_domain: "
1577 "domain=%s denials=[0-9]\\+$",
1578 domain);
1579 if (tracefs_count_matches(buf, free_pattern) >= 1)
1580 abort_found = true;
1581 }
1582
1583 cd = strstr(cursor, "landlock_create_domain:");
1584 if (!cd)
1585 break;
1586 nl = strchr(cd, '\n');
1587 if (!nl)
1588 break;
1589 cursor = nl + 1;
1590 }
1591
1592 if (!abort_found) {
1593 free(buf);
1594 SKIP(return, "signal race did not produce a thread-sync abort");
1595 }
1596
1597 free(buf);
1598 }
1599
1600 /*
1601 * The following tests are intentionally elided because the underlying kernel
1602 * mechanisms are already validated by audit tests:
1603 *
1604 * - Domain ID monotonicity: validated by audit_test.c:layers. The same
1605 * landlock_get_id_range() function serves both audit and trace.
1606 *
1607 * - Domain deallocation order (LIFO): validated by audit_test.c:layers. Trace
1608 * events fire from the same free_domain_work() code path.
1609 *
1610 * - Max-layer stacking (16 domains): validated by audit_test.c:layers.
1611 *
1612 * - IPv6 network tests: IPv6 hook dispatch uses the same
1613 * current_check_access_socket() as IPv4, validated by net_test.c:audit tests.
1614 *
1615 * - Per-access-right full matrix (all 16 FS rights): hook dispatch is validated
1616 * by fs_test.c:audit tests. Trace tests verify representative samples to
1617 * ensure bitmask encoding is correct.
1618 *
1619 * - Combined log flag variants (e.g., LOG_SUBDOMAINS_OFF + LOG_NEW_EXEC_ON):
1620 * individual flag tests above cover each flag's effect on trace fields. Flag
1621 * combination logic is validated by audit_test.c:audit_flags tests.
1622 *
1623 * - fs.refer multi-record denials and fs.change_topology (mount):
1624 * trace_denial() uses the same code path for all FS request types. The
1625 * DENTRY union member is validated by the deny_access_fs_fields
1626 * test. Audit tests in fs_test.c cover refer and mount denial specifics.
1627 */
1628
1629 TEST_HARNESS_MAIN
1630