diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-21 11:51:01 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-21 11:51:01 +0200 |
| commit | 6c912a9d72ae2a43923c638538d320e6bf585952 (patch) | |
| tree | 727f66d158210e01abf8c18a83ef4db6066e0c1a /internal/generate | |
| parent | 32136b8cb18944157ff1f361bc0755f6b627fd47 (diff) | |
Migrate make targets to mage
Amp-Thread-ID: https://ampcode.com/threads/T-019c7f4e-cc5f-76f1-aaf0-dd7cbaabbb18
Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal/generate')
| -rw-r--r-- | internal/generate/bpfhandler.go | 152 | ||||
| -rw-r--r-- | internal/generate/classify.go | 214 | ||||
| -rw-r--r-- | internal/generate/classify_test.go | 332 | ||||
| -rw-r--r-- | internal/generate/codegen.go | 152 | ||||
| -rw-r--r-- | internal/generate/codegen_test.go | 263 | ||||
| -rw-r--r-- | internal/generate/format.go | 145 | ||||
| -rw-r--r-- | internal/generate/format_test.go | 174 | ||||
| -rw-r--r-- | internal/generate/retclassify_test.go | 59 | ||||
| -rw-r--r-- | internal/generate/testdata.go | 666 | ||||
| -rw-r--r-- | internal/generate/tracepointsgo.go | 43 | ||||
| -rw-r--r-- | internal/generate/tracepointsgo_test.go | 92 | ||||
| -rw-r--r-- | internal/generate/typesgo.go | 341 | ||||
| -rw-r--r-- | internal/generate/typesgo_test.go | 257 |
13 files changed, 2890 insertions, 0 deletions
diff --git a/internal/generate/bpfhandler.go b/internal/generate/bpfhandler.go new file mode 100644 index 0000000..1ce6d3e --- /dev/null +++ b/internal/generate/bpfhandler.go @@ -0,0 +1,152 @@ +package generate + +import ( + "fmt" + "strings" +) + +func generateBPFHandler(tp GeneratedTracepoint) string { + f := tp.Format + isEnter := strings.Split(f.Name, "_")[1] == "enter" + + ctxStruct := "trace_event_raw_sys_exit" + if isEnter { + ctxStruct = "trace_event_raw_sys_enter" + } + + eventStruct := eventStructName(tp.Classification.Kind) + comment := eventStruct + if tp.Classification.Kind == KindRet { + comment = fmt.Sprintf("%s (%s)", eventStruct, ClassifyRet(f.Name)) + } + + eventTypeConst := eventTypeConstant(tp.Classification.Kind, isEnter) + extra := generateExtra(tp, isEnter) + + return renderHandler(f.Name, ctxStruct, eventStruct, comment, eventTypeConst, extra) +} + +func renderHandler(name, ctxStruct, eventStruct, comment, eventTypeConst, extra string) string { + var b strings.Builder + fmt.Fprintf(&b, "/// %s is a struct %s\n", name, comment) + fmt.Fprintf(&b, "SEC(\"tracepoint/syscalls/%s\")\n", name) + fmt.Fprintf(&b, "int handle_%s(struct %s *ctx) {\n", strings.ToLower(name), ctxStruct) + b.WriteString(" __u32 pid, tid;\n") + b.WriteString(" if (filter(&pid, &tid))\n") + b.WriteString(" return 0;\n") + b.WriteString("\n") + fmt.Fprintf(&b, " struct %s *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct %s), 0);\n", eventStruct, eventStruct) + b.WriteString(" if (!ev)\n") + b.WriteString(" return 0;\n") + b.WriteString("\n") + fmt.Fprintf(&b, " ev->event_type = %s;\n", eventTypeConst) + fmt.Fprintf(&b, " ev->trace_id = %s;\n", strings.ToUpper(name)) + b.WriteString(" ev->pid = pid;\n") + b.WriteString(" ev->tid = tid;\n") + b.WriteString(" ev->time = bpf_ktime_get_boot_ns();\n") + if extra != "" { + b.WriteString(extra) + } + b.WriteString("\n") + b.WriteString(" bpf_ringbuf_submit(ev, 0);\n") + b.WriteString(" return 0;\n") + b.WriteString("}\n") + return b.String() +} + +func generateExtra(tp GeneratedTracepoint, isEnter bool) string { + f := tp.Format + + switch tp.Classification.Kind { + case KindFd: + return " ev->fd = (__s32)ctx->args[0];\n" + + case KindDup3: + return " ev->fd = (__s32)ctx->args[0];\n ev->flags = (__s32)ctx->args[2];\n" + + case KindOpenByHandleAt: + return " ev->flags = (__s32)ctx->args[2];\n" + + case KindOpen: + filenameIdx := f.FieldNumber("filename") + flagsIdx := f.FieldNumber("flags") + var b strings.Builder + b.WriteString(" __builtin_memset(&(ev->filename), 0, sizeof(ev->filename) + sizeof(ev->comm));\n") + fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->filename, sizeof(ev->filename), (void *)ctx->args[%d]);\n", filenameIdx) + b.WriteString(" bpf_get_current_comm(&ev->comm, sizeof(ev->comm));\n") + if flagsIdx > -1 { + fmt.Fprintf(&b, " ev->flags = ctx->args[%d];\n", flagsIdx) + } else { + b.WriteString(" ev->flags = -1; // Probably OK\n") + } + return b.String() + + case KindPathname: + fieldName := tp.Classification.PathnameField + fieldIdx := f.FieldNumber(fieldName) + var b strings.Builder + b.WriteString(" __builtin_memset(&(ev->pathname), 0, sizeof(ev->pathname));\n") + fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[%d]);\n", fieldIdx) + return b.String() + + case KindName: + oldIdx := f.FieldNumber("oldname") + newIdx := f.FieldNumber("newname") + var b strings.Builder + b.WriteString(" __builtin_memset(&(ev->oldname), 0, sizeof(ev->oldname) + sizeof(ev->newname));\n") + fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->oldname, sizeof(ev->oldname), (void*)ctx->args[%d]);\n", oldIdx) + fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->newname, sizeof(ev->newname), (void*)ctx->args[%d]);\n", newIdx) + return b.String() + + case KindFcntl: + fdIdx := f.FieldNumber("fd") + cmdIdx := f.FieldNumber("cmd") + argIdx := f.FieldNumber("arg") + return fmt.Sprintf( + " ev->fd = ctx->args[%d];\n ev->cmd = ctx->args[%d];\n ev->arg = ctx->args[%d];\n", + fdIdx, cmdIdx, argIdx, + ) + + case KindRet: + classification := ClassifyRet(f.Name) + return fmt.Sprintf(" ev->ret = ctx->ret;\n ev->ret_type = %s;\n", classification) + + case KindNull: + return "" + } + + return "" +} + +func eventStructName(kind TracepointKind) string { + switch kind { + case KindFd: + return "fd_event" + case KindOpen: + return "open_event" + case KindPathname: + return "path_event" + case KindName: + return "name_event" + case KindRet: + return "ret_event" + case KindFcntl: + return "fcntl_event" + case KindNull: + return "null_event" + case KindDup3: + return "dup3_event" + case KindOpenByHandleAt: + return "open_by_handle_at_event" + default: + return "unknown_event" + } +} + +func eventTypeConstant(kind TracepointKind, isEnter bool) string { + prefix := "EXIT_" + if isEnter { + prefix = "ENTER_" + } + return prefix + strings.ToUpper(eventStructName(kind)) +} diff --git a/internal/generate/classify.go b/internal/generate/classify.go new file mode 100644 index 0000000..75a12fe --- /dev/null +++ b/internal/generate/classify.go @@ -0,0 +1,214 @@ +package generate + +import "strings" + +type TracepointKind int + +const ( + KindNone TracepointKind = iota + KindFd + KindOpen + KindPathname + KindName + KindRet + KindFcntl + KindNull + KindDup3 + KindOpenByHandleAt +) + +type RetClassification string + +const ( + Unclassified RetClassification = "UNCLASSIFIED" + ReadClassified RetClassification = "READ_CLASSIFIED" + WriteClassified RetClassification = "WRITE_CLASSIFIED" + TransferClassified RetClassification = "TRANSFER_CLASSIFIED" +) + +type ClassificationResult struct { + Kind TracepointKind + PathnameField string // for KindPathname: "pathname", "path", or "filename" +} + +// ClassifyFormat determines the tracepoint kind for a parsed format section. +// It mirrors the Raku multi-dispatch: name-based ignores take priority, +// then name-only mappings, then each external field is tried in order until +// one matches a name+field or generic field pattern. +func ClassifyFormat(f *Format) ClassificationResult { + if len(f.ExternalFields) == 0 { + return ClassificationResult{Kind: KindNone} + } + + if shouldIgnore(f.Name) { + return ClassificationResult{Kind: KindNone} + } + + if r, ok := classifyNameOnly(f.Name); ok { + return r + } + + for _, field := range f.ExternalFields { + if field.Name == "__syscall_nr" { + continue + } + if r, ok := classifyNameAndField(f.Name, field.Type, field.Name); ok { + return r + } + if r, ok := classifyByField(field.Type, field.Name); ok { + return r + } + } + + return ClassificationResult{Kind: KindNone} +} + +func shouldIgnore(name string) bool { + prefixIgnores := []string{ + "sys_enter_mknod", + "sys_enter_execve", + "sys_enter_accept", + "sys_enter_listen", + "sys_enter_epoll", + } + for _, p := range prefixIgnores { + if strings.HasPrefix(name, p) { + return true + } + } + + if strings.HasPrefix(name, "sys_enter_") { + containsIgnores := []string{"recv", "send", "sock", "inotify", "pidfd"} + for _, sub := range containsIgnores { + if strings.Contains(name, sub) { + return true + } + } + } + + exactIgnores := map[string]bool{ + "sys_enter_bind": true, + "sys_enter_setns": true, + "sys_enter_shutdown": true, + "sys_enter_connect": true, + "sys_enter_fanotify_init": true, + "sys_enter_getpeername": true, + } + return exactIgnores[name] +} + +// classifyNameOnly handles tracepoints classified by name alone, +// independent of any field. +func classifyNameOnly(name string) (ClassificationResult, bool) { + switch name { + case "sys_enter_open_by_handle_at": + return ClassificationResult{Kind: KindOpenByHandleAt}, true + case "sys_enter_fcntl": + return ClassificationResult{Kind: KindFcntl}, true + case "sys_enter_syslog": + return ClassificationResult{Kind: KindNull}, true + case "sys_enter_sync": + return ClassificationResult{Kind: KindNull}, true + } + if strings.HasPrefix(name, "sys_enter_io_") { + return ClassificationResult{Kind: KindNull}, true + } + return ClassificationResult{}, false +} + +// classifyNameAndField handles tracepoints that need both the name and +// a specific field to classify. +func classifyNameAndField(name, fieldType, fieldName string) (ClassificationResult, bool) { + switch name { + case "sys_enter_dup": + if fieldType == "unsigned int" && fieldName == "fildes" { + return ClassificationResult{Kind: KindFd}, true + } + case "sys_enter_dup2": + if fieldType == "unsigned int" && fieldName == "oldfd" { + return ClassificationResult{Kind: KindFd}, true + } + case "sys_enter_dup3": + if fieldType == "unsigned int" && fieldName == "oldfd" { + return ClassificationResult{Kind: KindDup3}, true + } + } + + if strings.HasPrefix(name, "sys_enter") && + strings.Contains(name, "open") && + fieldType == "const char *" && fieldName == "filename" { + return ClassificationResult{Kind: KindOpen}, true + } + + return ClassificationResult{}, false +} + +func classifyByField(fieldType, fieldName string) (ClassificationResult, bool) { + switch { + case fieldName == "fd" && isFdType(fieldType): + return ClassificationResult{Kind: KindFd}, true + case fieldType == "const char *" && fieldName == "newname": + return ClassificationResult{Kind: KindName}, true + case fieldType == "const char *" && fieldName == "pathname": + return ClassificationResult{Kind: KindPathname, PathnameField: "pathname"}, true + case fieldType == "const char *" && fieldName == "path": + return ClassificationResult{Kind: KindPathname, PathnameField: "path"}, true + case fieldType == "const char *" && fieldName == "filename": + return ClassificationResult{Kind: KindPathname, PathnameField: "filename"}, true + case fieldType == "long" && fieldName == "ret": + return ClassificationResult{Kind: KindRet}, true + } + return ClassificationResult{}, false +} + +func isFdType(t string) bool { + return t == "unsigned int" || t == "unsigned long" || t == "int" +} + +// ClassifyRet returns the RetClassification for a syscall exit name. +func ClassifyRet(name string) RetClassification { + syscall := strings.ToLower(strings.TrimPrefix(name, "sys_exit_")) + if c, ok := retClassifications[syscall]; ok { + return c + } + return Unclassified +} + +var retClassifications = map[string]RetClassification{ + "fgetxattr": ReadClassified, + "flistxattr": ReadClassified, + "getdents": ReadClassified, + "getdents64": ReadClassified, + "getxattr": ReadClassified, + "lgetxattr": ReadClassified, + "listxattr": ReadClassified, + "llistxattr": ReadClassified, + "pread64": ReadClassified, + "preadv": ReadClassified, + "preadv2": ReadClassified, + "process_vm_readv": ReadClassified, + "read": ReadClassified, + "readlink": ReadClassified, + "readlinkat": ReadClassified, + "readv": ReadClassified, + "recvmmsg": ReadClassified, + "recvmsg": ReadClassified, + "recvfrom": ReadClassified, + "syslog": ReadClassified, + + "copy_file_range": TransferClassified, + "sendfile64": TransferClassified, + "splice": TransferClassified, + "tee": TransferClassified, + "vmsplice": TransferClassified, + + "process_vm_writev": WriteClassified, + "pwrite64": WriteClassified, + "pwritev": WriteClassified, + "pwritev2": WriteClassified, + "sendmmsg": WriteClassified, + "sendmsg": WriteClassified, + "sendto": WriteClassified, + "write": WriteClassified, + "writev": WriteClassified, +} diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go new file mode 100644 index 0000000..c94e359 --- /dev/null +++ b/internal/generate/classify_test.go @@ -0,0 +1,332 @@ +package generate + +import ( + "strings" + "testing" +) + +func classifyFromData(t *testing.T, data string) ClassificationResult { + t.Helper() + f := mustParseOne(t, data) + return ClassifyFormat(&f) +} + +func TestClassifyFdRead(t *testing.T) { + r := classifyFromData(t, FormatRead) + if r.Kind != KindFd { + t.Errorf("read: got kind %d, want KindFd", r.Kind) + } +} + +func TestClassifyFdClose(t *testing.T) { + r := classifyFromData(t, FormatClose) + if r.Kind != KindFd { + t.Errorf("close: got kind %d, want KindFd", r.Kind) + } +} + +func TestClassifyFdPread64(t *testing.T) { + r := classifyFromData(t, FormatPread64) + if r.Kind != KindFd { + t.Errorf("pread64: got kind %d, want KindFd", r.Kind) + } +} + +func TestClassifyFdWrite(t *testing.T) { + r := classifyFromData(t, FormatWrite) + if r.Kind != KindFd { + t.Errorf("write: got kind %d, want KindFd", r.Kind) + } +} + +func TestClassifyOpenOpenat(t *testing.T) { + r := classifyFromData(t, FormatOpenat) + if r.Kind != KindOpen { + t.Errorf("openat: got kind %d, want KindOpen", r.Kind) + } +} + +func TestClassifyOpenOpen(t *testing.T) { + r := classifyFromData(t, FormatOpen) + if r.Kind != KindOpen { + t.Errorf("open: got kind %d, want KindOpen", r.Kind) + } +} + +func TestClassifyOpenOpenat2(t *testing.T) { + r := classifyFromData(t, FormatOpenat2) + if r.Kind != KindOpen { + t.Errorf("openat2: got kind %d, want KindOpen", r.Kind) + } +} + +func TestClassifyPathnameCreat(t *testing.T) { + r := classifyFromData(t, FormatCreat) + if r.Kind != KindPathname { + t.Errorf("creat: got kind %d, want KindPathname", r.Kind) + } + if r.PathnameField != "pathname" { + t.Errorf("creat: PathnameField = %q, want pathname", r.PathnameField) + } +} + +func TestClassifyPathnameUnlink(t *testing.T) { + r := classifyFromData(t, FormatUnlink) + if r.Kind != KindPathname { + t.Errorf("unlink: got kind %d, want KindPathname", r.Kind) + } + if r.PathnameField != "pathname" { + t.Errorf("unlink: PathnameField = %q, want pathname", r.PathnameField) + } +} + +func TestClassifyNameRename(t *testing.T) { + r := classifyFromData(t, FormatRename) + if r.Kind != KindName { + t.Errorf("rename: got kind %d, want KindName", r.Kind) + } +} + +func TestClassifyNameLinkat(t *testing.T) { + r := classifyFromData(t, FormatLinkat) + if r.Kind != KindName { + t.Errorf("linkat: got kind %d, want KindName", r.Kind) + } +} + +func TestClassifyNameSymlink(t *testing.T) { + r := classifyFromData(t, FormatSymlink) + if r.Kind != KindName { + t.Errorf("symlink: got kind %d, want KindName", r.Kind) + } +} + +func TestClassifyFcntl(t *testing.T) { + r := classifyFromData(t, FormatFcntl) + if r.Kind != KindFcntl { + t.Errorf("fcntl: got kind %d, want KindFcntl", r.Kind) + } +} + +func TestClassifyDup(t *testing.T) { + r := classifyFromData(t, FormatDup) + if r.Kind != KindFd { + t.Errorf("dup: got kind %d, want KindFd", r.Kind) + } +} + +func TestClassifyDup2(t *testing.T) { + r := classifyFromData(t, FormatDup2) + if r.Kind != KindFd { + t.Errorf("dup2: got kind %d, want KindFd", r.Kind) + } +} + +func TestClassifyDup3(t *testing.T) { + r := classifyFromData(t, FormatDup3) + if r.Kind != KindDup3 { + t.Errorf("dup3: got kind %d, want KindDup3", r.Kind) + } +} + +func TestClassifyOpenByHandleAt(t *testing.T) { + r := classifyFromData(t, FormatOpenByHandleAt) + if r.Kind != KindOpenByHandleAt { + t.Errorf("open_by_handle_at: got kind %d, want KindOpenByHandleAt", r.Kind) + } +} + +func TestClassifyNullSync(t *testing.T) { + r := classifyFromData(t, FormatSync) + if r.Kind != KindNull { + t.Errorf("sync: got kind %d, want KindNull", r.Kind) + } +} + +func TestClassifyNullSyslog(t *testing.T) { + r := classifyFromData(t, FormatSyslog) + if r.Kind != KindNull { + t.Errorf("syslog: got kind %d, want KindNull", r.Kind) + } +} + +func TestClassifyNullIoUring(t *testing.T) { + r := classifyFromData(t, FormatIoUringEnter) + if r.Kind != KindNull { + t.Errorf("io_uring_enter: got kind %d, want KindNull", r.Kind) + } +} + +func TestClassifyRetExitRead(t *testing.T) { + r := classifyFromData(t, FormatExitRead) + if r.Kind != KindRet { + t.Errorf("exit_read: got kind %d, want KindRet", r.Kind) + } +} + +func TestClassifyRetExitWrite(t *testing.T) { + r := classifyFromData(t, FormatExitWrite) + if r.Kind != KindRet { + t.Errorf("exit_write: got kind %d, want KindRet", r.Kind) + } +} + +func TestClassifyRetExitOpenat(t *testing.T) { + r := classifyFromData(t, FormatExitOpenat) + if r.Kind != KindRet { + t.Errorf("exit_openat: got kind %d, want KindRet", r.Kind) + } +} + +func TestClassifyRetExitPread64(t *testing.T) { + r := classifyFromData(t, FormatExitPread64) + if r.Kind != KindRet { + t.Errorf("exit_pread64: got kind %d, want KindRet", r.Kind) + } +} + +func TestClassifyRetExitSymlink(t *testing.T) { + r := classifyFromData(t, FormatExitSymlink) + if r.Kind != KindRet { + t.Errorf("exit_symlink: got kind %d, want KindRet", r.Kind) + } +} + +// --- Ignore tests --- + +func TestIgnoreMknod(t *testing.T) { + r := classifyFromData(t, FormatMknod) + if r.Kind != KindNone { + t.Errorf("mknod: got kind %d, want KindNone (ignored)", r.Kind) + } +} + +func TestIgnoreExecve(t *testing.T) { + r := classifyFromData(t, FormatExecve) + if r.Kind != KindNone { + t.Errorf("execve: got kind %d, want KindNone (ignored)", r.Kind) + } +} + +func TestIgnoreAccept(t *testing.T) { + r := classifyFromData(t, FormatAccept) + if r.Kind != KindNone { + t.Errorf("accept: got kind %d, want KindNone (ignored)", r.Kind) + } +} + +func TestIgnoreSocket(t *testing.T) { + r := classifyFromData(t, FormatSocket) + if r.Kind != KindNone { + t.Errorf("socket: got kind %d, want KindNone (ignored)", r.Kind) + } +} + +func TestIgnoreKill(t *testing.T) { + r := classifyFromData(t, FormatKill) + if r.Kind != KindNone { + t.Errorf("kill: got kind %d, want KindNone (no matching type)", r.Kind) + } +} + +func TestShouldIgnorePatterns(t *testing.T) { + ignoreNames := []string{ + "sys_enter_mknod", "sys_enter_mknodat", + "sys_enter_execve", "sys_enter_execveat", + "sys_enter_accept", "sys_enter_accept4", + "sys_enter_listen", + "sys_enter_epoll_ctl", "sys_enter_epoll_pwait", + "sys_enter_recvfrom", "sys_enter_recvmsg", "sys_enter_recvmmsg", + "sys_enter_sendto", "sys_enter_sendmsg", "sys_enter_sendmmsg", + "sys_enter_socket", "sys_enter_socketpair", "sys_enter_getsockname", + "sys_enter_inotify_init", "sys_enter_inotify_add_watch", + "sys_enter_pidfd_open", "sys_enter_pidfd_getfd", + "sys_enter_bind", "sys_enter_setns", "sys_enter_shutdown", + "sys_enter_connect", "sys_enter_fanotify_init", "sys_enter_getpeername", + } + for _, name := range ignoreNames { + if !shouldIgnore(name) { + t.Errorf("shouldIgnore(%q) = false, want true", name) + } + } +} + +func TestShouldNotIgnore(t *testing.T) { + noIgnore := []string{ + "sys_enter_read", "sys_enter_write", "sys_enter_openat", + "sys_enter_close", "sys_enter_rename", "sys_enter_unlink", + "sys_exit_read", "sys_exit_openat", + } + for _, name := range noIgnore { + if shouldIgnore(name) { + t.Errorf("shouldIgnore(%q) = true, want false", name) + } + } +} + +// --- End-to-end classification with enter+exit pairs --- + +func TestClassifySyscallPairAccepted(t *testing.T) { + tests := []struct { + name string + enter string + exit string + enterKind TracepointKind + }{ + {"read", FormatRead, FormatExitRead, KindFd}, + {"openat", FormatOpenat, FormatExitOpenat, KindOpen}, + {"rename", FormatRename, FormatExitRename, KindName}, + {"close", FormatClose, FormatExitClose, KindFd}, + {"dup3", FormatDup3, FormatExitDup3, KindDup3}, + {"fcntl", FormatFcntl, FormatExitFcntl, KindFcntl}, + {"sync", FormatSync, FormatExitSync, KindNull}, + {"syslog", FormatSyslog, FormatExitSyslog, KindNull}, + {"open_by_handle_at", FormatOpenByHandleAt, FormatExitOpenByHandleAt, KindOpenByHandleAt}, + {"io_uring_enter", FormatIoUringEnter, FormatExitIoUringEnter, KindNull}, + {"pread64", FormatPread64, FormatExitPread64, KindFd}, + {"symlink", FormatSymlink, FormatExitSymlink, KindName}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := tt.enter + "\n" + tt.exit + output := GenerateTracepointsC(mustParseAll(t, input)) + if strings.Contains(output, "Ignoring") { + t.Errorf("syscall %s was ignored, expected accepted", tt.name) + } + }) + } +} + +func TestClassifySyscallPairIgnored(t *testing.T) { + tests := []struct { + name string + enter string + exit string + }{ + {"mknod", FormatMknod, FormatExitMknod}, + {"execve", FormatExecve, FormatExitExecve}, + {"accept", FormatAccept, FormatExitAccept}, + {"socket", FormatSocket, FormatExitSocket}, + {"kill", FormatKill, FormatExitKill}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := tt.enter + "\n" + tt.exit + output := GenerateTracepointsC(mustParseAll(t, input)) + if !strings.Contains(output, "Ignoring") { + t.Errorf("syscall %s was accepted, expected ignored", tt.name) + } + }) + } +} + +func mustParseAll(t *testing.T, data string) []Format { + t.Helper() + formats, err := ParseFormats(strings.NewReader(data)) + if err != nil { + t.Fatalf("ParseFormats failed: %v", err) + } + return formats +} diff --git a/internal/generate/codegen.go b/internal/generate/codegen.go new file mode 100644 index 0000000..9b9f52c --- /dev/null +++ b/internal/generate/codegen.go @@ -0,0 +1,152 @@ +package generate + +import ( + "fmt" + "sort" + "strings" +) + +// Syscall groups enter+exit formats by syscall name. +type Syscall struct { + Name string + Enter *Format + Exit *Format +} + +// GeneratedTracepoint holds a classified format ready for code generation. +type GeneratedTracepoint struct { + Format *Format + Classification ClassificationResult +} + +// GenerateTracepointsC produces the full generated_tracepoints.c content from +// concatenated sysfs format data parsed into formats. +func GenerateTracepointsC(formats []Format) string { + syscalls := groupBySyscall(formats) + var b strings.Builder + + b.WriteString("// Code generated - don't change manually!\n\n") + + var accepted []GeneratedTracepoint + for _, sc := range syscalls { + tracepoints, reason := classifySyscall(sc) + if reason != "" { + fmt.Fprintf(&b, "/// %s\n", reason) + continue + } + accepted = append(accepted, tracepoints...) + } + + sort.Slice(accepted, func(i, j int) bool { + return accepted[i].Format.ID > accepted[j].Format.ID + }) + + b.WriteString("\n") + for _, tp := range accepted { + fmt.Fprintf(&b, "#define %s %d\n", strings.ToUpper(tp.Format.Name), tp.Format.ID) + } + b.WriteString("\n") + + for _, tp := range accepted { + b.WriteString(generateBPFHandler(tp)) + b.WriteString("\n") + } + + return b.String() +} + +func groupBySyscall(formats []Format) []Syscall { + m := make(map[string]*Syscall) + var order []string + + for i := range formats { + f := &formats[i] + parts := strings.SplitN(f.Name, "_", 3) + if len(parts) < 3 { + continue + } + enterExit := parts[1] + what := parts[2] + + sc, ok := m[what] + if !ok { + sc = &Syscall{Name: what} + m[what] = sc + order = append(order, what) + } + if enterExit == "enter" { + sc.Enter = f + } else { + sc.Exit = f + } + } + + result := make([]Syscall, 0, len(order)) + for _, name := range order { + result = append(result, *m[name]) + } + return result +} + +func classifySyscall(sc Syscall) ([]GeneratedTracepoint, string) { + var enterClass, exitClass ClassificationResult + allCanGenerate := true + + if sc.Enter != nil { + enterClass = ClassifyFormat(sc.Enter) + if enterClass.Kind == KindNone { + allCanGenerate = false + } + } else { + allCanGenerate = false + } + + if sc.Exit != nil { + exitClass = ClassifyFormat(sc.Exit) + if exitClass.Kind == KindNone { + allCanGenerate = false + } + } else { + allCanGenerate = false + } + + if !allCanGenerate { + names := syscallFormatNames(sc) + return nil, fmt.Sprintf("Ignoring %s as possibly not file I/O related", strings.Join(names, " ")) + } + + if isEnterRejected(enterClass.Kind) { + names := syscallFormatNames(sc) + return nil, fmt.Sprintf("Ignoring %s as enter-rejected", strings.Join(names, " ")) + } + + var result []GeneratedTracepoint + if sc.Enter != nil { + result = append(result, GeneratedTracepoint{Format: sc.Enter, Classification: enterClass}) + } + if sc.Exit != nil { + result = append(result, GeneratedTracepoint{Format: sc.Exit, Classification: exitClass}) + } + return result, "" +} + +func isEnterRejected(kind TracepointKind) bool { + switch kind { + case KindFd, KindName, KindOpen, KindPathname, KindFcntl, KindNull, KindDup3, KindOpenByHandleAt: + return false + default: + return true + } +} + +func syscallFormatNames(sc Syscall) []string { + var names []string + if sc.Enter != nil { + names = append(names, sc.Enter.Name) + } + if sc.Exit != nil { + names = append(names, sc.Exit.Name) + } + sort.Strings(names) + return names +} diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go new file mode 100644 index 0000000..b19a824 --- /dev/null +++ b/internal/generate/codegen_test.go @@ -0,0 +1,263 @@ +package generate + +import ( + "strings" + "testing" +) + +func generateFromPair(t *testing.T, enter, exit string) string { + t.Helper() + input := enter + "\n" + exit + formats := mustParseAll(t, input) + return GenerateTracepointsC(formats) +} + +func TestGenerateFdHandler(t *testing.T) { + output := generateFromPair(t, FormatRead, FormatExitRead) + + requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_read")`) + requireContains(t, output, "struct trace_event_raw_sys_enter *ctx") + requireContains(t, output, "struct fd_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct fd_event), 0);") + requireContains(t, output, "ev->event_type = ENTER_FD_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_ENTER_READ;") + requireContains(t, output, "ev->fd = (__s32)ctx->args[0];") + requireContains(t, output, "#define SYS_ENTER_READ 844") +} + +func TestGenerateOpenHandler(t *testing.T) { + output := generateFromPair(t, FormatOpenat, FormatExitOpenat) + + requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_openat")`) + requireContains(t, output, "struct open_event *ev") + requireContains(t, output, "ev->event_type = ENTER_OPEN_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_ENTER_OPENAT;") + requireContains(t, output, "__builtin_memset(&(ev->filename), 0, sizeof(ev->filename) + sizeof(ev->comm));") + requireContains(t, output, "bpf_probe_read_user_str(ev->filename, sizeof(ev->filename), (void *)ctx->args[1]);") + requireContains(t, output, "bpf_get_current_comm(&ev->comm, sizeof(ev->comm));") + requireContains(t, output, "ev->flags = ctx->args[2];") +} + +func TestGenerateOpenHandlerDirect(t *testing.T) { + output := generateFromPair(t, FormatOpen, FormatExitOpen) + + requireContains(t, output, "bpf_probe_read_user_str(ev->filename, sizeof(ev->filename), (void *)ctx->args[0]);") + requireContains(t, output, "ev->flags = ctx->args[1];") +} + +func TestGenerateOpenat2Handler(t *testing.T) { + f := mustParseOne(t, FormatOpenat2) + r := ClassifyFormat(&f) + if r.Kind != KindOpen { + t.Fatalf("openat2 classified as %d, want KindOpen", r.Kind) + } + // openat2 has filename at args[1] but flags field name = "how" (not "flags"), + // so FieldNumber("flags") returns -1 + if n := f.FieldNumber("flags"); n != -1 { + t.Errorf("openat2 FieldNumber(flags) = %d, want -1", n) + } +} + +func TestGenerateRetHandlerRead(t *testing.T) { + output := generateFromPair(t, FormatRead, FormatExitRead) + + requireContains(t, output, `SEC("tracepoint/syscalls/sys_exit_read")`) + requireContains(t, output, "struct trace_event_raw_sys_exit *ctx") + requireContains(t, output, "struct ret_event *ev") + requireContains(t, output, "ev->event_type = EXIT_RET_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_EXIT_READ;") + requireContains(t, output, "ev->ret = ctx->ret;") + requireContains(t, output, "ev->ret_type = READ_CLASSIFIED;") +} + +func TestGenerateRetHandlerWrite(t *testing.T) { + output := generateFromPair(t, FormatWrite, FormatExitWrite) + + requireContains(t, output, "ev->ret_type = WRITE_CLASSIFIED;") + requireContains(t, output, "ev->trace_id = SYS_EXIT_WRITE;") +} + +func TestGenerateRetHandlerOpenat(t *testing.T) { + output := generateFromPair(t, FormatOpenat, FormatExitOpenat) + + requireContains(t, output, "ev->ret_type = UNCLASSIFIED;") + requireContains(t, output, "ev->trace_id = SYS_EXIT_OPENAT;") +} + +func TestGenerateNameHandler(t *testing.T) { + output := generateFromPair(t, FormatRename, FormatExitRename) + + requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_rename")`) + requireContains(t, output, "struct name_event *ev") + requireContains(t, output, "ev->event_type = ENTER_NAME_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_ENTER_RENAME;") + requireContains(t, output, "__builtin_memset(&(ev->oldname), 0, sizeof(ev->oldname) + sizeof(ev->newname));") + requireContains(t, output, "bpf_probe_read_user_str(ev->oldname, sizeof(ev->oldname), (void*)ctx->args[0]);") + requireContains(t, output, "bpf_probe_read_user_str(ev->newname, sizeof(ev->newname), (void*)ctx->args[1]);") +} + +func TestGeneratePathnameHandler(t *testing.T) { + // Use exit_unlink (same structure as exit_read) paired with enter_unlink + exitUnlink := strings.Replace(FormatExitRead, "sys_exit_read", "sys_exit_unlink", 1) + exitUnlink = strings.Replace(exitUnlink, "ID: 843", "ID: 883", 1) + output := generateFromPair(t, FormatUnlink, exitUnlink) + + requireContains(t, output, "struct path_event *ev") + requireContains(t, output, "ev->event_type = ENTER_PATH_EVENT;") + requireContains(t, output, "__builtin_memset(&(ev->pathname), 0, sizeof(ev->pathname));") + requireContains(t, output, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[0]);") +} + +func TestGenerateFcntlHandler(t *testing.T) { + output := generateFromPair(t, FormatFcntl, FormatExitFcntl) + + requireContains(t, output, "struct fcntl_event *ev") + requireContains(t, output, "ev->event_type = ENTER_FCNTL_EVENT;") + requireContains(t, output, "ev->fd = ctx->args[0];") + requireContains(t, output, "ev->cmd = ctx->args[1];") + requireContains(t, output, "ev->arg = ctx->args[2];") +} + +func TestGenerateNullHandler(t *testing.T) { + output := generateFromPair(t, FormatSync, FormatExitSync) + + requireContains(t, output, "struct null_event *ev") + requireContains(t, output, "ev->event_type = ENTER_NULL_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_ENTER_SYNC;") + // Null handler should NOT have ev->fd, ev->filename, etc. + if strings.Contains(output, "ev->fd") { + t.Error("null handler should not have ev->fd") + } +} + +func TestGenerateDup3Handler(t *testing.T) { + output := generateFromPair(t, Forma |
