diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-06 09:08:39 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-06 09:08:39 +0300 |
| commit | ade74696f89dc98b3472bbacd9f36860ca83e3c5 (patch) | |
| tree | 8a0f31d238fd875b113cf9cbfff57938bb5334dd | |
| parent | 33dfe4ee3cf948444571554aa35508605fee0474 (diff) | |
test(retbytes): assert readlinkat READ_CLASSIFIED byte count end-to-end
readlink/readlinkat are READ_CLASSIFIED (exit ctx->ret = link-target byte
count), but the integration suite only asserted the enter_readlinkat
pathname tracepoint via MinCount in link_test.go. The exit byte
classification and positive duration were never validated end-to-end,
unlike sibling READ-classified syscalls (read/recvfrom/getxattrat/
getdents64) in retbytes_test.go.
Add retbytesReadlinkat to the phase-A workload: it creates a symlink with
a known non-empty absolute target, opens the parent O_DIRECTORY, and
re-issues SYS_READLINKAT in a short spaced window (mirroring the
getdents64 driver) so ior can attach and capture an enter/exit pair under
parallel load. Each call re-resolves the same link, so ctx->ret stays
equal to the target length and is strictly positive.
Add readlinkat (and symlink, used to build the link without mixing
tracepoints) to retbytesTraceArgs, assert enter_readlinkat presence
(MinCount) plus bytes>=1 via assertEventBytesAtLeast and a positive
duration. bytes>=1 (not an exact target length) because the resolved
path varies across temp dirs; >=1 is the safest invariant.
Coverage hardening only; classify.go readlink/readlinkat=ReadClassified
and the BPF arg capture (args[1]=pathname for readlinkat) are correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | cmd/ioworkload/scenario_retbytes.go | 61 | ||||
| -rw-r--r-- | integrationtests/retbytes_test.go | 13 |
2 files changed, 73 insertions, 1 deletions
diff --git a/cmd/ioworkload/scenario_retbytes.go b/cmd/ioworkload/scenario_retbytes.go index 0382db2..c1da350 100644 --- a/cmd/ioworkload/scenario_retbytes.go +++ b/cmd/ioworkload/scenario_retbytes.go @@ -43,6 +43,9 @@ func retbytesPhaseA() error { if err := retbytesGetdents(); err != nil { return err } + if err := retbytesReadlinkat(); err != nil { + return err + } return retbytesProcessVM() } @@ -95,6 +98,64 @@ func retbytesGetdents() error { return nil } +// retbytesReadlinkat creates a symlink with a known non-empty target and reads +// it back via readlinkat(2). readlinkat is READ_CLASSIFIED, so a successful +// call returns ctx->ret > 0: the byte length of the target string copied into +// the caller's buffer. This drives the exit byte-count assertion in +// TestRetbytesPhaseA. +func retbytesReadlinkat() error { + dir, cleanup, err := makeTempDir("retbytes-readlinkat") + if err != nil { + return err + } + defer cleanup() + + // Point the symlink at an absolute path inside the temp dir. The target is + // deliberately non-empty so readlinkat reports a strictly positive byte + // count (the length of this target string). + target := filepath.Join(dir, "retbytes-readlinkat-target.txt") + linkPath := filepath.Join(dir, "retbytes-readlinkat-link.txt") + if err := syscall.Symlink(target, linkPath); err != nil { + return fmt.Errorf("symlink: %w", err) + } + + dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + return fmt.Errorf("open dir: %w", err) + } + defer syscall.Close(dirFD) + + linkName, err := syscall.BytePtrFromString("retbytes-readlinkat-link.txt") + if err != nil { + return fmt.Errorf("link name bytes: %w", err) + } + + // Re-issue readlinkat in a short window so ior has enough time to attach and + // capture an enter/exit pair under high parallel integration load. Each call + // re-resolves the same symlink, so ctx->ret stays equal to the target length. + buf := make([]byte, 256) + for i := 0; i < 40; i++ { + n, _, errno := syscall.Syscall6( + syscall.SYS_READLINKAT, + uintptr(dirFD), + uintptr(unsafe.Pointer(linkName)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + 0, 0, + ) + runtime.KeepAlive(linkName) + runtime.KeepAlive(buf) + if errno != 0 { + return fmt.Errorf("readlinkat: %w", errno) + } + if n == 0 { + return fmt.Errorf("readlinkat returned 0 bytes for a non-empty link target") + } + time.Sleep(25 * time.Millisecond) + } + return nil +} + func retbytesSocketIO() error { fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) if err != nil { diff --git a/integrationtests/retbytes_test.go b/integrationtests/retbytes_test.go index 37eb45b..a7cb45a 100644 --- a/integrationtests/retbytes_test.go +++ b/integrationtests/retbytes_test.go @@ -2,7 +2,7 @@ package integrationtests import "testing" -var retbytesTraceArgs = []string{"-trace-syscalls", "sendto,recvfrom,sendmsg,recvmsg,sendmmsg,recvmmsg,sendfile64,splice,tee,process_vm_writev,process_vm_readv,socketpair,pipe2,openat,write,read,close,lseek,fcntl,unlinkat,mkdirat,getdents64"} +var retbytesTraceArgs = []string{"-trace-syscalls", "sendto,recvfrom,sendmsg,recvmsg,sendmmsg,recvmmsg,sendfile64,splice,tee,process_vm_writev,process_vm_readv,socketpair,pipe2,openat,write,read,close,lseek,fcntl,unlinkat,mkdirat,getdents64,readlinkat,symlink"} func TestRetbytesPhaseA(t *testing.T) { const payloadLen = uint64(18) @@ -20,6 +20,7 @@ func TestRetbytesPhaseA(t *testing.T) { {Tracepoint: "enter_process_vm_writev", Comm: "ioworkload", MinCount: 1}, {Tracepoint: "enter_process_vm_readv", Comm: "ioworkload", MinCount: 1}, {Tracepoint: "enter_getdents64", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_readlinkat", Comm: "ioworkload", MinCount: 1}, }, retbytesTraceArgs) for _, tracepoint := range []string{ @@ -51,4 +52,14 @@ func TestRetbytesPhaseA(t *testing.T) { getdentsExp := ExpectedEvent{Tracepoint: "enter_getdents64", Comm: "ioworkload"} assertEventBytesAtLeast(t, result, getdentsExp, 1) assertEventDurationPositive(t, result, getdentsExp) + + // readlinkat is READ_CLASSIFIED: a successful call reports ctx->ret > 0, + // the byte length of the resolved link target written into the caller's + // buffer. The retbytes driver points the symlink at a known non-empty + // target, so the exit byte count is strictly positive. Assert a + // conservative bytes>=1 minimum (the exact target length is deterministic + // but path-dependent across temp dirs, so >=1 is the safest invariant). + readlinkatExp := ExpectedEvent{Tracepoint: "enter_readlinkat", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, readlinkatExp, 1) + assertEventDurationPositive(t, result, readlinkatExp) } |
