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