| /* SPDX-License-Identifier: GPL-2.0 */ | |
| /* | |
| * Landlock trace test helpers | |
| * | |
| * Copyright © 2026 Cloudflare, Inc. | |
| */ | |
| #define _GNU_SOURCE | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <regex.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/mount.h> | |
| #include <sys/stat.h> | |
| #include <unistd.h> | |
| #include "kselftest_harness.h" | |
| #define TRACEFS_ROOT "/sys/kernel/tracing" | |
| #define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock" | |
| #define TRACEFS_CREATE_RULESET_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable" | |
| #define TRACEFS_CREATE_DOMAIN_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable" | |
| #define TRACEFS_ENFORCE_DOMAIN_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable" | |
| #define TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_add_rule_path_beneath/enable" | |
| #define TRACEFS_ADD_RULE_NET_PORT_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net_port/enable" | |
| #define TRACEFS_ADD_RULE_NAMESPACE_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_add_rule_namespace/enable" | |
| #define TRACEFS_ADD_RULE_CAPABILITY_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_add_rule_capability/enable" | |
| #define TRACEFS_CHECK_RULE_FS_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_check_rule_inode/enable" | |
| #define TRACEFS_CHECK_RULE_NET_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net_port/enable" | |
| #define TRACEFS_DENY_ACCESS_FS_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable" | |
| #define TRACEFS_DENY_ACCESS_NET_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable" | |
| #define TRACEFS_DENY_PERMISSION_NAMESPACE_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_deny_permission_namespace/enable" | |
| #define TRACEFS_DENY_PERMISSION_CAPABILITY_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_deny_permission_capability/enable" | |
| #define TRACEFS_DENY_PTRACE_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable" | |
| #define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable" | |
| #define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR \ | |
| "/landlock_deny_scope_abstract_unix_socket/enable" | |
| #define TRACEFS_FREE_DOMAIN_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable" | |
| #define TRACEFS_FREE_RULESET_ENABLE \ | |
| TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable" | |
| #define TRACEFS_TRACE TRACEFS_ROOT "/trace" | |
| #define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid" | |
| #define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork" | |
| #define TRACE_BUFFER_SIZE (64 * 1024) | |
| /* | |
| * Trace line prefix: matches the ftrace "trace" file format. Format: " | |
| * <task>-<pid> [<cpu>] <flags> <timestamp>: " | |
| * | |
| * The task parameter must be a string literal truncated to 15 chars | |
| * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The | |
| * pattern accepts either the expected task name or "<...>" because the ftrace | |
| * comm cache may evict short-lived processes (e.g., forked children that exit | |
| * before the trace buffer is read). | |
| * | |
| * No unescaped '.' in any REGEX macro; literal dots use '\\.'. | |
| */ | |
| #define TRACE_PREFIX(task) \ | |
| "^ *\\(<\\.\\.\\.>" \ | |
| "\\|" task "\\)" \ | |
| "-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: " | |
| /* | |
| * Task name for events emitted by kworker threads (e.g., free_domain fires from | |
| * a work queue, not from the test process). | |
| */ | |
| #define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+" | |
| #define REGEX_ADD_RULE_PATH_BENEATH(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_add_rule_path_beneath: " \ | |
| "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ | |
| "access_rights=[a-z_|]* " \ | |
| "dev=[0-9]\\+:[0-9]\\+ " \ | |
| "ino=[0-9]\\+ " \ | |
| "path=[^ ]\\+$" | |
| #define REGEX_ADD_RULE_NET_PORT(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_add_rule_net_port: " \ | |
| "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ | |
| "access_rights=[a-z_|]* " \ | |
| "port=[0-9]\\+$" | |
| #define REGEX_ADD_RULE_NAMESPACE_VERSION(task, version) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_add_rule_namespace: " \ | |
| "ruleset=[0-9a-f]\\+\\." version " " \ | |
| "permissions=[a-z._|]* " \ | |
| "allowed_namespace_types=0x[0-9a-f]\\+ " \ | |
| "quiet_namespace_types=0x[0-9a-f]\\+$" | |
| #define REGEX_ADD_RULE_NAMESPACE(task) \ | |
| REGEX_ADD_RULE_NAMESPACE_VERSION(task, "[0-9]\\+") | |
| #define REGEX_ADD_RULE_CAPABILITY_VERSION(task, version) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_add_rule_capability: " \ | |
| "ruleset=[0-9a-f]\\+\\." version " " \ | |
| "permissions=[a-z._|]* " \ | |
| "allowed_capabilities=0x[0-9a-f]\\+ " \ | |
| "quiet_capabilities=0x[0-9a-f]\\+$" | |
| #define REGEX_ADD_RULE_CAPABILITY(task) \ | |
| REGEX_ADD_RULE_CAPABILITY_VERSION(task, "[0-9]\\+") | |
| #define REGEX_CREATE_RULESET(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_create_ruleset: " \ | |
| "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ | |
| "handled_fs=[a-z_|]* " \ | |
| "handled_net=[a-z_|]* " \ | |
| "scoped=[a-z_|]* " \ | |
| "handled_permissions=[a-z._|]*$" | |
| #define REGEX_CREATE_DOMAIN(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_create_domain: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "parent=[0-9a-f]\\+ " \ | |
| "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" | |
| #define REGEX_CHECK_RULE_FS(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_check_rule_inode: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "access_request=[a-z_|]* " \ | |
| "dev=[0-9]\\+:[0-9]\\+ " \ | |
| "ino=[0-9]\\+ " \ | |
| "grants={[a-z_|,]*}$" | |
| #define REGEX_CHECK_RULE_NET(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_check_rule_net_port: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "access_request=[a-z_|]* " \ | |
| "port=[0-9]\\+ " \ | |
| "grants={[a-z_|,]*}$" | |
| #define REGEX_DENY_ACCESS_FS(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_access_fs: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "blockers=[a-z_|]* " \ | |
| "dev=[0-9]\\+:[0-9]\\+ " \ | |
| "ino=[0-9]\\+ " \ | |
| "path=[^ ]*$" | |
| #define REGEX_DENY_ACCESS_NET(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_access_net: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "blockers=[a-z_|]* " \ | |
| "port=-\\?[0-9]\\+$" | |
| #define REGEX_DENY_PERMISSION_NAMESPACE(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_permission_namespace: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "blockers=[a-z_|]* " \ | |
| "namespace_type=0x[0-9a-f]\\+ " \ | |
| "namespace_id=[0-9]\\+$" | |
| #define REGEX_DENY_PERMISSION_CAPABILITY(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_permission_capability: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "blockers=[a-z_|]* " \ | |
| "capability=[0-9]\\+$" | |
| #define REGEX_DENY_PTRACE(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_ptrace: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "tracee_domain=[0-9a-f]\\+ " \ | |
| "tracee_pid=[0-9]\\+ " \ | |
| "tracee_comm=[^ ]*$" | |
| #define REGEX_DENY_SCOPE_SIGNAL(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_scope_signal: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "target_domain=[0-9a-f]\\+ " \ | |
| "target_pid=[0-9]\\+ " \ | |
| "target_comm=[^ ]*$" | |
| #define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_deny_scope_abstract_unix_socket: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "same_exec=[01] " \ | |
| "logged=[01] " \ | |
| "peer_domain=[0-9a-f]\\+ " \ | |
| "peer_pid=[0-9]\\+ " \ | |
| "sun_path=[^ ]*$" | |
| #define REGEX_FREE_DOMAIN(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_free_domain: " \ | |
| "domain=[0-9a-f]\\+ " \ | |
| "denials=[0-9]\\+$" | |
| #define REGEX_FREE_RULESET(task) \ | |
| TRACE_PREFIX(task) \ | |
| "landlock_free_ruleset: " \ | |
| "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" | |
| static int __maybe_unused tracefs_write(const char *path, const char *value) | |
| { | |
| int fd; | |
| ssize_t ret; | |
| size_t len = strlen(value); | |
| fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC); | |
| if (fd < 0) | |
| return -errno; | |
| ret = write(fd, value, len); | |
| close(fd); | |
| if (ret < 0) | |
| return -errno; | |
| if ((size_t)ret != len) | |
| return -EIO; | |
| return 0; | |
| } | |
| static int __maybe_unused tracefs_write_int(const char *path, int value) | |
| { | |
| char buf[32]; | |
| snprintf(buf, sizeof(buf), "%d", value); | |
| return tracefs_write(path, buf); | |
| } | |
| static int __maybe_unused tracefs_setup(void) | |
| { | |
| struct stat st; | |
| /* Mount tracefs if not already mounted. */ | |
| if (stat(TRACEFS_ROOT, &st) != 0) { | |
| int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL); | |
| if (ret) | |
| return -errno; | |
| } | |
| /* Verify landlock events are available. */ | |
| if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0) | |
| return -ENOENT; | |
| return 0; | |
| } | |
| /* | |
| * Set up PID-based event filtering so only events from the current process and | |
| * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it | |
| * prevents events from unrelated processes from polluting the trace buffer. | |
| */ | |
| static int __maybe_unused tracefs_set_pid_filter(pid_t pid) | |
| { | |
| int ret; | |
| /* Enable event-fork so children inherit the PID filter. */ | |
| ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1"); | |
| if (ret) | |
| return ret; | |
| return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid); | |
| } | |
| /* Clear the PID filter to stop filtering by PID. */ | |
| static int __maybe_unused tracefs_clear_pid_filter(void) | |
| { | |
| return tracefs_write(TRACEFS_SET_EVENT_PID, ""); | |
| } | |
| static int __maybe_unused tracefs_enable_event(const char *enable_path, | |
| bool enable) | |
| { | |
| return tracefs_write(enable_path, enable ? "1" : "0"); | |
| } | |
| static int __maybe_unused tracefs_clear(void) | |
| { | |
| return tracefs_write(TRACEFS_TRACE, ""); | |
| } | |
| /* | |
| * Reads the trace buffer content into a newly allocated buffer. The caller is | |
| * responsible for freeing the returned buffer. Returns NULL on error. | |
| */ | |
| static char __maybe_unused *tracefs_read_trace(void) | |
| { | |
| char *buf; | |
| int fd; | |
| ssize_t total = 0, ret; | |
| buf = malloc(TRACE_BUFFER_SIZE); | |
| if (!buf) | |
| return NULL; | |
| fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC); | |
| if (fd < 0) { | |
| free(buf); | |
| return NULL; | |
| } | |
| while (total < TRACE_BUFFER_SIZE - 1) { | |
| ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total); | |
| if (ret <= 0) | |
| break; | |
| total += ret; | |
| } | |
| close(fd); | |
| buf[total] = '\0'; | |
| return buf; | |
| } | |
| /* Counts the number of lines in @buf matching the basic regex @pattern. */ | |
| static int __maybe_unused tracefs_count_matches(const char *buf, | |
| const char *pattern) | |
| { | |
| regex_t regex; | |
| int count = 0; | |
| const char *line, *end; | |
| if (regcomp(®ex, pattern, 0) != 0) | |
| return -EINVAL; | |
| line = buf; | |
| while (*line) { | |
| end = strchr(line, '\n'); | |
| if (!end) | |
| end = line + strlen(line); | |
| /* Create a temporary NUL-terminated line. */ | |
| size_t len = end - line; | |
| char *tmp = malloc(len + 1); | |
| if (tmp) { | |
| memcpy(tmp, line, len); | |
| tmp[len] = '\0'; | |
| if (regexec(®ex, tmp, 0, NULL, 0) == 0) | |
| count++; | |
| free(tmp); | |
| } | |
| if (*end == '\n') | |
| line = end + 1; | |
| else | |
| break; | |
| } | |
| regfree(®ex); | |
| return count; | |
| } | |
| /* | |
| * Extracts the value of a named field from a trace line in @buf. Searches for | |
| * the first line matching @line_pattern, then extracts the value after | |
| * "@field_name=" into @out. Stops at space or newline. | |
| * | |
| * Returns 0 on success, -ENOENT if no match. | |
| */ | |
| static int __maybe_unused tracefs_extract_field(const char *buf, | |
| const char *line_pattern, | |
| const char *field_name, | |
| char *out, size_t out_size) | |
| { | |
| regex_t regex; | |
| const char *line, *end; | |
| if (regcomp(®ex, line_pattern, 0) != 0) | |
| return -EINVAL; | |
| line = buf; | |
| while (*line) { | |
| end = strchr(line, '\n'); | |
| if (!end) | |
| end = line + strlen(line); | |
| size_t len = end - line; | |
| char *tmp = malloc(len + 1); | |
| if (tmp) { | |
| const char *field, *val_start; | |
| size_t field_len, val_len; | |
| memcpy(tmp, line, len); | |
| tmp[len] = '\0'; | |
| if (regexec(®ex, tmp, 0, NULL, 0) != 0) { | |
| free(tmp); | |
| goto next; | |
| } | |
| /* | |
| * Find "field_name=" in the line, ensuring a word | |
| * boundary before the field name to avoid substring | |
| * matches (e.g., "port" in "sport"). | |
| */ | |
| field_len = strlen(field_name); | |
| field = tmp; | |
| while ((field = strstr(field, field_name))) { | |
| if (field[field_len] == '=' && | |
| (field == tmp || field[-1] == ' ')) | |
| break; | |
| field++; | |
| } | |
| if (!field) { | |
| free(tmp); | |
| regfree(®ex); | |
| return -ENOENT; | |
| } | |
| val_start = field + field_len + 1; | |
| val_len = 0; | |
| while (val_start[val_len] && | |
| val_start[val_len] != ' ' && | |
| val_start[val_len] != '\n') | |
| val_len++; | |
| if (val_len >= out_size) | |
| val_len = out_size - 1; | |
| memcpy(out, val_start, val_len); | |
| out[val_len] = '\0'; | |
| free(tmp); | |
| regfree(®ex); | |
| return 0; | |
| } | |
| next: | |
| if (*end == '\n') | |
| line = end + 1; | |
| else | |
| break; | |
| } | |
| regfree(®ex); | |
| return -ENOENT; | |
| } | |
| /* | |
| * Common fixture setup for trace tests. Mounts tracefs if needed and sets a | |
| * PID filter. The caller must create a mount namespace first | |
| * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs | |
| * mount; the trace buffer, per-event enable flags, and PID filter are global | |
| * kernel state, scoped to the test by the PID filter. | |
| * | |
| * Returns 0 on success, -errno on failure (caller should SKIP). | |
| */ | |
| static int __maybe_unused tracefs_fixture_setup(void) | |
| { | |
| int ret; | |
| ret = tracefs_setup(); | |
| if (ret) | |
| return ret; | |
| return tracefs_set_pid_filter(getpid()); | |
| } | |
| static void __maybe_unused tracefs_fixture_teardown(void) | |
| { | |
| tracefs_clear_pid_filter(); | |
| } | |
| /* | |
| * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then | |
| * drops the capability. Returns the value from @func, or -EPERM if the | |
| * capability manipulation fails. | |
| */ | |
| static int __maybe_unused tracefs_priv_call(int (*func)(void)) | |
| { | |
| const cap_value_t admin = CAP_SYS_ADMIN; | |
| cap_t cap_p; | |
| int ret; | |
| cap_p = cap_get_proc(); | |
| if (!cap_p) | |
| return -EPERM; | |
| if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || | |
| cap_set_proc(cap_p)) { | |
| cap_free(cap_p); | |
| return -EPERM; | |
| } | |
| ret = func(); | |
| cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); | |
| cap_set_proc(cap_p); | |
| cap_free(cap_p); | |
| return ret; | |
| } | |
| /* Read the trace buffer with elevated privileges. Returns NULL on failure. */ | |
| static char __maybe_unused *tracefs_read_buf(void) | |
| { | |
| /* Cannot use tracefs_priv_call() because the return type is char *. */ | |
| cap_t cap_p; | |
| char *buf; | |
| const cap_value_t admin = CAP_SYS_ADMIN; | |
| cap_p = cap_get_proc(); | |
| if (!cap_p) | |
| return NULL; | |
| if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || | |
| cap_set_proc(cap_p)) { | |
| cap_free(cap_p); | |
| return NULL; | |
| } | |
| buf = tracefs_read_trace(); | |
| cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); | |
| cap_set_proc(cap_p); | |
| cap_free(cap_p); | |
| return buf; | |
| } | |
| /* Clear the trace buffer with elevated privileges. Returns 0 on success. */ | |
| static int __maybe_unused tracefs_clear_buf(void) | |
| { | |
| return tracefs_priv_call(tracefs_clear); | |
| } | |
| /* | |
| * Forks a child that creates a Landlock sandbox and performs an FS access. The | |
| * parent waits for the child, then reads the trace buffer. | |
| * | |
| * Requires common.h and wrappers.h to be included before trace.h. | |
| */ | |
| static void __maybe_unused sandbox_child_fs_access( | |
| struct __test_metadata *const _metadata, const char *rule_path, | |
| __u64 handled_access, __u64 allowed_access, const char *access_path) | |
| { | |
| pid_t pid; | |
| int status; | |
| pid = fork(); | |
| ASSERT_LE(0, pid); | |
| if (pid == 0) { | |
| struct landlock_ruleset_attr ruleset_attr = { | |
| .handled_access_fs = handled_access, | |
| }; | |
| struct landlock_path_beneath_attr path_beneath = { | |
| .allowed_access = allowed_access, | |
| }; | |
| int ruleset_fd, fd; | |
| ruleset_fd = landlock_create_ruleset(&ruleset_attr, | |
| sizeof(ruleset_attr), 0); | |
| if (ruleset_fd < 0) | |
| _exit(1); | |
| path_beneath.parent_fd = | |
| open(rule_path, 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); | |
| fd = open(access_path, 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)); | |
| } | |
| /* | |
| * Forks a child that creates a Landlock sandbox allowing execute+read_dir for | |
| * /usr and execute-only for ".", then execs ./true. The true binary opens "." | |
| * on startup, triggering a read_dir denial with same_exec=0. The parent waits | |
| * for the child to exit. | |
| */ | |
| static void __maybe_unused sandbox_child_exec_true( | |
| struct __test_metadata *const _metadata, __u32 restrict_flags) | |
| { | |
| pid_t pid; | |
| int status; | |
| pid = fork(); | |
| ASSERT_LE(0, pid); | |
| if (pid == 0) { | |
| struct landlock_ruleset_attr attr = { | |
| .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR | | |
| LANDLOCK_ACCESS_FS_EXECUTE, | |
| }; | |
| struct landlock_path_beneath_attr path_beneath = { | |
| .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE | | |
| LANDLOCK_ACCESS_FS_READ_DIR, | |
| }; | |
| int ruleset_fd; | |
| ruleset_fd = landlock_create_ruleset(&attr, sizeof(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) { | |
| landlock_add_rule(ruleset_fd, | |
| LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0); | |
| close(path_beneath.parent_fd); | |
| } | |
| path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE; | |
| path_beneath.parent_fd = | |
| open(".", O_PATH | O_DIRECTORY | O_CLOEXEC); | |
| if (path_beneath.parent_fd >= 0) { | |
| landlock_add_rule(ruleset_fd, | |
| LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0); | |
| close(path_beneath.parent_fd); | |
| } | |
| prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); | |
| if (landlock_restrict_self(ruleset_fd, restrict_flags)) | |
| _exit(1); | |
| close(ruleset_fd); | |
| execl("./true", "./true", NULL); | |
| _exit(1); | |
| } | |
| ASSERT_EQ(pid, waitpid(pid, &status, 0)); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| EXPECT_EQ(0, WEXITSTATUS(status)); | |
| } |