12651712aSMickaël Salaün /* SPDX-License-Identifier: GPL-2.0 */ 22651712aSMickaël Salaün /* 32651712aSMickaël Salaün * Landlock trace test helpers 42651712aSMickaël Salaün * 52651712aSMickaël Salaün * Copyright © 2026 Cloudflare, Inc. 62651712aSMickaël Salaün */ 72651712aSMickaël Salaün 82651712aSMickaël Salaün #define _GNU_SOURCE 92651712aSMickaël Salaün #include <errno.h> 102651712aSMickaël Salaün #include <fcntl.h> 112651712aSMickaël Salaün #include <regex.h> 122651712aSMickaël Salaün #include <stdbool.h> 132651712aSMickaël Salaün #include <stdio.h> 142651712aSMickaël Salaün #include <stdlib.h> 152651712aSMickaël Salaün #include <string.h> 162651712aSMickaël Salaün #include <sys/mount.h> 172651712aSMickaël Salaün #include <sys/stat.h> 182651712aSMickaël Salaün #include <unistd.h> 192651712aSMickaël Salaün 202651712aSMickaël Salaün #include "kselftest_harness.h" 212651712aSMickaël Salaün 222651712aSMickaël Salaün #define TRACEFS_ROOT "/sys/kernel/tracing" 232651712aSMickaël Salaün #define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock" 242651712aSMickaël Salaün #define TRACEFS_CREATE_RULESET_ENABLE \ 252651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable" 262651712aSMickaël Salaün #define TRACEFS_CREATE_DOMAIN_ENABLE \ 272651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable" 28*ac6d193dSMickaël Salaün #define TRACEFS_ENFORCE_DOMAIN_ENABLE \ 29*ac6d193dSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable" 302651712aSMickaël Salaün #define TRACEFS_ADD_RULE_FS_ENABLE \ 312651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable" 322651712aSMickaël Salaün #define TRACEFS_ADD_RULE_NET_ENABLE \ 332651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable" 342651712aSMickaël Salaün #define TRACEFS_CHECK_RULE_FS_ENABLE \ 352651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable" 362651712aSMickaël Salaün #define TRACEFS_CHECK_RULE_NET_ENABLE \ 372651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable" 382651712aSMickaël Salaün #define TRACEFS_DENY_ACCESS_FS_ENABLE \ 392651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable" 402651712aSMickaël Salaün #define TRACEFS_DENY_ACCESS_NET_ENABLE \ 412651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable" 422651712aSMickaël Salaün #define TRACEFS_DENY_PTRACE_ENABLE \ 432651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable" 442651712aSMickaël Salaün #define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \ 452651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable" 462651712aSMickaël Salaün #define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \ 472651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR \ 482651712aSMickaël Salaün "/landlock_deny_scope_abstract_unix_socket/enable" 492651712aSMickaël Salaün #define TRACEFS_FREE_DOMAIN_ENABLE \ 502651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable" 512651712aSMickaël Salaün #define TRACEFS_FREE_RULESET_ENABLE \ 522651712aSMickaël Salaün TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable" 532651712aSMickaël Salaün #define TRACEFS_TRACE TRACEFS_ROOT "/trace" 542651712aSMickaël Salaün #define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid" 552651712aSMickaël Salaün #define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork" 562651712aSMickaël Salaün 572651712aSMickaël Salaün #define TRACE_BUFFER_SIZE (64 * 1024) 582651712aSMickaël Salaün 592651712aSMickaël Salaün /* 602651712aSMickaël Salaün * Trace line prefix: matches the ftrace "trace" file format. Format: " 612651712aSMickaël Salaün * <task>-<pid> [<cpu>] <flags> <timestamp>: " 622651712aSMickaël Salaün * 632651712aSMickaël Salaün * The task parameter must be a string literal truncated to 15 chars 642651712aSMickaël Salaün * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The 652651712aSMickaël Salaün * pattern accepts either the expected task name or "<...>" because the ftrace 662651712aSMickaël Salaün * comm cache may evict short-lived processes (e.g., forked children that exit 672651712aSMickaël Salaün * before the trace buffer is read). 682651712aSMickaël Salaün * 692651712aSMickaël Salaün * No unescaped '.' in any REGEX macro; literal dots use '\\.'. 702651712aSMickaël Salaün */ 712651712aSMickaël Salaün #define TRACE_PREFIX(task) \ 722651712aSMickaël Salaün "^ *\\(<\\.\\.\\.>" \ 732651712aSMickaël Salaün "\\|" task "\\)" \ 742651712aSMickaël Salaün "-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: " 752651712aSMickaël Salaün 762651712aSMickaël Salaün /* 772651712aSMickaël Salaün * Task name for events emitted by kworker threads (e.g., free_domain fires from 782651712aSMickaël Salaün * a work queue, not from the test process). 792651712aSMickaël Salaün */ 802651712aSMickaël Salaün #define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+" 812651712aSMickaël Salaün 822651712aSMickaël Salaün #define REGEX_ADD_RULE_FS(task) \ 832651712aSMickaël Salaün TRACE_PREFIX(task) \ 842651712aSMickaël Salaün "landlock_add_rule_fs: " \ 852651712aSMickaël Salaün "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ 862651712aSMickaël Salaün "access_rights=[a-z_|]* " \ 872651712aSMickaël Salaün "dev=[0-9]\\+:[0-9]\\+ " \ 882651712aSMickaël Salaün "ino=[0-9]\\+ " \ 892651712aSMickaël Salaün "path=[^ ]\\+$" 902651712aSMickaël Salaün 912651712aSMickaël Salaün #define REGEX_ADD_RULE_NET(task) \ 922651712aSMickaël Salaün TRACE_PREFIX(task) \ 932651712aSMickaël Salaün "landlock_add_rule_net: " \ 942651712aSMickaël Salaün "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ 952651712aSMickaël Salaün "access_rights=[a-z_|]* " \ 962651712aSMickaël Salaün "port=[0-9]\\+$" 972651712aSMickaël Salaün 982651712aSMickaël Salaün #define REGEX_CREATE_RULESET(task) \ 992651712aSMickaël Salaün TRACE_PREFIX(task) \ 1002651712aSMickaël Salaün "landlock_create_ruleset: " \ 1012651712aSMickaël Salaün "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ 1022651712aSMickaël Salaün "handled_fs=[a-z_|]* " \ 1032651712aSMickaël Salaün "handled_net=[a-z_|]* " \ 1042651712aSMickaël Salaün "scoped=[a-z_|]*$" 1052651712aSMickaël Salaün 1062651712aSMickaël Salaün #define REGEX_CREATE_DOMAIN(task) \ 1072651712aSMickaël Salaün TRACE_PREFIX(task) \ 1082651712aSMickaël Salaün "landlock_create_domain: " \ 1092651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1102651712aSMickaël Salaün "parent=[0-9a-f]\\+ " \ 1112651712aSMickaël Salaün "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" 1122651712aSMickaël Salaün 1132651712aSMickaël Salaün #define REGEX_CHECK_RULE_FS(task) \ 1142651712aSMickaël Salaün TRACE_PREFIX(task) \ 1152651712aSMickaël Salaün "landlock_check_rule_fs: " \ 1162651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1172651712aSMickaël Salaün "access_request=[a-z_|]* " \ 1182651712aSMickaël Salaün "dev=[0-9]\\+:[0-9]\\+ " \ 1192651712aSMickaël Salaün "ino=[0-9]\\+ " \ 1202651712aSMickaël Salaün "grants={[a-z_|,]*}$" 1212651712aSMickaël Salaün 1222651712aSMickaël Salaün #define REGEX_CHECK_RULE_NET(task) \ 1232651712aSMickaël Salaün TRACE_PREFIX(task) \ 1242651712aSMickaël Salaün "landlock_check_rule_net: " \ 1252651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1262651712aSMickaël Salaün "access_request=[a-z_|]* " \ 1272651712aSMickaël Salaün "port=[0-9]\\+ " \ 1282651712aSMickaël Salaün "grants={[a-z_|,]*}$" 1292651712aSMickaël Salaün 1302651712aSMickaël Salaün #define REGEX_DENY_ACCESS_FS(task) \ 1312651712aSMickaël Salaün TRACE_PREFIX(task) \ 1322651712aSMickaël Salaün "landlock_deny_access_fs: " \ 1332651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1342651712aSMickaël Salaün "same_exec=[01] " \ 1352651712aSMickaël Salaün "logged=[01] " \ 1362651712aSMickaël Salaün "blockers=[a-z_|]* " \ 1372651712aSMickaël Salaün "dev=[0-9]\\+:[0-9]\\+ " \ 1382651712aSMickaël Salaün "ino=[0-9]\\+ " \ 1392651712aSMickaël Salaün "path=[^ ]*$" 1402651712aSMickaël Salaün 1412651712aSMickaël Salaün #define REGEX_DENY_ACCESS_NET(task) \ 1422651712aSMickaël Salaün TRACE_PREFIX(task) \ 1432651712aSMickaël Salaün "landlock_deny_access_net: " \ 1442651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1452651712aSMickaël Salaün "same_exec=[01] " \ 1462651712aSMickaël Salaün "logged=[01] " \ 1472651712aSMickaël Salaün "blockers=[a-z_|]* " \ 1482651712aSMickaël Salaün "sport=[0-9]\\+ " \ 1492651712aSMickaël Salaün "dport=[0-9]\\+$" 1502651712aSMickaël Salaün 1512651712aSMickaël Salaün #define REGEX_DENY_PTRACE(task) \ 1522651712aSMickaël Salaün TRACE_PREFIX(task) \ 1532651712aSMickaël Salaün "landlock_deny_ptrace: " \ 1542651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1552651712aSMickaël Salaün "same_exec=[01] " \ 1562651712aSMickaël Salaün "logged=[01] " \ 157aef2dd32SMickaël Salaün "tracee_domain=[0-9a-f]\\+ " \ 1582651712aSMickaël Salaün "tracee_pid=[0-9]\\+ " \ 1592651712aSMickaël Salaün "tracee_comm=[^ ]*$" 1602651712aSMickaël Salaün 1612651712aSMickaël Salaün #define REGEX_DENY_SCOPE_SIGNAL(task) \ 1622651712aSMickaël Salaün TRACE_PREFIX(task) \ 1632651712aSMickaël Salaün "landlock_deny_scope_signal: " \ 1642651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1652651712aSMickaël Salaün "same_exec=[01] " \ 1662651712aSMickaël Salaün "logged=[01] " \ 167aef2dd32SMickaël Salaün "target_domain=[0-9a-f]\\+ " \ 1682651712aSMickaël Salaün "target_pid=[0-9]\\+ " \ 1692651712aSMickaël Salaün "target_comm=[^ ]*$" 1702651712aSMickaël Salaün 1712651712aSMickaël Salaün #define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \ 1722651712aSMickaël Salaün TRACE_PREFIX(task) \ 1732651712aSMickaël Salaün "landlock_deny_scope_abstract_unix_socket: " \ 1742651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1752651712aSMickaël Salaün "same_exec=[01] " \ 1762651712aSMickaël Salaün "logged=[01] " \ 177aef2dd32SMickaël Salaün "peer_domain=[0-9a-f]\\+ " \ 1782651712aSMickaël Salaün "peer_pid=[0-9]\\+ " \ 1792651712aSMickaël Salaün "sun_path=[^ ]*$" 1802651712aSMickaël Salaün 1812651712aSMickaël Salaün #define REGEX_FREE_DOMAIN(task) \ 1822651712aSMickaël Salaün TRACE_PREFIX(task) \ 1832651712aSMickaël Salaün "landlock_free_domain: " \ 1842651712aSMickaël Salaün "domain=[0-9a-f]\\+ " \ 1852651712aSMickaël Salaün "denials=[0-9]\\+$" 1862651712aSMickaël Salaün 1872651712aSMickaël Salaün #define REGEX_FREE_RULESET(task) \ 1882651712aSMickaël Salaün TRACE_PREFIX(task) \ 1892651712aSMickaël Salaün "landlock_free_ruleset: " \ 1902651712aSMickaël Salaün "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" 1912651712aSMickaël Salaün 1922651712aSMickaël Salaün static int __maybe_unused tracefs_write(const char *path, const char *value) 1932651712aSMickaël Salaün { 1942651712aSMickaël Salaün int fd; 1952651712aSMickaël Salaün ssize_t ret; 1962651712aSMickaël Salaün size_t len = strlen(value); 1972651712aSMickaël Salaün 1982651712aSMickaël Salaün fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC); 1992651712aSMickaël Salaün if (fd < 0) 2002651712aSMickaël Salaün return -errno; 2012651712aSMickaël Salaün 2022651712aSMickaël Salaün ret = write(fd, value, len); 2032651712aSMickaël Salaün close(fd); 2042651712aSMickaël Salaün if (ret < 0) 2052651712aSMickaël Salaün return -errno; 2062651712aSMickaël Salaün if ((size_t)ret != len) 2072651712aSMickaël Salaün return -EIO; 2082651712aSMickaël Salaün 2092651712aSMickaël Salaün return 0; 2102651712aSMickaël Salaün } 2112651712aSMickaël Salaün 2122651712aSMickaël Salaün static int __maybe_unused tracefs_write_int(const char *path, int value) 2132651712aSMickaël Salaün { 2142651712aSMickaël Salaün char buf[32]; 2152651712aSMickaël Salaün 2162651712aSMickaël Salaün snprintf(buf, sizeof(buf), "%d", value); 2172651712aSMickaël Salaün return tracefs_write(path, buf); 2182651712aSMickaël Salaün } 2192651712aSMickaël Salaün 2202651712aSMickaël Salaün static int __maybe_unused tracefs_setup(void) 2212651712aSMickaël Salaün { 2222651712aSMickaël Salaün struct stat st; 2232651712aSMickaël Salaün 2242651712aSMickaël Salaün /* Mount tracefs if not already mounted. */ 2252651712aSMickaël Salaün if (stat(TRACEFS_ROOT, &st) != 0) { 2262651712aSMickaël Salaün int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL); 2272651712aSMickaël Salaün 2282651712aSMickaël Salaün if (ret) 2292651712aSMickaël Salaün return -errno; 2302651712aSMickaël Salaün } 2312651712aSMickaël Salaün 2322651712aSMickaël Salaün /* Verify landlock events are available. */ 2332651712aSMickaël Salaün if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0) 2342651712aSMickaël Salaün return -ENOENT; 2352651712aSMickaël Salaün 2362651712aSMickaël Salaün return 0; 2372651712aSMickaël Salaün } 2382651712aSMickaël Salaün 2392651712aSMickaël Salaün /* 2402651712aSMickaël Salaün * Set up PID-based event filtering so only events from the current process and 2412651712aSMickaël Salaün * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it 2422651712aSMickaël Salaün * prevents events from unrelated processes from polluting the trace buffer. 2432651712aSMickaël Salaün */ 2442651712aSMickaël Salaün static int __maybe_unused tracefs_set_pid_filter(pid_t pid) 2452651712aSMickaël Salaün { 2462651712aSMickaël Salaün int ret; 2472651712aSMickaël Salaün 2482651712aSMickaël Salaün /* Enable event-fork so children inherit the PID filter. */ 2492651712aSMickaël Salaün ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1"); 2502651712aSMickaël Salaün if (ret) 2512651712aSMickaël Salaün return ret; 2522651712aSMickaël Salaün 2532651712aSMickaël Salaün return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid); 2542651712aSMickaël Salaün } 2552651712aSMickaël Salaün 2562651712aSMickaël Salaün /* Clear the PID filter to stop filtering by PID. */ 2572651712aSMickaël Salaün static int __maybe_unused tracefs_clear_pid_filter(void) 2582651712aSMickaël Salaün { 2592651712aSMickaël Salaün return tracefs_write(TRACEFS_SET_EVENT_PID, ""); 2602651712aSMickaël Salaün } 2612651712aSMickaël Salaün 2622651712aSMickaël Salaün static int __maybe_unused tracefs_enable_event(const char *enable_path, 2632651712aSMickaël Salaün bool enable) 2642651712aSMickaël Salaün { 2652651712aSMickaël Salaün return tracefs_write(enable_path, enable ? "1" : "0"); 2662651712aSMickaël Salaün } 2672651712aSMickaël Salaün 2682651712aSMickaël Salaün static int __maybe_unused tracefs_clear(void) 2692651712aSMickaël Salaün { 2702651712aSMickaël Salaün return tracefs_write(TRACEFS_TRACE, ""); 2712651712aSMickaël Salaün } 2722651712aSMickaël Salaün 2732651712aSMickaël Salaün /* 2742651712aSMickaël Salaün * Reads the trace buffer content into a newly allocated buffer. The caller is 2752651712aSMickaël Salaün * responsible for freeing the returned buffer. Returns NULL on error. 2762651712aSMickaël Salaün */ 2772651712aSMickaël Salaün static char __maybe_unused *tracefs_read_trace(void) 2782651712aSMickaël Salaün { 2792651712aSMickaël Salaün char *buf; 2802651712aSMickaël Salaün int fd; 2812651712aSMickaël Salaün ssize_t total = 0, ret; 2822651712aSMickaël Salaün 2832651712aSMickaël Salaün buf = malloc(TRACE_BUFFER_SIZE); 2842651712aSMickaël Salaün if (!buf) 2852651712aSMickaël Salaün return NULL; 2862651712aSMickaël Salaün 2872651712aSMickaël Salaün fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC); 2882651712aSMickaël Salaün if (fd < 0) { 2892651712aSMickaël Salaün free(buf); 2902651712aSMickaël Salaün return NULL; 2912651712aSMickaël Salaün } 2922651712aSMickaël Salaün 2932651712aSMickaël Salaün while (total < TRACE_BUFFER_SIZE - 1) { 2942651712aSMickaël Salaün ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total); 2952651712aSMickaël Salaün if (ret <= 0) 2962651712aSMickaël Salaün break; 2972651712aSMickaël Salaün total += ret; 2982651712aSMickaël Salaün } 2992651712aSMickaël Salaün close(fd); 3002651712aSMickaël Salaün buf[total] = '\0'; 3012651712aSMickaël Salaün return buf; 3022651712aSMickaël Salaün } 3032651712aSMickaël Salaün 3042651712aSMickaël Salaün /* Counts the number of lines in @buf matching the basic regex @pattern. */ 3052651712aSMickaël Salaün static int __maybe_unused tracefs_count_matches(const char *buf, 3062651712aSMickaël Salaün const char *pattern) 3072651712aSMickaël Salaün { 3082651712aSMickaël Salaün regex_t regex; 3092651712aSMickaël Salaün int count = 0; 3102651712aSMickaël Salaün const char *line, *end; 3112651712aSMickaël Salaün 3122651712aSMickaël Salaün if (regcomp(®ex, pattern, 0) != 0) 3132651712aSMickaël Salaün return -EINVAL; 3142651712aSMickaël Salaün 3152651712aSMickaël Salaün line = buf; 3162651712aSMickaël Salaün while (*line) { 3172651712aSMickaël Salaün end = strchr(line, '\n'); 3182651712aSMickaël Salaün if (!end) 3192651712aSMickaël Salaün end = line + strlen(line); 3202651712aSMickaël Salaün 3212651712aSMickaël Salaün /* Create a temporary NUL-terminated line. */ 3222651712aSMickaël Salaün size_t len = end - line; 3232651712aSMickaël Salaün char *tmp = malloc(len + 1); 3242651712aSMickaël Salaün 3252651712aSMickaël Salaün if (tmp) { 3262651712aSMickaël Salaün memcpy(tmp, line, len); 3272651712aSMickaël Salaün tmp[len] = '\0'; 3282651712aSMickaël Salaün if (regexec(®ex, tmp, 0, NULL, 0) == 0) 3292651712aSMickaël Salaün count++; 3302651712aSMickaël Salaün free(tmp); 3312651712aSMickaël Salaün } 3322651712aSMickaël Salaün 3332651712aSMickaël Salaün if (*end == '\n') 3342651712aSMickaël Salaün line = end + 1; 3352651712aSMickaël Salaün else 3362651712aSMickaël Salaün break; 3372651712aSMickaël Salaün } 3382651712aSMickaël Salaün 3392651712aSMickaël Salaün regfree(®ex); 3402651712aSMickaël Salaün return count; 3412651712aSMickaël Salaün } 3422651712aSMickaël Salaün 3432651712aSMickaël Salaün /* 3442651712aSMickaël Salaün * Extracts the value of a named field from a trace line in @buf. Searches for 3452651712aSMickaël Salaün * the first line matching @line_pattern, then extracts the value after 3462651712aSMickaël Salaün * "@field_name=" into @out. Stops at space or newline. 3472651712aSMickaël Salaün * 3482651712aSMickaël Salaün * Returns 0 on success, -ENOENT if no match. 3492651712aSMickaël Salaün */ 3502651712aSMickaël Salaün static int __maybe_unused tracefs_extract_field(const char *buf, 3512651712aSMickaël Salaün const char *line_pattern, 3522651712aSMickaël Salaün const char *field_name, 3532651712aSMickaël Salaün char *out, size_t out_size) 3542651712aSMickaël Salaün { 3552651712aSMickaël Salaün regex_t regex; 3562651712aSMickaël Salaün const char *line, *end; 3572651712aSMickaël Salaün 3582651712aSMickaël Salaün if (regcomp(®ex, line_pattern, 0) != 0) 3592651712aSMickaël Salaün return -EINVAL; 3602651712aSMickaël Salaün 3612651712aSMickaël Salaün line = buf; 3622651712aSMickaël Salaün while (*line) { 3632651712aSMickaël Salaün end = strchr(line, '\n'); 3642651712aSMickaël Salaün if (!end) 3652651712aSMickaël Salaün end = line + strlen(line); 3662651712aSMickaël Salaün 3672651712aSMickaël Salaün size_t len = end - line; 3682651712aSMickaël Salaün char *tmp = malloc(len + 1); 3692651712aSMickaël Salaün 3702651712aSMickaël Salaün if (tmp) { 3712651712aSMickaël Salaün const char *field, *val_start; 3722651712aSMickaël Salaün size_t field_len, val_len; 3732651712aSMickaël Salaün 3742651712aSMickaël Salaün memcpy(tmp, line, len); 3752651712aSMickaël Salaün tmp[len] = '\0'; 3762651712aSMickaël Salaün 3772651712aSMickaël Salaün if (regexec(®ex, tmp, 0, NULL, 0) != 0) { 3782651712aSMickaël Salaün free(tmp); 3792651712aSMickaël Salaün goto next; 3802651712aSMickaël Salaün } 3812651712aSMickaël Salaün 3822651712aSMickaël Salaün /* 3832651712aSMickaël Salaün * Find "field_name=" in the line, ensuring a word 3842651712aSMickaël Salaün * boundary before the field name to avoid substring 3852651712aSMickaël Salaün * matches (e.g., "port" in "sport"). 3862651712aSMickaël Salaün */ 3872651712aSMickaël Salaün field_len = strlen(field_name); 3882651712aSMickaël Salaün field = tmp; 3892651712aSMickaël Salaün while ((field = strstr(field, field_name))) { 3902651712aSMickaël Salaün if (field[field_len] == '=' && 3912651712aSMickaël Salaün (field == tmp || field[-1] == ' ')) 3922651712aSMickaël Salaün break; 3932651712aSMickaël Salaün field++; 3942651712aSMickaël Salaün } 3952651712aSMickaël Salaün if (!field) { 3962651712aSMickaël Salaün free(tmp); 3972651712aSMickaël Salaün regfree(®ex); 3982651712aSMickaël Salaün return -ENOENT; 3992651712aSMickaël Salaün } 4002651712aSMickaël Salaün 4012651712aSMickaël Salaün val_start = field + field_len + 1; 4022651712aSMickaël Salaün val_len = 0; 4032651712aSMickaël Salaün while (val_start[val_len] && 4042651712aSMickaël Salaün val_start[val_len] != ' ' && 4052651712aSMickaël Salaün val_start[val_len] != '\n') 4062651712aSMickaël Salaün val_len++; 4072651712aSMickaël Salaün 4082651712aSMickaël Salaün if (val_len >= out_size) 4092651712aSMickaël Salaün val_len = out_size - 1; 4102651712aSMickaël Salaün memcpy(out, val_start, val_len); 4112651712aSMickaël Salaün out[val_len] = '\0'; 4122651712aSMickaël Salaün 4132651712aSMickaël Salaün free(tmp); 4142651712aSMickaël Salaün regfree(®ex); 4152651712aSMickaël Salaün return 0; 4162651712aSMickaël Salaün } 4172651712aSMickaël Salaün next: 4182651712aSMickaël Salaün if (*end == '\n') 4192651712aSMickaël Salaün line = end + 1; 4202651712aSMickaël Salaün else 4212651712aSMickaël Salaün break; 4222651712aSMickaël Salaün } 4232651712aSMickaël Salaün 4242651712aSMickaël Salaün regfree(®ex); 4252651712aSMickaël Salaün return -ENOENT; 4262651712aSMickaël Salaün } 4272651712aSMickaël Salaün 4282651712aSMickaël Salaün /* 4292651712aSMickaël Salaün * Common fixture setup for trace tests. Mounts tracefs if needed and sets a 4302651712aSMickaël Salaün * PID filter. The caller must create a mount namespace first 4312651712aSMickaël Salaün * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs 4322651712aSMickaël Salaün * mount; the trace buffer, per-event enable flags, and PID filter are global 4332651712aSMickaël Salaün * kernel state, scoped to the test by the PID filter. 4342651712aSMickaël Salaün * 4352651712aSMickaël Salaün * Returns 0 on success, -errno on failure (caller should SKIP). 4362651712aSMickaël Salaün */ 4372651712aSMickaël Salaün static int __maybe_unused tracefs_fixture_setup(void) 4382651712aSMickaël Salaün { 4392651712aSMickaël Salaün int ret; 4402651712aSMickaël Salaün 4412651712aSMickaël Salaün ret = tracefs_setup(); 4422651712aSMickaël Salaün if (ret) 4432651712aSMickaël Salaün return ret; 4442651712aSMickaël Salaün 4452651712aSMickaël Salaün return tracefs_set_pid_filter(getpid()); 4462651712aSMickaël Salaün } 4472651712aSMickaël Salaün 4482651712aSMickaël Salaün static void __maybe_unused tracefs_fixture_teardown(void) 4492651712aSMickaël Salaün { 4502651712aSMickaël Salaün tracefs_clear_pid_filter(); 4512651712aSMickaël Salaün } 4522651712aSMickaël Salaün 4532651712aSMickaël Salaün /* 4542651712aSMickaël Salaün * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then 4552651712aSMickaël Salaün * drops the capability. Returns the value from @func, or -EPERM if the 4562651712aSMickaël Salaün * capability manipulation fails. 4572651712aSMickaël Salaün */ 4582651712aSMickaël Salaün static int __maybe_unused tracefs_priv_call(int (*func)(void)) 4592651712aSMickaël Salaün { 4602651712aSMickaël Salaün const cap_value_t admin = CAP_SYS_ADMIN; 4612651712aSMickaël Salaün cap_t cap_p; 4622651712aSMickaël Salaün int ret; 4632651712aSMickaël Salaün 4642651712aSMickaël Salaün cap_p = cap_get_proc(); 4652651712aSMickaël Salaün if (!cap_p) 4662651712aSMickaël Salaün return -EPERM; 4672651712aSMickaël Salaün 4682651712aSMickaël Salaün if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || 4692651712aSMickaël Salaün cap_set_proc(cap_p)) { 4702651712aSMickaël Salaün cap_free(cap_p); 4712651712aSMickaël Salaün return -EPERM; 4722651712aSMickaël Salaün } 4732651712aSMickaël Salaün 4742651712aSMickaël Salaün ret = func(); 4752651712aSMickaël Salaün 4762651712aSMickaël Salaün cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); 4772651712aSMickaël Salaün cap_set_proc(cap_p); 4782651712aSMickaël Salaün cap_free(cap_p); 4792651712aSMickaël Salaün return ret; 4802651712aSMickaël Salaün } 4812651712aSMickaël Salaün 4822651712aSMickaël Salaün /* Read the trace buffer with elevated privileges. Returns NULL on failure. */ 4832651712aSMickaël Salaün static char __maybe_unused *tracefs_read_buf(void) 4842651712aSMickaël Salaün { 4852651712aSMickaël Salaün /* Cannot use tracefs_priv_call() because the return type is char *. */ 4862651712aSMickaël Salaün cap_t cap_p; 4872651712aSMickaël Salaün char *buf; 4882651712aSMickaël Salaün const cap_value_t admin = CAP_SYS_ADMIN; 4892651712aSMickaël Salaün 4902651712aSMickaël Salaün cap_p = cap_get_proc(); 4912651712aSMickaël Salaün if (!cap_p) 4922651712aSMickaël Salaün return NULL; 4932651712aSMickaël Salaün 4942651712aSMickaël Salaün if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || 4952651712aSMickaël Salaün cap_set_proc(cap_p)) { 4962651712aSMickaël Salaün cap_free(cap_p); 4972651712aSMickaël Salaün return NULL; 4982651712aSMickaël Salaün } 4992651712aSMickaël Salaün 5002651712aSMickaël Salaün buf = tracefs_read_trace(); 5012651712aSMickaël Salaün 5022651712aSMickaël Salaün cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); 5032651712aSMickaël Salaün cap_set_proc(cap_p); 5042651712aSMickaël Salaün cap_free(cap_p); 5052651712aSMickaël Salaün return buf; 5062651712aSMickaël Salaün } 5072651712aSMickaël Salaün 5082651712aSMickaël Salaün /* Clear the trace buffer with elevated privileges. Returns 0 on success. */ 5092651712aSMickaël Salaün static int __maybe_unused tracefs_clear_buf(void) 5102651712aSMickaël Salaün { 5112651712aSMickaël Salaün return tracefs_priv_call(tracefs_clear); 5122651712aSMickaël Salaün } 5132651712aSMickaël Salaün 5142651712aSMickaël Salaün /* 5152651712aSMickaël Salaün * Forks a child that creates a Landlock sandbox and performs an FS access. The 5162651712aSMickaël Salaün * parent waits for the child, then reads the trace buffer. 5172651712aSMickaël Salaün * 5182651712aSMickaël Salaün * Requires common.h and wrappers.h to be included before trace.h. 5192651712aSMickaël Salaün */ 5202651712aSMickaël Salaün static void __maybe_unused sandbox_child_fs_access( 5212651712aSMickaël Salaün struct __test_metadata *const _metadata, const char *rule_path, 5222651712aSMickaël Salaün __u64 handled_access, __u64 allowed_access, const char *access_path) 5232651712aSMickaël Salaün { 5242651712aSMickaël Salaün pid_t pid; 5252651712aSMickaël Salaün int status; 5262651712aSMickaël Salaün 5272651712aSMickaël Salaün pid = fork(); 5282651712aSMickaël Salaün ASSERT_LE(0, pid); 5292651712aSMickaël Salaün 5302651712aSMickaël Salaün if (pid == 0) { 5312651712aSMickaël Salaün struct landlock_ruleset_attr ruleset_attr = { 5322651712aSMickaël Salaün .handled_access_fs = handled_access, 5332651712aSMickaël Salaün }; 5342651712aSMickaël Salaün struct landlock_path_beneath_attr path_beneath = { 5352651712aSMickaël Salaün .allowed_access = allowed_access, 5362651712aSMickaël Salaün }; 5372651712aSMickaël Salaün int ruleset_fd, fd; 5382651712aSMickaël Salaün 5392651712aSMickaël Salaün ruleset_fd = landlock_create_ruleset(&ruleset_attr, 5402651712aSMickaël Salaün sizeof(ruleset_attr), 0); 5412651712aSMickaël Salaün if (ruleset_fd < 0) 5422651712aSMickaël Salaün _exit(1); 5432651712aSMickaël Salaün 5442651712aSMickaël Salaün path_beneath.parent_fd = 5452651712aSMickaël Salaün open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC); 5462651712aSMickaël Salaün if (path_beneath.parent_fd < 0) { 5472651712aSMickaël Salaün close(ruleset_fd); 5482651712aSMickaël Salaün _exit(1); 5492651712aSMickaël Salaün } 5502651712aSMickaël Salaün 5512651712aSMickaël Salaün if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 5522651712aSMickaël Salaün &path_beneath, 0)) { 5532651712aSMickaël Salaün close(path_beneath.parent_fd); 5542651712aSMickaël Salaün close(ruleset_fd); 5552651712aSMickaël Salaün _exit(1); 5562651712aSMickaël Salaün } 5572651712aSMickaël Salaün close(path_beneath.parent_fd); 5582651712aSMickaël Salaün 5592651712aSMickaël Salaün prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 5602651712aSMickaël Salaün if (landlock_restrict_self(ruleset_fd, 0)) { 5612651712aSMickaël Salaün close(ruleset_fd); 5622651712aSMickaël Salaün _exit(1); 5632651712aSMickaël Salaün } 5642651712aSMickaël Salaün close(ruleset_fd); 5652651712aSMickaël Salaün 5662651712aSMickaël Salaün fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); 5672651712aSMickaël Salaün if (fd >= 0) 5682651712aSMickaël Salaün close(fd); 5692651712aSMickaël Salaün 5702651712aSMickaël Salaün _exit(0); 5712651712aSMickaël Salaün } 5722651712aSMickaël Salaün 5732651712aSMickaël Salaün ASSERT_EQ(pid, waitpid(pid, &status, 0)); 5742651712aSMickaël Salaün ASSERT_TRUE(WIFEXITED(status)); 5752651712aSMickaël Salaün EXPECT_EQ(0, WEXITSTATUS(status)); 5762651712aSMickaël Salaün } 5772651712aSMickaël Salaün 5782651712aSMickaël Salaün /* 5792651712aSMickaël Salaün * Forks a child that creates a Landlock sandbox allowing execute+read_dir for 5802651712aSMickaël Salaün * /usr and execute-only for ".", then execs ./true. The true binary opens "." 5812651712aSMickaël Salaün * on startup, triggering a read_dir denial with same_exec=0. The parent waits 5822651712aSMickaël Salaün * for the child to exit. 5832651712aSMickaël Salaün */ 5842651712aSMickaël Salaün static void __maybe_unused sandbox_child_exec_true( 5852651712aSMickaël Salaün struct __test_metadata *const _metadata, __u32 restrict_flags) 5862651712aSMickaël Salaün { 5872651712aSMickaël Salaün pid_t pid; 5882651712aSMickaël Salaün int status; 5892651712aSMickaël Salaün 5902651712aSMickaël Salaün pid = fork(); 5912651712aSMickaël Salaün ASSERT_LE(0, pid); 5922651712aSMickaël Salaün 5932651712aSMickaël Salaün if (pid == 0) { 5942651712aSMickaël Salaün struct landlock_ruleset_attr attr = { 5952651712aSMickaël Salaün .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR | 5962651712aSMickaël Salaün LANDLOCK_ACCESS_FS_EXECUTE, 5972651712aSMickaël Salaün }; 5982651712aSMickaël Salaün struct landlock_path_beneath_attr path_beneath = { 5992651712aSMickaël Salaün .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE | 6002651712aSMickaël Salaün LANDLOCK_ACCESS_FS_READ_DIR, 6012651712aSMickaël Salaün }; 6022651712aSMickaël Salaün int ruleset_fd; 6032651712aSMickaël Salaün 6042651712aSMickaël Salaün ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0); 6052651712aSMickaël Salaün if (ruleset_fd < 0) 6062651712aSMickaël Salaün _exit(1); 6072651712aSMickaël Salaün 6082651712aSMickaël Salaün path_beneath.parent_fd = 6092651712aSMickaël Salaün open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); 6102651712aSMickaël Salaün if (path_beneath.parent_fd >= 0) { 6112651712aSMickaël Salaün landlock_add_rule(ruleset_fd, 6122651712aSMickaël Salaün LANDLOCK_RULE_PATH_BENEATH, 6132651712aSMickaël Salaün &path_beneath, 0); 6142651712aSMickaël Salaün close(path_beneath.parent_fd); 6152651712aSMickaël Salaün } 6162651712aSMickaël Salaün 6172651712aSMickaël Salaün path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE; 6182651712aSMickaël Salaün path_beneath.parent_fd = 6192651712aSMickaël Salaün open(".", O_PATH | O_DIRECTORY | O_CLOEXEC); 6202651712aSMickaël Salaün if (path_beneath.parent_fd >= 0) { 6212651712aSMickaël Salaün landlock_add_rule(ruleset_fd, 6222651712aSMickaël Salaün LANDLOCK_RULE_PATH_BENEATH, 6232651712aSMickaël Salaün &path_beneath, 0); 6242651712aSMickaël Salaün close(path_beneath.parent_fd); 6252651712aSMickaël Salaün } 6262651712aSMickaël Salaün 6272651712aSMickaël Salaün prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 6282651712aSMickaël Salaün if (landlock_restrict_self(ruleset_fd, restrict_flags)) 6292651712aSMickaël Salaün _exit(1); 6302651712aSMickaël Salaün close(ruleset_fd); 6312651712aSMickaël Salaün 6322651712aSMickaël Salaün execl("./true", "./true", NULL); 6332651712aSMickaël Salaün _exit(1); 6342651712aSMickaël Salaün } 6352651712aSMickaël Salaün 6362651712aSMickaël Salaün ASSERT_EQ(pid, waitpid(pid, &status, 0)); 6372651712aSMickaël Salaün ASSERT_TRUE(WIFEXITED(status)); 6382651712aSMickaël Salaün EXPECT_EQ(0, WEXITSTATUS(status)); 6392651712aSMickaël Salaün } 640