1 /* SPDX-License-Identifier: GPL-2.0 */
2 /*
3 * Landlock trace test helpers
4 *
5 * Copyright © 2026 Cloudflare, Inc.
6 */
7
8 #define _GNU_SOURCE
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <regex.h>
12 #include <stdbool.h>
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <sys/mount.h>
17 #include <sys/stat.h>
18 #include <unistd.h>
19
20 #include "kselftest_harness.h"
21
22 #define TRACEFS_ROOT "/sys/kernel/tracing"
23 #define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
24 #define TRACEFS_CREATE_RULESET_ENABLE \
25 TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
26 #define TRACEFS_CREATE_DOMAIN_ENABLE \
27 TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
28 #define TRACEFS_ENFORCE_DOMAIN_ENABLE \
29 TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable"
30 #define TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE \
31 TRACEFS_LANDLOCK_DIR "/landlock_add_rule_path_beneath/enable"
32 #define TRACEFS_ADD_RULE_NET_PORT_ENABLE \
33 TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net_port/enable"
34 #define TRACEFS_CHECK_RULE_FS_ENABLE \
35 TRACEFS_LANDLOCK_DIR "/landlock_check_rule_inode/enable"
36 #define TRACEFS_CHECK_RULE_NET_ENABLE \
37 TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net_port/enable"
38 #define TRACEFS_DENY_ACCESS_FS_ENABLE \
39 TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
40 #define TRACEFS_DENY_ACCESS_NET_ENABLE \
41 TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
42 #define TRACEFS_DENY_PTRACE_ENABLE \
43 TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
44 #define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
45 TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
46 #define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
47 TRACEFS_LANDLOCK_DIR \
48 "/landlock_deny_scope_abstract_unix_socket/enable"
49 #define TRACEFS_FREE_DOMAIN_ENABLE \
50 TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
51 #define TRACEFS_FREE_RULESET_ENABLE \
52 TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
53 #define TRACEFS_TRACE TRACEFS_ROOT "/trace"
54 #define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
55 #define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
56
57 #define TRACE_BUFFER_SIZE (64 * 1024)
58
59 /*
60 * Trace line prefix: matches the ftrace "trace" file format. Format: "
61 * <task>-<pid> [<cpu>] <flags> <timestamp>: "
62 *
63 * The task parameter must be a string literal truncated to 15 chars
64 * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The
65 * pattern accepts either the expected task name or "<...>" because the ftrace
66 * comm cache may evict short-lived processes (e.g., forked children that exit
67 * before the trace buffer is read).
68 *
69 * No unescaped '.' in any REGEX macro; literal dots use '\\.'.
70 */
71 #define TRACE_PREFIX(task) \
72 "^ *\\(<\\.\\.\\.>" \
73 "\\|" task "\\)" \
74 "-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
75
76 /*
77 * Task name for events emitted by kworker threads (e.g., free_domain fires from
78 * a work queue, not from the test process).
79 */
80 #define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
81
82 #define REGEX_ADD_RULE_PATH_BENEATH(task) \
83 TRACE_PREFIX(task) \
84 "landlock_add_rule_path_beneath: " \
85 "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
86 "access_rights=[a-z_|]* " \
87 "dev=[0-9]\\+:[0-9]\\+ " \
88 "ino=[0-9]\\+ " \
89 "path=[^ ]\\+$"
90
91 #define REGEX_ADD_RULE_NET_PORT(task) \
92 TRACE_PREFIX(task) \
93 "landlock_add_rule_net_port: " \
94 "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
95 "access_rights=[a-z_|]* " \
96 "port=[0-9]\\+$"
97
98 #define REGEX_CREATE_RULESET(task) \
99 TRACE_PREFIX(task) \
100 "landlock_create_ruleset: " \
101 "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
102 "handled_fs=[a-z_|]* " \
103 "handled_net=[a-z_|]* " \
104 "scoped=[a-z_|]*$"
105
106 #define REGEX_CREATE_DOMAIN(task) \
107 TRACE_PREFIX(task) \
108 "landlock_create_domain: " \
109 "domain=[0-9a-f]\\+ " \
110 "parent=[0-9a-f]\\+ " \
111 "ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
112
113 #define REGEX_CHECK_RULE_FS(task) \
114 TRACE_PREFIX(task) \
115 "landlock_check_rule_inode: " \
116 "domain=[0-9a-f]\\+ " \
117 "access_request=[a-z_|]* " \
118 "dev=[0-9]\\+:[0-9]\\+ " \
119 "ino=[0-9]\\+ " \
120 "grants={[a-z_|,]*}$"
121
122 #define REGEX_CHECK_RULE_NET(task) \
123 TRACE_PREFIX(task) \
124 "landlock_check_rule_net_port: " \
125 "domain=[0-9a-f]\\+ " \
126 "access_request=[a-z_|]* " \
127 "port=[0-9]\\+ " \
128 "grants={[a-z_|,]*}$"
129
130 #define REGEX_DENY_ACCESS_FS(task) \
131 TRACE_PREFIX(task) \
132 "landlock_deny_access_fs: " \
133 "domain=[0-9a-f]\\+ " \
134 "same_exec=[01] " \
135 "logged=[01] " \
136 "blockers=[a-z_|]* " \
137 "dev=[0-9]\\+:[0-9]\\+ " \
138 "ino=[0-9]\\+ " \
139 "path=[^ ]*$"
140
141 #define REGEX_DENY_ACCESS_NET(task) \
142 TRACE_PREFIX(task) \
143 "landlock_deny_access_net: " \
144 "domain=[0-9a-f]\\+ " \
145 "same_exec=[01] " \
146 "logged=[01] " \
147 "blockers=[a-z_|]* " \
148 "port=-\\?[0-9]\\+$"
149
150 #define REGEX_DENY_PTRACE(task) \
151 TRACE_PREFIX(task) \
152 "landlock_deny_ptrace: " \
153 "domain=[0-9a-f]\\+ " \
154 "same_exec=[01] " \
155 "logged=[01] " \
156 "tracee_domain=[0-9a-f]\\+ " \
157 "tracee_pid=[0-9]\\+ " \
158 "tracee_comm=[^ ]*$"
159
160 #define REGEX_DENY_SCOPE_SIGNAL(task) \
161 TRACE_PREFIX(task) \
162 "landlock_deny_scope_signal: " \
163 "domain=[0-9a-f]\\+ " \
164 "same_exec=[01] " \
165 "logged=[01] " \
166 "target_domain=[0-9a-f]\\+ " \
167 "target_pid=[0-9]\\+ " \
168 "target_comm=[^ ]*$"
169
170 #define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \
171 TRACE_PREFIX(task) \
172 "landlock_deny_scope_abstract_unix_socket: " \
173 "domain=[0-9a-f]\\+ " \
174 "same_exec=[01] " \
175 "logged=[01] " \
176 "peer_domain=[0-9a-f]\\+ " \
177 "peer_pid=[0-9]\\+ " \
178 "sun_path=[^ ]*$"
179
180 #define REGEX_FREE_DOMAIN(task) \
181 TRACE_PREFIX(task) \
182 "landlock_free_domain: " \
183 "domain=[0-9a-f]\\+ " \
184 "denials=[0-9]\\+$"
185
186 #define REGEX_FREE_RULESET(task) \
187 TRACE_PREFIX(task) \
188 "landlock_free_ruleset: " \
189 "ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
190
tracefs_write(const char * path,const char * value)191 static int __maybe_unused tracefs_write(const char *path, const char *value)
192 {
193 int fd;
194 ssize_t ret;
195 size_t len = strlen(value);
196
197 fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
198 if (fd < 0)
199 return -errno;
200
201 ret = write(fd, value, len);
202 close(fd);
203 if (ret < 0)
204 return -errno;
205 if ((size_t)ret != len)
206 return -EIO;
207
208 return 0;
209 }
210
tracefs_write_int(const char * path,int value)211 static int __maybe_unused tracefs_write_int(const char *path, int value)
212 {
213 char buf[32];
214
215 snprintf(buf, sizeof(buf), "%d", value);
216 return tracefs_write(path, buf);
217 }
218
tracefs_setup(void)219 static int __maybe_unused tracefs_setup(void)
220 {
221 struct stat st;
222
223 /* Mount tracefs if not already mounted. */
224 if (stat(TRACEFS_ROOT, &st) != 0) {
225 int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
226
227 if (ret)
228 return -errno;
229 }
230
231 /* Verify landlock events are available. */
232 if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
233 return -ENOENT;
234
235 return 0;
236 }
237
238 /*
239 * Set up PID-based event filtering so only events from the current process and
240 * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it
241 * prevents events from unrelated processes from polluting the trace buffer.
242 */
tracefs_set_pid_filter(pid_t pid)243 static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
244 {
245 int ret;
246
247 /* Enable event-fork so children inherit the PID filter. */
248 ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
249 if (ret)
250 return ret;
251
252 return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
253 }
254
255 /* Clear the PID filter to stop filtering by PID. */
tracefs_clear_pid_filter(void)256 static int __maybe_unused tracefs_clear_pid_filter(void)
257 {
258 return tracefs_write(TRACEFS_SET_EVENT_PID, "");
259 }
260
tracefs_enable_event(const char * enable_path,bool enable)261 static int __maybe_unused tracefs_enable_event(const char *enable_path,
262 bool enable)
263 {
264 return tracefs_write(enable_path, enable ? "1" : "0");
265 }
266
tracefs_clear(void)267 static int __maybe_unused tracefs_clear(void)
268 {
269 return tracefs_write(TRACEFS_TRACE, "");
270 }
271
272 /*
273 * Reads the trace buffer content into a newly allocated buffer. The caller is
274 * responsible for freeing the returned buffer. Returns NULL on error.
275 */
tracefs_read_trace(void)276 static char __maybe_unused *tracefs_read_trace(void)
277 {
278 char *buf;
279 int fd;
280 ssize_t total = 0, ret;
281
282 buf = malloc(TRACE_BUFFER_SIZE);
283 if (!buf)
284 return NULL;
285
286 fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
287 if (fd < 0) {
288 free(buf);
289 return NULL;
290 }
291
292 while (total < TRACE_BUFFER_SIZE - 1) {
293 ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
294 if (ret <= 0)
295 break;
296 total += ret;
297 }
298 close(fd);
299 buf[total] = '\0';
300 return buf;
301 }
302
303 /* Counts the number of lines in @buf matching the basic regex @pattern. */
tracefs_count_matches(const char * buf,const char * pattern)304 static int __maybe_unused tracefs_count_matches(const char *buf,
305 const char *pattern)
306 {
307 regex_t regex;
308 int count = 0;
309 const char *line, *end;
310
311 if (regcomp(®ex, pattern, 0) != 0)
312 return -EINVAL;
313
314 line = buf;
315 while (*line) {
316 end = strchr(line, '\n');
317 if (!end)
318 end = line + strlen(line);
319
320 /* Create a temporary NUL-terminated line. */
321 size_t len = end - line;
322 char *tmp = malloc(len + 1);
323
324 if (tmp) {
325 memcpy(tmp, line, len);
326 tmp[len] = '\0';
327 if (regexec(®ex, tmp, 0, NULL, 0) == 0)
328 count++;
329 free(tmp);
330 }
331
332 if (*end == '\n')
333 line = end + 1;
334 else
335 break;
336 }
337
338 regfree(®ex);
339 return count;
340 }
341
342 /*
343 * Extracts the value of a named field from a trace line in @buf. Searches for
344 * the first line matching @line_pattern, then extracts the value after
345 * "@field_name=" into @out. Stops at space or newline.
346 *
347 * Returns 0 on success, -ENOENT if no match.
348 */
tracefs_extract_field(const char * buf,const char * line_pattern,const char * field_name,char * out,size_t out_size)349 static int __maybe_unused tracefs_extract_field(const char *buf,
350 const char *line_pattern,
351 const char *field_name,
352 char *out, size_t out_size)
353 {
354 regex_t regex;
355 const char *line, *end;
356
357 if (regcomp(®ex, line_pattern, 0) != 0)
358 return -EINVAL;
359
360 line = buf;
361 while (*line) {
362 end = strchr(line, '\n');
363 if (!end)
364 end = line + strlen(line);
365
366 size_t len = end - line;
367 char *tmp = malloc(len + 1);
368
369 if (tmp) {
370 const char *field, *val_start;
371 size_t field_len, val_len;
372
373 memcpy(tmp, line, len);
374 tmp[len] = '\0';
375
376 if (regexec(®ex, tmp, 0, NULL, 0) != 0) {
377 free(tmp);
378 goto next;
379 }
380
381 /*
382 * Find "field_name=" in the line, ensuring a word
383 * boundary before the field name to avoid substring
384 * matches (e.g., "port" in "sport").
385 */
386 field_len = strlen(field_name);
387 field = tmp;
388 while ((field = strstr(field, field_name))) {
389 if (field[field_len] == '=' &&
390 (field == tmp || field[-1] == ' '))
391 break;
392 field++;
393 }
394 if (!field) {
395 free(tmp);
396 regfree(®ex);
397 return -ENOENT;
398 }
399
400 val_start = field + field_len + 1;
401 val_len = 0;
402 while (val_start[val_len] &&
403 val_start[val_len] != ' ' &&
404 val_start[val_len] != '\n')
405 val_len++;
406
407 if (val_len >= out_size)
408 val_len = out_size - 1;
409 memcpy(out, val_start, val_len);
410 out[val_len] = '\0';
411
412 free(tmp);
413 regfree(®ex);
414 return 0;
415 }
416 next:
417 if (*end == '\n')
418 line = end + 1;
419 else
420 break;
421 }
422
423 regfree(®ex);
424 return -ENOENT;
425 }
426
427 /*
428 * Common fixture setup for trace tests. Mounts tracefs if needed and sets a
429 * PID filter. The caller must create a mount namespace first
430 * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs
431 * mount; the trace buffer, per-event enable flags, and PID filter are global
432 * kernel state, scoped to the test by the PID filter.
433 *
434 * Returns 0 on success, -errno on failure (caller should SKIP).
435 */
tracefs_fixture_setup(void)436 static int __maybe_unused tracefs_fixture_setup(void)
437 {
438 int ret;
439
440 ret = tracefs_setup();
441 if (ret)
442 return ret;
443
444 return tracefs_set_pid_filter(getpid());
445 }
446
tracefs_fixture_teardown(void)447 static void __maybe_unused tracefs_fixture_teardown(void)
448 {
449 tracefs_clear_pid_filter();
450 }
451
452 /*
453 * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
454 * drops the capability. Returns the value from @func, or -EPERM if the
455 * capability manipulation fails.
456 */
tracefs_priv_call(int (* func)(void))457 static int __maybe_unused tracefs_priv_call(int (*func)(void))
458 {
459 const cap_value_t admin = CAP_SYS_ADMIN;
460 cap_t cap_p;
461 int ret;
462
463 cap_p = cap_get_proc();
464 if (!cap_p)
465 return -EPERM;
466
467 if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
468 cap_set_proc(cap_p)) {
469 cap_free(cap_p);
470 return -EPERM;
471 }
472
473 ret = func();
474
475 cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
476 cap_set_proc(cap_p);
477 cap_free(cap_p);
478 return ret;
479 }
480
481 /* Read the trace buffer with elevated privileges. Returns NULL on failure. */
tracefs_read_buf(void)482 static char __maybe_unused *tracefs_read_buf(void)
483 {
484 /* Cannot use tracefs_priv_call() because the return type is char *. */
485 cap_t cap_p;
486 char *buf;
487 const cap_value_t admin = CAP_SYS_ADMIN;
488
489 cap_p = cap_get_proc();
490 if (!cap_p)
491 return NULL;
492
493 if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
494 cap_set_proc(cap_p)) {
495 cap_free(cap_p);
496 return NULL;
497 }
498
499 buf = tracefs_read_trace();
500
501 cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
502 cap_set_proc(cap_p);
503 cap_free(cap_p);
504 return buf;
505 }
506
507 /* Clear the trace buffer with elevated privileges. Returns 0 on success. */
tracefs_clear_buf(void)508 static int __maybe_unused tracefs_clear_buf(void)
509 {
510 return tracefs_priv_call(tracefs_clear);
511 }
512
513 /*
514 * Forks a child that creates a Landlock sandbox and performs an FS access. The
515 * parent waits for the child, then reads the trace buffer.
516 *
517 * Requires common.h and wrappers.h to be included before trace.h.
518 */
sandbox_child_fs_access(struct __test_metadata * const _metadata,const char * rule_path,__u64 handled_access,__u64 allowed_access,const char * access_path)519 static void __maybe_unused sandbox_child_fs_access(
520 struct __test_metadata *const _metadata, const char *rule_path,
521 __u64 handled_access, __u64 allowed_access, const char *access_path)
522 {
523 pid_t pid;
524 int status;
525
526 pid = fork();
527 ASSERT_LE(0, pid);
528
529 if (pid == 0) {
530 struct landlock_ruleset_attr ruleset_attr = {
531 .handled_access_fs = handled_access,
532 };
533 struct landlock_path_beneath_attr path_beneath = {
534 .allowed_access = allowed_access,
535 };
536 int ruleset_fd, fd;
537
538 ruleset_fd = landlock_create_ruleset(&ruleset_attr,
539 sizeof(ruleset_attr), 0);
540 if (ruleset_fd < 0)
541 _exit(1);
542
543 path_beneath.parent_fd =
544 open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
545 if (path_beneath.parent_fd < 0) {
546 close(ruleset_fd);
547 _exit(1);
548 }
549
550 if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
551 &path_beneath, 0)) {
552 close(path_beneath.parent_fd);
553 close(ruleset_fd);
554 _exit(1);
555 }
556 close(path_beneath.parent_fd);
557
558 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
559 if (landlock_restrict_self(ruleset_fd, 0)) {
560 close(ruleset_fd);
561 _exit(1);
562 }
563 close(ruleset_fd);
564
565 fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
566 if (fd >= 0)
567 close(fd);
568
569 _exit(0);
570 }
571
572 ASSERT_EQ(pid, waitpid(pid, &status, 0));
573 ASSERT_TRUE(WIFEXITED(status));
574 EXPECT_EQ(0, WEXITSTATUS(status));
575 }
576
577 /*
578 * Forks a child that creates a Landlock sandbox allowing execute+read_dir for
579 * /usr and execute-only for ".", then execs ./true. The true binary opens "."
580 * on startup, triggering a read_dir denial with same_exec=0. The parent waits
581 * for the child to exit.
582 */
sandbox_child_exec_true(struct __test_metadata * const _metadata,__u32 restrict_flags)583 static void __maybe_unused sandbox_child_exec_true(
584 struct __test_metadata *const _metadata, __u32 restrict_flags)
585 {
586 pid_t pid;
587 int status;
588
589 pid = fork();
590 ASSERT_LE(0, pid);
591
592 if (pid == 0) {
593 struct landlock_ruleset_attr attr = {
594 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
595 LANDLOCK_ACCESS_FS_EXECUTE,
596 };
597 struct landlock_path_beneath_attr path_beneath = {
598 .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
599 LANDLOCK_ACCESS_FS_READ_DIR,
600 };
601 int ruleset_fd;
602
603 ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
604 if (ruleset_fd < 0)
605 _exit(1);
606
607 path_beneath.parent_fd =
608 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
609 if (path_beneath.parent_fd >= 0) {
610 landlock_add_rule(ruleset_fd,
611 LANDLOCK_RULE_PATH_BENEATH,
612 &path_beneath, 0);
613 close(path_beneath.parent_fd);
614 }
615
616 path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
617 path_beneath.parent_fd =
618 open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
619 if (path_beneath.parent_fd >= 0) {
620 landlock_add_rule(ruleset_fd,
621 LANDLOCK_RULE_PATH_BENEATH,
622 &path_beneath, 0);
623 close(path_beneath.parent_fd);
624 }
625
626 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
627 if (landlock_restrict_self(ruleset_fd, restrict_flags))
628 _exit(1);
629 close(ruleset_fd);
630
631 execl("./true", "./true", NULL);
632 _exit(1);
633 }
634
635 ASSERT_EQ(pid, waitpid(pid, &status, 0));
636 ASSERT_TRUE(WIFEXITED(status));
637 EXPECT_EQ(0, WEXITSTATUS(status));
638 }
639