From 9a22816887b492ea0192ac096514568c7df80b01 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 3 Jun 2026 08:13:53 +0300 Subject: 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 --- cmd/ioworkload/scenario_security.go | 58 +++++++++++++++++++++++++++++++++++++ cmd/ioworkload/scenarios.go | 1 + integrationtests/security_test.go | 54 ++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) 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 { -- cgit v1.2.3