xref: /linux/tools/testing/selftests/landlock/trace_fs_test.c (revision d41d0021a6ea3e9fcd14126a00fead47f981c46e)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * Landlock tests - Filesystem tracepoints
4  *
5  * Copyright © 2026 Cloudflare, Inc.
6  */
7 
8 #define _GNU_SOURCE
9 #include <assert.h>
10 #include <errno.h>
11 #include <fcntl.h>
12 #include <limits.h>
13 #include <linux/landlock.h>
14 #include <sched.h>
15 #include <stdio.h>
16 #include <string.h>
17 #include <sys/mount.h>
18 #include <sys/stat.h>
19 #include <sys/types.h>
20 #include <sys/wait.h>
21 #include <unistd.h>
22 
23 #include "common.h"
24 #include "trace.h"
25 
26 #define TRACE_TASK "trace_fs_test"
27 
28 /* Mirrors TRACE_SEQ_SIZE, conservatively larger than the usable buffer. */
29 #define TRACE_SEQUENCE_SIZE 8192
30 #define OCTAL_ESCAPE_LEN 4
31 #define LONG_PATH_COMPONENT_COUNT 11
32 #define LONG_PATH_COMPONENT_LEN 240
33 #define LONG_PATH_LEN                                                \
34 	(LONG_PATH_COMPONENT_COUNT * (LONG_PATH_COMPONENT_LEN + 1) + \
35 	 sizeof("/tmp"))
36 #define LONG_ESCAPED_PATH_LEN \
37 	(LONG_PATH_COMPONENT_COUNT * LONG_PATH_COMPONENT_LEN * OCTAL_ESCAPE_LEN)
38 
39 static_assert(LONG_ESCAPED_PATH_LEN > TRACE_SEQUENCE_SIZE,
40 	      "escaped path must exceed the trace sequence");
41 static_assert(LONG_PATH_LEN < PATH_MAX, "path must fit in PATH_MAX");
42 
43 static void create_long_path(struct __test_metadata *const _metadata,
44 			     char *path)
45 {
46 	size_t path_len;
47 
48 	strcpy(path, "/tmp");
49 	path_len = strlen(path);
50 
51 	set_cap(_metadata, CAP_SYS_ADMIN);
52 	ASSERT_EQ(0, mount("tmpfs", "/tmp", "tmpfs", 0, NULL));
53 	clear_cap(_metadata, CAP_SYS_ADMIN);
54 
55 	for (int i = 0; i < LONG_PATH_COMPONENT_COUNT; i++) {
56 		path[path_len++] = '/';
57 		memset(path + path_len, ' ', LONG_PATH_COMPONENT_LEN);
58 		path_len += LONG_PATH_COMPONENT_LEN;
59 		path[path_len] = '\0';
60 		ASSERT_EQ(0, mkdir(path, 0700));
61 	}
62 }
63 
64 static void expect_truncated_path(struct __test_metadata *const _metadata,
65 				  const char *const trace,
66 				  const char *const event_regex)
67 {
68 	static const char marker[] = "\xe2\x80\xa6";
69 	char *path;
70 	size_t path_len;
71 
72 	path = malloc(TRACE_SEQUENCE_SIZE);
73 	ASSERT_NE(NULL, path);
74 	ASSERT_EQ(0, tracefs_extract_field(trace, event_regex, "path", path,
75 					   TRACE_SEQUENCE_SIZE));
76 	EXPECT_EQ(path, strstr(path, "/tmp/"));
77 	EXPECT_NE(NULL, strstr(path, "\\040"));
78 
79 	path_len = strlen(path);
80 	ASSERT_LE(sizeof(marker) - 1, path_len);
81 	EXPECT_STREQ(marker, path + path_len - (sizeof(marker) - 1));
82 	free(path);
83 }
84 
85 /*
86  * Like REGEX_DENY_ACCESS_FS(), but pins the logged field to a specific value
87  * ("0" or "1") so a test can tell a suppressed (quiet) denial from a logged
88  * one.  The tracepoint fires for every denial; logged carries the audit
89  * verdict.
90  */
91 #define REGEX_DENY_ACCESS_FS_LOGGED(task, log) \
92 	TRACE_PREFIX(task)                     \
93 	"landlock_deny_access_fs: "            \
94 	"domain=[0-9a-f]\\+ "                  \
95 	"same_exec=[01] "                      \
96 	"logged=" log " "                      \
97 	"blockers=[a-z_|]* "                   \
98 	"dev=[0-9]\\+:[0-9]\\+ "               \
99 	"ino=[0-9]\\+ "                        \
100 	"path=[^ ]*$"
101 
102 /* clang-format off */
103 FIXTURE(trace_fs) {
104 	/* clang-format on */
105 	int tracefs_ok;
106 };
107 
108 FIXTURE_SETUP(trace_fs)
109 {
110 	int ret;
111 
112 	set_cap(_metadata, CAP_SYS_ADMIN);
113 	ASSERT_EQ(0, unshare(CLONE_NEWNS));
114 	ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
115 
116 	ret = tracefs_fixture_setup();
117 	if (ret) {
118 		clear_cap(_metadata, CAP_SYS_ADMIN);
119 		self->tracefs_ok = 0;
120 		SKIP(return, "tracefs not available");
121 	}
122 	self->tracefs_ok = 1;
123 
124 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
125 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
126 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
127 	ASSERT_EQ(0, tracefs_clear());
128 	clear_cap(_metadata, CAP_SYS_ADMIN);
129 }
130 
131 FIXTURE_TEARDOWN(trace_fs)
132 {
133 	if (!self->tracefs_ok)
134 		return;
135 
136 	set_cap(_metadata, CAP_SYS_ADMIN);
137 	tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
138 	tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
139 	tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
140 	tracefs_fixture_teardown();
141 	clear_cap(_metadata, CAP_SYS_ADMIN);
142 }
143 
144 /*
145  * Baseline: verifies that without Landlock, the operation succeeds and no
146  * check_rule or deny_access trace events fire.
147  */
148 TEST_F(trace_fs, unsandboxed)
149 {
150 	char *buf;
151 	int count, status, fd;
152 	pid_t pid;
153 
154 	ASSERT_EQ(0, tracefs_clear_buf());
155 
156 	pid = fork();
157 	ASSERT_LE(0, pid);
158 
159 	if (pid == 0) {
160 		/*
161 		 * No sandbox: verify that a normal FS access does not produce
162 		 * Landlock trace events.
163 		 */
164 		fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
165 		if (fd >= 0)
166 			close(fd);
167 		_exit(0);
168 	}
169 
170 	ASSERT_EQ(pid, waitpid(pid, &status, 0));
171 	ASSERT_TRUE(WIFEXITED(status));
172 	EXPECT_EQ(0, WEXITSTATUS(status));
173 
174 	buf = tracefs_read_buf();
175 	ASSERT_NE(NULL, buf);
176 
177 	count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
178 	EXPECT_EQ(0, count);
179 	count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
180 	EXPECT_EQ(0, count);
181 
182 	free(buf);
183 }
184 
185 /*
186  * Verifies that adding a filesystem rule emits a landlock_add_rule_fs trace
187  * event with the expected path and field values: ruleset ID is non-zero,
188  * access_rights is non-zero, and path matches.
189  */
190 TEST_F(trace_fs, add_rule_fs)
191 {
192 	struct landlock_ruleset_attr ruleset_attr = {
193 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
194 				     LANDLOCK_ACCESS_FS_WRITE_FILE |
195 				     LANDLOCK_ACCESS_FS_READ_DIR,
196 	};
197 	struct landlock_path_beneath_attr path_beneath = {
198 		.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
199 	};
200 	char *buf, field_buf[64];
201 	int ruleset_fd, count;
202 
203 	ruleset_fd =
204 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
205 	ASSERT_LE(0, ruleset_fd);
206 
207 	path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
208 	ASSERT_LE(0, path_beneath.parent_fd);
209 
210 	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
211 				       &path_beneath, 0));
212 	ASSERT_EQ(0, close(path_beneath.parent_fd));
213 	ASSERT_EQ(0, close(ruleset_fd));
214 
215 	buf = tracefs_read_buf();
216 	ASSERT_NE(NULL, buf);
217 
218 	count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK));
219 	EXPECT_EQ(1, count)
220 	{
221 		TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
222 	}
223 
224 	/* Ruleset ID should be non-zero. */
225 	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
226 					   "ruleset", field_buf,
227 					   sizeof(field_buf)));
228 	EXPECT_STRNE("0", field_buf);
229 
230 	/* Access rights should be non-zero. */
231 	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
232 					   "access_rights", field_buf,
233 					   sizeof(field_buf)));
234 	EXPECT_STRNE("", field_buf);
235 
236 	/* Path should be /usr. */
237 	ASSERT_EQ(0,
238 		  tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
239 					"path", field_buf, sizeof(field_buf)));
240 	EXPECT_STREQ("/usr", field_buf);
241 
242 	free(buf);
243 }
244 
245 /*
246  * Verifies that a path whose escaping exceeds the trace scratch sequence does
247  * not corrupt a sibling symbolic field.
248  */
249 TEST_F(trace_fs, add_rule_fs_escaped_path_overflow)
250 {
251 	static const char access_prefix[] = "execute|write_file|read_file|";
252 	static const char access_suffix[] = "|ioctl_dev|resolve_unix";
253 	struct landlock_ruleset_attr ruleset_attr = {
254 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
255 	};
256 	struct landlock_path_beneath_attr path_beneath = {
257 		.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
258 	};
259 	char path[PATH_MAX];
260 	char *buf, field_buf[256];
261 	size_t field_len;
262 	int ruleset_fd, count;
263 
264 	create_long_path(_metadata, path);
265 
266 	ruleset_fd =
267 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
268 	ASSERT_LE(0, ruleset_fd);
269 	path_beneath.parent_fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC);
270 	ASSERT_LE(0, path_beneath.parent_fd);
271 
272 	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
273 				       &path_beneath, 0));
274 	ASSERT_EQ(0, close(path_beneath.parent_fd));
275 	ASSERT_EQ(0, close(ruleset_fd));
276 
277 	buf = tracefs_read_buf();
278 	ASSERT_NE(NULL, buf);
279 
280 	count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK));
281 	EXPECT_EQ(1, count)
282 	{
283 		TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
284 	}
285 
286 	/*
287 	 * The marker catches a full revert with any compiler.  The symbolic
288 	 * field also catches scratch-sequence poisoning when the compiler
289 	 * evaluates the overflowing path first, as GCC currently does.
290 	 */
291 	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
292 					   "access_rights", field_buf,
293 					   sizeof(field_buf)));
294 	EXPECT_EQ(0,
295 		  strncmp(field_buf, access_prefix, sizeof(access_prefix) - 1));
296 	EXPECT_EQ(NULL, strstr(field_buf, "|refer|"));
297 	field_len = strlen(field_buf);
298 	ASSERT_LE(sizeof(access_suffix) - 1, field_len);
299 	EXPECT_STREQ(access_suffix,
300 		     field_buf + field_len - (sizeof(access_suffix) - 1));
301 	expect_truncated_path(_metadata, buf, REGEX_ADD_RULE_FS(TRACE_TASK));
302 
303 	free(buf);
304 }
305 
306 /*
307  * Verifies that an overflowing denied path does not corrupt its sibling
308  * symbolic blockers field.
309  */
310 TEST_F(trace_fs, deny_access_fs_escaped_path_overflow)
311 {
312 	char path[PATH_MAX];
313 	char *buf, field_buf[64];
314 	int count, err;
315 
316 	create_long_path(_metadata, path);
317 	ASSERT_EQ(0, tracefs_clear_buf());
318 
319 	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
320 				LANDLOCK_ACCESS_FS_READ_DIR, path);
321 
322 	buf = tracefs_read_buf();
323 	ASSERT_NE(NULL, buf);
324 
325 	count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
326 	EXPECT_EQ(1, count)
327 	{
328 		TH_LOG("Expected 1 deny_access_fs event, got %d\n%s", count,
329 		       buf);
330 	}
331 
332 	/*
333 	 * The marker catches a full revert with any compiler.  The symbolic
334 	 * field also catches scratch-sequence poisoning when the compiler
335 	 * evaluates the overflowing path first, as GCC currently does.
336 	 */
337 	err = tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
338 				    "blockers", field_buf, sizeof(field_buf));
339 	ASSERT_EQ(0, err);
340 	EXPECT_STREQ("read_dir", field_buf);
341 	expect_truncated_path(_metadata, buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
342 
343 	free(buf);
344 }
345 
346 /*
347  * Verifies that an allowed access emits check_rule events (rule matched during
348  * pathwalk) but does NOT emit deny_access events (no denial).
349  */
350 TEST_F(trace_fs, allowed_access)
351 {
352 	char *buf, field_buf[64];
353 	int count;
354 
355 	ASSERT_EQ(0, tracefs_clear_buf());
356 
357 	/* Rule allows READ_DIR for /usr, access /usr which is allowed. */
358 	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
359 				LANDLOCK_ACCESS_FS_READ_DIR, "/usr");
360 
361 	buf = tracefs_read_buf();
362 	ASSERT_NE(NULL, buf);
363 
364 	count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
365 	EXPECT_LE(1, count);
366 
367 	/* Single-layer grants array, intersected with the request. */
368 	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
369 					   "grants", field_buf,
370 					   sizeof(field_buf)));
371 	EXPECT_STREQ("{read_dir}", field_buf);
372 
373 	count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
374 	EXPECT_EQ(0, count);
375 
376 	free(buf);
377 }
378 
379 /*
380  * Verifies that accessing a path whose access type is not in the handled set
381  * does not emit landlock_check_rule events.  The ruleset handles READ_FILE, but
382  * the directory open checks READ_DIR which is unhandled; Landlock has no
383  * opinion and no rule evaluation occurs.
384  */
385 TEST_F(trace_fs, check_rule_unhandled)
386 {
387 	char *buf;
388 	int count;
389 
390 	ASSERT_EQ(0, tracefs_clear_buf());
391 
392 	/* Handles READ_FILE only; READ_DIR is unhandled. */
393 	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_FILE,
394 				LANDLOCK_ACCESS_FS_READ_FILE, "/tmp");
395 
396 	buf = tracefs_read_buf();
397 	ASSERT_NE(NULL, buf);
398 
399 	/* No check_rule events because READ_DIR is not in the handled set. */
400 	count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
401 	EXPECT_EQ(0, count);
402 
403 	free(buf);
404 }
405 
406 /*
407  * Verifies that nested domains (child sandboxed under a parent domain) emit
408  * check_rule events from both layers and produce a deny_access event when the
409  * inner domain's rule does not cover the access.
410  */
411 TEST_F(trace_fs, check_rule_nested)
412 {
413 	char *buf, field_buf[64], *comma;
414 	size_t first_len, second_len;
415 	int count_rule, count_access, status;
416 	pid_t pid;
417 
418 	ASSERT_EQ(0, tracefs_clear_buf());
419 
420 	pid = fork();
421 	ASSERT_LE(0, pid);
422 
423 	if (pid == 0) {
424 		struct landlock_ruleset_attr ruleset_attr = {
425 			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
426 		};
427 		struct landlock_path_beneath_attr path_beneath = {
428 			.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
429 		};
430 		int ruleset_fd, fd;
431 
432 		/* First layer: allow /usr. */
433 		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
434 						     sizeof(ruleset_attr), 0);
435 		if (ruleset_fd < 0)
436 			_exit(1);
437 
438 		path_beneath.parent_fd =
439 			open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
440 		if (path_beneath.parent_fd < 0) {
441 			close(ruleset_fd);
442 			_exit(1);
443 		}
444 
445 		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
446 				      &path_beneath, 0)) {
447 			close(path_beneath.parent_fd);
448 			close(ruleset_fd);
449 			_exit(1);
450 		}
451 		close(path_beneath.parent_fd);
452 
453 		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
454 		if (landlock_restrict_self(ruleset_fd, 0)) {
455 			close(ruleset_fd);
456 			_exit(1);
457 		}
458 		close(ruleset_fd);
459 
460 		/* Second layer: also allow /usr. */
461 		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
462 						     sizeof(ruleset_attr), 0);
463 		if (ruleset_fd < 0)
464 			_exit(1);
465 
466 		path_beneath.parent_fd =
467 			open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
468 		if (path_beneath.parent_fd < 0) {
469 			close(ruleset_fd);
470 			_exit(1);
471 		}
472 
473 		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
474 				      &path_beneath, 0)) {
475 			close(path_beneath.parent_fd);
476 			close(ruleset_fd);
477 			_exit(1);
478 		}
479 		close(path_beneath.parent_fd);
480 
481 		if (landlock_restrict_self(ruleset_fd, 0)) {
482 			close(ruleset_fd);
483 			_exit(1);
484 		}
485 		close(ruleset_fd);
486 
487 		/* Access /usr which is allowed by both layers. */
488 		fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
489 		if (fd >= 0)
490 			close(fd);
491 
492 		/* Access /tmp which has no rule in either layer. */
493 		fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
494 		if (fd >= 0)
495 			close(fd);
496 
497 		_exit(0);
498 	}
499 
500 	ASSERT_EQ(pid, waitpid(pid, &status, 0));
501 	ASSERT_TRUE(WIFEXITED(status));
502 	EXPECT_EQ(0, WEXITSTATUS(status));
503 
504 	buf = tracefs_read_buf();
505 	ASSERT_NE(NULL, buf);
506 
507 	count_rule =
508 		tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
509 	EXPECT_LE(1, count_rule);
510 
511 	/*
512 	 * Both layers have the same rule, so the grants array must have two
513 	 * identical symbolic entries, e.g. {read_dir,read_dir}.
514 	 */
515 	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
516 					   "grants", field_buf,
517 					   sizeof(field_buf)));
518 	comma = strchr(field_buf, ',');
519 	EXPECT_NE(0, !!comma);
520 	if (comma) {
521 		/*
522 		 * Verify both entries are identical: compare the substring
523 		 * before the comma with the substring after it (stripping the
524 		 * braces).
525 		 */
526 		first_len = comma - field_buf - 1;
527 		second_len = strlen(comma + 1) - 1;
528 		EXPECT_EQ(first_len, second_len);
529 		EXPECT_EQ(0, strncmp(field_buf + 1, comma + 1, first_len));
530 	}
531 
532 	count_access =
533 		tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
534 	EXPECT_LE(1, count_access);
535 
536 	free(buf);
537 }
538 
539 /*
540  * Verifies that a denied FS access emits a landlock_deny_access_fs trace event
541  * with the blocked access and path.
542  */
543 TEST_F(trace_fs, deny_access_fs_denied)
544 {
545 	char *buf;
546 	int count;
547 
548 	ASSERT_EQ(0, tracefs_clear_buf());
549 
550 	/*
551 	 * Rule allows READ_DIR for /usr, but access /tmp which has no rule.
552 	 * READ_DIR access to /tmp is denied by absence and should emit a
553 	 * deny_access_fs event.
554 	 */
555 	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
556 				LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
557 
558 	buf = tracefs_read_buf();
559 	ASSERT_NE(NULL, buf);
560 
561 	count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
562 	EXPECT_LE(1, count);
563 
564 	free(buf);
565 }
566 
567 /*
568  * A denied FS access covered by a quiet rule (LANDLOCK_ADD_RULE_QUIET with the
569  * access listed in quiet_access_fs) still emits a landlock_deny_access_fs
570  * event, but with logged=0, the same audit-logging verdict audit would apply to
571  * suppress the record.
572  */
573 TEST_F(trace_fs, deny_access_fs_quiet)
574 {
575 	char *buf, field[64];
576 	pid_t pid;
577 	int status;
578 
579 	ASSERT_EQ(0, tracefs_clear_buf());
580 
581 	pid = fork();
582 	ASSERT_LE(0, pid);
583 	if (pid == 0) {
584 		struct landlock_ruleset_attr ruleset_attr = {
585 			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
586 			.quiet_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
587 		};
588 		struct landlock_path_beneath_attr path_beneath = {
589 			.allowed_access = 0,
590 		};
591 		int ruleset_fd, fd;
592 
593 		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
594 						     sizeof(ruleset_attr), 0);
595 		if (ruleset_fd < 0)
596 			_exit(1);
597 
598 		/* Marks /tmp quiet without granting any access. */
599 		path_beneath.parent_fd =
600 			open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
601 		if (path_beneath.parent_fd < 0) {
602 			close(ruleset_fd);
603 			_exit(1);
604 		}
605 		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
606 				      &path_beneath, LANDLOCK_ADD_RULE_QUIET)) {
607 			close(path_beneath.parent_fd);
608 			close(ruleset_fd);
609 			_exit(1);
610 		}
611 		close(path_beneath.parent_fd);
612 
613 		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
614 		if (landlock_restrict_self(ruleset_fd, 0)) {
615 			close(ruleset_fd);
616 			_exit(1);
617 		}
618 		close(ruleset_fd);
619 
620 		/* Denied READ_DIR on the quiet /tmp: suppressed, logged=0. */
621 		fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
622 		if (fd >= 0)
623 			close(fd);
624 		_exit(0);
625 	}
626 	ASSERT_EQ(pid, waitpid(pid, &status, 0));
627 	ASSERT_TRUE(WIFEXITED(status));
628 	EXPECT_EQ(0, WEXITSTATUS(status));
629 
630 	buf = tracefs_read_buf();
631 	ASSERT_NE(NULL, buf);
632 
633 	/* The event fires with the suppressed verdict. */
634 	EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED(
635 							TRACE_TASK, "0")));
636 	/* The quiet rule must not leave the denial logged. */
637 	EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED(
638 							TRACE_TASK, "1")));
639 
640 	/*
641 	 * Quiet suppresses only the logged verdict: the rest of the denial
642 	 * event stays populated (non-zero domain, non-empty blockers).
643 	 */
644 	ASSERT_EQ(0, tracefs_extract_field(
645 			     buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"),
646 			     "domain", field, sizeof(field)));
647 	EXPECT_STRNE("0", field);
648 	ASSERT_EQ(0, tracefs_extract_field(
649 			     buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"),
650 			     "blockers", field, sizeof(field)));
651 	EXPECT_STRNE("", field);
652 
653 	free(buf);
654 }
655 
656 TEST_HARNESS_MAIN
657