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_FS_ENABLE \ 31 TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable" 32 #define TRACEFS_ADD_RULE_NET_ENABLE \ 33 TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable" 34 #define TRACEFS_CHECK_RULE_FS_ENABLE \ 35 TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable" 36 #define TRACEFS_CHECK_RULE_NET_ENABLE \ 37 TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/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_FS(task) \ 83 TRACE_PREFIX(task) \ 84 "landlock_add_rule_fs: " \ 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(task) \ 92 TRACE_PREFIX(task) \ 93 "landlock_add_rule_net: " \ 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_fs: " \ 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: " \ 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 "sport=[0-9]\\+ " \ 149 "dport=[0-9]\\+$" 150 151 #define REGEX_DENY_PTRACE(task) \ 152 TRACE_PREFIX(task) \ 153 "landlock_deny_ptrace: " \ 154 "domain=[0-9a-f]\\+ " \ 155 "same_exec=[01] " \ 156 "logged=[01] " \ 157 "tracee_domain=[0-9a-f]\\+ " \ 158 "tracee_pid=[0-9]\\+ " \ 159 "tracee_comm=[^ ]*$" 160 161 #define REGEX_DENY_SCOPE_SIGNAL(task) \ 162 TRACE_PREFIX(task) \ 163 "landlock_deny_scope_signal: " \ 164 "domain=[0-9a-f]\\+ " \ 165 "same_exec=[01] " \ 166 "logged=[01] " \ 167 "target_domain=[0-9a-f]\\+ " \ 168 "target_pid=[0-9]\\+ " \ 169 "target_comm=[^ ]*$" 170 171 #define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \ 172 TRACE_PREFIX(task) \ 173 "landlock_deny_scope_abstract_unix_socket: " \ 174 "domain=[0-9a-f]\\+ " \ 175 "same_exec=[01] " \ 176 "logged=[01] " \ 177 "peer_domain=[0-9a-f]\\+ " \ 178 "peer_pid=[0-9]\\+ " \ 179 "sun_path=[^ ]*$" 180 181 #define REGEX_FREE_DOMAIN(task) \ 182 TRACE_PREFIX(task) \ 183 "landlock_free_domain: " \ 184 "domain=[0-9a-f]\\+ " \ 185 "denials=[0-9]\\+$" 186 187 #define REGEX_FREE_RULESET(task) \ 188 TRACE_PREFIX(task) \ 189 "landlock_free_ruleset: " \ 190 "ruleset=[0-9a-f]\\+\\.[0-9]\\+$" 191 192 static int __maybe_unused tracefs_write(const char *path, const char *value) 193 { 194 int fd; 195 ssize_t ret; 196 size_t len = strlen(value); 197 198 fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC); 199 if (fd < 0) 200 return -errno; 201 202 ret = write(fd, value, len); 203 close(fd); 204 if (ret < 0) 205 return -errno; 206 if ((size_t)ret != len) 207 return -EIO; 208 209 return 0; 210 } 211 212 static int __maybe_unused tracefs_write_int(const char *path, int value) 213 { 214 char buf[32]; 215 216 snprintf(buf, sizeof(buf), "%d", value); 217 return tracefs_write(path, buf); 218 } 219 220 static int __maybe_unused tracefs_setup(void) 221 { 222 struct stat st; 223 224 /* Mount tracefs if not already mounted. */ 225 if (stat(TRACEFS_ROOT, &st) != 0) { 226 int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL); 227 228 if (ret) 229 return -errno; 230 } 231 232 /* Verify landlock events are available. */ 233 if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0) 234 return -ENOENT; 235 236 return 0; 237 } 238 239 /* 240 * Set up PID-based event filtering so only events from the current process and 241 * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it 242 * prevents events from unrelated processes from polluting the trace buffer. 243 */ 244 static int __maybe_unused tracefs_set_pid_filter(pid_t pid) 245 { 246 int ret; 247 248 /* Enable event-fork so children inherit the PID filter. */ 249 ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1"); 250 if (ret) 251 return ret; 252 253 return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid); 254 } 255 256 /* Clear the PID filter to stop filtering by PID. */ 257 static int __maybe_unused tracefs_clear_pid_filter(void) 258 { 259 return tracefs_write(TRACEFS_SET_EVENT_PID, ""); 260 } 261 262 static int __maybe_unused tracefs_enable_event(const char *enable_path, 263 bool enable) 264 { 265 return tracefs_write(enable_path, enable ? "1" : "0"); 266 } 267 268 static int __maybe_unused tracefs_clear(void) 269 { 270 return tracefs_write(TRACEFS_TRACE, ""); 271 } 272 273 /* 274 * Reads the trace buffer content into a newly allocated buffer. The caller is 275 * responsible for freeing the returned buffer. Returns NULL on error. 276 */ 277 static char __maybe_unused *tracefs_read_trace(void) 278 { 279 char *buf; 280 int fd; 281 ssize_t total = 0, ret; 282 283 buf = malloc(TRACE_BUFFER_SIZE); 284 if (!buf) 285 return NULL; 286 287 fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC); 288 if (fd < 0) { 289 free(buf); 290 return NULL; 291 } 292 293 while (total < TRACE_BUFFER_SIZE - 1) { 294 ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total); 295 if (ret <= 0) 296 break; 297 total += ret; 298 } 299 close(fd); 300 buf[total] = '\0'; 301 return buf; 302 } 303 304 /* Counts the number of lines in @buf matching the basic regex @pattern. */ 305 static int __maybe_unused tracefs_count_matches(const char *buf, 306 const char *pattern) 307 { 308 regex_t regex; 309 int count = 0; 310 const char *line, *end; 311 312 if (regcomp(®ex, pattern, 0) != 0) 313 return -EINVAL; 314 315 line = buf; 316 while (*line) { 317 end = strchr(line, '\n'); 318 if (!end) 319 end = line + strlen(line); 320 321 /* Create a temporary NUL-terminated line. */ 322 size_t len = end - line; 323 char *tmp = malloc(len + 1); 324 325 if (tmp) { 326 memcpy(tmp, line, len); 327 tmp[len] = '\0'; 328 if (regexec(®ex, tmp, 0, NULL, 0) == 0) 329 count++; 330 free(tmp); 331 } 332 333 if (*end == '\n') 334 line = end + 1; 335 else 336 break; 337 } 338 339 regfree(®ex); 340 return count; 341 } 342 343 /* 344 * Extracts the value of a named field from a trace line in @buf. Searches for 345 * the first line matching @line_pattern, then extracts the value after 346 * "@field_name=" into @out. Stops at space or newline. 347 * 348 * Returns 0 on success, -ENOENT if no match. 349 */ 350 static int __maybe_unused tracefs_extract_field(const char *buf, 351 const char *line_pattern, 352 const char *field_name, 353 char *out, size_t out_size) 354 { 355 regex_t regex; 356 const char *line, *end; 357 358 if (regcomp(®ex, line_pattern, 0) != 0) 359 return -EINVAL; 360 361 line = buf; 362 while (*line) { 363 end = strchr(line, '\n'); 364 if (!end) 365 end = line + strlen(line); 366 367 size_t len = end - line; 368 char *tmp = malloc(len + 1); 369 370 if (tmp) { 371 const char *field, *val_start; 372 size_t field_len, val_len; 373 374 memcpy(tmp, line, len); 375 tmp[len] = '\0'; 376 377 if (regexec(®ex, tmp, 0, NULL, 0) != 0) { 378 free(tmp); 379 goto next; 380 } 381 382 /* 383 * Find "field_name=" in the line, ensuring a word 384 * boundary before the field name to avoid substring 385 * matches (e.g., "port" in "sport"). 386 */ 387 field_len = strlen(field_name); 388 field = tmp; 389 while ((field = strstr(field, field_name))) { 390 if (field[field_len] == '=' && 391 (field == tmp || field[-1] == ' ')) 392 break; 393 field++; 394 } 395 if (!field) { 396 free(tmp); 397 regfree(®ex); 398 return -ENOENT; 399 } 400 401 val_start = field + field_len + 1; 402 val_len = 0; 403 while (val_start[val_len] && 404 val_start[val_len] != ' ' && 405 val_start[val_len] != '\n') 406 val_len++; 407 408 if (val_len >= out_size) 409 val_len = out_size - 1; 410 memcpy(out, val_start, val_len); 411 out[val_len] = '\0'; 412 413 free(tmp); 414 regfree(®ex); 415 return 0; 416 } 417 next: 418 if (*end == '\n') 419 line = end + 1; 420 else 421 break; 422 } 423 424 regfree(®ex); 425 return -ENOENT; 426 } 427 428 /* 429 * Common fixture setup for trace tests. Mounts tracefs if needed and sets a 430 * PID filter. The caller must create a mount namespace first 431 * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs 432 * mount; the trace buffer, per-event enable flags, and PID filter are global 433 * kernel state, scoped to the test by the PID filter. 434 * 435 * Returns 0 on success, -errno on failure (caller should SKIP). 436 */ 437 static int __maybe_unused tracefs_fixture_setup(void) 438 { 439 int ret; 440 441 ret = tracefs_setup(); 442 if (ret) 443 return ret; 444 445 return tracefs_set_pid_filter(getpid()); 446 } 447 448 static void __maybe_unused tracefs_fixture_teardown(void) 449 { 450 tracefs_clear_pid_filter(); 451 } 452 453 /* 454 * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then 455 * drops the capability. Returns the value from @func, or -EPERM if the 456 * capability manipulation fails. 457 */ 458 static int __maybe_unused tracefs_priv_call(int (*func)(void)) 459 { 460 const cap_value_t admin = CAP_SYS_ADMIN; 461 cap_t cap_p; 462 int ret; 463 464 cap_p = cap_get_proc(); 465 if (!cap_p) 466 return -EPERM; 467 468 if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || 469 cap_set_proc(cap_p)) { 470 cap_free(cap_p); 471 return -EPERM; 472 } 473 474 ret = func(); 475 476 cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); 477 cap_set_proc(cap_p); 478 cap_free(cap_p); 479 return ret; 480 } 481 482 /* Read the trace buffer with elevated privileges. Returns NULL on failure. */ 483 static char __maybe_unused *tracefs_read_buf(void) 484 { 485 /* Cannot use tracefs_priv_call() because the return type is char *. */ 486 cap_t cap_p; 487 char *buf; 488 const cap_value_t admin = CAP_SYS_ADMIN; 489 490 cap_p = cap_get_proc(); 491 if (!cap_p) 492 return NULL; 493 494 if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) || 495 cap_set_proc(cap_p)) { 496 cap_free(cap_p); 497 return NULL; 498 } 499 500 buf = tracefs_read_trace(); 501 502 cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR); 503 cap_set_proc(cap_p); 504 cap_free(cap_p); 505 return buf; 506 } 507 508 /* Clear the trace buffer with elevated privileges. Returns 0 on success. */ 509 static int __maybe_unused tracefs_clear_buf(void) 510 { 511 return tracefs_priv_call(tracefs_clear); 512 } 513 514 /* 515 * Forks a child that creates a Landlock sandbox and performs an FS access. The 516 * parent waits for the child, then reads the trace buffer. 517 * 518 * Requires common.h and wrappers.h to be included before trace.h. 519 */ 520 static void __maybe_unused sandbox_child_fs_access( 521 struct __test_metadata *const _metadata, const char *rule_path, 522 __u64 handled_access, __u64 allowed_access, const char *access_path) 523 { 524 pid_t pid; 525 int status; 526 527 pid = fork(); 528 ASSERT_LE(0, pid); 529 530 if (pid == 0) { 531 struct landlock_ruleset_attr ruleset_attr = { 532 .handled_access_fs = handled_access, 533 }; 534 struct landlock_path_beneath_attr path_beneath = { 535 .allowed_access = allowed_access, 536 }; 537 int ruleset_fd, fd; 538 539 ruleset_fd = landlock_create_ruleset(&ruleset_attr, 540 sizeof(ruleset_attr), 0); 541 if (ruleset_fd < 0) 542 _exit(1); 543 544 path_beneath.parent_fd = 545 open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC); 546 if (path_beneath.parent_fd < 0) { 547 close(ruleset_fd); 548 _exit(1); 549 } 550 551 if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 552 &path_beneath, 0)) { 553 close(path_beneath.parent_fd); 554 close(ruleset_fd); 555 _exit(1); 556 } 557 close(path_beneath.parent_fd); 558 559 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 560 if (landlock_restrict_self(ruleset_fd, 0)) { 561 close(ruleset_fd); 562 _exit(1); 563 } 564 close(ruleset_fd); 565 566 fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); 567 if (fd >= 0) 568 close(fd); 569 570 _exit(0); 571 } 572 573 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 574 ASSERT_TRUE(WIFEXITED(status)); 575 EXPECT_EQ(0, WEXITSTATUS(status)); 576 } 577 578 /* 579 * Forks a child that creates a Landlock sandbox allowing execute+read_dir for 580 * /usr and execute-only for ".", then execs ./true. The true binary opens "." 581 * on startup, triggering a read_dir denial with same_exec=0. The parent waits 582 * for the child to exit. 583 */ 584 static void __maybe_unused sandbox_child_exec_true( 585 struct __test_metadata *const _metadata, __u32 restrict_flags) 586 { 587 pid_t pid; 588 int status; 589 590 pid = fork(); 591 ASSERT_LE(0, pid); 592 593 if (pid == 0) { 594 struct landlock_ruleset_attr attr = { 595 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR | 596 LANDLOCK_ACCESS_FS_EXECUTE, 597 }; 598 struct landlock_path_beneath_attr path_beneath = { 599 .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE | 600 LANDLOCK_ACCESS_FS_READ_DIR, 601 }; 602 int ruleset_fd; 603 604 ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0); 605 if (ruleset_fd < 0) 606 _exit(1); 607 608 path_beneath.parent_fd = 609 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); 610 if (path_beneath.parent_fd >= 0) { 611 landlock_add_rule(ruleset_fd, 612 LANDLOCK_RULE_PATH_BENEATH, 613 &path_beneath, 0); 614 close(path_beneath.parent_fd); 615 } 616 617 path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE; 618 path_beneath.parent_fd = 619 open(".", O_PATH | O_DIRECTORY | O_CLOEXEC); 620 if (path_beneath.parent_fd >= 0) { 621 landlock_add_rule(ruleset_fd, 622 LANDLOCK_RULE_PATH_BENEATH, 623 &path_beneath, 0); 624 close(path_beneath.parent_fd); 625 } 626 627 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 628 if (landlock_restrict_self(ruleset_fd, restrict_flags)) 629 _exit(1); 630 close(ruleset_fd); 631 632 execl("./true", "./true", NULL); 633 _exit(1); 634 } 635 636 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 637 ASSERT_TRUE(WIFEXITED(status)); 638 EXPECT_EQ(0, WEXITSTATUS(status)); 639 } 640