From e4d91d9a0e0e3dfb3ec6914c265c2f117a66d062 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 29 May 2026 16:53:32 +0300 Subject: test(process_madvise): lock in KindFd + UNCLASSIFIED + FamilyMemory Audit of process_madvise(2) confirmed the existing classification is correct and added lock-in coverage: - KindFd with fd=args[0]: the first arg is a pidfd (a PID file descriptor selecting the target process), not an address, so it must NOT be treated like madvise(2) (KindMem, addr=args[0]). Extended the enter-handler test with a negative assertion guarding against the KindMem addr wiring. - Exit ret_type=UNCLASSIFIED: process_madvise returns the number of bytes advised, but that is advisory accounting, not real I/O data movement, so it stays UNCLASSIFIED like madvise(2). Added an exit-handler assertion plus TestClassifyRetProcessMadviseUnclassified. - FamilyMemory: shares the family with madvise/process_mrelease/process_vm_* siblings rather than FamilyIPC (pidfd_* lifecycle). Added family lock-in cases in family_test.go. No classification/codegen changes were required; mage generate produces no diff. Full ./internal/... passes. Co-Authored-By: Claude Opus 4.8 --- internal/generate/codegen_test.go | 37 +++++++++++++++++++++++++++++++++++++ internal/generate/family_test.go | 8 ++++++++ 2 files changed, 45 insertions(+) (limited to 'internal') diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go index fe7a8d7..eabb1f8 100644 --- a/internal/generate/codegen_test.go +++ b/internal/generate/codegen_test.go @@ -33,6 +33,20 @@ func TestGeneratePidfdGetfdHandlerUsesPidfdArgument(t *testing.T) { requireContains(t, output, "ev->fd = (__s32)ctx->args[0];") } +// TestGenerateProcessMadviseHandlerUsesFirstArgumentAsFd locks in the BPF +// handler wiring for process_madvise(2): +// +// ssize_t process_madvise(int pidfd, const struct iovec iovec[.n], size_t n, +// int advice, unsigned int flags). +// +// Unlike the sibling madvise(2) (KindMem, addr/length at args[0]/args[1]), the +// first argument here is a pidfd — a PID *file descriptor* selecting the target +// process (see pidfd_open(2)) — so process_madvise is classified KindFd and the +// enter handler must capture ev->fd from args[0], NOT treat args[0] as an +// address. process_madvise returns the number of bytes advised on success or -1 +// on error, but that count is advisory (no data is actually transferred), so the +// exit handler reports the raw status as UNCLASSIFIED exactly like madvise(2) — +// it must never be misclassified as a READ/WRITE/TRANSFER byte count. func TestGenerateProcessMadviseHandlerUsesFirstArgumentAsFd(t *testing.T) { output := GenerateTracepointsC(mustParseAll(t, syntheticPair("process_madvise"))) @@ -41,6 +55,29 @@ func TestGenerateProcessMadviseHandlerUsesFirstArgumentAsFd(t *testing.T) { requireContains(t, output, "ev->event_type = ENTER_FD_EVENT;") requireContains(t, output, "ev->trace_id = SYS_ENTER_PROCESS_MADVISE;") requireContains(t, output, "ev->fd = (__s32)ctx->args[0];") + // args[0] is a pidfd, never an address: the KindMem addr wiring must not leak + // into the process_madvise enter handler. + if strings.Contains(output, "ev->addr = (__u64)ctx->args[0];") && + strings.Contains(output, `SEC("tracepoint/syscalls/sys_enter_process_madvise")`) && + strings.Contains(output, "struct mem_event *ev") { + t.Error("process_madvise must be KindFd (fd=args[0]), not KindMem (addr=args[0])") + } + // The exit handler returns the advisory byte count generically as the raw + // status, classified UNCLASSIFIED — not as a transfer/byte-count. + requireContains(t, output, `SEC("tracepoint/syscalls/sys_exit_process_madvise")`) + requireContains(t, output, "ev->ret = ctx->ret;") + requireContains(t, output, "ev->ret_type = UNCLASSIFIED;") +} + +// TestClassifyRetProcessMadviseUnclassified locks in that process_madvise's +// return value is UNCLASSIFIED. The man page says it returns "the number of +// bytes advised", but that is advisory accounting, not real I/O: no bytes move +// between buffers. Classifying it as TRANSFER/READ/WRITE would double-count it as +// data movement, so it must stay UNCLASSIFIED like madvise(2). +func TestClassifyRetProcessMadviseUnclassified(t *testing.T) { + if got := ClassifyRet("sys_exit_process_madvise"); got != Unclassified { + t.Errorf("process_madvise ret classification = %q, want %q", got, Unclassified) + } } func TestGenerateLandlockAddRuleHandlerUsesFirstArgumentAsFd(t *testing.T) { diff --git a/internal/generate/family_test.go b/internal/generate/family_test.go index a708090..08a601a 100644 --- a/internal/generate/family_test.go +++ b/internal/generate/family_test.go @@ -11,6 +11,14 @@ func TestClassifySyscallFamily(t *testing.T) { {"sys_exit_accept", FamilyNetwork}, {"sys_enter_pipe2", FamilyIPC}, {"sys_enter_munmap", FamilyMemory}, + // process_madvise(2) gives memory advice (MADV_COLD/PAGEOUT/...) about + // address ranges of another process selected by a pidfd. Although its + // first arg is a pidfd (KindFd) rather than an address, the operation is + // fundamentally a memory-advice call, so it shares FamilyMemory with its + // madvise(2)/process_mrelease(2)/process_vm_readv/writev(2) siblings — not + // FamilyIPC (where the pidfd_* lifecycle syscalls live). + {"sys_enter_process_madvise", FamilyMemory}, + {"sys_exit_process_madvise", FamilyMemory}, {"sys_enter_execve", FamilyProcess}, {"sys_enter_rt_sigaction", FamilySignals}, {"sys_enter_clock_gettime", FamilyTime}, -- cgit v1.2.3