| // 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 |