diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-06 09:49:04 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-06 09:49:04 +0300 |
| commit | 381581b373329b67187be494118df49e5ec2acca (patch) | |
| tree | d7b163dc347b1a595c23e467f5c21b0a60cfc5e1 /cmd | |
| parent | 4292b4ef116ec72b66f3c19f8a9a00458d441b79 (diff) | |
test(landlock): cover landlock_add_rule end-to-end (nj0)
Extend the existing security-landlock scenario to also exercise
landlock_add_rule in-process. After creating the ruleset fd, the
scenario builds a struct landlock_path_beneath_attr (allowed_access =
LANDLOCK_ACCESS_FS_READ_FILE, parent_fd = open("/", O_PATH)) and calls
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0)
(syscall nr 445), then closes both fds.
landlock_add_rule is unprivileged and has no process-wide side effects
(it only builds a ruleset that is never enforced), so it is safe to run
in the shared workload process. The call is issued unconditionally even
when ruleset creation fails: sys_enter_landlock_add_rule fires before
the kernel validates the fd, so the enter tracepoint is captured
regardless of whether Landlock is enabled, matching create_ruleset
coverage. ret is UNCLASSIFIED (0/-1, not a byte count).
TestSecurityLandlockCreateRuleset now adds landlock_add_rule to the
trace-arg set and asserts enter_landlock_add_rule MinCount>=1 plus a
positive event duration, capturing ruleset_fd at args[0] (KindFd).
landlock_restrict_self (ci0) is intentionally NOT covered here: it would
require a traced child subprocess, which the integration harness cannot
support (it filters by the single workload PID at the BPF layer).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/ioworkload/scenario_security.go | 95 |
1 files changed, 85 insertions, 10 deletions
diff --git a/cmd/ioworkload/scenario_security.go b/cmd/ioworkload/scenario_security.go index 9b06a12..f3cf9ba 100644 --- a/cmd/ioworkload/scenario_security.go +++ b/cmd/ioworkload/scenario_security.go @@ -5,6 +5,8 @@ import ( "runtime" "syscall" "unsafe" + + "golang.org/x/sys/unix" ) var keySpecProcessKeyringArg = ^uintptr(1) @@ -144,6 +146,17 @@ func landlockSyscallNumber(arch string) (uintptr, error) { } } +// landlockAddRuleSyscallNumber is the landlock_add_rule syscall number. +// It is 445 on both amd64 and arm64 (one above landlock_create_ruleset). +func landlockAddRuleSyscallNumber(arch string) (uintptr, error) { + switch arch { + case "amd64", "arm64": + return 445, nil + default: + return 0, fmt.Errorf("landlock_add_rule syscall number not defined for GOARCH=%s", arch) + } +} + // landlockRulesetAttr mirrors struct landlock_ruleset_attr (uapi/linux/landlock.h). // handled_access_fs is the set of filesystem access rights the ruleset will // govern; handled_access_net (added in Landlock ABI v4) governs TCP access. @@ -157,19 +170,39 @@ type landlockRulesetAttr struct { // filesystem access right used to populate a minimal, valid ruleset attribute. const landlockAccessFsReadFile = 0x4 -// securityLandlockCreateRuleset exercises the landlock_create_ruleset syscall -// end-to-end. It builds a minimal valid struct landlock_ruleset_attr (handling -// only LANDLOCK_ACCESS_FS_READ_FILE), calls landlock_create_ruleset(&attr, -// sizeof(attr), 0) to obtain a fresh ruleset fd, and closes it. +// LANDLOCK_RULE_PATH_BENEATH (uapi/linux/landlock.h) — the rule type identifying +// a struct landlock_path_beneath_attr, the only rule type defined for the +// filesystem since Landlock ABI v1. +const landlockRulePathBeneath = 1 + +// landlockPathBeneathAttr mirrors struct landlock_path_beneath_attr +// (uapi/linux/landlock.h). allowed_access is the set of filesystem access +// rights granted beneath parent_fd, and parent_fd is an O_PATH fd to the +// directory hierarchy the rule applies to. The struct is __attribute__((packed)) +// in the kernel headers, so the Go layout must match: a __u64 followed by a +// __s32 with no trailing padding. +type landlockPathBeneathAttr struct { + allowedAccess uint64 + parentFd int32 +} + +// securityLandlockCreateRuleset exercises the landlock_create_ruleset and +// landlock_add_rule syscalls end-to-end. It builds a minimal valid struct +// landlock_ruleset_attr (handling only LANDLOCK_ACCESS_FS_READ_FILE), calls +// landlock_create_ruleset(&attr, sizeof(attr), 0) to obtain a fresh ruleset fd, +// then adds a PATH_BENEATH rule granting READ_FILE under "/" to that ruleset, +// and finally closes both fds. // // SAFETY: this scenario deliberately does NOT call landlock_restrict_self. // landlock_restrict_self irreversibly sandboxes the calling process for its // entire lifetime, which would break the shared integration-test runner. -// Creating and closing a ruleset fd has no process-wide side effects. +// landlock_create_ruleset and landlock_add_rule are unprivileged and have NO +// process-wide side effects (they only build a ruleset that is never enforced), +// so both are safe to run in-process in the shared workload. // -// The call is tolerated to fail with ENOSYS/EOPNOTSUPP (kernel < 5.13 or -// Landlock LSM disabled): the sys_enter_landlock_create_ruleset tracepoint -// fires before any such error, so the tracer still observes the enter event. +// The calls are tolerated to fail with ENOSYS/EOPNOTSUPP (kernel < 5.13 or +// Landlock LSM disabled): the sys_enter tracepoints fire before any such error, +// so the tracer still observes both enter events regardless. func securityLandlockCreateRuleset() error { nr, err := landlockSyscallNumber(runtime.GOARCH) if err != nil { @@ -185,8 +218,50 @@ func securityLandlockCreateRuleset() error { unsafe.Sizeof(attr), 0, // flags = 0: create a real ruleset (not the ABI-version query) ) - if int64(fd) >= 0 { - _ = syscall.Close(int(fd)) + + // Always attempt landlock_add_rule, even if ruleset creation failed + // (fd < 0). The sys_enter_landlock_add_rule tracepoint fires before the + // kernel validates the (possibly invalid) ruleset fd, so the enter event is + // captured unconditionally — matching how create_ruleset coverage works. + rulesetFd := int(int64(fd)) + addLandlockReadRule(rulesetFd) + + if rulesetFd >= 0 { + _ = syscall.Close(rulesetFd) } return nil } + +// addLandlockReadRule adds a single LANDLOCK_RULE_PATH_BENEATH rule to rulesetFd +// granting LANDLOCK_ACCESS_FS_READ_FILE beneath "/", exercising the +// landlock_add_rule(ruleset_fd, rule_type, &attr, 0) syscall end-to-end. +// +// The parent_fd must be an O_PATH descriptor to a directory; "/" always exists. +// Failures are tolerated: the sys_enter_landlock_add_rule tracepoint fires +// before any error, capturing ruleset_fd at args[0], which is the coverage goal. +func addLandlockReadRule(rulesetFd int) { + nr, err := landlockAddRuleSyscallNumber(runtime.GOARCH) + if err != nil { + return + } + + parentFd, err := unix.Open("/", unix.O_PATH|unix.O_CLOEXEC, 0) + if err != nil { + return + } + defer syscall.Close(parentFd) + + attr := landlockPathBeneathAttr{ + allowedAccess: landlockAccessFsReadFile, + parentFd: int32(parentFd), + } + _, _, _ = syscall.Syscall6( + nr, + uintptr(rulesetFd), + landlockRulePathBeneath, + uintptr(unsafe.Pointer(&attr)), + 0, // flags = 0 (required; must be zero per the man page) + 0, + 0, + ) +} |
