summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 09:08:39 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 09:08:39 +0300
commitade74696f89dc98b3472bbacd9f36860ca83e3c5 (patch)
tree8a0f31d238fd875b113cf9cbfff57938bb5334dd
parent33dfe4ee3cf948444571554aa35508605fee0474 (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.go61
-rw-r--r--integrationtests/retbytes_test.go13
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)
}