1 /* SPDX-License-Identifier: GPL-2.0 */ 2 /* 3 * Landlock trace test helpers 4 * 5 * Copyright © 2026 Cloudflare, Inc. 6 */ 7 8 #define _GNU_SOURCE 9 #include <errno.h> 10 #include <fcntl.h> 11 #include <regex.h> 12 #include <stdbool.h> 13 #include <stdio.h> 14 #include <stdlib.h> 15 #include <string.h> 16 #include <sys/mount.h> 17 #include <sys/stat.h> 18 #include <unistd.h> 19 20 #include "kselftest_harness.h" 21 22 #define TRACEFS_ROOT "/sys/kernel/tracing" 23 #define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock" 24 #define TRACEFS_CREATE_RULESET_ENABLE \ 25 TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable" 26 #define TRACEFS_CREATE_DOMAIN_ENABLE \ 27 TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable" 28 #define TRACEFS_ENFORCE_DOMAIN_ENABLE \ 29 TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable" 30 #define TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE \ 31 TRACEFS_LANDLOCK_DIR "/landlock_add_rule_path_beneath/enable" 32 #define TRACEFS_ADD_RULE_NET_PORT_ENABLE \ 33 TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net_port/enable" 34 #define TRACEFS_CHECK_RULE_FS_ENABLE \ 35 TRACEFS_LANDLOCK_DIR "/landlock_check_rule_inode/enable" 36 #define TRACEFS_CHECK_RULE_NET_ENABLE \ 37 TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net_port/enable" 38 #define TRACEFS_DENY_ACCESS_FS_ENABLE \ 39 TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable" 40 #define TRACEFS_DENY_ACCESS_NET_ENABLE \ 41 TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable" 42 #define TRACEFS_DENY_PTRACE_ENABLE \ 43 TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable" 44 #define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \ 45 TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable" 46 #define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \ 47 TRACEFS_LANDLOCK_DIR \ 48 "/landlock_deny_scope_abstract_unix_socket/enable" 49 #define TRACEFS_FREE_DOMAIN_ENABLE \ 50 TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable" 51 #define TRACEFS_FREE_RULESET_ENABLE \ 52 TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable" 53 #define TRACEFS_TRACE TRACEFS_ROOT "/trace" 54 #define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid" 55 #define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork" 56 57 #define TRACE_BUFFER_SIZE (64 * 1024) 58 59 /* 60 * Trace line prefix: matches the ftrace "trace" file format. Format: " 61 * <task>-<pid> [<cpu>] <flags> <timestamp>: " 62 * 63 * The task parameter must be a string literal truncated to 15 chars 64 * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The 65 * pattern accepts either the expected task name or "<...>" because the ftrace 66 * comm cache may evict short-lived processes (e.g., forked children that exit 67 * before the trace buffer is read). 68 * 69 * No unescaped '.' in any REGEX macro; literal dots use '\\.'. 70 */ 71 #define TRACE_PREFIX(task) \ 72 "^ *\\(<\\.\\.\\.>" \ 73 "\\|" task "\\)" \ 74 "-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: " 75 76 /* 77 * Task name for events emitted by kworker threads (e.g., free_domain fires from 78 * a work queue, not from the test process). 79 */ 80 #define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+" 81 82 #define REGEX_ADD_RULE_PATH_BENEATH(task) \ 83 TRACE_PREFIX(task) \ 84 "landlock_add_rule_path_beneath: " \ 85 "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ 86 "access_rights=[a-z_|]* " \ 87 "dev=[0-9]\\+:[0-9]\\+ " \ 88 "ino=[0-9]\\+ " \ 89 "path=[^ ]\\+$" 90 91 #define REGEX_ADD_RULE_NET_PORT(task) \ 92 TRACE_PREFIX(task) \ 93 "landlock_add_rule_net_port: " \ 94 "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ 95 "access_rights=[a-z_|]* " \ 96 "port=[0-9]\\+$" 97 98 #define REGEX_CREATE_RULESET(task) \ 99 TRACE_PREFIX(task) \ 100 "landlock_create_ruleset: " \ 101 "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \ 102 "handled_fs=[a-z_|]* " \ 103 "handled_net=[a-z_|]* " \ 104 "scoped=[a-z_|]*$" 105 106 #define REGEX_CREATE_DOMAIN(task) \ 107 TRACE_PREFIX(task) \ 108 "landlock_create_domain: " \ 109 "domain=[0-9a-f]\\+ " \ 110 "parent=[0-9a-f]\\+ " \ 111 "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" 112 113 #define REGEX_CHECK_RULE_FS(task) \ 114 TRACE_PREFIX(task) \ 115 "landlock_check_rule_inode: " \ 116 "domain=[0-9a-f]\\+ " \ 117 "access_request=[a-z_|]* " \ 118 "dev=[0-9]\\+:[0-9]\\+ " \ 119 "ino=[0-9]\\+ " \ 120 "grants={[a-z_|,]*}$" 121 122 #define REGEX_CHECK_RULE_NET(task) \ 123 TRACE_PREFIX(task) \ 124 "landlock_check_rule_net_port: " \ 125 "domain=[0-9a-f]\\+ " \ 126 "access_request=[a-z_|]* " \ 127 "port=[0-9]\\+ " \ 128 "grants={[a-z_|,]*}$" 129 130 #define REGEX_DENY_ACCESS_FS(task) \ 131 TRACE_PREFIX(task) \ 132 "landlock_deny_access_fs: " \ 133 "domain=[0-9a-f]\\+ " \ 134 "same_exec=[01] " \ 135 "logged=[01] " \ 136 "blockers=[a-z_|]* " \ 137 "dev=[0-9]\\+:[0-9]\\+ " \ 138 "ino=[0-9]\\+ " \ 139 "path=[^ ]*$" 140 141 #define REGEX_DENY_ACCESS_NET(task) \ 142 TRACE_PREFIX(task) \ 143 "landlock_deny_access_net: " \ 144 "domain=[0-9a-f]\\+ " \ 145 "same_exec=[01] " \ 146 "logged=[01] " \ 147 "blockers=[a-z_|]* " \ 148 "port=-\\?[0-9]\\+$" 149 150 #define REGEX_DENY_PTRACE(task) \ 151 TRACE_PREFIX(task) \ 152 "landlock_deny_ptrace: " \ 153 "domain=[0-9a-f]\\+ " \ 154 "same_exec=[01] " \ 155 "logged=[01] " \ 156 "tracee_domain=[0-9a-f]\\+ " \ 157 "tracee_pid=[0-9]\\+ " \ 158 "tracee_comm=[^ ]*$" 159 160 #define REGEX_DENY_SCOPE_SIGNAL(task) \ 161 TRACE_PREFIX(task) \ 162 "landlock_deny_scope_signal: " \ 163 "domain=[0-9a-f]\\+ " \ 164 "same_exec=[01] " \ 165 "logged=[01] " \ 166 "target_domain=[0-9a-f]\\+ " \ 167 "target_pid=[0-9]\\+ " \ 168 "target_comm=[^ ]*$" 169 170 #define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \ 171 TRACE_PREFIX(task) \ 172 "landlock_deny_scope_abstract_unix_socket: " \ 173 "domain=[0-9a-f]\\+ " \ 174 "same_exec=[01] " \ 175 "logged=[01] " \ 176 "peer_domain=[0-9a-f]\\+ " \ 177 "peer_pid=[0-9]\\+ " \ 178 "sun_path=[^ ]*$" 179 180 #define REGEX_FREE_DOMAIN(task) \ 181 TRACE_PREFIX(task) \ 182 "landlock_free_domain: " \ 183 "domain=[0-9a-f]\\+ " \ 184 "denials=[0-9]\\+$" 185 186 #define REGEX_FREE_RULESET(task) \ 187 TRACE_PREFIX(task) \ 188 "landlock_free_ruleset: " \ 189 "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" 190 191 static int __maybe_unused tracefs_write(const char *path, const char *value) 192 { 193 int fd; 194 ssize_t ret; 195 size_t len = strlen(value); 196 197 fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC); 198 if (fd < 0) 199 return -errno; 200 201 ret = write(fd, value, len); 202 close(fd); 203 if (ret < 0) 204 return -errno; 205 if ((size_t)ret != len) 206 return -EIO; 207 208 return 0; 209 } 210 211 static int __maybe_unused tracefs_write_int(const char *path, int value) 212 { 213 char buf[32]; 214 215 snprintf(buf, sizeof(buf), "%d", value); 216 return tracefs_write(path, buf); 217 } 218 219 static int __maybe_unused tracefs_setup(void) 220 { 221 struct stat st; 222 223 /* Mount tracefs if not already mounted. */ 224 if (stat(TRACEFS_ROOT, &st) != 0) { 225 int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL); 226 227 if (ret) 228 return -errno; 229 } 230 231 /* Verify landlock events are available. */ 232 if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0) 233 return -ENOENT; 234 235 return 0; 236 } 237 238 /* 239 * Set up PID-based event filtering so only events from the current process and 240 * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it 241 * prevents events from unrelated processes from polluting the trace buffer. 242 */ 243 static int __maybe_unused tracefs_set_pid_filter(pid_t pid) 244 { 245 int ret; 246 247 /* Enable event-fork so children inherit the PID filter. */ 248 ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1"); 249 if (ret) 250 return ret; 251 252 return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid); 253 } 254 255 /* Clear the PID filter to stop filtering by PID. */ 256 static int __maybe_unused tracefs_clear_pid_filter(void) 257 { 258 return tracefs_write(TRACEFS_SET_EVENT_PID, ""); 259 } 260 261 static int __maybe_unused tracefs_enable_event(const char *enable_path, 262 bool enable) 263 { 264 return tracefs_write(enable_path, enable ? "1" : "0"); 265 } 266 267 static int __maybe_unused tracefs_clear(void) 268 { 269 return tracefs_write(TRACEFS_TRACE, ""); 270 } 271 272 /* 273 * Reads the trace buffer content into a newly allocated buffer. The caller is 274 * responsible for freeing the returned buffer. Returns NULL on error. 275 */ 276 static char __maybe_unused *tracefs_read_trace(void) 277 { 278 char *buf; 279 int fd; 280 ssize_t total = 0, ret; 281 282 buf = malloc(TRACE_BUFFER_SIZE); 283 if (!buf) 284 return NULL; 285 286 fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC); 287 if (fd < 0) { 288 free(buf); 289 return NULL; 290 } 291 292 while (total < TRACE_BUFFER_SIZE - 1) { 293 ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total); 294 if (ret <= 0) 295 break; 296 total += ret; 297 } 298 close(fd); 299 buf[total] = '\0'; 300 return buf; 301 } 302 303 /* Counts the number of lines in @buf matching the basic regex @pattern. */ 304 static int __maybe_unused tracefs_count_matches(const char *buf, 305 const char *pattern) 306 { 307 regex_t regex; 308 int count = 0; 309 const char *line, *end; 310 311 if (regcomp(®ex, pattern, 0) != 0) 312 return -EINVAL; 313 314 line = buf; 315 while (*line) { 316 end = strchr(line, '\n'); 317 if (!end) 318 end = line + strlen(line); 319 320 /* Create a temporary NUL-terminated line. */ 321 size_t len = end - line; 322 char *tmp = malloc(len + 1); 323 324 if (tmp) { 325 memcpy(tmp, line, len); 326 tmp[len] = '\0'; 327 if (regexec(®ex, tmp, 0, NULL, 0) == 0) 328 count++; 329 free(tmp); 330 } 331 332 if (*end == '\n') 333 line = end + 1; 334 else 335 break; 336 } 337 338 regfree(®ex); 339 return count; 340 } 341 342 /* 343 * Extracts the value of a named field from a trace line in @buf. Searches for 344 * the first line matching @line_pattern, then extracts the value after 345 * "@field_name=" into @out. Stops at space or newline. 346 * 347 * Returns 0 on success, -ENOENT if no match. 348 */ 349 static int __maybe_unused tracefs_extract_field(const char *buf, 350 const char *line_pattern, 351 const char *field_name, 352 char *out, size_t out_size) 353 { 354 regex_t regex; 355 const char *line, *end; 356 357 if (regcomp(®ex, line_pattern, 0) != 0) 358 return -EINVAL; 359 360 line = buf; 361 while (*line) { 362 end = strchr(line, '\n'); 363 if (!end) 364 end = line + strlen(line); 365 366 size_t len = end - line; 367 char *tmp = malloc(len + 1); 368 369 if (tmp) { 370 const char *field, *val_start; 371 size_t field_len, val_len; 372 373 memcpy(tmp, line, len); 374 tmp[len] = '\0'; 375 376 if (regexec(®ex, tmp, 0, NULL, 0) != 0) { 377 free(tmp); 378 goto next; 379 } 380 381 /* 382 * Find "field_name=" in the line, ensuring a word 383 * boundary before the field name to avoid substring 384 * matches (e.g., "port" in "sport"). 385 */ 386 field_len = strlen(field_name); 387 field = tmp; 388 while ((field = strstr(field, field_name))) { 389 if (field[field_len] == '=' && 390 (field == tmp || field[-1] == ' ')) 391 break; 392 field++; 393 } 394 if (!field) { 395 free(tmp); 396 regfree(®ex); 397 return -ENOENT; 398 } 399 400 val_start = field + field_len + 1; 401 val_len = 0; 402 while (val_start[val_len] && 403 val_start[val_len] != ' ' && 404 val_start[val_len] != '\n') 405 val_len++; 406 407 if (val_len >= out_size) 408 val_len = out_size - 1; 409 memcpy(out, val_start, val_len); 410 out[val_len] = '\0'; 411 412 free(tmp); 413 regfree(®ex); 414 return 0; 415 } 416 next: 417 if (*end == '\n') 418 line = end + 1; 419 else 420 break; 421 } 422 423 regfree(®ex); 424 return -ENOENT; 425 } 426 427 /* 428 * Common fixture setup for trace tests. Mounts tracefs if needed and sets a 429 * PID filter. The caller must create a mount namespace first 430 * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs 431 * mount; the trace buffer, per-event enable flags, and PID filter are global 432 * kernel state, scoped to the test by the PID filter. 433 * 434 * Returns 0 on success, -errno on failure (caller should SKIP). 435 */ 436 static int __maybe_unused tracefs_fixture_setup(void) 437 { 438 int ret; 439 440 ret = tracefs_setup(); 441 if (ret) 442 return ret; 443 444 return tracefs_set_pid_filter(getpid()); 445 } 446 447 static void __maybe_unused tracefs_fixture_teardown(void) 448 { 449 tracefs_clear_pid_filter(); 450 } 451 452 /* 453 * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then 454 * drops the capability. Returns the value from @func, or -EPERM if the 455 * capability manipulation fails. 456 */ 457 static int __maybe_unused tracefs_priv_call(int (*func)(void)) 458 { 459 const cap_value_t admin = CAP_SYS_ADMIN; 460 cap_t cap_p; 461 int ret; 462 463 cap_p = cap_get_proc(); 464 if (!cap_p) 465 return -EPERM; 466 467 if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || 468 cap_set_proc(cap_p)) { 469 cap_free(cap_p); 470 return -EPERM; 471 } 472 473 ret = func(); 474 475 cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); 476 cap_set_proc(cap_p); 477 cap_free(cap_p); 478 return ret; 479 } 480 481 /* Read the trace buffer with elevated privileges. Returns NULL on failure. */ 482 static char __maybe_unused *tracefs_read_buf(void) 483 { 484 /* Cannot use tracefs_priv_call() because the return type is char *. */ 485 cap_t cap_p; 486 char *buf; 487 const cap_value_t admin = CAP_SYS_ADMIN; 488 489 cap_p = cap_get_proc(); 490 if (!cap_p) 491 return NULL; 492 493 if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || 494 cap_set_proc(cap_p)) { 495 cap_free(cap_p); 496 return NULL; 497 } 498 499 buf = tracefs_read_trace(); 500 501 cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); 502 cap_set_proc(cap_p); 503 cap_free(cap_p); 504 return buf; 505 } 506 507 /* Clear the trace buffer with elevated privileges. Returns 0 on success. */ 508 static int __maybe_unused tracefs_clear_buf(void) 509 { 510 return tracefs_priv_call(tracefs_clear); 511 } 512 513 /* 514 * Forks a child that creates a Landlock sandbox and performs an FS access. The 515 * parent waits for the child, then reads the trace buffer. 516 * 517 * Requires common.h and wrappers.h to be included before trace.h. 518 */ 519 static void __maybe_unused sandbox_child_fs_access( 520 struct __test_metadata *const _metadata, const char *rule_path, 521 __u64 handled_access, __u64 allowed_access, const char *access_path) 522 { 523 pid_t pid; 524 int status; 525 526 pid = fork(); 527 ASSERT_LE(0, pid); 528 529 if (pid == 0) { 530 struct landlock_ruleset_attr ruleset_attr = { 531 .handled_access_fs = handled_access, 532 }; 533 struct landlock_path_beneath_attr path_beneath = { 534 .allowed_access = allowed_access, 535 }; 536 int ruleset_fd, fd; 537 538 ruleset_fd = landlock_create_ruleset(&ruleset_attr, 539 sizeof(ruleset_attr), 0); 540 if (ruleset_fd < 0) 541 _exit(1); 542 543 path_beneath.parent_fd = 544 open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC); 545 if (path_beneath.parent_fd < 0) { 546 close(ruleset_fd); 547 _exit(1); 548 } 549 550 if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 551 &path_beneath, 0)) { 552 close(path_beneath.parent_fd); 553 close(ruleset_fd); 554 _exit(1); 555 } 556 close(path_beneath.parent_fd); 557 558 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 559 if (landlock_restrict_self(ruleset_fd, 0)) { 560 close(ruleset_fd); 561 _exit(1); 562 } 563 close(ruleset_fd); 564 565 fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); 566 if (fd >= 0) 567 close(fd); 568 569 _exit(0); 570 } 571 572 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 573 ASSERT_TRUE(WIFEXITED(status)); 574 EXPECT_EQ(0, WEXITSTATUS(status)); 575 } 576 577 /* 578 * Forks a child that creates a Landlock sandbox allowing execute+read_dir for 579 * /usr and execute-only for ".", then execs ./true. The true binary opens "." 580 * on startup, triggering a read_dir denial with same_exec=0. The parent waits 581 * for the child to exit. 582 */ 583 static void __maybe_unused sandbox_child_exec_true( 584 struct __test_metadata *const _metadata, __u32 restrict_flags) 585 { 586 pid_t pid; 587 int status; 588 589 pid = fork(); 590 ASSERT_LE(0, pid); 591 592 if (pid == 0) { 593 struct landlock_ruleset_attr attr = { 594 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR | 595 LANDLOCK_ACCESS_FS_EXECUTE, 596 }; 597 struct landlock_path_beneath_attr path_beneath = { 598 .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE | 599 LANDLOCK_ACCESS_FS_READ_DIR, 600 }; 601 int ruleset_fd; 602 603 ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0); 604 if (ruleset_fd < 0) 605 _exit(1); 606 607 path_beneath.parent_fd = 608 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); 609 if (path_beneath.parent_fd >= 0) { 610 landlock_add_rule(ruleset_fd, 611 LANDLOCK_RULE_PATH_BENEATH, 612 &path_beneath, 0); 613 close(path_beneath.parent_fd); 614 } 615 616 path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE; 617 path_beneath.parent_fd = 618 open(".", O_PATH | O_DIRECTORY | O_CLOEXEC); 619 if (path_beneath.parent_fd >= 0) { 620 landlock_add_rule(ruleset_fd, 621 LANDLOCK_RULE_PATH_BENEATH, 622 &path_beneath, 0); 623 close(path_beneath.parent_fd); 624 } 625 626 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 627 if (landlock_restrict_self(ruleset_fd, restrict_flags)) 628 _exit(1); 629 close(ruleset_fd); 630 631 execl("./true", "./true", NULL); 632 _exit(1); 633 } 634 635 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 636 ASSERT_TRUE(WIFEXITED(status)); 637 EXPECT_EQ(0, WEXITSTATUS(status)); 638 } 639