diff options
Diffstat (limited to 'tools/testing/selftests/landlock/trace_fs_test.c')
| -rw-r--r-- | tools/testing/selftests/landlock/trace_fs_test.c | 732 |
1 files changed, 732 insertions, 0 deletions
diff --git a/tools/testing/selftests/landlock/trace_fs_test.c b/tools/testing/selftests/landlock/trace_fs_test.c new file mode 100644 index 000000000000..64014ade3a0e --- /dev/null +++ b/tools/testing/selftests/landlock/trace_fs_test.c @@ -0,0 +1,732 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Filesystem tracepoints + * + * Copyright © 2026 Cloudflare, Inc. + */ + +#define _GNU_SOURCE +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <linux/landlock.h> +#include <sched.h> +#include <stdio.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "common.h" +#include "trace.h" + +#define TRACE_TASK "trace_fs_test" + +/* Mirrors TRACE_SEQ_SIZE, conservatively larger than the usable buffer. */ +#define TRACE_SEQUENCE_SIZE 8192 +#define OCTAL_ESCAPE_LEN 4 +#define LONG_PATH_COMPONENT_COUNT 11 +#define LONG_PATH_COMPONENT_LEN 240 +#define LONG_PATH_LEN \ + (LONG_PATH_COMPONENT_COUNT * (LONG_PATH_COMPONENT_LEN + 1) + \ + sizeof("/tmp")) +#define LONG_ESCAPED_PATH_LEN \ + (LONG_PATH_COMPONENT_COUNT * LONG_PATH_COMPONENT_LEN * OCTAL_ESCAPE_LEN) + +static_assert(LONG_ESCAPED_PATH_LEN > TRACE_SEQUENCE_SIZE, + "escaped path must exceed the trace sequence"); +static_assert(LONG_PATH_LEN < PATH_MAX, "path must fit in PATH_MAX"); + +static void create_long_path(struct __test_metadata *const _metadata, + char *path) +{ + size_t path_len; + + strcpy(path, "/tmp"); + path_len = strlen(path); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount("tmpfs", "/tmp", "tmpfs", 0, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + for (int i = 0; i < LONG_PATH_COMPONENT_COUNT; i++) { + path[path_len++] = '/'; + memset(path + path_len, ' ', LONG_PATH_COMPONENT_LEN); + path_len += LONG_PATH_COMPONENT_LEN; + path[path_len] = '\0'; + ASSERT_EQ(0, mkdir(path, 0700)); + } +} + +static void expect_truncated_path(struct __test_metadata *const _metadata, + const char *const trace, + const char *const event_regex) +{ + static const char marker[] = "\xe2\x80\xa6"; + char *path; + size_t path_len; + + path = malloc(TRACE_SEQUENCE_SIZE); + ASSERT_NE(NULL, path); + ASSERT_EQ(0, tracefs_extract_field(trace, event_regex, "path", path, + TRACE_SEQUENCE_SIZE)); + EXPECT_EQ(path, strstr(path, "/tmp/")); + EXPECT_NE(NULL, strstr(path, "\\040")); + + path_len = strlen(path); + ASSERT_LE(sizeof(marker) - 1, path_len); + EXPECT_STREQ(marker, path + path_len - (sizeof(marker) - 1)); + free(path); +} + +/* + * Like REGEX_DENY_ACCESS_FS(), but pins the logged field to a specific value + * ("0" or "1") so a test can tell a suppressed (quiet) denial from a logged + * one. The tracepoint fires for every denial; logged carries the audit + * verdict. + */ +#define REGEX_DENY_ACCESS_FS_LOGGED(task, log) \ + TRACE_PREFIX(task) \ + "landlock_deny_access_fs: " \ + "domain=[0-9a-f]\\+ " \ + "same_exec=[01] " \ + "logged=" log " " \ + "blockers=[a-z_|]* " \ + "dev=[0-9]\\+:[0-9]\\+ " \ + "ino=[0-9]\\+ " \ + "path=[^ ]*$" + +/* clang-format off */ +FIXTURE(trace_fs) { + /* clang-format on */ + int tracefs_ok; +}; + +FIXTURE_SETUP(trace_fs) +{ + int ret; + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, unshare(CLONE_NEWNS)); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); + + ret = tracefs_fixture_setup(); + if (ret) { + clear_cap(_metadata, CAP_SYS_ADMIN); + self->tracefs_ok = 0; + SKIP(return, "tracefs not available"); + } + self->tracefs_ok = 1; + + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, + true)); + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true)); + ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true)); + ASSERT_EQ(0, tracefs_clear()); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +FIXTURE_TEARDOWN(trace_fs) +{ + if (!self->tracefs_ok) + return; + + set_cap(_metadata, CAP_SYS_ADMIN); + tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false); + tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false); + tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false); + tracefs_fixture_teardown(); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +/* + * Baseline: verifies that without Landlock, the operation succeeds and no + * check_rule or deny_access trace events fire. + */ +TEST_F(trace_fs, unsandboxed) +{ + char *buf; + int count, status, fd; + pid_t pid; + + ASSERT_EQ(0, tracefs_clear_buf()); + + pid = fork(); + ASSERT_LE(0, pid); + + if (pid == 0) { + /* + * No sandbox: verify that a normal FS access does not produce + * Landlock trace events. + */ + fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd >= 0) + close(fd); + _exit(0); + } + + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); + EXPECT_EQ(0, count); + count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + EXPECT_EQ(0, count); + + free(buf); +} + +/* + * Verifies that adding a filesystem rule emits a landlock_add_rule_path_beneath + * event with the expected path and field values: the ruleset ID and + * access_rights are non-zero, and the path matches. + */ +TEST_F(trace_fs, add_rule_path_beneath) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + }; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE, + }; + char *buf, field_buf[64]; + int ruleset_fd, count; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(0, close(path_beneath.parent_fd)); + ASSERT_EQ(0, close(ruleset_fd)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, + REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s", + count, buf); + } + + /* Ruleset ID should be non-zero. */ + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "ruleset", field_buf, sizeof(field_buf))); + EXPECT_STRNE("0", field_buf); + + /* Access rights should be non-zero. */ + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "access_rights", field_buf, sizeof(field_buf))); + EXPECT_STRNE("", field_buf); + + /* Path should be /usr. */ + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "path", field_buf, sizeof(field_buf))); + EXPECT_STREQ("/usr", field_buf); + + free(buf); +} + +/* + * Verifies that a path whose escaping exceeds the trace scratch sequence does + * not corrupt a sibling symbolic field. + */ +TEST_F(trace_fs, add_rule_path_beneath_escaped_path_overflow) +{ + static const char access_prefix[] = "execute|write_file|read_file|"; + static const char access_suffix[] = "|ioctl_dev|resolve_unix"; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE, + }; + char path[PATH_MAX]; + char *buf, field_buf[256]; + size_t field_len; + int ruleset_fd, count; + + create_long_path(_metadata, path); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + path_beneath.parent_fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(0, close(path_beneath.parent_fd)); + ASSERT_EQ(0, close(ruleset_fd)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, + REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s", + count, buf); + } + + /* + * The marker catches a full revert with any compiler. The symbolic + * field also catches scratch-sequence poisoning when the compiler + * evaluates the overflowing path first, as GCC currently does. + */ + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK), + "access_rights", field_buf, sizeof(field_buf))); + EXPECT_EQ(0, + strncmp(field_buf, access_prefix, sizeof(access_prefix) - 1)); + EXPECT_EQ(NULL, strstr(field_buf, "|refer|")); + field_len = strlen(field_buf); + ASSERT_LE(sizeof(access_suffix) - 1, field_len); + EXPECT_STREQ(access_suffix, + field_buf + field_len - (sizeof(access_suffix) - 1)); + expect_truncated_path(_metadata, buf, + REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)); + + free(buf); +} + +/* + * Verifies that an overflowing denied path does not corrupt its sibling + * symbolic blockers field. + */ +TEST_F(trace_fs, deny_access_fs_escaped_path_overflow) +{ + char path[PATH_MAX]; + char *buf, field_buf[64]; + int count, err; + + create_long_path(_metadata, path); + ASSERT_EQ(0, tracefs_clear_buf()); + + sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR, + LANDLOCK_ACCESS_FS_READ_DIR, path); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 deny_access_fs event, got %d\n%s", count, + buf); + } + + /* + * The marker catches a full revert with any compiler. The symbolic + * field also catches scratch-sequence poisoning when the compiler + * evaluates the overflowing path first, as GCC currently does. + */ + err = tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), + "blockers", field_buf, sizeof(field_buf)); + ASSERT_EQ(0, err); + EXPECT_STREQ("read_dir", field_buf); + expect_truncated_path(_metadata, buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + + free(buf); +} + +/* + * Verifies that an allowed access emits check_rule events (rule matched during + * pathwalk) but does NOT emit deny_access events (no denial). + */ +TEST_F(trace_fs, allowed_access) +{ + char *buf, field_buf[64]; + int count; + + ASSERT_EQ(0, tracefs_clear_buf()); + + /* Rule allows READ_DIR for /usr, access /usr which is allowed. */ + sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR, + LANDLOCK_ACCESS_FS_READ_DIR, "/usr"); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); + EXPECT_LE(1, count); + + /* Single-layer grants array, intersected with the request. */ + ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), + "grants", field_buf, + sizeof(field_buf))); + EXPECT_STREQ("{read_dir}", field_buf); + + count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + EXPECT_EQ(0, count); + + free(buf); +} + +/* + * Verifies that accessing a path whose access type is not in the handled set + * does not emit landlock_check_rule events. The ruleset handles READ_FILE, but + * the directory open checks READ_DIR which is unhandled; Landlock has no + * opinion and no rule evaluation occurs. + */ +TEST_F(trace_fs, check_rule_unhandled) +{ + char *buf; + int count; + + ASSERT_EQ(0, tracefs_clear_buf()); + + /* Handles READ_FILE only; READ_DIR is unhandled. */ + sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_FILE, + LANDLOCK_ACCESS_FS_READ_FILE, "/tmp"); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + /* No check_rule events because READ_DIR is not in the handled set. */ + count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); + EXPECT_EQ(0, count); + + free(buf); +} + +/* + * Verifies that nested domains (child sandboxed under a parent domain) emit + * check_rule events from both layers and produce a deny_access event when the + * inner domain's rule does not cover the access. + */ +TEST_F(trace_fs, check_rule_nested) +{ + char *buf, field_buf[64], *comma; + size_t first_len, second_len; + int count_rule, count_access, status; + pid_t pid; + + ASSERT_EQ(0, tracefs_clear_buf()); + + pid = fork(); + ASSERT_LE(0, pid); + + if (pid == 0) { + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, + }; + int ruleset_fd, fd; + + /* First layer: allow /usr. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) + _exit(1); + + path_beneath.parent_fd = + open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + close(ruleset_fd); + _exit(1); + } + + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)) { + close(path_beneath.parent_fd); + close(ruleset_fd); + _exit(1); + } + close(path_beneath.parent_fd); + + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + if (landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + _exit(1); + } + close(ruleset_fd); + + /* Second layer: also allow /usr. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) + _exit(1); + + path_beneath.parent_fd = + open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + close(ruleset_fd); + _exit(1); + } + + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)) { + close(path_beneath.parent_fd); + close(ruleset_fd); + _exit(1); + } + close(path_beneath.parent_fd); + + if (landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + _exit(1); + } + close(ruleset_fd); + + /* Access /usr which is allowed by both layers. */ + fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd >= 0) + close(fd); + + /* Access /tmp which has no rule in either layer. */ + fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd >= 0) + close(fd); + + _exit(0); + } + + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count_rule = + tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); + EXPECT_LE(1, count_rule); + + /* + * Both layers have the same rule, so the grants array must have two + * identical symbolic entries, e.g. {read_dir,read_dir}. + */ + ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), + "grants", field_buf, + sizeof(field_buf))); + comma = strchr(field_buf, ','); + EXPECT_NE(0, !!comma); + if (comma) { + /* + * Verify both entries are identical: compare the substring + * before the comma with the substring after it (stripping the + * braces). + */ + first_len = comma - field_buf - 1; + second_len = strlen(comma + 1) - 1; + EXPECT_EQ(first_len, second_len); + EXPECT_EQ(0, strncmp(field_buf + 1, comma + 1, first_len)); + } + + count_access = + tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + EXPECT_LE(1, count_access); + + free(buf); +} + +/* + * Verifies that a denied FS access emits a landlock_deny_access_fs trace event + * with the blocked access and path. + */ +TEST_F(trace_fs, deny_access_fs_denied) +{ + const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK); + char *buf, blockers[64]; + int count; + + ASSERT_EQ(0, tracefs_clear_buf()); + + /* + * Rule allows READ_DIR for /usr, but access /tmp which has no rule. + * READ_DIR access to /tmp is denied by absence and should emit a + * deny_access_fs event. + */ + sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR, + LANDLOCK_ACCESS_FS_READ_DIR, "/tmp"); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, event_regex); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 access denial, got %d\n%s", count, buf); + } + ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers", + blockers, sizeof(blockers))); + EXPECT_STREQ("read_dir", blockers); + + free(buf); +} + +/* + * Verifies that a denied mount reports the singleton topology blocker rather + * than an empty access mask. + */ +TEST_F(trace_fs, deny_change_topology) +{ + const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_REFER, + }; + char *buf, blockers[64]; + int count, ruleset_fd, status; + pid_t pid; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, tracefs_clear_buf()); + + /* Ensure that Landlock is the only expected mount denial. */ + set_cap(_metadata, CAP_SYS_ADMIN); + pid = fork(); + ASSERT_LE(0, pid); + if (pid == 0) { + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + close(ruleset_fd); + _exit(1); + } + if (landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + _exit(2); + } + close(ruleset_fd); + + if (mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) != -1) + _exit(3); + + if (errno != EPERM) + _exit(4); + + _exit(0); + } + close(ruleset_fd); + clear_cap(_metadata, CAP_SYS_ADMIN); + + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + count = tracefs_count_matches(buf, event_regex); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 topology denial, got %d\n%s", count, buf); + } + ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers", + blockers, sizeof(blockers))); + EXPECT_STREQ("change_topology", blockers); + + free(buf); +} + +/* + * A denied FS access covered by a quiet rule (LANDLOCK_ADD_RULE_QUIET with the + * access listed in quiet_access_fs) still emits a landlock_deny_access_fs + * event, but with logged=0, the same audit-logging verdict audit would apply to + * suppress the record. + */ +TEST_F(trace_fs, deny_access_fs_quiet) +{ + char *buf, field[64]; + pid_t pid; + int status; + + ASSERT_EQ(0, tracefs_clear_buf()); + + pid = fork(); + ASSERT_LE(0, pid); + if (pid == 0) { + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = 0, + }; + int ruleset_fd, fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) + _exit(1); + + /* Marks /tmp quiet without granting any access. */ + path_beneath.parent_fd = + open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + close(ruleset_fd); + _exit(1); + } + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, LANDLOCK_ADD_RULE_QUIET)) { + close(path_beneath.parent_fd); + close(ruleset_fd); + _exit(1); + } + close(path_beneath.parent_fd); + + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + if (landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + _exit(1); + } + close(ruleset_fd); + + /* Denied READ_DIR on the quiet /tmp: suppressed, logged=0. */ + fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd >= 0) + close(fd); + _exit(0); + } + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + /* The event fires with the suppressed verdict. */ + EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED( + TRACE_TASK, "0"))); + /* The quiet rule must not leave the denial logged. */ + EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED( + TRACE_TASK, "1"))); + + /* + * Quiet suppresses only the logged verdict: the rest of the denial + * event stays populated (non-zero domain, non-empty blockers). + */ + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"), + "domain", field, sizeof(field))); + EXPECT_STRNE("0", field); + ASSERT_EQ(0, tracefs_extract_field( + buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"), + "blockers", field, sizeof(field))); + EXPECT_STRNE("", field); + + free(buf); +} + +TEST_HARNESS_MAIN |
