summaryrefslogtreecommitdiff
path: root/internal/generate
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-21 11:51:01 +0200
committerPaul Buetow <paul@buetow.org>2026-02-21 11:51:01 +0200
commit6c912a9d72ae2a43923c638538d320e6bf585952 (patch)
tree727f66d158210e01abf8c18a83ef4db6066e0c1a /internal/generate
parent32136b8cb18944157ff1f361bc0755f6b627fd47 (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.go152
-rw-r--r--internal/generate/classify.go214
-rw-r--r--internal/generate/classify_test.go332
-rw-r--r--internal/generate/codegen.go152
-rw-r--r--internal/generate/codegen_test.go263
-rw-r--r--internal/generate/format.go145
-rw-r--r--internal/generate/format_test.go174
-rw-r--r--internal/generate/retclassify_test.go59
-rw-r--r--internal/generate/testdata.go666
-rw-r--r--internal/generate/tracepointsgo.go43
-rw-r--r--internal/generate/tracepointsgo_test.go92
-rw-r--r--internal/generate/typesgo.go341
-rw-r--r--internal/generate/typesgo_test.go257
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