From 0a9a92e359e06df0c6cd8e8eff1c165ed7fc2fa0 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 29 May 2026 17:40:45 +0300 Subject: test(classify): lock in pwritev2 WRITE byte-count classification Audit of pwritev2(2) confirmed its tracing is already correct: enter is KindFd (fd at args[0]), return is WRITE_CLASSIFIED so the byte count is counted as written like its pwritev/writev/write/pwrite64 siblings, and family is FS. Add a dedicated lock-in test pinning kind, family, and the WRITE return classification, with the read-side preadv2 as a contrast and the whole p/readv/writev family asserted together to guard against a stray off-by-one reclassification. Co-Authored-By: Claude Opus 4.8 --- internal/generate/classify_test.go | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) (limited to 'internal/generate') diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go index b03164b..41e1bf8 100644 --- a/internal/generate/classify_test.go +++ b/internal/generate/classify_test.go @@ -2075,6 +2075,104 @@ func TestClassifySendmsgWriteByteCount(t *testing.T) { } } +// TestClassifyPwritev2WriteByteCount locks in the classification of the +// pwritev2(2) audit. The syscall signature is: +// +// ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt, +// off_t offset, int flags) +// +// args[0] is the file descriptor written to (the real kernel tracepoint format +// names the first field "fd"), iov/iovcnt describe the gather buffers, offset is +// the file position, and flags is an int (RWF_*). On success pwritev2 returns +// the NUMBER OF BYTES WRITTEN, so its exit must be WRITE_CLASSIFIED — those +// bytes are counted as written, exactly like the pwritev/writev/write/pwrite64 +// siblings, and never as a read. +// +// The invariants pinned here: +// - enter classifies as KindFd off the first "fd" field (fd at args[0]), +// - family is FS (matches pwritev/writev/pwrite64/preadv2 file-IO siblings), +// - return classifies as WRITE_CLASSIFIED (bytes-written → written). +// +// Contrast case guards the easy off-by-one mistake: preadv2 is the read-side +// sibling (READ_CLASSIFIED, never write). The whole p/readv/writev family is +// asserted together so a stray reclassification of any sibling trips the test. +func TestClassifyPwritev2WriteByteCount(t *testing.T) { + // Field layout mirrors the actual kernel tracepoint format for + // sys_enter_pwritev2: int fd, struct iovec *vec, unsigned long vlen, + // unsigned long pos_l, unsigned long pos_h, rwf_t flags. + pwritev2 := ClassifyFormat(&Format{ + Name: "sys_enter_pwritev2", + ExternalFields: []Field{ + {Type: "int", Name: "__syscall_nr"}, + {Type: "unsigned long", Name: "fd"}, + {Type: "const struct iovec *", Name: "vec"}, + {Type: "unsigned long", Name: "vlen"}, + {Type: "unsigned long", Name: "pos_l"}, + {Type: "unsigned long", Name: "pos_h"}, + {Type: "rwf_t", Name: "flags"}, + }, + }) + if pwritev2.Kind != KindFd { + t.Fatalf("pwritev2: got kind %d, want KindFd (fd at args[0])", pwritev2.Kind) + } + + if fam := ClassifySyscallFamily("sys_enter_pwritev2"); fam != FamilyFS { + t.Fatalf("pwritev2: got family %s, want FamilyFS", fam) + } + + // Return value is a byte count of data written → counted as written. + if got := ClassifyRet("sys_exit_pwritev2"); got != WriteClassified { + t.Fatalf("pwritev2: ClassifyRet = %q, want WRITE_CLASSIFIED (return is bytes written)", got) + } + + // preadv2 is the read-side sibling; it must never be a write. + if got := ClassifyRet("sys_exit_preadv2"); got != ReadClassified { + t.Fatalf("preadv2: ClassifyRet = %q, want READ_CLASSIFIED", got) + } + + // All write-side vectored/positional siblings count their byte-count return + // as written; assert the whole group so a stray reclassification trips here. + for _, name := range []string{ + "sys_exit_pwritev", + "sys_exit_writev", + "sys_exit_write", + "sys_exit_pwrite64", + } { + if got := ClassifyRet(name); got != WriteClassified { + t.Errorf("%s: ClassifyRet = %q, want WRITE_CLASSIFIED", name, got) + } + } + + // All read-side siblings stay READ_CLASSIFIED (off-by-one guard). + for _, name := range []string{ + "sys_exit_preadv", + "sys_exit_readv", + "sys_exit_read", + "sys_exit_pread64", + } { + if got := ClassifyRet(name); got != ReadClassified { + t.Errorf("%s: ClassifyRet = %q, want READ_CLASSIFIED", name, got) + } + } + + // The whole p/readv/writev family shares the FS family. + for _, name := range []string{ + "sys_enter_pwritev", + "sys_enter_writev", + "sys_enter_write", + "sys_enter_pwrite64", + "sys_enter_preadv2", + "sys_enter_preadv", + "sys_enter_readv", + "sys_enter_read", + "sys_enter_pread64", + } { + if fam := ClassifySyscallFamily(name); fam != FamilyFS { + t.Errorf("%s: got family %s, want FamilyFS", name, fam) + } + } +} + func mustParseAll(t *testing.T, data string) []Format { t.Helper() formats, err := ParseFormats(strings.NewReader(data)) -- cgit v1.2.3