summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 09:49:04 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 09:49:04 +0300
commit381581b373329b67187be494118df49e5ec2acca (patch)
treed7b163dc347b1a595c23e467f5c21b0a60cfc5e1 /cmd
parent4292b4ef116ec72b66f3c19f8a9a00458d441b79 (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.go95
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,
+ )
+}