summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-31 10:36:10 +0300
committerPaul Buetow <paul@buetow.org>2026-05-31 10:36:10 +0300
commit6d0d7814c052f5540746456c8e3c47cc1657bf61 (patch)
treeafa58c4fe1cd434befa1056e93bf9623ef26992d
parent783c551f8e7f293b723e44386e50c4739075e2d4 (diff)
test(retbytes): assert read byte counts for pread64/preadv/preadv2
The retbytes integration coverage exercised read/write/sendto/etc but the positional read p-variants only had presence assertions (pread64) or no coverage at all (preadv/preadv2), so their READ_CLASSIFIED byte accounting was validated only by unit tests, not end-to-end. Add a positive byte-count assertion to TestReadwritePread and new readwrite-preadv / readwrite-preadv2 workload scenarios plus integration tests that read a known payload and assert the attributed byte count, mirroring the existing pwrite64 assertion. preadv2 lacks a Go syscall.SYS_PREADV2 constant, so its number is provided per-GOARCH (amd64=327, arm64=286) following the securitySyscallNumbers pattern. Addresses the read side of b20. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--cmd/ioworkload/scenario_readwrite.go97
-rw-r--r--cmd/ioworkload/scenarios.go2
-rw-r--r--integrationtests/readwrite_test.go50
3 files changed, 148 insertions, 1 deletions
diff --git a/cmd/ioworkload/scenario_readwrite.go b/cmd/ioworkload/scenario_readwrite.go
index c676b90..c69e588 100644
--- a/cmd/ioworkload/scenario_readwrite.go
+++ b/cmd/ioworkload/scenario_readwrite.go
@@ -65,6 +65,103 @@ func readwritePread() error {
return nil
}
+// readwritePreadv opens a file, writes data, then reads it back via preadv
+// (positional vectored read). preadv returns the number of bytes read and is
+// READ_CLASSIFIED, so the scenario reads a known payload to validate
+// end-to-end byte attribution for the positional vectored read variant.
+func readwritePreadv() error {
+ dir, cleanup, err := makeTempDir("readwrite-preadv")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "preadvfile.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ defer syscall.Close(fd)
+
+ data := []byte("preadv test data")
+ if _, err := syscall.Write(fd, data); err != nil {
+ return fmt.Errorf("write: %w", err)
+ }
+
+ buf1 := make([]byte, 6)
+ buf2 := make([]byte, 10)
+ iovs := []syscall.Iovec{
+ {Base: &buf1[0], Len: uint64(len(buf1))},
+ {Base: &buf2[0], Len: uint64(len(buf2))},
+ }
+ // preadv(fd, iov, iovcnt, offset) reads at the given offset (0) without
+ // changing the file position.
+ _, _, errno := syscall.Syscall6(syscall.SYS_PREADV, uintptr(fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs)), 0, 0, 0)
+ runtime.KeepAlive(buf1)
+ runtime.KeepAlive(buf2)
+ if errno != 0 {
+ return fmt.Errorf("preadv: %w", errno)
+ }
+ return nil
+}
+
+// readwritePreadv2 opens a file, writes data, then reads it back via preadv2
+// (positional vectored read with flags). Like preadv it returns the bytes read
+// and is READ_CLASSIFIED; the scenario reads a known payload to validate
+// end-to-end byte attribution.
+func readwritePreadv2() error {
+ dir, cleanup, err := makeTempDir("readwrite-preadv2")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "preadv2file.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ defer syscall.Close(fd)
+
+ data := []byte("preadv2 test data")
+ if _, err := syscall.Write(fd, data); err != nil {
+ return fmt.Errorf("write: %w", err)
+ }
+
+ buf1 := make([]byte, 7)
+ buf2 := make([]byte, 10)
+ iovs := []syscall.Iovec{
+ {Base: &buf1[0], Len: uint64(len(buf1))},
+ {Base: &buf2[0], Len: uint64(len(buf2))},
+ }
+ nr, err := preadv2SyscallNr(runtime.GOARCH)
+ if err != nil {
+ return err
+ }
+ // preadv2(fd, iov, iovcnt, pos_l, pos_h, flags): offset 0, no flags.
+ _, _, errno := syscall.Syscall6(nr, uintptr(fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs)), 0, 0, 0)
+ runtime.KeepAlive(buf1)
+ runtime.KeepAlive(buf2)
+ if errno != 0 {
+ return fmt.Errorf("preadv2: %w", errno)
+ }
+ return nil
+}
+
+// preadv2SyscallNr returns the preadv2(2) syscall number for the given GOARCH.
+// Go's syscall package lacks a SYS_PREADV2 constant, so the architecture
+// numbers are provided explicitly (matching securitySyscallNumbers' pattern).
+func preadv2SyscallNr(arch string) (uintptr, error) {
+ switch arch {
+ case "amd64":
+ return 327, nil
+ case "arm64":
+ return 286, nil
+ default:
+ return 0, fmt.Errorf("preadv2 syscall number not defined for GOARCH=%s", arch)
+ }
+}
+
// readwritePwrite opens a file and writes data via pwrite64.
func readwritePwrite() error {
dir, cleanup, err := makeTempDir("readwrite-pwrite")
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index 821c8b2..08ac7a3 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -18,6 +18,8 @@ var scenarios = map[string]func() error{
"open-pid-filter": openPidFilter,
"readwrite-basic": readwriteBasic,
"readwrite-pread": readwritePread,
+ "readwrite-preadv": readwritePreadv,
+ "readwrite-preadv2": readwritePreadv2,
"readwrite-pwrite": readwritePwrite,
"readwrite-readv": readwriteReadv,
"readwrite-writev": readwriteWritev,
diff --git a/integrationtests/readwrite_test.go b/integrationtests/readwrite_test.go
index 69f60ea..7a479b7 100644
--- a/integrationtests/readwrite_test.go
+++ b/integrationtests/readwrite_test.go
@@ -31,7 +31,11 @@ func TestReadwriteBasic(t *testing.T) {
}
func TestReadwritePread(t *testing.T) {
- runScenario(t, "readwrite-pread", []ExpectedEvent{
+ // pread64 returns the number of bytes read (READ_CLASSIFIED), so a
+ // successful positional read must attribute the payload byte count
+ // end-to-end, mirroring the pwrite64 byte-count assertion below.
+ const payloadLen = uint64(len("pread test data"))
+ result, _ := runScenarioResult(t, "readwrite-pread", []ExpectedEvent{
{
PathContains: "preadfile.txt",
Tracepoint: "enter_pread64",
@@ -39,6 +43,11 @@ func TestReadwritePread(t *testing.T) {
MinCount: 1,
},
})
+ assertEventBytesAtLeast(t, result, ExpectedEvent{
+ PathContains: "preadfile.txt",
+ Tracepoint: "enter_pread64",
+ Comm: "ioworkload",
+ }, payloadLen)
}
func TestReadwritePwrite(t *testing.T) {
@@ -57,6 +66,45 @@ func TestReadwritePwrite(t *testing.T) {
}, 1)
}
+func TestReadwritePreadv(t *testing.T) {
+ // preadv is the positional vectored read sibling of pread64/readv and is
+ // READ_CLASSIFIED, so a successful read must attribute the payload bytes
+ // end-to-end.
+ const payloadLen = uint64(len("preadv test data"))
+ result, _ := runScenarioResult(t, "readwrite-preadv", []ExpectedEvent{
+ {
+ PathContains: "preadvfile.txt",
+ Tracepoint: "enter_preadv",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+ assertEventBytesAtLeast(t, result, ExpectedEvent{
+ PathContains: "preadvfile.txt",
+ Tracepoint: "enter_preadv",
+ Comm: "ioworkload",
+ }, payloadLen)
+}
+
+func TestReadwritePreadv2(t *testing.T) {
+ // preadv2 is the positional vectored read variant with flags; like preadv
+ // it is READ_CLASSIFIED and must attribute the payload bytes end-to-end.
+ const payloadLen = uint64(len("preadv2 test data"))
+ result, _ := runScenarioResult(t, "readwrite-preadv2", []ExpectedEvent{
+ {
+ PathContains: "preadv2file.txt",
+ Tracepoint: "enter_preadv2",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+ assertEventBytesAtLeast(t, result, ExpectedEvent{
+ PathContains: "preadv2file.txt",
+ Tracepoint: "enter_preadv2",
+ Comm: "ioworkload",
+ }, payloadLen)
+}
+
func TestReadwriteReadv(t *testing.T) {
result, _ := runScenarioResult(t, "readwrite-readv", []ExpectedEvent{
{