diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-30 22:11:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-30 22:11:15 +0300 |
| commit | 04881431fb051fc9915184c54dffdcbb9aa5c65e (patch) | |
| tree | cd7a88571aaf0032ab8560f4abb51554d9517bee | |
| parent | dfb6190d109593227545df2e0caf82b6ee2c578f (diff) | |
test(perf_event_open): lock in audit findings
Audited perf_event_open(2) against the man page: it returns a new fd (or
-1), args[0] is a struct perf_event_attr* userspace pointer (NOT an fd),
args[1] is a monitored pid, and only args[3] group_fd is a real fd.
The existing implementation is correct (KindPerfOpen by name, not KindFd;
FamilySecurity; exit as UNCLASSIFIED RetEvent). Add lock-in tests:
- codegen: assert args[0] is read via bpf_probe_read_user as the attr
struct and never captured as an fd (negative assertions on args[0]/args[1]).
- eventloop: a failed return (-1) registers no fd in fdState.
- perfDescriptorName format pin (perf: prefix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | internal/eventloop_security_test.go | 63 | ||||
| -rw-r--r-- | internal/generate/codegen_test.go | 10 |
2 files changed, 73 insertions, 0 deletions
diff --git a/internal/eventloop_security_test.go b/internal/eventloop_security_test.go index 0dd9ae7..e00b98a 100644 --- a/internal/eventloop_security_test.go +++ b/internal/eventloop_security_test.go @@ -43,6 +43,69 @@ func TestHandlePerfOpenExitTracksReturnedFd(t *testing.T) { } } +// TestHandlePerfOpenExitFailedReturnNoFd locks in the audit finding that +// perf_event_open(2) returns a new fd on success or -1 on error. A negative +// return (e.g. EPERM, which is common for perf_event_open under a restrictive +// perf_event_paranoid setting) must not be recorded as an fd, since it is not +// a real descriptor. The return is an fd number, never a byte count. +func TestHandlePerfOpenExitFailedReturnNoFd(t *testing.T) { + el := mustNewEventLoop(t, eventLoopConfig{}) + + enter := &types.PerfOpenEvent{ + EventType: types.ENTER_PERF_OPEN_EVENT, + TraceId: types.SYS_ENTER_PERF_EVENT_OPEN, + Time: 100, + Pid: 300, + Tid: 301, + AttrType: 1, + AttrSize: 64, + Config: 2, + TargetPid: -1, + Cpu: 0, + GroupFd: -1, + Flags: 0, + } + exit := &types.RetEvent{ + EventType: types.EXIT_RET_EVENT, + TraceId: types.SYS_EXIT_PERF_EVENT_OPEN, + Time: 200, + Ret: -1, // -EPERM mapped to -1 return + Pid: 300, + Tid: 301, + } + ep := &event.Pair{EnterEv: enter, ExitEv: exit} + + if ok := el.handlePerfOpenExit(ep, enter); !ok { + t.Fatal("handlePerfOpenExit returned false") + } + if ep.File != nil { + t.Fatalf("expected no fd recorded for failed perf_event_open, got file=%v", ep.File) + } + if _, ok := el.fdState().get(-1); ok { + t.Fatal("failed perf_event_open must not register an fd in fdState") + } +} + +// TestPerfDescriptorNameFormat pins the descriptor format produced for a +// successful perf_event_open. The audit confirmed args[0] (attr pointer) is +// captured as the attr struct's type/config, not as an fd, and args[1] is the +// monitored pid. The descriptor encodes attr_type, config, target pid, cpu, +// and group_fd under the "perf:" prefix (matched by the integration test). +func TestPerfDescriptorNameFormat(t *testing.T) { + ev := &types.PerfOpenEvent{ + AttrType: 4, + Config: 7, + TargetPid: 0, + Cpu: -1, + GroupFd: -1, + } + got := perfDescriptorName(ev) + const want = "perf:4:7:0:-1:-1" + if got != want { + t.Fatalf("perfDescriptorName = %q, want %q", got, want) + } +} + func TestHandlePerfOpenExitAppliesPairFilter(t *testing.T) { el := mustNewEventLoop(t, eventLoopConfig{ filter: globalfilter.Filter{ diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go index e841f45..58ed60c 100644 --- a/internal/generate/codegen_test.go +++ b/internal/generate/codegen_test.go @@ -1992,6 +1992,16 @@ func TestGeneratePerfEventOpenHandler(t *testing.T) { requireContains(t, output, "ev->target_pid = (__s32)ctx->args[1];") requireContains(t, output, "ev->group_fd = (__s32)ctx->args[3];") requireContains(t, output, "ev->event_type = EXIT_RET_EVENT;") + + // Audit lock-in (perf_event_open(2)): args[0] is a + // `struct perf_event_attr *` userspace pointer, NOT an fd, and args[1] + // is a pid (not an fd). The handler must read args[0] only via + // bpf_probe_read_user (the attr struct) and never capture args[0] or + // args[1] as an fd. Only group_fd at args[3] is a genuine fd. + requireContains(t, output, "bpf_probe_read_user(&attr, sizeof(attr), (void *)ctx->args[0])") + requireNotContains(t, output, "ev->fd = (__s32)ctx->args[0];") + requireNotContains(t, output, "ev->fd = (__s32)ctx->args[1];") + requireNotContains(t, output, "ev->group_fd = (__s32)ctx->args[0];") } func TestGenerateNameToHandleAtHandler(t *testing.T) { |
