diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-21 08:16:08 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-21 08:16:08 +0300 |
| commit | be8735fe701f7398c19c17c394f4827614eab875 (patch) | |
| tree | aba59890563edb6e03f2eb82fee5d89b49fa2c81 | |
| parent | 3a5706f21d30258577a5934efb93c400dad723db (diff) | |
p7 add attach-time trace dimension gating
| -rw-r--r-- | Magefile.go | 9 | ||||
| -rw-r--r-- | integrationtests/attach_tracepoints_test.go | 41 | ||||
| -rw-r--r-- | internal/flags/flags.go | 28 | ||||
| -rw-r--r-- | internal/flags/flags_test.go | 99 | ||||
| -rw-r--r-- | internal/generate/bpfhandler.go | 4 | ||||
| -rw-r--r-- | internal/generate/classify.go | 55 | ||||
| -rw-r--r-- | internal/generate/tracepointsgo.go | 107 | ||||
| -rw-r--r-- | internal/generate/tracepointsgo_test.go | 22 | ||||
| -rw-r--r-- | internal/probemanager/manager_test.go | 41 | ||||
| -rw-r--r-- | internal/tracepoints/dimension_selector.go | 219 | ||||
| -rw-r--r-- | internal/tracepoints/dimension_selector_test.go | 147 | ||||
| -rw-r--r-- | internal/tracepoints/generated_tracepoints.go | 740 | ||||
| -rw-r--r-- | internal/tracepoints/selector.go | 46 | ||||
| -rw-r--r-- | internal/tracepoints/selector_test.go | 14 |
14 files changed, 1556 insertions, 16 deletions
diff --git a/Magefile.go b/Magefile.go index 8a6770c..2a996eb 100644 --- a/Magefile.go +++ b/Magefile.go @@ -390,7 +390,14 @@ func GenerateTracepointsGo() error { if err != nil { return fmt.Errorf("read %s: %w", tracepointsCPath, err) } - output, err := generate.ExtractTracepoints(strings.NewReader(string(input))) + kindsInput, err := os.ReadFile(tracepointsResult) + if err != nil { + return fmt.Errorf("read %s: %w", tracepointsResult, err) + } + output, err := generate.ExtractTracepointsWithKinds( + strings.NewReader(string(input)), + strings.NewReader(string(kindsInput)), + ) if err != nil { return err } diff --git a/integrationtests/attach_tracepoints_test.go b/integrationtests/attach_tracepoints_test.go index b2392cf..2dcb28d 100644 --- a/integrationtests/attach_tracepoints_test.go +++ b/integrationtests/attach_tracepoints_test.go @@ -59,3 +59,44 @@ func TestAttachTracepointsExcludeByInclusion(t *testing.T) { }, }) } + +func TestAttachTraceFamiliesTimeOnly(t *testing.T) { + enableParallelIfRequested(t) + h := newTestHarness(t) + + result, pid, err := h.RunWithIorArgs("sleep-syscalls", defaultDuration, []string{ + "-trace-families", "Time", + }) + if err != nil { + t.Fatalf("run scenario sleep-syscalls with trace-families=Time: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + {Tracepoint: "enter_nanosleep", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_clock_nanosleep", Comm: "ioworkload", MinCount: 1}, + }) +} + +func TestAttachTraceSyscallsWithExclusion(t *testing.T) { + enableParallelIfRequested(t) + h := newTestHarness(t) + + result, pid, err := h.RunWithIorArgs("open-rdonly-write", defaultDuration, []string{ + "-trace-syscalls", "openat,write", + "-no-trace-syscalls", "openat", + }) + if err != nil { + t.Fatalf("run scenario open-rdonly-write with syscall include/exclude: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + {Tracepoint: "enter_write", Comm: "ioworkload", MinCount: 1}, + }) + AssertEventsAbsent(t, result, []ExpectedEvent{ + {Tracepoint: "enter_openat", Comm: "ioworkload"}, + }) +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 3a6456a..547ba8e 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -148,12 +148,12 @@ func Parse() (Config, error) { // fresh FlagSet and custom argument slices without touching global state. func parseFromFlagSet(fs *flag.FlagSet, args []string) (Config, error) { cfg := NewFlags() - tpsAttach, tpsExclude, fields, familySampling, syscallSampling := registerFlags(fs, &cfg) + tpsAttach, tpsExclude, fields, familySampling, syscallSampling, dims := registerFlags(fs, &cfg) if err := fs.Parse(args); err != nil { return Config{}, err } - if err := resolvePostParseFields(&cfg, tpsAttach, tpsExclude, fields); err != nil { + if err := resolvePostParseFields(&cfg, tpsAttach, tpsExclude, fields, dims); err != nil { return Config{}, err } if err := resolveSamplingRates(&cfg, familySampling, syscallSampling); err != nil { @@ -167,9 +167,10 @@ func parseFromFlagSet(fs *flag.FlagSet, args []string) (Config, error) { // registerFlags binds all CLI flags to cfg and returns the string pointers for // fields that require post-parse resolution (tracepoint regexes, collapse fields). -func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields, familySampling, syscallSampling *string) { +func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields, familySampling, syscallSampling *string, dims *tracepoints.DimensionSelectorConfig) { validFields := collapse.ValidFields() validCounts := collapse.ValidCountFields() + dimensionCfg := &tracepoints.DimensionSelectorConfig{} fs.IntVar(&cfg.PidFilter, "pid", cfg.PidFilter, "Filter for processes ID") fs.IntVar(&cfg.TidFilter, "tid", cfg.TidFilter, "Filter for thread ID") @@ -182,6 +183,18 @@ func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields tpsAttach = fs.String("tps", "", "Comma separated list regexes for tracepoints to load") tpsExclude = fs.String("tpsExclude", "", "Comma separated list regexes for tracepoints to exclude") + fs.StringVar(&dimensionCfg.TraceFamilies, "trace-families", "", + "Comma separated syscall families to attach (for example FS,Time,Network)") + fs.StringVar(&dimensionCfg.TraceKinds, "trace-kinds", "", + "Comma separated tracepoint kinds to attach (for example fd,open,sleep,epoll-ctl)") + fs.StringVar(&dimensionCfg.TraceSyscalls, "trace-syscalls", "", + "Comma separated syscall names to attach (for example openat,read,nanosleep)") + fs.StringVar(&dimensionCfg.NoTraceFamilies, "no-trace-families", "", + "Comma separated syscall families to exclude from attachment") + fs.StringVar(&dimensionCfg.NoTraceKinds, "no-trace-kinds", "", + "Comma separated tracepoint kinds to exclude from attachment") + fs.StringVar(&dimensionCfg.NoTraceSyscalls, "no-trace-syscalls", "", + "Comma separated syscall names to exclude from attachment") fs.BoolVar(&cfg.PlainMode, "plain", false, "Enable plain CSV output mode (disable TUI)") fs.BoolVar(&cfg.FlamegraphOutput, "flamegraph", false, "Write aggregated .ior.zst output for trace/integration workflows") @@ -204,15 +217,18 @@ func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validFields)) fs.StringVar(&cfg.CountField, "count", cfg.CountField, fmt.Sprintf("Count field to collapse, valid are: %v", validCounts)) - return tpsAttach, tpsExclude, fields, familySampling, syscallSampling + return tpsAttach, tpsExclude, fields, familySampling, syscallSampling, dimensionCfg } // resolvePostParseFields compiles the tracepoint selector and collapse field // list from the raw string flags that cannot be bound directly to cfg fields. -func resolvePostParseFields(cfg *Config, tpsAttach, tpsExclude, fields *string) error { +func resolvePostParseFields(cfg *Config, tpsAttach, tpsExclude, fields *string, dims *tracepoints.DimensionSelectorConfig) error { // Parse the tracepoint include/exclude regex lists into a Selector. // The Selector owns all matching logic; Config is purely a data carrier. - sel, err := tracepoints.ParseSelector(*tpsAttach, *tpsExclude) + if dims == nil { + dims = &tracepoints.DimensionSelectorConfig{} + } + sel, err := tracepoints.ParseSelectorWithDimensions(*tpsAttach, *tpsExclude, *dims) if err != nil { return err } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 1feeafd..93df7b0 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -152,6 +152,105 @@ func TestParseInvalidTracepointRegexReturnsError(t *testing.T) { } } +func TestParseDefaultTraceDimensionsFSOnly(t *testing.T) { + cfg, err := parseForTest(t) + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat attached by default") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep excluded by default") + } +} + +func TestParseTraceFamiliesFlag(t *testing.T) { + cfg, err := parseForTest(t, "-trace-families", "Time") + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep attached for Time family") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat excluded when only Time family enabled") + } +} + +func TestParseTraceKindsFlag(t *testing.T) { + cfg, err := parseForTest(t, "-trace-kinds", "sleep") + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep attached for sleep kind") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat excluded for sleep-only selector") + } +} + +func TestParseTraceSyscallsFlag(t *testing.T) { + cfg, err := parseForTest(t, "-trace-syscalls", "openat") + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_openat") || !cfg.TracepointSelector.ShouldAttach("sys_exit_openat") { + t.Fatal("expected openat enter/exit attached") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_write") { + t.Fatal("expected write excluded when only openat enabled") + } +} + +func TestParseTraceDimensionsUnionAndExclusions(t *testing.T) { + cfg, err := parseForTest(t, + "-trace-families", "Time", + "-trace-syscalls", "openat", + "-no-trace-syscalls", "openat", + ) + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat excluded by no-trace-syscalls") + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep still attached from trace-families") + } +} + +func TestParseTraceFamiliesRejectsUnknown(t *testing.T) { + _, err := parseForTest(t, "-trace-families", "Nope") + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "invalid syscall family") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseTraceKindsRejectsUnknown(t *testing.T) { + _, err := parseForTest(t, "-trace-kinds", "not-a-kind") + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "invalid syscall kind") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseTraceSyscallsRejectsUnknown(t *testing.T) { + _, err := parseForTest(t, "-trace-syscalls", "definitely_not_syscall") + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "invalid syscall in trace selector") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestParseResetTimerDefault(t *testing.T) { cfg, err := parseForTest(t) if err != nil { diff --git a/internal/generate/bpfhandler.go b/internal/generate/bpfhandler.go index 9365c52..dce776a 100644 --- a/internal/generate/bpfhandler.go +++ b/internal/generate/bpfhandler.go @@ -23,9 +23,9 @@ func generateBPFHandler(tp GeneratedTracepoint) string { } eventStruct := eventStructName(tp.Classification.Kind) - comment := eventStruct + comment := fmt.Sprintf("%s (kind=%s)", eventStruct, tp.Classification.Kind.MetadataName()) if tp.Classification.Kind == KindRet { - comment = fmt.Sprintf("%s (%s)", eventStruct, ClassifyRet(f.Name)) + comment = fmt.Sprintf("%s (%s) (kind=%s)", eventStruct, ClassifyRet(f.Name), tp.Classification.Kind.MetadataName()) } eventTypeConst := eventTypeConstant(tp.Classification.Kind, isEnter) diff --git a/internal/generate/classify.go b/internal/generate/classify.go index af7d78d..9e9df9c 100644 --- a/internal/generate/classify.go +++ b/internal/generate/classify.go @@ -32,6 +32,61 @@ const ( KindPerfOpen ) +func (k TracepointKind) MetadataName() string { + switch k { + case KindFd: + return "fd" + case KindOpen: + return "open" + case KindMqOpen: + return "mq-open" + case KindExec: + return "exec" + case KindPathname: + return "pathname" + case KindName: + return "name" + case KindRet: + return "ret" + case KindFcntl: + return "fcntl" + case KindNull: + return "null" + case KindDup3: + return "dup3" + case KindOpenByHandleAt: + return "open-by-handle-at" + case KindSocket: + return "socket" + case KindSocketpair: + return "socketpair" + case KindAccept: + return "accept" + case KindPipe: + return "pipe" + case KindEventfd: + return "eventfd" + case KindEpollCtl: + return "epoll-ctl" + case KindTwoFd: + return "two-fd" + case KindPoll: + return "poll" + case KindMem: + return "mem" + case KindSleep: + return "sleep" + case KindKeyctl: + return "keyctl" + case KindPtrace: + return "ptrace" + case KindPerfOpen: + return "perf-open" + default: + return "none" + } +} + type RetClassification string const ( diff --git a/internal/generate/tracepointsgo.go b/internal/generate/tracepointsgo.go index 0542f64..9d92047 100644 --- a/internal/generate/tracepointsgo.go +++ b/internal/generate/tracepointsgo.go @@ -5,14 +5,35 @@ import ( "fmt" "io" "regexp" + "sort" "strings" ) var secRe = regexp.MustCompile(`^SEC.*sys_((?:enter|exit)_[a-z_0-9]+)`) +var kindLineRe = regexp.MustCompile(`^(sys_enter_[a-z0-9_]+)\s+is a struct\s+([a-z0-9_]+)\s*$`) // ExtractTracepoints reads generated C code and extracts tracepoint names from // SEC annotations, producing the generated_tracepoints.go content. func ExtractTracepoints(r io.Reader) (string, error) { + return ExtractTracepointsWithKinds(r, strings.NewReader("")) +} + +// ExtractTracepointsWithKinds reads generated C code and the +// generated_tracepoints_result.txt metadata to produce +// internal/tracepoints/generated_tracepoints.go. +func ExtractTracepointsWithKinds(cReader io.Reader, kindsReader io.Reader) (string, error) { + tracepoints, err := extractTracepoints(cReader) + if err != nil { + return "", err + } + syscallKinds, err := extractSyscallKinds(kindsReader) + if err != nil { + return "", err + } + return formatTracepointsGo(tracepoints, syscallKinds), nil +} + +func extractTracepoints(r io.Reader) ([]string, error) { scanner := bufio.NewScanner(r) var tracepoints []string @@ -24,13 +45,58 @@ func ExtractTracepoints(r io.Reader) (string, error) { } if err := scanner.Err(); err != nil { - return "", fmt.Errorf("scanning input: %w", err) + return nil, fmt.Errorf("scanning input: %w", err) } + return tracepoints, nil +} - return formatTracepointsGo(tracepoints), nil +func extractSyscallKinds(r io.Reader) (map[string]string, error) { + kinds := make(map[string]string) + if r == nil { + return kinds, nil + } + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + m := kindLineRe.FindStringSubmatch(line) + if m == nil { + continue + } + syscall := strings.TrimPrefix(m[1], "sys_enter_") + kind := normalizeStructKind(m[2]) + if kind == "" { + continue + } + kinds[syscall] = kind + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan kind metadata: %w", err) + } + return kinds, nil +} + +func normalizeStructKind(structName string) string { + base := strings.TrimSuffix(structName, "_event") + switch base { + case "mq_open": + return "mq-open" + case "open_by_handle_at": + return "open-by-handle-at" + case "two_fd": + return "two-fd" + case "epoll_ctl": + return "epoll-ctl" + case "perf_open": + return "perf-open" + default: + return base + } } -func formatTracepointsGo(tracepoints []string) string { +func formatTracepointsGo(tracepoints []string, syscallKinds map[string]string) string { var b strings.Builder b.WriteString("// Code generated - don't change manually!\n") b.WriteString("package tracepoints\n\n") @@ -39,5 +105,40 @@ func formatTracepointsGo(tracepoints []string) string { fmt.Fprintf(&b, "\t%q,\n", tp) } b.WriteString("}\n") + b.WriteString("\n") + b.WriteString("var syscallFamilies = map[string]string{\n") + syscallFamilies := make(map[string]string) + for _, tp := range tracepoints { + syscall := strings.TrimPrefix(tp, "sys_enter_") + syscall = strings.TrimPrefix(syscall, "sys_exit_") + if syscall == "" { + continue + } + if _, ok := syscallFamilies[syscall]; ok { + continue + } + family := ClassifySyscallFamily("sys_enter_" + syscall) + syscallFamilies[syscall] = string(family) + } + familySyscalls := make([]string, 0, len(syscallFamilies)) + for syscall := range syscallFamilies { + familySyscalls = append(familySyscalls, syscall) + } + sort.Strings(familySyscalls) + for _, syscall := range familySyscalls { + fmt.Fprintf(&b, "\t%q: %q,\n", syscall, syscallFamilies[syscall]) + } + b.WriteString("}\n") + b.WriteString("\n") + b.WriteString("var syscallKinds = map[string]string{\n") + syscalls := make([]string, 0, len(syscallKinds)) + for syscall := range syscallKinds { + syscalls = append(syscalls, syscall) + } + sort.Strings(syscalls) + for _, syscall := range syscalls { + fmt.Fprintf(&b, "\t%q: %q,\n", syscall, syscallKinds[syscall]) + } + b.WriteString("}\n") return b.String() } diff --git a/internal/generate/tracepointsgo_test.go b/internal/generate/tracepointsgo_test.go index 978633b..ebad63d 100644 --- a/internal/generate/tracepointsgo_test.go +++ b/internal/generate/tracepointsgo_test.go @@ -51,6 +51,8 @@ func TestExtractTracepoints(t *testing.T) { requireContains(t, output, `"sys_enter_close",`) requireContains(t, output, `"sys_exit_close",`) requireContains(t, output, "var List = []string{") + requireContains(t, output, "var syscallFamilies = map[string]string{") + requireContains(t, output, "var syscallKinds = map[string]string{") // Should NOT contain ignore comments or defines if strings.Contains(output, "kill") { @@ -78,6 +80,8 @@ func TestExtractTracepointsEmpty(t *testing.T) { t.Fatal(err) } requireContains(t, output, "var List = []string{") + requireContains(t, output, "var syscallFamilies = map[string]string{") + requireContains(t, output, "var syscallKinds = map[string]string{") requireContains(t, output, "}") } @@ -115,7 +119,25 @@ func TestExtractTracepointsNoSECLines(t *testing.T) { t.Fatalf("unexpected error: %v", err) } requireContains(t, output, "var List = []string{") + requireContains(t, output, "var syscallFamilies = map[string]string{") + requireContains(t, output, "var syscallKinds = map[string]string{") if strings.Contains(output, `"sys_`) { t.Error("input with no SEC lines should produce empty list") } } + +func TestExtractTracepointsWithKinds(t *testing.T) { + kindData := `sys_enter_read is a struct fd_event +sys_enter_open_by_handle_at is a struct open_by_handle_at_event +sys_enter_mq_open is a struct mq_open_event +sys_enter_epoll_ctl is a struct epoll_ctl_event +` + output, err := ExtractTracepointsWithKinds(strings.NewReader(sampleGeneratedC), strings.NewReader(kindData)) + if err != nil { + t.Fatalf("ExtractTracepointsWithKinds failed: %v", err) + } + requireContains(t, output, `"read": "fd",`) + requireContains(t, output, `"open_by_handle_at": "open-by-handle-at",`) + requireContains(t, output, `"mq_open": "mq-open",`) + requireContains(t, output, `"epoll_ctl": "epoll-ctl",`) +} diff --git a/internal/probemanager/manager_test.go b/internal/probemanager/manager_test.go index b75a579..ee436b9 100644 --- a/internal/probemanager/manager_test.go +++ b/internal/probemanager/manager_test.go @@ -2,6 +2,7 @@ package probemanager import ( "errors" + "ior/internal/tracepoints" "strings" "sync" "testing" @@ -127,6 +128,46 @@ func TestManagerAttachAllToggleAndCounts(t *testing.T) { } } +func TestManagerAttachAllWithDimensionSelectorAttachesOnlyEnabledSyscalls(t *testing.T) { + attacher := &fakeAttacher{ + programs: map[string]*fakeProgram{ + "handle_sys_enter_openat": {}, + "handle_sys_exit_openat": {}, + "handle_sys_enter_write": {}, + "handle_sys_exit_write": {}, + }, + errs: map[string]error{}, + } + mgr := NewManager(attacher) + selector, err := tracepoints.ParseSelectorWithDimensions("", "", tracepoints.DimensionSelectorConfig{ + TraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("build selector: %v", err) + } + + err = mgr.AttachAll(selector.ShouldAttach, []string{ + "sys_enter_openat", "sys_exit_openat", + "sys_enter_write", "sys_exit_write", + }, nil) + if err != nil { + t.Fatalf("AttachAll returned error: %v", err) + } + + if got := attacher.programs["handle_sys_enter_openat"].attachCalls(); got != 1 { + t.Fatalf("openat enter attach calls = %d, want 1", got) + } + if got := attacher.programs["handle_sys_exit_openat"].attachCalls(); got != 1 { + t.Fatalf("openat exit attach calls = %d, want 1", got) + } + if got := attacher.programs["handle_sys_enter_write"].attachCalls(); got != 0 { + t.Fatalf("write enter attach calls = %d, want 0", got) + } + if got := attacher.programs["handle_sys_exit_write"].attachCalls(); got != 0 { + t.Fatalf("write exit attach calls = %d, want 0", got) + } +} + func TestManagerAttachSerializesConcurrentCalls(t *testing.T) { enterBlocked := make(chan struct{}) releaseEnter := make(chan struct{}) diff --git a/internal/tracepoints/dimension_selector.go b/internal/tracepoints/dimension_selector.go new file mode 100644 index 0000000..22a70fb --- /dev/null +++ b/internal/tracepoints/dimension_selector.go @@ -0,0 +1,219 @@ +package tracepoints + +import ( + "fmt" + "sort" + "strings" + + "ior/internal/types" +) + +// DimensionSelectorConfig holds attach-time syscall-dimension selection inputs. +// Each field accepts comma-separated values. +type DimensionSelectorConfig struct { + TraceFamilies string + TraceKinds string + TraceSyscalls string + NoTraceFamilies string + NoTraceKinds string + NoTraceSyscalls string +} + +// ParseSelectorWithDimensions compiles regex-based attach/exclude filters and +// applies attach-time syscall dimension gating. +func ParseSelectorWithDimensions(attach, exclude string, dims DimensionSelectorConfig) (Selector, error) { + sel, err := ParseSelector(attach, exclude) + if err != nil { + return Selector{}, err + } + + allow, err := buildAllowedSyscalls(dims) + if err != nil { + return Selector{}, err + } + sel.RestrictSyscalls = true + sel.Syscalls = allow + return sel, nil +} + +func buildAllowedSyscalls(dims DimensionSelectorConfig) (map[string]struct{}, error) { + knownSyscalls := allKnownSyscalls() + knownKinds := allKnownKinds() + + includeFamilies, familyFilterProvided, err := parseFamiliesCSV(dims.TraceFamilies) + if err != nil { + return nil, err + } + includeKinds, kindFilterProvided, err := parseKindsCSV(dims.TraceKinds, knownKinds) + if err != nil { + return nil, err + } + includeSyscalls, syscallFilterProvided, err := parseSyscallsCSV(dims.TraceSyscalls, knownSyscalls) + if err != nil { + return nil, err + } + + allow := make(map[string]struct{}) + hasPositive := familyFilterProvided || kindFilterProvided || syscallFilterProvided + if hasPositive { + for syscall, family := range syscallFamilies { + if _, ok := includeFamilies[family]; ok { + allow[syscall] = struct{}{} + } + } + for syscall, kind := range syscallKinds { + if _, ok := includeKinds[kind]; ok { + allow[syscall] = struct{}{} + } + } + for syscall := range includeSyscalls { + allow[syscall] = struct{}{} + } + } else { + // Backward compatibility default: keep existing file-I/O coverage on and + // leave newly-expanded non-IO families disabled unless explicitly opted in. + for syscall, family := range syscallFamilies { + if family == string(types.FamilyFS) { + allow[syscall] = struct{}{} + } + } + } + + excludeFamilies, _, err := parseFamiliesCSV(dims.NoTraceFamilies) + if err != nil { + return nil, err + } + excludeKinds, _, err := parseKindsCSV(dims.NoTraceKinds, knownKinds) + if err != nil { + return nil, err + } + excludeSyscalls, _, err := parseSyscallsCSV(dims.NoTraceSyscalls, knownSyscalls) + if err != nil { + return nil, err + } + + for syscall := range allow { + if _, ok := excludeSyscalls[syscall]; ok { + delete(allow, syscall) + continue + } + if family, ok := syscallFamilies[syscall]; ok { + if _, excluded := excludeFamilies[family]; excluded { + delete(allow, syscall) + continue + } + } + if kind, ok := syscallKinds[syscall]; ok { + if _, excluded := excludeKinds[kind]; excluded { + delete(allow, syscall) + } + } + } + + return allow, nil +} + +func parseFamiliesCSV(raw string) (map[string]struct{}, bool, error) { + values, provided := splitCSV(raw) + if !provided { + return map[string]struct{}{}, false, nil + } + out := make(map[string]struct{}, len(values)) + for _, value := range values { + family, ok := types.ParseSyscallFamily(value) + if !ok { + return nil, false, fmt.Errorf("invalid syscall family in trace selector: %q", value) + } + out[string(family)] = struct{}{} + } + return out, true, nil +} + +func parseKindsCSV(raw string, knownKinds map[string]struct{}) (map[string]struct{}, bool, error) { + values, provided := splitCSV(raw) + if !provided { + return map[string]struct{}{}, false, nil + } + out := make(map[string]struct{}, len(values)) + for _, value := range values { + kind := normalizeKind(value) + if _, ok := knownKinds[kind]; !ok { + return nil, false, fmt.Errorf("invalid syscall kind in trace selector: %q", value) + } + out[kind] = struct{}{} + } + return out, true, nil +} + +func parseSyscallsCSV(raw string, knownSyscalls map[string]struct{}) (map[string]struct{}, bool, error) { + values, provided := splitCSV(raw) + if !provided { + return map[string]struct{}{}, false, nil + } + out := make(map[string]struct{}, len(values)) + for _, value := range values { + syscall := strings.ToLower(strings.TrimSpace(value)) + if _, ok := knownSyscalls[syscall]; !ok { + return nil, false, fmt.Errorf("invalid syscall in trace selector: %q", value) + } + out[syscall] = struct{}{} + } + return out, true, nil +} + +func splitCSV(raw string) ([]string, bool) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, false + } + parts := strings.Split(raw, ",") + values := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + values = append(values, part) + } + if len(values) == 0 { + return nil, false + } + return values, true +} + +func normalizeKind(raw string) string { + normalized := strings.ToLower(strings.TrimSpace(raw)) + normalized = strings.ReplaceAll(normalized, "_", "-") + return normalized +} + +func allKnownSyscalls() map[string]struct{} { + out := make(map[string]struct{}, len(syscallFamilies)) + for syscall := range syscallFamilies { + out[syscall] = struct{}{} + } + return out +} + +func allKnownKinds() map[string]struct{} { + out := make(map[string]struct{}) + for _, kind := range syscallKinds { + if kind == "" { + continue + } + out[kind] = struct{}{} + } + return out +} + +// KnownKinds returns the sorted, normalized attach-time kind names accepted by +// -trace-kinds and -no-trace-kinds. +func KnownKinds() []string { + kindSet := allKnownKinds() + kinds := make([]string, 0, len(kindSet)) + for kind := range kindSet { + kinds = append(kinds, kind) + } + sort.Strings(kinds) + return kinds +} diff --git a/internal/tracepoints/dimension_selector_test.go b/internal/tracepoints/dimension_selector_test.go new file mode 100644 index 0000000..cd7b0f8 --- /dev/null +++ b/internal/tracepoints/dimension_selector_test.go @@ -0,0 +1,147 @@ +package tracepoints + +import ( + "strings" + "testing" +) + +func TestParseSelectorWithDimensionsDefaultFSOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected FS syscall openat to be attached by default") + } + if sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected non-FS syscall nanosleep to be excluded by default") + } +} + +func TestParseSelectorWithDimensionsFamilyOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "Time", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep to be attached when Time family is enabled") + } + if sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to be excluded when only Time family is enabled") + } +} + +func TestParseSelectorWithDimensionsKindOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceKinds: "sleep", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep to be attached for sleep kind") + } + if sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to be excluded when only sleep kind is enabled") + } +} + +func TestParseSelectorWithDimensionsSyscallOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") || !sel.ShouldAttach("sys_exit_openat") { + t.Fatal("expected both openat enter/exit tracepoints to be attached") + } + if sel.ShouldAttach("sys_enter_write") { + t.Fatal("expected write to be excluded when only openat is selected") + } +} + +func TestParseSelectorWithDimensionsUnionSemantics(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "Time", + TraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat from syscall selector") + } + if !sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep from family selector") + } +} + +func TestParseSelectorWithDimensionsExclusionsOverridePositives(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "FS", + NoTraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to be excluded by -no-trace-syscalls") + } + if !sel.ShouldAttach("sys_enter_read") { + t.Fatal("expected other FS syscall (read) to remain attached") + } +} + +func TestParseSelectorWithDimensionsRegexStillApplies(t *testing.T) { + sel, err := ParseSelectorWithDimensions("^sys_enter_openat$,^sys_exit_openat$", "", DimensionSelectorConfig{ + TraceFamilies: "FS", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to pass regex+dimension filters") + } + if sel.ShouldAttach("sys_enter_read") { + t.Fatal("expected read to fail regex attach filters") + } +} + +func TestParseSelectorWithDimensionsRejectsInvalidFamily(t *testing.T) { + _, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "Nope", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid syscall family") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseSelectorWithDimensionsRejectsInvalidKind(t *testing.T) { + _, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceKinds: "not-a-kind", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid syscall kind") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseSelectorWithDimensionsRejectsInvalidSyscall(t *testing.T) { + _, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceSyscalls: "not_a_syscall", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid syscall in trace selector") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/tracepoints/generated_tracepoints.go b/internal/tracepoints/generated_tracepoints.go index 8277cbf..11913c8 100644 --- a/internal/tracepoints/generated_tracepoints.go +++ b/internal/tracepoints/generated_tracepoints.go @@ -737,3 +737,743 @@ var List = []string{ "sys_enter_rt_sigreturn", "sys_exit_rt_sigreturn", } + +var syscallFamilies = map[string]string{ + "accept": "Network", + "accept4": "Network", + "access": "FS", + "acct": "Misc", + "add_key": "Security", + "adjtimex": "Misc", + "alarm": "Misc", + "arch_prctl": "Process", + "bind": "Network", + "bpf": "Security", + "brk": "Memory", + "cachestat": "FS", + "capget": "Security", + "capset": "Security", + "chdir": "FS", + "chmod": "FS", + "chown": "FS", + "chroot": "FS", + "clock_adjtime": "Time", + "clock_getres": "Time", + "clock_gettime": "Time", + "clock_nanosleep": "Time", + "clock_settime": "Time", + "clone": "Process", + "clone3": "Process", + "close": "FS", + "close_range": "FS", + "connect": "Network", + "copy_file_range": "FS", + "creat": "FS", + "delete_module": "Security", + "dup": "FS", + "dup2": "FS", + "dup3": "FS", + "epoll_create": "Polling", + "epoll_create1": "Polling", + "epoll_ctl": "Polling", + "epoll_pwait": "Polling", + "epoll_pwait2": "Polling", + "epoll_wait": "Polling", + "eventfd": "IPC", + "eventfd2": "IPC", + "execve": "Process", + "execveat": "Process", + "exit": "Process", + "exit_group": "Process", + "faccessat": "FS", + "faccessat2": "FS", + "fadvise64": "FS", + "fallocate": "FS", + "fanotify_init": "Misc", + "fanotify_mark": "Misc", + "fchdir": "FS", + "fchmod": "FS", + "fchmodat": "FS", + "fchmodat2": "FS", + "fchown": "FS", + "fchownat": "FS", + "fcntl": "FS", + "fdatasync": "FS", + "fgetxattr": "FS", + "file_getattr": "Misc", + "file_setattr": "Misc", + "finit_module": "Security", + "flistxattr": "FS", + "flock": "FS", + "fork": "Process", + "fremovexattr": "FS", + "fsconfig": "FS", + "fsetxattr": "FS", + "fsmount": "FS", + "fsopen": "FS", + "fspick": "FS", + "fstatfs": "FS", + "fsync": "FS", + "ftruncate": "FS", + "futex": "Misc", + "futex_requeue": "Misc", + "futex_wait": "Misc", + "futex_waitv": "Misc", + "futex_wake": "Misc", + "futimesat": "FS", + "get_mempolicy": "Security", + "get_robust_list": "Misc", + "getcpu": "Misc", + "getcwd": "FS", + "getdents": "FS", + "getdents64": "FS", + "getegid": "Process", + "geteuid": "Process", + "getgid": "Process", + "getgroups": "Process", + "getitimer": "Time", + "getpeername": "Network", + "getpgid": "Process", + "getpgrp": "Process", + "getpid": "Process", + "getppid": "Process", + "getpriority": "Process", + "getrandom": "Security", + "getresgid": "Process", + "getresuid": "Process", + "getrlimit": "Process", + "getrusage": "Process", + "getsid": "Process", + "getsockname": "Network", + "getsockopt": "Network", + "gettid": "Process", + "gettimeofday": "Time", + "getuid": "Process", + "getxattr": "FS", + "getxattrat": "FS", + "init_module": "Security", + "inotify_add_watch": "IPC", + "inotify_init": "IPC", + "inotify_init1": "IPC", + "inotify_rm_watch": "IPC", + "io_cancel": "AIO", + "io_destroy": "AIO", + "io_getevents": "AIO", + "io_pgetevents": "AIO", + "io_setup": "AIO", + "io_submit": "AIO", + "io_uring_enter": "AIO", + "io_uring_register": "AIO", + "io_uring_setup": "AIO", + "ioctl": "FS", + "ioperm": "Misc", + "iopl": "Misc", + "ioprio_get": "Misc", + "ioprio_set": "Misc", + "kcmp": "Process", + "kexec_file_load": "Security", + "kexec_load": "Misc", + "keyctl": "Security", + "kill": "Signals", + "landlock_add_rule": "Security", + "landlock_create_ruleset": "Security", + "landlock_restrict_self": "Security", + "lchown": "FS", + "lgetxattr": "FS", + "link": "FS", + "linkat": "FS", + "listen": "Network", + "listmount": "FS", + "listns": "FS", + "listxattr": "FS", + "listxattrat": "FS", + "llistxattr": "FS", + "lremovexattr": "FS", + "lseek": "FS", + "lsetxattr": "FS", + "lsm_get_self_attr": "Misc", + "lsm_list_modules": "Misc", + "lsm_set_self_attr": "Misc", + "madvise": "Memory", + "map_shadow_stack": "Memory", + "mbind": "Memory", + "membarrier": "Memory", + "memfd_create": "IPC", + "memfd_secret": "IPC", + "migrate_pages": "Memory", + "mincore": "Memory", + "mkdir": "FS", + "mkdirat": "FS", + "mknod": "FS", + "mknodat": "FS", + "mlock": "Memory", + "mlock2": "Memory", + "mlockall": "Memory", + "mmap": "Memory", + "modify_ldt": "Misc", + "mount": "FS", + "mount_setattr": "FS", + "move_mount": "FS", + "move_pages": "Memory", + "mprotect": "Memory", + "mq_getsetattr": "IPC", + "mq_notify": "IPC", + "mq_open": "IPC", + "mq_timedreceive": "IPC", + "mq_timedsend": "IPC", + "mq_unlink": "IPC", + "mremap": "Memory", + "mseal": "Memory", + "msgctl": "IPC", + "msgget": "IPC", + "msgrcv": "IPC", + "msgsnd": "IPC", + "msync": "FS", + "munlock": "Memory", + "munlockall": "Memory", + "munmap": "Memory", + "name_to_handle_at": "FS", + "nanosleep": "Time", + "newfstat": "FS", + "newfstatat": "FS", + "newlstat": "FS", + "newstat": "FS", + "newuname": "Misc", + "open": "FS", + "open_by_handle_at": "FS", + "open_tree": "FS", + "open_tree_attr": "FS", + "openat": "FS", + "openat2": "FS", + "pause": "Signals", + "perf_event_open": "Security", + "personality": "Process", + "pidfd_getfd": "IPC", + "pidfd_open": "IPC", + "pidfd_send_signal": "IPC", + "pipe": "IPC", + "pipe2": "IPC", + "pivot_root": "Process", + "pkey_alloc": "Memory", + "pkey_free": "Memory", + "pkey_mprotect": "Memory", + "poll": "Polling", + "ppoll": "Polling", + "prctl": "Process", + "pread64": "FS", + "preadv": "FS", + "preadv2": "FS", + "prlimit64": "Process", + "process_madvise": "Memory", + "process_mrelease": "Memory", + "process_vm_readv": "Memory", + "process_vm_writev": "Memory", + "pselect6": "Polling", + "ptrace": "Security", + "pwrite64": "FS", + "pwritev": "FS", + "pwritev2": "FS", + "quotactl": "FS", + "quotactl_fd": "FS", + "read": "FS", + "readahead": "FS", + "readlink": "FS", + "readlinkat": "FS", + "readv": "FS", + "reboot": "Process", + "recvfrom": "Network", + "recvmmsg": "Network", + "recvmsg": "Network", + "remap_file_pages": "Memory", + "removexattr": "FS", + "removexattrat": "FS", + "rename": "FS", + "renameat": "FS", + "renameat2": "FS", + "request_key": "Security", + "restart_syscall": "Process", + "rmdir": "FS", + "rseq": "Misc", + "rt_sigaction": "Signals", + "rt_sigpending": "Signals", + "rt_sigprocmask": "Signals", + "rt_sigqueueinfo": "Signals", + "rt_sigreturn": "Signals", + "rt_sigsuspend": "Signals", + "rt_sigtimedwait": "Signals", + "rt_tgsigqueueinfo": "Signals", + "sched_get_priority_max": "Sched", + "sched_get_priority_min": "Sched", + "sched_getaffinity": "Sched", + "sched_getattr": "Sched", + "sched_getparam": "Sched", + "sched_getscheduler": "Sched", + "sched_rr_get_interval": "Sched", + "sched_setaffinity": "Sched", + "sched_setattr": "Sched", + "sched_setparam": "Sched", + "sched_setscheduler": "Sched", + "sched_yield": "Sched", + "seccomp": "Security", + "select": "Polling", + "semctl": "IPC", + "semget": "IPC", + "semop": "IPC", + "semtimedop": "IPC", + "sendfile64": "Network", + "sendmmsg": "Network", + "sendmsg": "Network", + "sendto": "Network", + "set_mempolicy": "Memory", + "set_mempolicy_home_node": "Memory", + "set_robust_list": "Misc", + "set_tid_address": "Process", + "setdomainname": "Misc", + "setfsgid": "Process", + "setfsuid": "Process", + "setgid": "Process", + "setgroups": "Process", + "sethostname": "Misc", + "setitimer": "Time", + "setns": "Process", + "setpgid": "Process", + "setpriority": "Process", + "setregid": "Process", + "setresgid": "Process", + "setresuid": "Process", + "setreuid": "Process", + "setrlimit": "Process", + "setsid": "Process", + "setsockopt": "Network", + "settimeofday": "Time", + "setuid": "Process", + "setxattr": "FS", + "setxattrat": "FS", + "shmat": "IPC", + "shmctl": "IPC", + "shmdt": "IPC", + "shmget": "IPC", + "shutdown": "Network", + "sigaltstack": "Signals", + "signalfd": "IPC", + "signalfd4": "IPC", + "socket": "Network", + "socketpair": "Network", + "splice": "Network", + "statfs": "FS", + "statmount": "FS", + "statx": "FS", + "swapoff": "FS", + "swapon": "FS", + "symlink": "FS", + "symlinkat": "FS", + "sync": "FS", + "sync_file_range": "FS", + "syncfs": "FS", + "sysfs": "Misc", + "sysinfo": "Misc", + "syslog": "Misc", + "tee": "Network", + "tgkill": "Signals", + "time": "Time", + "timer_create": "Time", + "timer_delete": "Time", + "timer_getoverrun": "Time", + "timer_gettime": "Time", + "timer_settime": "Time", + "timerfd_create": "IPC", + "timerfd_gettime": "IPC", + "timerfd_settime": "IPC", + "times": "Time", + "tkill": "Signals", + "truncate": "FS", + "umask": "Process", + "umount": "FS", + "unlink": "FS", + "unlinkat": "FS", + "unshare": "Process", + "uprobe": "Misc", + "uretprobe": "Misc", + "userfaultfd": "IPC", + "ustat": "FS", + "utime": "Misc", + "utimensat": "FS", + "utimes": "Misc", + "vfork": "Process", + "vhangup": "Process", + "vmsplice": "Misc", + "wait4": "Process", + "waitid": "Process", + "write": "FS", + "writev": "FS", +} + +var syscallKinds = map[string]string{ + "accept": "accept", + "accept4": "accept", + "access": "path", + "acct": "null", + "add_key": "keyctl", + "adjtimex": "null", + "alarm": "null", + "arch_prctl": "null", + "bind": "fd", + "bpf": "null", + "brk": "null", + "cachestat": "fd", + "capget": "null", + "capset": "null", + "chdir": "path", + "chmod": "path", + "chown": "path", + "chroot": "path", + "clock_adjtime": "null", + "clock_getres": "null", + "clock_gettime": "null", + "clock_nanosleep": "sleep", + "clock_settime": "null", + "clone": "null", + "clone3": "null", + "close": "fd", + "close_range": "fd", + "connect": "fd", + "copy_file_range": "fd", + "creat": "path", + "delete_module": "null", + "dup": "fd", + "dup2": "fd", + "dup3": "dup3", + "epoll_create": "null", + "epoll_create1": "null", + "epoll_ctl": "epoll-ctl", + "epoll_pwait": "fd", + "epoll_pwait2": "fd", + "epoll_wait": "fd", + "eventfd": "eventfd", + "eventfd2": "eventfd", + "execve": "exec", + "execveat": "exec", + "exit": "null", + "exit_group": "null", + "faccessat": "path", + "faccessat2": "path", + "fadvise64": "fd", + "fallocate": "fd", + "fanotify_init": "null", + "fanotify_mark": "path", + "fchdir": "fd", + "fchmod": "fd", + "fchmodat": "path", + "fchmodat2": "path", + "fchown": "fd", + "fchownat": "path", + "fcntl": "fcntl", + "fdatasync": "fd", + "fgetxattr": "fd", + "file_getattr": "path", + "file_setattr": "path", + "finit_module": "fd", + "flistxattr": "fd", + "flock": "fd", + "fork": "null", + "fremovexattr": "fd", + "fsconfig": "fd", + "fsetxattr": "fd", + "fsmount": "eventfd", + "fsopen": "null", + "fspick": "path", + "fstatfs": "fd", + "fsync": "fd", + "ftruncate": "fd", + "futex": "null", + "futex_requeue": "null", + "futex_wait": "null", + "futex_waitv": "null", + "futex_wake": "null", + "futimesat": "path", + "get_mempolicy": "null", + "get_robust_list": "null", + "getcpu": "null", + "getcwd": "null", + "getdents": "fd", + "getdents64": "fd", + "getegid": "null", + "geteuid": "null", + "getgid": "null", + "getgroups": "null", + "getitimer": "null", + "getpeername": "fd", + "getpgid": "null", + "getpgrp": "null", + "getpid": "null", + "getppid": "null", + "getpriority": "null", + "getrandom": "null", + "getresgid": "null", + "getresuid": "null", + "getrlimit": "null", + "getrusage": "null", + "getsid": "null", + "getsockname": "fd", + "getsockopt": "fd", + "gettid": "null", + "gettimeofday": "null", + "getuid": "null", + "getxattr": "path", + "getxattrat": "path", + "init_module": "null", + "inotify_add_watch": "fd", + "inotify_init": "null", + "inotify_init1": "null", + "inotify_rm_watch": "fd", + "io_cancel": "null", + "io_destroy": "null", + "io_getevents": "null", + "io_pgetevents": "null", + "io_setup": "null", + "io_submit": "null", + "io_uring_enter": "fd", + "io_uring_register": "fd", + "io_uring_setup": "null", + "ioctl": "fd", + "ioperm": "null", + "iopl": "null", + "ioprio_get": "null", + "ioprio_set": "null", + "kcmp": "null", + "kexec_file_load": "null", + "kexec_load": "null", + "keyctl": "keyctl", + "kill": "null", + "landlock_add_rule": "null", + "landlock_create_ruleset": "null", + "landlock_restrict_self": "null", + "lchown": "path", + "lgetxattr": "path", + "link": "name", + "linkat": "name", + "listen": "fd", + "listmount": "null", + "listns": "null", + "listxattr": "path", + "listxattrat": "path", + "llistxattr": "path", + "lremovexattr": "path", + "lseek": "fd", + "lsetxattr": "path", + "lsm_get_self_attr": "null", + "lsm_list_modules": "null", + "lsm_set_self_attr": "null", + "madvise": "null", + "map_shadow_stack": "null", + "mbind": "null", + "membarrier": "null", + "memfd_create": "null", + "memfd_secret": "null", + "migrate_pages": "null", + "mincore": "null", + "mkdir": "path", + "mkdirat": "path", + "mknod": "path", + "mknodat": "path", + "mlock": "null", + "mlock2": "null", + "mlockall": "null", + "mmap": "fd", + "modify_ldt": "null", + "mount": "path", + "mount_setattr": "path", + "move_mount": "two-fd", + "move_pages": "null", + "mprotect": "null", + "mq_getsetattr": "fd", + "mq_notify": "fd", + "mq_open": "open", + "mq_timedreceive": "fd", + "mq_timedsend": "fd", + "mq_unlink": "path", + "mremap": "mem", + "mseal": "null", + "msgctl": "null", + "msgget": "null", + "msgrcv": "null", + "msgsnd": "null", + "msync": "null", + "munlock": "null", + "munlockall": "null", + "munmap": "mem", + "name_to_handle_at": "path", + "nanosleep": "sleep", + "newfstat": "fd", + "newfstatat": "path", + "newlstat": "path", + "newstat": "path", + "newuname": "null", + "open": "open", + "open_by_handle_at": "open-by-handle-at", + "open_tree": "open", + "open_tree_attr": "open", + "openat": "open", + "openat2": "open", + "pause": "null", + "perf_event_open": "perf-open", + "personality": "null", + "pidfd_getfd": "fd", + "pidfd_open": "null", + "pidfd_send_signal": "null", + "pipe": "pipe", + "pipe2": "pipe", + "pivot_root": "path", + "pkey_alloc": "null", + "pkey_free": "null", + "pkey_mprotect": "null", + "poll": "poll", + "ppoll": "poll", + "prctl": "null", + "pread64": "fd", + "preadv": "fd", + "preadv2": "fd", + "prlimit64": "null", + "process_madvise": "null", + "process_mrelease": "null", + "process_vm_readv": "null", + "process_vm_writev": "null", + "pselect6": "poll", + "ptrace": "ptrace", + "pwrite64": "fd", + "pwritev": "fd", + "pwritev2": "fd", + "quotactl": "path", + "quotactl_fd": "fd", + "read": "fd", + "readahead": "fd", + "readlink": "path", + "readlinkat": "path", + "readv": "fd", + "reboot": "null", + "recvfrom": "fd", + "recvmmsg": "fd", + "recvmsg": "fd", + "remap_file_pages": "null", + "removexattr": "path", + "removexattrat": "path", + "rename": "name", + "renameat": "name", + "renameat2": "name", + "request_key": "keyctl", + "restart_syscall": "null", + "rmdir": "path", + "rseq": "null", + "rt_sigaction": "null", + "rt_sigpending": "null", + "rt_sigprocmask": "null", + "rt_sigqueueinfo": "null", + "rt_sigreturn": "null", + "rt_sigsuspend": "null", + "rt_sigtimedwait": "null", + "rt_tgsigqueueinfo": "null", + "sched_get_priority_max": "null", + "sched_get_priority_min": "null", + "sched_getaffinity": "null", + "sched_getattr": "null", + "sched_getparam": "null", + "sched_getscheduler": "null", + "sched_rr_get_interval": "null", + "sched_setaffinity": "null", + "sched_setattr": "null", + "sched_setparam": "null", + "sched_setscheduler": "null", + "sched_yield": "null", + "seccomp": "null", + "select": "poll", + "semctl": "null", + "semget": "null", + "semop": "null", + "semtimedop": "null", + "sendfile64": "null", + "sendmmsg": "fd", + "sendmsg": "fd", + "sendto": "fd", + "set_mempolicy": "null", + "set_mempolicy_home_node": "null", + "set_robust_list": "null", + "set_tid_address": "null", + "setdomainname": "null", + "setfsgid": "null", + "setfsuid": "null", + "setgid": "null", + "setgroups": "null", + "sethostname": "null", + "setitimer": "null", + "setns": "fd", + "setpgid": "null", + "setpriority": "null", + "setregid": "null", + "setresgid": "null", + "setresuid": "null", + "setreuid": "null", + "setrlimit": "null", + "setsid": "null", + "setsockopt": "fd", + "settimeofday": "null", + "setuid": "null", + "setxattr": "path", + "setxattrat": "path", + "shmat": "null", + "shmctl": "null", + "shmdt": "null", + "shmget": "null", + "shutdown": "fd", + "sigaltstack": "null", + "signalfd": "null", + "signalfd4": "null", + "socket": "socket", + "socketpair": "socketpair", + "splice": "null", + "statfs": "path", + "statmount": "null", + "statx": "path", + "swapoff": "path", + "swapon": "path", + "symlink": "name", + "symlinkat": "name", + "sync": "null", + "sync_file_range": "fd", + "syncfs": "fd", + "sysfs": "null", + "sysinfo": "null", + "syslog": "null", + "tee": "null", + "tgkill": "null", + "time": "null", + "timer_create": "null", + "timer_delete": "null", + "timer_getoverrun": "null", + "timer_gettime": "null", + "timer_settime": "null", + "timerfd_create": "null", + "timerfd_gettime": "null", + "timerfd_settime": "null", + "times": "null", + "tkill": "null", + "truncate": "path", + "umask": "null", + "umount": "path", + "unlink": "path", + "unlinkat": "path", + "unshare": "null", + "uprobe": "null", + "uretprobe": "null", + "userfaultfd": "null", + "ustat": "null", + "utime": "path", + "utimensat": "path", + "utimes": "path", + "vfork": "null", + "vhangup": "null", + "vmsplice": "fd", + "wait4": "null", + "waitid": "null", + "write": "fd", + "writev": "fd", +} diff --git a/internal/tracepoints/selector.go b/internal/tracepoints/selector.go index af2f39e..91df58f 100644 --- a/internal/tracepoints/selector.go +++ b/internal/tracepoints/selector.go @@ -2,6 +2,7 @@ package tracepoints import ( "fmt" + "maps" "regexp" "slices" "strings" @@ -18,6 +19,12 @@ type Selector struct { // Exclude is the list of compiled regexes that suppress specific // tracepoints even when they match the Attach list. Exclude []*regexp.Regexp + // Syscalls optionally restricts attach to an explicit syscall allowlist. + // Keys are bare syscall names (for example "openat", not "sys_enter_openat"). + // When RestrictSyscalls is true, only entries in this map are attached. + Syscalls map[string]struct{} + // RestrictSyscalls gates whether Syscalls should be enforced. + RestrictSyscalls bool } // ParseSelector parses the comma-separated regex strings for the -tps and @@ -65,11 +72,27 @@ func (s Selector) ShouldAttach(tracepointName string) bool { } } if len(s.Attach) == 0 { - return true + if !s.RestrictSyscalls { + return true + } + syscall, ok := SyscallNameFromTracepoint(tracepointName) + if !ok { + return false + } + _, allowed := s.Syscalls[syscall] + return allowed } for _, re := range s.Attach { if re.MatchString(tracepointName) { - return true + if !s.RestrictSyscalls { + return true + } + syscall, ok := SyscallNameFromTracepoint(tracepointName) + if !ok { + return false + } + _, allowed := s.Syscalls[syscall] + return allowed } } return false @@ -79,7 +102,22 @@ func (s Selector) ShouldAttach(tracepointName string) bool { // copy's slices do not affect the original. func (s Selector) Clone() Selector { return Selector{ - Attach: slices.Clone(s.Attach), - Exclude: slices.Clone(s.Exclude), + Attach: slices.Clone(s.Attach), + Exclude: slices.Clone(s.Exclude), + Syscalls: maps.Clone(s.Syscalls), + RestrictSyscalls: s.RestrictSyscalls, + } +} + +// SyscallNameFromTracepoint returns the bare syscall name for a tracepoint +// (for example "openat" from "sys_enter_openat"). +func SyscallNameFromTracepoint(tracepointName string) (string, bool) { + switch { + case strings.HasPrefix(tracepointName, "sys_enter_"): + return strings.TrimPrefix(tracepointName, "sys_enter_"), true + case strings.HasPrefix(tracepointName, "sys_exit_"): + return strings.TrimPrefix(tracepointName, "sys_exit_"), true + default: + return "", false } } diff --git a/internal/tracepoints/selector_test.go b/internal/tracepoints/selector_test.go index d12f24b..dd9084b 100644 --- a/internal/tracepoints/selector_test.go +++ b/internal/tracepoints/selector_test.go @@ -82,3 +82,17 @@ func TestSelectorCloneIsIndependent(t *testing.T) { t.Error("original Selector was mutated through clone") } } + +func TestSelectorCloneCopiesSyscallAllowlist(t *testing.T) { + sel := Selector{ + Syscalls: map[string]struct{}{ + "openat": {}, + }, + RestrictSyscalls: true, + } + clone := sel.Clone() + delete(clone.Syscalls, "openat") + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("original syscall allowlist mutated through clone") + } +} |
