summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-30 22:11:15 +0300
committerPaul Buetow <paul@buetow.org>2026-05-30 22:11:15 +0300
commit04881431fb051fc9915184c54dffdcbb9aa5c65e (patch)
treecd7a88571aaf0032ab8560f4abb51554d9517bee /internal
parentdfb6190d109593227545df2e0caf82b6ee2c578f (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>
Diffstat (limited to 'internal')
-rw-r--r--internal/eventloop_security_test.go63
-rw-r--r--internal/generate/codegen_test.go10
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) {