From b7a63e9964a865441b4e0791a19b5a7bbfa2eff4 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 1 Jun 2026 10:35:18 +0300 Subject: test(integration): add SysV shm tracing coverage The SysV shared-memory family (shmget/shmat/shmdt/shmctl) had no end-to-end integration coverage. Add an ioworkload `sysv-shm-basic` scenario that, without privileges, runs shmget(IPC_PRIVATE) -> shmat -> write into the mapped segment -> shmdt -> shmctl(IPC_RMID), always issuing IPC_RMID (via defer) so no kernel segment leaks. Add TestSysVShmBasic asserting enter_shmget/enter_shmat/enter_shmdt/ enter_shmctl are each traced with a positive (paired enter/exit) duration. msg/sem coverage is scoped out and tracked as a follow-up task (7i0). Co-Authored-By: Claude Opus 4.8 --- cmd/ioworkload/scenario_sysv.go | 98 +++++++++++++++++++++++++++++++++++++++ cmd/ioworkload/scenarios.go | 1 + integrationtests/ipc_sysv_test.go | 38 +++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 cmd/ioworkload/scenario_sysv.go create mode 100644 integrationtests/ipc_sysv_test.go diff --git a/cmd/ioworkload/scenario_sysv.go b/cmd/ioworkload/scenario_sysv.go new file mode 100644 index 0000000..178f04b --- /dev/null +++ b/cmd/ioworkload/scenario_sysv.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +// sysvShmPayload is written into the attached SysV shared-memory segment so the +// scenario touches the mapping (forcing a real page fault) rather than just +// attaching and detaching it. +var sysvShmPayload = []byte("ior-sysv-shm") + +// sysvShmBasic exercises the SysV shared-memory family end-to-end without any +// privileges: it creates a private anonymous segment (shmget IPC_PRIVATE), +// attaches it (shmat), writes a few bytes into the mapped region, detaches it +// (shmdt), and finally removes it (shmctl IPC_RMID). IPC_RMID is always issued +// (even on a partial failure) so the test never leaks a kernel IPC object. +func sysvShmBasic() error { + const segSize = 4096 + shmid, err := shmGet(segSize) + if err != nil { + return err + } + // Guarantee the segment is removed regardless of how the rest fares; an + // orphaned IPC_PRIVATE segment would otherwise leak until reboot. + defer func() { _ = shmRemove(shmid) }() + + addr, err := shmAttach(shmid) + if err != nil { + return err + } + if err := shmWrite(addr, segSize); err != nil { + _ = shmDetach(addr) + return err + } + if err := shmDetach(addr); err != nil { + return err + } + return nil +} + +// shmGet creates a new private SysV shared-memory segment of the given size and +// returns its identifier. +func shmGet(size uintptr) (uintptr, error) { + shmid, _, errno := syscall.Syscall( + syscall.SYS_SHMGET, + uintptr(unix.IPC_PRIVATE), + size, + uintptr(unix.IPC_CREAT|0o600), + ) + if errno != 0 { + return 0, fmt.Errorf("shmget: %w", errno) + } + return shmid, nil +} + +// shmAttach attaches the segment into the address space and returns its base +// address. shmflg 0 requests a read/write mapping at a kernel-chosen address. +func shmAttach(shmid uintptr) (uintptr, error) { + addr, _, errno := syscall.Syscall(syscall.SYS_SHMAT, shmid, 0, 0) + if errno != 0 { + return 0, fmt.Errorf("shmat: %w", errno) + } + return addr, nil +} + +// shmWrite copies the payload into the mapped segment, faulting the page in so +// the workload genuinely uses the shared memory. +func shmWrite(addr, size uintptr) error { + if uintptr(len(sysvShmPayload)) > size { + return fmt.Errorf("shm payload (%d) exceeds segment size (%d)", len(sysvShmPayload), size) + } + seg := unsafe.Slice((*byte)(unsafe.Pointer(addr)), len(sysvShmPayload)) + copy(seg, sysvShmPayload) + return nil +} + +// shmDetach detaches the previously attached segment from the address space. +func shmDetach(addr uintptr) error { + _, _, errno := syscall.Syscall(syscall.SYS_SHMDT, addr, 0, 0) + if errno != 0 { + return fmt.Errorf("shmdt: %w", errno) + } + return nil +} + +// shmRemove marks the segment for destruction (IPC_RMID); the kernel frees it +// once the last attachment is gone. +func shmRemove(shmid uintptr) error { + _, _, errno := syscall.Syscall(syscall.SYS_SHMCTL, shmid, uintptr(unix.IPC_RMID), 0) + if errno != 0 { + return fmt.Errorf("shmctl IPC_RMID: %w", errno) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 597d0e8..ae003b6 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -43,6 +43,7 @@ var scenarios = map[string]func() error{ "eventfd2-basic": eventfd2Basic, "fd-from-air-eventfd-users": fdFromAirEventfdUsers, "mq-posix-basic": mqPosixBasic, + "sysv-shm-basic": sysvShmBasic, "mountfs-management": mountfsManagement, "polling-epoll": pollingEpoll, "sleep-syscalls": sleepSyscalls, diff --git a/integrationtests/ipc_sysv_test.go b/integrationtests/ipc_sysv_test.go new file mode 100644 index 0000000..6983c08 --- /dev/null +++ b/integrationtests/ipc_sysv_test.go @@ -0,0 +1,38 @@ +package integrationtests + +import "testing" + +// sysvShmTraceArgs restricts tracing to the SysV shared-memory family so the +// captured output is dominated by the lifecycle calls the workload issues. +var sysvShmTraceArgs = []string{ + "-trace-syscalls", + "shmget,shmat,shmdt,shmctl", +} + +// TestSysVShmBasic verifies the SysV shared-memory family is traced end-to-end. +// The workload runs shmget(IPC_PRIVATE) -> shmat -> write -> shmdt -> +// shmctl(IPC_RMID); each call must appear as an enter event, and each must have +// a positive duration (proving the enter/exit pair was correlated by the +// tracer). The workload always issues IPC_RMID, so no kernel segment leaks. +func TestSysVShmBasic(t *testing.T) { + h := newTestHarness(t) + result, pid, err := h.RunWithIorArgs("sysv-shm-basic", defaultDuration, sysvShmTraceArgs) + if err != nil { + t.Fatalf("run scenario sysv-shm-basic: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + {Tracepoint: "enter_shmget", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_shmat", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_shmdt", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_shmctl", Comm: "ioworkload", MinCount: 1}, + }) + + // Each syscall must have been correlated to its exit (positive duration), + // proving the enter/exit pair was traced rather than just the enter. + for _, tp := range []string{"enter_shmget", "enter_shmat", "enter_shmdt", "enter_shmctl"} { + assertEventDurationPositive(t, result, ExpectedEvent{Tracepoint: tp, Comm: "ioworkload"}) + } +} -- cgit v1.2.3