1 // SPDX-License-Identifier: GPL-2.0 2 /* 3 * Landlock tests - Filesystem tracepoints 4 * 5 * Copyright © 2026 Cloudflare, Inc. 6 */ 7 8 #define _GNU_SOURCE 9 #include <errno.h> 10 #include <fcntl.h> 11 #include <linux/landlock.h> 12 #include <sched.h> 13 #include <stdio.h> 14 #include <string.h> 15 #include <sys/mount.h> 16 #include <sys/stat.h> 17 #include <sys/types.h> 18 #include <sys/wait.h> 19 #include <unistd.h> 20 21 #include "common.h" 22 #include "trace.h" 23 24 #define TRACE_TASK "trace_fs_test" 25 26 /* 27 * Like REGEX_DENY_ACCESS_FS(), but pins the logged field to a specific value 28 * ("0" or "1") so a test can tell a suppressed (quiet) denial from a logged 29 * one. The tracepoint fires for every denial; logged carries the audit 30 * verdict. 31 */ 32 #define REGEX_DENY_ACCESS_FS_LOGGED(task, log) \ 33 TRACE_PREFIX(task) \ 34 "landlock_deny_access_fs: " \ 35 "domain=[0-9a-f]\\+ " \ 36 "same_exec=[01] " \ 37 "logged=" log " " \ 38 "blockers=[a-z_|]* " \ 39 "dev=[0-9]\\+:[0-9]\\+ " \ 40 "ino=[0-9]\\+ " \ 41 "path=[^ ]*$" 42 43 /* clang-format off */ 44 FIXTURE(trace_fs) { 45 /* clang-format on */ 46 int tracefs_ok; 47 }; 48 49 FIXTURE_SETUP(trace_fs) 50 { 51 int ret; 52 53 set_cap(_metadata, CAP_SYS_ADMIN); 54 ASSERT_EQ(0, unshare(CLONE_NEWNS)); 55 ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); 56 57 ret = tracefs_fixture_setup(); 58 if (ret) { 59 clear_cap(_metadata, CAP_SYS_ADMIN); 60 self->tracefs_ok = 0; 61 SKIP(return, "tracefs not available"); 62 } 63 self->tracefs_ok = 1; 64 65 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true)); 66 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true)); 67 ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true)); 68 ASSERT_EQ(0, tracefs_clear()); 69 clear_cap(_metadata, CAP_SYS_ADMIN); 70 } 71 72 FIXTURE_TEARDOWN(trace_fs) 73 { 74 if (!self->tracefs_ok) 75 return; 76 77 set_cap(_metadata, CAP_SYS_ADMIN); 78 tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false); 79 tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false); 80 tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false); 81 tracefs_fixture_teardown(); 82 clear_cap(_metadata, CAP_SYS_ADMIN); 83 } 84 85 /* 86 * Baseline: verifies that without Landlock, the operation succeeds and no 87 * check_rule or deny_access trace events fire. 88 */ 89 TEST_F(trace_fs, unsandboxed) 90 { 91 char *buf; 92 int count, status, fd; 93 pid_t pid; 94 95 ASSERT_EQ(0, tracefs_clear_buf()); 96 97 pid = fork(); 98 ASSERT_LE(0, pid); 99 100 if (pid == 0) { 101 /* 102 * No sandbox: verify that a normal FS access does not produce 103 * Landlock trace events. 104 */ 105 fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC); 106 if (fd >= 0) 107 close(fd); 108 _exit(0); 109 } 110 111 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 112 ASSERT_TRUE(WIFEXITED(status)); 113 EXPECT_EQ(0, WEXITSTATUS(status)); 114 115 buf = tracefs_read_buf(); 116 ASSERT_NE(NULL, buf); 117 118 count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); 119 EXPECT_EQ(0, count); 120 count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); 121 EXPECT_EQ(0, count); 122 123 free(buf); 124 } 125 126 /* 127 * Verifies that adding a filesystem rule emits a landlock_add_rule_fs trace 128 * event with the expected path and field values: ruleset ID is non-zero, 129 * access_rights is non-zero, and path matches. 130 */ 131 TEST_F(trace_fs, add_rule_fs) 132 { 133 struct landlock_ruleset_attr ruleset_attr = { 134 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE | 135 LANDLOCK_ACCESS_FS_WRITE_FILE | 136 LANDLOCK_ACCESS_FS_READ_DIR, 137 }; 138 struct landlock_path_beneath_attr path_beneath = { 139 .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE, 140 }; 141 char *buf, field_buf[64]; 142 int ruleset_fd, count; 143 144 ruleset_fd = 145 landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); 146 ASSERT_LE(0, ruleset_fd); 147 148 path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); 149 ASSERT_LE(0, path_beneath.parent_fd); 150 151 ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 152 &path_beneath, 0)); 153 ASSERT_EQ(0, close(path_beneath.parent_fd)); 154 ASSERT_EQ(0, close(ruleset_fd)); 155 156 buf = tracefs_read_buf(); 157 ASSERT_NE(NULL, buf); 158 159 count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)); 160 EXPECT_EQ(1, count) 161 { 162 TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf); 163 } 164 165 /* Ruleset ID should be non-zero. */ 166 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), 167 "ruleset", field_buf, 168 sizeof(field_buf))); 169 EXPECT_STRNE("0", field_buf); 170 171 /* Access rights should be non-zero. */ 172 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), 173 "access_rights", field_buf, 174 sizeof(field_buf))); 175 EXPECT_STRNE("", field_buf); 176 177 /* Path should be /usr. */ 178 ASSERT_EQ(0, 179 tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), 180 "path", field_buf, sizeof(field_buf))); 181 EXPECT_STREQ("/usr", field_buf); 182 183 free(buf); 184 } 185 186 /* 187 * Verifies that an allowed access emits check_rule events (rule matched during 188 * pathwalk) but does NOT emit deny_access events (no denial). 189 */ 190 TEST_F(trace_fs, allowed_access) 191 { 192 char *buf, field_buf[64]; 193 int count; 194 195 ASSERT_EQ(0, tracefs_clear_buf()); 196 197 /* Rule allows READ_DIR for /usr, access /usr which is allowed. */ 198 sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR, 199 LANDLOCK_ACCESS_FS_READ_DIR, "/usr"); 200 201 buf = tracefs_read_buf(); 202 ASSERT_NE(NULL, buf); 203 204 count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); 205 EXPECT_LE(1, count); 206 207 /* Single-layer grants array, intersected with the request. */ 208 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), 209 "grants", field_buf, 210 sizeof(field_buf))); 211 EXPECT_STREQ("{read_dir}", field_buf); 212 213 count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); 214 EXPECT_EQ(0, count); 215 216 free(buf); 217 } 218 219 /* 220 * Verifies that accessing a path whose access type is not in the handled set 221 * does not emit landlock_check_rule events. The ruleset handles READ_FILE, but 222 * the directory open checks READ_DIR which is unhandled; Landlock has no 223 * opinion and no rule evaluation occurs. 224 */ 225 TEST_F(trace_fs, check_rule_unhandled) 226 { 227 char *buf; 228 int count; 229 230 ASSERT_EQ(0, tracefs_clear_buf()); 231 232 /* Handles READ_FILE only; READ_DIR is unhandled. */ 233 sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_FILE, 234 LANDLOCK_ACCESS_FS_READ_FILE, "/tmp"); 235 236 buf = tracefs_read_buf(); 237 ASSERT_NE(NULL, buf); 238 239 /* No check_rule events because READ_DIR is not in the handled set. */ 240 count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); 241 EXPECT_EQ(0, count); 242 243 free(buf); 244 } 245 246 /* 247 * Verifies that nested domains (child sandboxed under a parent domain) emit 248 * check_rule events from both layers and produce a deny_access event when the 249 * inner domain's rule does not cover the access. 250 */ 251 TEST_F(trace_fs, check_rule_nested) 252 { 253 char *buf, field_buf[64], *comma; 254 size_t first_len, second_len; 255 int count_rule, count_access, status; 256 pid_t pid; 257 258 ASSERT_EQ(0, tracefs_clear_buf()); 259 260 pid = fork(); 261 ASSERT_LE(0, pid); 262 263 if (pid == 0) { 264 struct landlock_ruleset_attr ruleset_attr = { 265 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, 266 }; 267 struct landlock_path_beneath_attr path_beneath = { 268 .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, 269 }; 270 int ruleset_fd, fd; 271 272 /* First layer: allow /usr. */ 273 ruleset_fd = landlock_create_ruleset(&ruleset_attr, 274 sizeof(ruleset_attr), 0); 275 if (ruleset_fd < 0) 276 _exit(1); 277 278 path_beneath.parent_fd = 279 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); 280 if (path_beneath.parent_fd < 0) { 281 close(ruleset_fd); 282 _exit(1); 283 } 284 285 if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 286 &path_beneath, 0)) { 287 close(path_beneath.parent_fd); 288 close(ruleset_fd); 289 _exit(1); 290 } 291 close(path_beneath.parent_fd); 292 293 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 294 if (landlock_restrict_self(ruleset_fd, 0)) { 295 close(ruleset_fd); 296 _exit(1); 297 } 298 close(ruleset_fd); 299 300 /* Second layer: also allow /usr. */ 301 ruleset_fd = landlock_create_ruleset(&ruleset_attr, 302 sizeof(ruleset_attr), 0); 303 if (ruleset_fd < 0) 304 _exit(1); 305 306 path_beneath.parent_fd = 307 open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC); 308 if (path_beneath.parent_fd < 0) { 309 close(ruleset_fd); 310 _exit(1); 311 } 312 313 if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 314 &path_beneath, 0)) { 315 close(path_beneath.parent_fd); 316 close(ruleset_fd); 317 _exit(1); 318 } 319 close(path_beneath.parent_fd); 320 321 if (landlock_restrict_self(ruleset_fd, 0)) { 322 close(ruleset_fd); 323 _exit(1); 324 } 325 close(ruleset_fd); 326 327 /* Access /usr which is allowed by both layers. */ 328 fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC); 329 if (fd >= 0) 330 close(fd); 331 332 /* Access /tmp which has no rule in either layer. */ 333 fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); 334 if (fd >= 0) 335 close(fd); 336 337 _exit(0); 338 } 339 340 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 341 ASSERT_TRUE(WIFEXITED(status)); 342 EXPECT_EQ(0, WEXITSTATUS(status)); 343 344 buf = tracefs_read_buf(); 345 ASSERT_NE(NULL, buf); 346 347 count_rule = 348 tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); 349 EXPECT_LE(1, count_rule); 350 351 /* 352 * Both layers have the same rule, so the grants array must have two 353 * identical symbolic entries, e.g. {read_dir,read_dir}. 354 */ 355 ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), 356 "grants", field_buf, 357 sizeof(field_buf))); 358 comma = strchr(field_buf, ','); 359 EXPECT_NE(0, !!comma); 360 if (comma) { 361 /* 362 * Verify both entries are identical: compare the substring 363 * before the comma with the substring after it (stripping the 364 * braces). 365 */ 366 first_len = comma - field_buf - 1; 367 second_len = strlen(comma + 1) - 1; 368 EXPECT_EQ(first_len, second_len); 369 EXPECT_EQ(0, strncmp(field_buf + 1, comma + 1, first_len)); 370 } 371 372 count_access = 373 tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); 374 EXPECT_LE(1, count_access); 375 376 free(buf); 377 } 378 379 /* 380 * Verifies that a denied FS access emits a landlock_deny_access_fs trace event 381 * with the blocked access and path. 382 */ 383 TEST_F(trace_fs, deny_access_fs_denied) 384 { 385 char *buf; 386 int count; 387 388 ASSERT_EQ(0, tracefs_clear_buf()); 389 390 /* 391 * Rule allows READ_DIR for /usr, but access /tmp which has no rule. 392 * READ_DIR access to /tmp is denied by absence and should emit a 393 * deny_access_fs event. 394 */ 395 sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR, 396 LANDLOCK_ACCESS_FS_READ_DIR, "/tmp"); 397 398 buf = tracefs_read_buf(); 399 ASSERT_NE(NULL, buf); 400 401 count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); 402 EXPECT_LE(1, count); 403 404 free(buf); 405 } 406 407 /* 408 * A denied FS access covered by a quiet rule (LANDLOCK_ADD_RULE_QUIET with the 409 * access listed in quiet_access_fs) still emits a landlock_deny_access_fs 410 * event, but with logged=0, the same audit-logging verdict audit would apply to 411 * suppress the record. 412 */ 413 TEST_F(trace_fs, deny_access_fs_quiet) 414 { 415 char *buf, field[64]; 416 pid_t pid; 417 int status; 418 419 ASSERT_EQ(0, tracefs_clear_buf()); 420 421 pid = fork(); 422 ASSERT_LE(0, pid); 423 if (pid == 0) { 424 struct landlock_ruleset_attr ruleset_attr = { 425 .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, 426 .quiet_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, 427 }; 428 struct landlock_path_beneath_attr path_beneath = { 429 .allowed_access = 0, 430 }; 431 int ruleset_fd, fd; 432 433 ruleset_fd = landlock_create_ruleset(&ruleset_attr, 434 sizeof(ruleset_attr), 0); 435 if (ruleset_fd < 0) 436 _exit(1); 437 438 /* Marks /tmp quiet without granting any access. */ 439 path_beneath.parent_fd = 440 open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC); 441 if (path_beneath.parent_fd < 0) { 442 close(ruleset_fd); 443 _exit(1); 444 } 445 if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 446 &path_beneath, LANDLOCK_ADD_RULE_QUIET)) { 447 close(path_beneath.parent_fd); 448 close(ruleset_fd); 449 _exit(1); 450 } 451 close(path_beneath.parent_fd); 452 453 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 454 if (landlock_restrict_self(ruleset_fd, 0)) { 455 close(ruleset_fd); 456 _exit(1); 457 } 458 close(ruleset_fd); 459 460 /* Denied READ_DIR on the quiet /tmp: suppressed, logged=0. */ 461 fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); 462 if (fd >= 0) 463 close(fd); 464 _exit(0); 465 } 466 ASSERT_EQ(pid, waitpid(pid, &status, 0)); 467 ASSERT_TRUE(WIFEXITED(status)); 468 EXPECT_EQ(0, WEXITSTATUS(status)); 469 470 buf = tracefs_read_buf(); 471 ASSERT_NE(NULL, buf); 472 473 /* The event fires with the suppressed verdict. */ 474 EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED( 475 TRACE_TASK, "0"))); 476 /* The quiet rule must not leave the denial logged. */ 477 EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED( 478 TRACE_TASK, "1"))); 479 480 /* 481 * Quiet suppresses only the logged verdict: the rest of the denial 482 * event stays populated (non-zero domain, non-empty blockers). 483 */ 484 ASSERT_EQ(0, tracefs_extract_field( 485 buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"), 486 "domain", field, sizeof(field))); 487 EXPECT_STRNE("0", field); 488 ASSERT_EQ(0, tracefs_extract_field( 489 buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"), 490 "blockers", field, sizeof(field))); 491 EXPECT_STRNE("", field); 492 493 free(buf); 494 } 495 496 TEST_HARNESS_MAIN 497