xref: /linux/tools/testing/selftests/landlock/trace.h (revision 415f2044228cc8b948dc04e079aaa86a4d1aa1d3)
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(&regex, 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(&regex, 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(&regex);
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(&regex, 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(&regex, 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(&regex);
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(&regex);
414 			return 0;
415 		}
416 next:
417 		if (*end == '\n')
418 			line = end + 1;
419 		else
420 			break;
421 	}
422 
423 	regfree(&regex);
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