summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-01 15:05:52 +0300
committerPaul Buetow <paul@buetow.org>2026-06-01 15:05:52 +0300
commit877cc685fe550f2c53a9dbdf230eaa28feaf3a16 (patch)
treee2c39419c01fae322772e868ad564c416a1c88e3
parent6a872804d93b822d530e9df93547f2fec0a8ea50 (diff)
test(integration): add SysV msg/sem tracing coverage
Add sysv-msg-basic and sysv-sem-basic ioworkload scenarios that exercise the SysV message-queue and semaphore families end-to-end via raw syscalls, mirroring the existing sysv-shm-basic scenario. sysv-msg-basic: msgget(IPC_PRIVATE) -> msgsnd -> msgrcv -> msgctl(IPC_RMID), using a struct msgbuf {int64 mtype; [16]byte mtext} and msgsz = body length (excluding mtype). sysv-sem-basic: semget(IPC_PRIVATE, 1) -> semop(+1) -> semop(-1) -> semctl(IPC_RMID), incrementing before decrementing so the operation can never block. Both defer IPC_RMID right after the get so no kernel IPC object leaks even on partial failure. Add TestSysVMsgBasic and TestSysVSemBasic asserting the enter_ events for msgget/msgsnd/msgrcv/msgctl and semget/semop/semctl are traced with MinCount>=1 and positive duration, plus PID/comm hermetic guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--cmd/ioworkload/scenario_sysv.go171
-rw-r--r--cmd/ioworkload/scenarios.go2
-rw-r--r--integrationtests/ipc_sysv_test.go65
3 files changed, 238 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_sysv.go b/cmd/ioworkload/scenario_sysv.go
index 178f04b..c1b917e 100644
--- a/cmd/ioworkload/scenario_sysv.go
+++ b/cmd/ioworkload/scenario_sysv.go
@@ -96,3 +96,174 @@ func shmRemove(shmid uintptr) error {
}
return nil
}
+
+// sysvMsgText is the message body copied into and read back out of the SysV
+// message queue. Its length (NOT including mtype) is the msgsz passed to
+// msgsnd/msgrcv.
+var sysvMsgText = []byte("ior-sysv-msg")
+
+// sysvMsgbuf mirrors the kernel's `struct msgbuf`: an 8-byte signed message
+// type (must be > 0 for msgsnd) followed by the message body. The body is sized
+// to hold sysvMsgText.
+type sysvMsgbuf struct {
+ mtype int64
+ mtext [16]byte
+}
+
+// sysvMsgBasic exercises the SysV message-queue family end-to-end: it creates a
+// private queue (msgget IPC_PRIVATE), sends a small message (msgsnd), receives
+// it back (msgrcv), and removes the queue (msgctl IPC_RMID). IPC_RMID is always
+// issued so no kernel message queue leaks.
+func sysvMsgBasic() error {
+ msqid, err := msgGet()
+ if err != nil {
+ return err
+ }
+ // Guarantee removal even on a partial failure; an orphaned IPC_PRIVATE
+ // queue would otherwise leak until reboot.
+ defer func() { _ = msgRemove(msqid) }()
+
+ if err := msgSend(msqid); err != nil {
+ return err
+ }
+ return msgReceive(msqid)
+}
+
+// msgGet creates a new private SysV message queue and returns its identifier.
+func msgGet() (uintptr, error) {
+ msqid, _, errno := syscall.Syscall(
+ syscall.SYS_MSGGET,
+ uintptr(unix.IPC_PRIVATE),
+ uintptr(unix.IPC_CREAT|0o600),
+ 0,
+ )
+ if errno != 0 {
+ return 0, fmt.Errorf("msgget: %w", errno)
+ }
+ return msqid, nil
+}
+
+// msgSend enqueues a single message. msgsz counts only the message body
+// (mtext), excluding the leading mtype field.
+func msgSend(msqid uintptr) error {
+ buf := sysvMsgbuf{mtype: 1}
+ copy(buf.mtext[:], sysvMsgText)
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_MSGSND,
+ msqid,
+ uintptr(unsafe.Pointer(&buf)),
+ uintptr(len(sysvMsgText)),
+ 0, // msgflg: blocking send (the empty queue cannot be full)
+ 0, 0,
+ )
+ if errno != 0 {
+ return fmt.Errorf("msgsnd: %w", errno)
+ }
+ return nil
+}
+
+// msgReceive dequeues the message previously sent. msgtyp 0 returns the first
+// message regardless of type; msgsz must be at least the body size sent.
+func msgReceive(msqid uintptr) error {
+ var buf sysvMsgbuf
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_MSGRCV,
+ msqid,
+ uintptr(unsafe.Pointer(&buf)),
+ uintptr(len(sysvMsgText)),
+ 0, // msgtyp 0: first message in the queue
+ 0, // msgflg: blocking receive (a message is already enqueued)
+ 0,
+ )
+ if errno != 0 {
+ return fmt.Errorf("msgrcv: %w", errno)
+ }
+ return nil
+}
+
+// msgRemove marks the message queue for immediate destruction (IPC_RMID).
+func msgRemove(msqid uintptr) error {
+ _, _, errno := syscall.Syscall(syscall.SYS_MSGCTL, msqid, uintptr(unix.IPC_RMID), 0)
+ if errno != 0 {
+ return fmt.Errorf("msgctl IPC_RMID: %w", errno)
+ }
+ return nil
+}
+
+// sysvSembuf mirrors the kernel's `struct sembuf` used by semop:
+// unsigned short sem_num; short sem_op; short sem_flg;
+type sysvSembuf struct {
+ semNum uint16
+ semOp int16
+ semFlg int16
+}
+
+// sysvSemBasic exercises the SysV semaphore family end-to-end: it creates a
+// private set of one semaphore (semget IPC_PRIVATE), increments it (semop
+// sem_op=+1), then decrements it back to zero (semop sem_op=-1), and removes the
+// set (semctl IPC_RMID). The increment happens first so the decrement can never
+// block, keeping the scenario hang-free. IPC_RMID is always issued so no kernel
+// semaphore set leaks.
+func sysvSemBasic() error {
+ semid, err := semGet()
+ if err != nil {
+ return err
+ }
+ // Guarantee removal even on a partial failure; an orphaned IPC_PRIVATE
+ // semaphore set would otherwise leak until reboot.
+ defer func() { _ = semRemove(semid) }()
+
+ if err := semOp(semid, +1); err != nil {
+ return err
+ }
+ return semOp(semid, -1)
+}
+
+// semGet creates a new private SysV semaphore set with a single semaphore and
+// returns its identifier.
+func semGet() (uintptr, error) {
+ semid, _, errno := syscall.Syscall(
+ syscall.SYS_SEMGET,
+ uintptr(unix.IPC_PRIVATE),
+ 1, // nsems: one semaphore in the set
+ uintptr(unix.IPC_CREAT|0o600),
+ )
+ if errno != 0 {
+ return 0, fmt.Errorf("semget: %w", errno)
+ }
+ return semid, nil
+}
+
+// semOp applies a single operation to semaphore 0 of the set. A positive delta
+// increments (never blocks); a negative delta decrements (only safe once the
+// value is high enough, which the caller guarantees by incrementing first).
+func semOp(semid uintptr, delta int16) error {
+ sop := sysvSembuf{semNum: 0, semOp: delta, semFlg: 0}
+ _, _, errno := syscall.Syscall(
+ syscall.SYS_SEMOP,
+ semid,
+ uintptr(unsafe.Pointer(&sop)),
+ 1, // nsops: one operation
+ )
+ if errno != 0 {
+ return fmt.Errorf("semop(%d): %w", delta, errno)
+ }
+ return nil
+}
+
+// semRemove removes the semaphore set (IPC_RMID). On Linux the union semun
+// fourth argument is ignored for IPC_RMID, so passing 0 is safe.
+func semRemove(semid uintptr) error {
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_SEMCTL,
+ semid,
+ 0, // semnum: ignored for IPC_RMID
+ uintptr(unix.IPC_RMID),
+ 0, // arg (union semun): ignored for IPC_RMID
+ 0, 0,
+ )
+ if errno != 0 {
+ return fmt.Errorf("semctl IPC_RMID: %w", errno)
+ }
+ return nil
+}
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index c11f25d..4fd2dd0 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -44,6 +44,8 @@ var scenarios = map[string]func() error{
"fd-from-air-eventfd-users": fdFromAirEventfdUsers,
"mq-posix-basic": mqPosixBasic,
"sysv-shm-basic": sysvShmBasic,
+ "sysv-msg-basic": sysvMsgBasic,
+ "sysv-sem-basic": sysvSemBasic,
"mountfs-management": mountfsManagement,
"polling-epoll": pollingEpoll,
"sleep-syscalls": sleepSyscalls,
diff --git a/integrationtests/ipc_sysv_test.go b/integrationtests/ipc_sysv_test.go
index 6983c08..d5f4fa9 100644
--- a/integrationtests/ipc_sysv_test.go
+++ b/integrationtests/ipc_sysv_test.go
@@ -36,3 +36,68 @@ func TestSysVShmBasic(t *testing.T) {
assertEventDurationPositive(t, result, ExpectedEvent{Tracepoint: tp, Comm: "ioworkload"})
}
}
+
+// sysvMsgTraceArgs restricts tracing to the SysV message-queue family so the
+// captured output is dominated by the lifecycle calls the workload issues.
+var sysvMsgTraceArgs = []string{
+ "-trace-syscalls",
+ "msgget,msgsnd,msgrcv,msgctl",
+}
+
+// TestSysVMsgBasic verifies the SysV message-queue family is traced end-to-end.
+// The workload runs msgget(IPC_PRIVATE) -> msgsnd -> msgrcv -> msgctl(IPC_RMID);
+// each call must appear as an enter event with a positive duration (proving the
+// enter/exit pair was correlated). The workload always issues IPC_RMID, so no
+// kernel message queue leaks.
+func TestSysVMsgBasic(t *testing.T) {
+ h := newTestHarness(t)
+ result, pid, err := h.RunWithIorArgs("sysv-msg-basic", defaultDuration, sysvMsgTraceArgs)
+ if err != nil {
+ t.Fatalf("run scenario sysv-msg-basic: %v", err)
+ }
+
+ AssertNoUnexpectedPID(t, result, pid)
+ AssertNoUnexpectedComm(t, result, "ioworkload")
+ AssertEventsPresent(t, result, []ExpectedEvent{
+ {Tracepoint: "enter_msgget", Comm: "ioworkload", MinCount: 1},
+ {Tracepoint: "enter_msgsnd", Comm: "ioworkload", MinCount: 1},
+ {Tracepoint: "enter_msgrcv", Comm: "ioworkload", MinCount: 1},
+ {Tracepoint: "enter_msgctl", Comm: "ioworkload", MinCount: 1},
+ })
+
+ for _, tp := range []string{"enter_msgget", "enter_msgsnd", "enter_msgrcv", "enter_msgctl"} {
+ assertEventDurationPositive(t, result, ExpectedEvent{Tracepoint: tp, Comm: "ioworkload"})
+ }
+}
+
+// sysvSemTraceArgs restricts tracing to the SysV semaphore family so the
+// captured output is dominated by the lifecycle calls the workload issues.
+var sysvSemTraceArgs = []string{
+ "-trace-syscalls",
+ "semget,semop,semctl",
+}
+
+// TestSysVSemBasic verifies the SysV semaphore family is traced end-to-end. The
+// workload runs semget(IPC_PRIVATE) -> semop(+1) -> semop(-1) ->
+// semctl(IPC_RMID); each distinct syscall must appear as an enter event with a
+// positive duration (proving the enter/exit pair was correlated). The workload
+// always issues IPC_RMID, so no kernel semaphore set leaks.
+func TestSysVSemBasic(t *testing.T) {
+ h := newTestHarness(t)
+ result, pid, err := h.RunWithIorArgs("sysv-sem-basic", defaultDuration, sysvSemTraceArgs)
+ if err != nil {
+ t.Fatalf("run scenario sysv-sem-basic: %v", err)
+ }
+
+ AssertNoUnexpectedPID(t, result, pid)
+ AssertNoUnexpectedComm(t, result, "ioworkload")
+ AssertEventsPresent(t, result, []ExpectedEvent{
+ {Tracepoint: "enter_semget", Comm: "ioworkload", MinCount: 1},
+ {Tracepoint: "enter_semop", Comm: "ioworkload", MinCount: 1},
+ {Tracepoint: "enter_semctl", Comm: "ioworkload", MinCount: 1},
+ })
+
+ for _, tp := range []string{"enter_semget", "enter_semop", "enter_semctl"} {
+ assertEventDurationPositive(t, result, ExpectedEvent{Tracepoint: tp, Comm: "ioworkload"})
+ }
+}