summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-03 08:13:53 +0300
committerPaul Buetow <paul@buetow.org>2026-06-03 08:13:53 +0300
commit9a22816887b492ea0192ac096514568c7df80b01 (patch)
treee3529172dcb1fceabf64e7ccb1d27876ffc15fbb
parenta59034b3d53300401433b8b5f2743f2f08e8f2d2 (diff)
test(integration): add landlock_create_ruleset coverage
Add a Security-family end-to-end scenario + test for landlock_create_ruleset, which was previously untested. The new securityLandlockCreateRuleset scenario (registered as "security-landlock") builds a minimal valid struct landlock_ruleset_attr{handled_access_fs=LANDLOCK_ACCESS_FS_READ_FILE}, calls landlock_create_ruleset(&attr, sizeof(attr), 0) via raw syscall (nr=444 on amd64/arm64), and closes the returned ruleset fd. It tolerates ENOSYS/EOPNOTSUPP (kernel < 5.13 or Landlock LSM disabled) since the sys_enter tracepoint fires before any such error. It deliberately never calls landlock_restrict_self, which would irreversibly sandbox the shared integration-test runner. TestSecurityLandlockCreateRuleset asserts enter_landlock_create_ruleset MinCount>=1 and positive duration unconditionally, plus conditional "landlockfd:" path-prefix assertions on the create/close pair with an open/close path-stability check. Verified: TEST_NAME=TestSecurityLandlockCreateRuleset mage testWithName PASS (kernel 7.0.9); mage build, go build ./cmd/ioworkload/, and go vet ./integrationtests/ all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--cmd/ioworkload/scenario_security.go58
-rw-r--r--cmd/ioworkload/scenarios.go1
-rw-r--r--integrationtests/security_test.go54
3 files changed, 113 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_security.go b/cmd/ioworkload/scenario_security.go
index 3adef75..9b06a12 100644
--- a/cmd/ioworkload/scenario_security.go
+++ b/cmd/ioworkload/scenario_security.go
@@ -132,3 +132,61 @@ func runPerfEventOpenSyscall(nr securitySyscalls) {
_ = syscall.Close(int(fd))
}
}
+
+// landlockSyscallNumber is the landlock_create_ruleset syscall number.
+// It is 444 on both amd64 and arm64 (and most modern arches).
+func landlockSyscallNumber(arch string) (uintptr, error) {
+ switch arch {
+ case "amd64", "arm64":
+ return 444, nil
+ default:
+ return 0, fmt.Errorf("landlock_create_ruleset 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.
+// We declare both fields so unsafe.Sizeof yields the current kernel struct size.
+type landlockRulesetAttr struct {
+ handledAccessFs uint64
+ handledAccessNet uint64
+}
+
+// LANDLOCK_ACCESS_FS_READ_FILE (uapi/linux/landlock.h) — a benign, always-valid
+// 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.
+//
+// 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.
+//
+// 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.
+func securityLandlockCreateRuleset() error {
+ nr, err := landlockSyscallNumber(runtime.GOARCH)
+ if err != nil {
+ return err
+ }
+
+ attr := landlockRulesetAttr{
+ handledAccessFs: landlockAccessFsReadFile,
+ }
+ fd, _, _ := syscall.Syscall(
+ nr,
+ uintptr(unsafe.Pointer(&attr)),
+ unsafe.Sizeof(attr),
+ 0, // flags = 0: create a real ruleset (not the ABI-version query)
+ )
+ if int64(fd) >= 0 {
+ _ = syscall.Close(int(fd))
+ }
+ return nil
+}
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index 7fa3535..8be86db 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -134,6 +134,7 @@ var scenarios = map[string]func() error{
"pidfd-getfd-success": pidfdGetfdSuccess,
"pidfd-getfd-failure": pidfdGetfdFailure,
"security-keys-ptrace-perf": securityKeysPtracePerf,
+ "security-landlock": securityLandlockCreateRuleset,
"iouring-setup": iouringSetup,
"iouring-enter": iouringEnter,
"iouring-register": iouringRegister,
diff --git a/integrationtests/security_test.go b/integrationtests/security_test.go
index 5b6e657..596c8f6 100644
--- a/integrationtests/security_test.go
+++ b/integrationtests/security_test.go
@@ -62,6 +62,60 @@ func TestSecurityKeysPtracePerf(t *testing.T) {
}
}
+var landlockTraceArgs = []string{"-trace-syscalls", "landlock_create_ruleset,close"}
+
+// TestSecurityLandlockCreateRuleset asserts end-to-end tracing of the
+// Security-family landlock_create_ruleset syscall. The security-landlock
+// scenario calls landlock_create_ruleset(&attr, sizeof(attr), 0) and closes
+// the returned ruleset fd (it deliberately never calls landlock_restrict_self,
+// which would irreversibly sandbox the shared test runner).
+//
+// The sys_enter tracepoint fires before any ENOSYS/EOPNOTSUPP error, so the
+// enter event is observed regardless of whether Landlock is enabled on the
+// running kernel; we therefore assert the enter MinCount unconditionally.
+// landlock_create_ruleset is KindEventfd (it captures flags at args[2]); when
+// the ruleset fd is successfully created and registered, it resolves to the
+// "landlockfd:" path label, which is also seen on the matching close.
+func TestSecurityLandlockCreateRuleset(t *testing.T) {
+ result, _ := runScenarioResultWithIorArgs(t, "security-landlock", []ExpectedEvent{
+ {Tracepoint: "enter_landlock_create_ruleset", Comm: "ioworkload", MinCount: 1},
+ }, landlockTraceArgs)
+
+ assertEventDurationPositive(t, result, ExpectedEvent{
+ Tracepoint: "enter_landlock_create_ruleset",
+ Comm: "ioworkload",
+ })
+
+ // landlock_create_ruleset may fail (ENOSYS on kernels < 5.13, or
+ // EOPNOTSUPP when the Landlock LSM is disabled). If a tracked ruleset fd
+ // appears, it must carry the "landlockfd:" label and be closed under the
+ // same label; otherwise we must observe no tracked landlock close events.
+ landlockOpenTracked := totalTracepointPathCount(result, "enter_landlock_create_ruleset", "landlockfd:")
+ landlockCloseTracked := totalTracepointPathCount(result, "enter_close", "landlockfd:")
+ if landlockOpenTracked == 0 {
+ if landlockCloseTracked != 0 {
+ t.Fatalf("unexpected tracked landlock close events without tracked ruleset open: close=%d", landlockCloseTracked)
+ }
+ return
+ }
+
+ assertTracepointPathPrefix(t, result, "enter_landlock_create_ruleset", "landlockfd:")
+ assertTracepointPathPrefix(t, result, "enter_close", "landlockfd:")
+ if landlockCloseTracked < landlockOpenTracked {
+ t.Fatalf("tracked landlock close count too small: close=%d open=%d", landlockCloseTracked, landlockOpenTracked)
+ }
+
+ // The tracked ruleset descriptor path should be stable between the
+ // create_ruleset record and its matching close record.
+ openPaths := uniqueTracepointPathsWithPrefix(result, "enter_landlock_create_ruleset", "landlockfd:")
+ closePaths := uniqueTracepointPathsWithPrefix(result, "enter_close", "landlockfd:")
+ for path := range openPaths {
+ if _, ok := closePaths[path]; !ok {
+ t.Fatalf("tracked landlock descriptor %q seen on create but not close", path)
+ }
+ }
+}
+
func uniqueTracepointPathsWithPrefix(result TestResult, tracepoint, wantPrefix string) map[string]struct{} {
paths := make(map[string]struct{})
for _, rec := range result.Records {