diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4249e..ed8080c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once a later for its own reasons — e.g. `trace_dns` then hits a CO-RE relocation against Inspektor Gadget's socket-enricher API type `gadget_socket_value`, which the IG loader supplies at runtime and a standalone load does not.) +- Manifest-declared program-type override: `program_types:` entries + (`{program: , type: }`) set a program's BPF type + explicitly before load, the general form of the auto-typing above — for any + program libbpf can't classify, not just socket-filter. Surfaced to the + validator as `--set-prog-type =`; takes precedence + over auto-typing and is reported per override in the run notes. - Generic inner-map prototype map fixup: a manifest `maps[].inner_map` (`type`/`key_size`/`value_size`/`max_entries`) installs an inner-map template on a `HASH_OF_MAPS`/`ARRAY_OF_MAPS` before load, so objects whose own loader diff --git a/docs/case-study-inspektor-gadget.md b/docs/case-study-inspektor-gadget.md index 07c1fc0..190449f 100644 --- a/docs/case-study-inspektor-gadget.md +++ b/docs/case-study-inspektor-gadget.md @@ -43,11 +43,13 @@ on AlmaLinux 8 (kernel 4.18)** — RHEL backported the ring buffer into 4.18, so the gadget loads there even though it fails on Ubuntu's *newer* vanilla 5.4. That is the canonical "kernel version ≠ feature support" case, shown empirically. -### `trace_dns` — a loader contract, not a kernel limit +### `trace_dns` — two loader contracts, neither a kernel limit `trace_dns` fails to load on **every** kernel (including 6.8 with BTF), which by -itself signals a loader contract rather than a compatibility boundary. bpfcompat -surfaces the exact reason: +itself signals loader contracts rather than a compatibility boundary. It hits two, +in order: + +**1. Program type (handled).** The first failure is: ``` prog 'ig_trace_dns': missing BPF prog type, check ELF section name 'socket1' @@ -55,10 +57,24 @@ prog 'ig_trace_dns': missing BPF prog type, check ELF section name 'socket1' The DNS gadget is a **socket-filter** program in a `socket1` section — a section name libbpf cannot map to a program type on its own, so IG's loader sets the type -explicitly. A generic load can't infer it, so the load fails identically across -kernels. This is *not* a kernel-version result; it is exactly the kind of -loader-side detail a gadget's OCI metadata can describe, which is the direction -for deriving load config automatically. +explicitly. bpfcompat now does the same: it auto-types `socket`-prefixed programs +to `SOCKET_FILTER` (and a manifest `program_types:` override can set any type for +any program/section). With that, `trace_dns` clears the program-type stage. + +**2. Framework API (the real boundary).** It then fails at a CO-RE relocation: + +``` +failed to resolve CO-RE relocation struct gadget_socket_value.ipv6only +``` + +`gadget_socket_value` / `gadget_sockets` is **Inspektor Gadget's socket-enricher +API** — not a kernel struct. Its BTF is supplied by IG's loader/runtime, so a +standalone load has nothing to relocate against, and it fails identically on 6.1 +and 6.8. This is *not* a kernel-version result; it is the honest **boundary of +standalone gadget validation**: a framework-coupled gadget that depends on its +host runtime's injected API can be load-checked up to that contract, but fully +loading it would mean reproducing the IG runtime. The same applies to gadgets +whose attach points are rewritten by a WASM module (`fsnotify`, `fsslower`). ## Why this matters diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index bf67556..437e695 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -15,6 +15,7 @@ type Manifest struct { Name string `yaml:"name"` Programs []Program `yaml:"programs"` Maps []MapFixup `yaml:"maps,omitempty"` + ProgramTypes []ProgramTypeOverride `yaml:"program_types,omitempty"` ProgramVariants []ProgramVariantGroup `yaml:"program_variants,omitempty"` // ProbeCompanions lists programs statically referenced from prog-array // map slots; they must stay autoloaded during trial_load probes or @@ -115,6 +116,33 @@ var innerMapTypes = map[string]bool{ "lru_percpu_hash": true, } +// ProgramTypeOverride sets a program's BPF type explicitly when libbpf can't +// map it from the ELF section name (e.g. Inspektor Gadget's socket-filter +// programs in a "socket1" section). Program matches the program name or its +// section name. +type ProgramTypeOverride struct { + Program string `yaml:"program"` + Type string `yaml:"type"` +} + +// progTypeNames is the set of program-type names the validator understands. +var progTypeNames = map[string]bool{ + "socket_filter": true, + "kprobe": true, + "tracepoint": true, + "raw_tracepoint": true, + "xdp": true, + "perf_event": true, + "cgroup_skb": true, + "cgroup_sock": true, + "sched_cls": true, + "sched_act": true, + "sk_skb": true, + "sk_msg": true, + "tracing": true, + "lsm": true, +} + // EntriesValue accepts either a YAML integer or the string "cpus". type EntriesValue string diff --git a/internal/manifest/validate.go b/internal/manifest/validate.go index f2a018c..4db3c67 100644 --- a/internal/manifest/validate.go +++ b/internal/manifest/validate.go @@ -70,6 +70,19 @@ func Validate(m Manifest) error { } } + for i := range m.ProgramTypes { + ov := &m.ProgramTypes[i] + if strings.TrimSpace(ov.Program) == "" { + return fmt.Errorf("program_types[%d].program is required (program name or ELF section)", i) + } + if !validProgramSelector(ov.Program) { + return fmt.Errorf("program_types[%d].program %q must be a program name or ELF section (letters, digits, '_', '.', '/', '-')", i, ov.Program) + } + if !progTypeNames[ov.Type] { + return fmt.Errorf("program_types[%d].type %q is not a recognized BPF program type", i, ov.Type) + } + } + seenGroups := make(map[string]struct{}, len(m.ProgramVariants)) seenVariantPrograms := make(map[string]struct{}) for i := range m.ProgramVariants { @@ -190,3 +203,18 @@ func validMapName(name string) bool { } return true } + +// validProgramSelector accepts a program name or an ELF section name (which can +// contain '/' and '-'), while staying shell-safe for the validator CLI arg. +func validProgramSelector(sel string) bool { + if sel == "" || len(sel) > 128 { + return false + } + for _, r := range sel { + if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') && + r != '_' && r != '.' && r != '/' && r != '-' { + return false + } + } + return true +} diff --git a/internal/manifest/validate_test.go b/internal/manifest/validate_test.go index 079f409..66a92c3 100644 --- a/internal/manifest/validate_test.go +++ b/internal/manifest/validate_test.go @@ -124,3 +124,24 @@ func TestValidateAcceptsSimpleManifest(t *testing.T) { t.Fatalf("unexpected validation error: %v", err) } } + +func TestValidateProgramTypes(t *testing.T) { + ok := Manifest{ProgramTypes: []ProgramTypeOverride{ + {Program: "ig_trace_dns", Type: "socket_filter"}, + {Program: "socket1", Type: "socket_filter"}, + {Program: "tracepoint/syscalls/sys_enter_open", Type: "tracepoint"}, + }} + if err := Validate(ok); err != nil { + t.Fatalf("unexpected validation error: %v", err) + } + bad := []Manifest{ + {ProgramTypes: []ProgramTypeOverride{{Type: "socket_filter"}}}, // missing program + {ProgramTypes: []ProgramTypeOverride{{Program: "p", Type: "no_such_type"}}}, // bad type + {ProgramTypes: []ProgramTypeOverride{{Program: "bad;rm -rf", Type: "kprobe"}}}, // shell-unsafe + } + for i, m := range bad { + if err := Validate(m); err == nil { + t.Errorf("case %d: expected validation error", i) + } + } +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 9928f7c..363dd7a 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -533,6 +533,7 @@ func executeTarget( ManifestPath: stagedManifest, FunctionalPlanPath: functionalPlanPath, MapFixups: tuning.mapFixups, + ProgTypes: tuning.progTypes, ProgVariants: tuning.progVariants, ProbeCompanions: tuning.probeCompanions, ValidatorBinary: validatorBinPath, @@ -578,6 +579,7 @@ func executeTarget( target.Notes = append(target.Notes, mapFixupNotes(vr)...) target.Notes = append(target.Notes, autoSizedMapNotes(vr)...) target.Notes = append(target.Notes, autoTypedProgramNotes(vr)...) + target.Notes = append(target.Notes, progTypeOverrideNotes(vr)...) target.Notes = append(target.Notes, progVariantNotes(vr)...) target.Notes = append(target.Notes, perProgramLoadNotes(vr)...) target.BTF = &schema.TargetBTF{ @@ -1007,10 +1009,28 @@ func autoTypedProgramNotes(vr validatorResult) []string { return notes } +// progTypeOverrideNotes reports the outcome of manifest-declared program-type +// overrides (maps[].program_types) the validator applied before load. +func progTypeOverrideNotes(vr validatorResult) []string { + notes := make([]string, 0, len(vr.ProgramTypeOverrides)) + for _, o := range vr.ProgramTypeOverrides { + switch o.Status { + case "applied": + notes = append(notes, fmt.Sprintf("program type override applied: %q", o.Selector)) + case "program_not_found": + notes = append(notes, fmt.Sprintf("program type override skipped: no program or section %q in artifact", o.Selector)) + case "error": + notes = append(notes, fmt.Sprintf("program type override failed: %q", o.Selector)) + } + } + return notes +} + // validatorTuning carries manifest-declared loader-contract settings (map // fixups, program variant groups) from manifest load to VM execution. type validatorTuning struct { mapFixups []vm.MapFixup + progTypes []vm.ProgTypeOverride progVariants []vm.ProgVariantGroup probeCompanions []string } @@ -1031,6 +1051,12 @@ func validatorTuningFromManifest(mf manifest.Manifest) validatorTuning { } tuning.mapFixups = append(tuning.mapFixups, vmFixup) } + for _, ov := range mf.ProgramTypes { + tuning.progTypes = append(tuning.progTypes, vm.ProgTypeOverride{ + Selector: ov.Program, + Type: ov.Type, + }) + } for _, group := range mf.ProgramVariants { vmGroup := vm.ProgVariantGroup{Group: group.Group} for _, variant := range group.Programs { diff --git a/internal/runner/validator_result.go b/internal/runner/validator_result.go index 4fda089..33248a1 100644 --- a/internal/runner/validator_result.go +++ b/internal/runner/validator_result.go @@ -98,6 +98,11 @@ type validatorResult struct { Section string `json:"section"` ProgType uint32 `json:"prog_type"` } `json:"auto_typed_programs"` + ProgramTypeOverrides []struct { + Selector string `json:"selector"` + ProgType uint32 `json:"prog_type"` + Status string `json:"status"` + } `json:"program_type_overrides"` Discovery struct { Programs []struct { Name string `json:"name"` diff --git a/internal/vm/qemu.go b/internal/vm/qemu.go index 8f03fa6..636b04f 100644 --- a/internal/vm/qemu.go +++ b/internal/vm/qemu.go @@ -19,6 +19,7 @@ type ExecutionRequest struct { ManifestPath string FunctionalPlanPath string MapFixups []MapFixup + ProgTypes []ProgTypeOverride ProgVariants []ProgVariantGroup ProbeCompanions []string ValidatorBinary string @@ -74,6 +75,22 @@ func mapFixupArgs(fixups []MapFixup) string { return b.String() } +// ProgTypeOverride mirrors a manifest program-type override for the validator +// command line. Selector (program name or section) and Type are validated at +// manifest load, so both are shell-safe here. +type ProgTypeOverride struct { + Selector string + Type string +} + +func progTypeArgs(overrides []ProgTypeOverride) string { + var b strings.Builder + for _, ov := range overrides { + fmt.Fprintf(&b, " --set-prog-type %s=%s", ov.Selector, ov.Type) + } + return b.String() +} + func progVariantArgs(groups []ProgVariantGroup) string { var b strings.Builder for _, group := range groups { @@ -95,7 +112,7 @@ func progVariantArgs(groups []ProgVariantGroup) string { // validatorTuningArgs renders all manifest-declared loader-contract flags // (map fixups, program variant groups) for the in-guest validator command. func validatorTuningArgs(req ExecutionRequest) string { - args := mapFixupArgs(req.MapFixups) + progVariantArgs(req.ProgVariants) + args := mapFixupArgs(req.MapFixups) + progTypeArgs(req.ProgTypes) + progVariantArgs(req.ProgVariants) if len(req.ProbeCompanions) > 0 { args += " --probe-companions " + strings.Join(req.ProbeCompanions, ",") } diff --git a/internal/vm/qemu_test.go b/internal/vm/qemu_test.go index 42e1f2e..44b154f 100644 --- a/internal/vm/qemu_test.go +++ b/internal/vm/qemu_test.go @@ -457,3 +457,14 @@ func TestMachineArgsForAccelFallback(t *testing.T) { }) } } + +func TestProgTypeArgs(t *testing.T) { + got := progTypeArgs([]ProgTypeOverride{ + {Selector: "socket1", Type: "socket_filter"}, + {Selector: "ig_trace_dns", Type: "socket_filter"}, + }) + want := " --set-prog-type socket1=socket_filter --set-prog-type ig_trace_dns=socket_filter" + if got != want { + t.Fatalf("unexpected prog-type args:\n got %q\nwant %q", got, want) + } +} diff --git a/validator/c-libbpf/src/main.c b/validator/c-libbpf/src/main.c index 2c0c622..78c6d2c 100644 --- a/validator/c-libbpf/src/main.c +++ b/validator/c-libbpf/src/main.c @@ -57,6 +57,37 @@ static unsigned int inner_map_type_from_name(const char *name) { return 0; } +/* Resolve the program-type names accepted in manifests to numeric + * BPF_PROG_TYPE_* values. Returns 0 (UNSPEC) for an unknown name. */ +static unsigned int prog_type_from_name(const char *name) { + if (strcmp(name, "socket_filter") == 0) return BPF_PROG_TYPE_SOCKET_FILTER; + if (strcmp(name, "kprobe") == 0) return BPF_PROG_TYPE_KPROBE; + if (strcmp(name, "tracepoint") == 0) return BPF_PROG_TYPE_TRACEPOINT; + if (strcmp(name, "raw_tracepoint") == 0) return BPF_PROG_TYPE_RAW_TRACEPOINT; + if (strcmp(name, "xdp") == 0) return BPF_PROG_TYPE_XDP; + if (strcmp(name, "perf_event") == 0) return BPF_PROG_TYPE_PERF_EVENT; + if (strcmp(name, "cgroup_skb") == 0) return BPF_PROG_TYPE_CGROUP_SKB; + if (strcmp(name, "cgroup_sock") == 0) return BPF_PROG_TYPE_CGROUP_SOCK; + if (strcmp(name, "sched_cls") == 0) return BPF_PROG_TYPE_SCHED_CLS; + if (strcmp(name, "sched_act") == 0) return BPF_PROG_TYPE_SCHED_ACT; + if (strcmp(name, "sk_skb") == 0) return BPF_PROG_TYPE_SK_SKB; + if (strcmp(name, "sk_msg") == 0) return BPF_PROG_TYPE_SK_MSG; + if (strcmp(name, "tracing") == 0) return BPF_PROG_TYPE_TRACING; + if (strcmp(name, "lsm") == 0) return BPF_PROG_TYPE_LSM; + return 0; +} + +#define MAX_PROG_TYPE_OVERRIDES 32 + +/* A manifest-declared program-type override: set a program's BPF type + * explicitly when its ELF section name can't be mapped by libbpf. `selector` + * matches either the program name or its section name. */ +struct prog_type_override { + char selector[128]; + unsigned int prog_type; + char status[32]; /* applied | program_not_found | error */ +}; + #define MAX_VARIANT_GROUPS 16 #define MAX_VARIANTS_PER_GROUP 6 @@ -85,6 +116,8 @@ struct options { bool probe_features; struct map_fixup map_fixups[MAX_MAP_FIXUPS]; int map_fixup_count; + struct prog_type_override prog_type_overrides[MAX_PROG_TYPE_OVERRIDES]; + int prog_type_override_count; struct prog_variant_group variant_groups[MAX_VARIANT_GROUPS]; int variant_group_count; /* Programs statically referenced from prog-array map slots; they must @@ -176,6 +209,7 @@ static void usage(const char *prog) { "[--probe-features ] [--set-map-max-entries =]... " "[--set-map-inner-ringbuf =]... " "[--set-map-inner-map =:::]... " + "[--set-prog-type =]... " "[--prog-variants =:,...]... " "[--probe-companions ,,...]\n", prog); @@ -364,6 +398,37 @@ static int add_inner_map_fixup(struct options *opts, const char *spec) { return 0; } +/* Parse "=" for --set-prog-type, declaring an explicit BPF + * program type for a program libbpf can't classify from its section name. + * selector matches the program name or its ELF section name. */ +static int add_prog_type_override(struct options *opts, const char *spec) { + const char *eq = spec ? strchr(spec, '=') : NULL; + if (!eq || eq == spec || !eq[1]) { + fprintf(stderr, "invalid prog-type spec (want =): %s\n", spec ? spec : ""); + return -1; + } + size_t sel_len = (size_t)(eq - spec); + if (sel_len >= sizeof(((struct prog_type_override *)0)->selector)) { + fprintf(stderr, "prog-type selector too long: %s\n", spec); + return -1; + } + unsigned int t = prog_type_from_name(eq + 1); + if (t == 0) { + fprintf(stderr, "unknown program type '%s' in: %s\n", eq + 1, spec); + return -1; + } + if (opts->prog_type_override_count >= MAX_PROG_TYPE_OVERRIDES) { + fprintf(stderr, "too many prog-type overrides: max %d\n", MAX_PROG_TYPE_OVERRIDES); + return -1; + } + struct prog_type_override *o = &opts->prog_type_overrides[opts->prog_type_override_count++]; + memset(o, 0, sizeof(*o)); + memcpy(o->selector, spec, sel_len); + o->selector[sel_len] = '\0'; + o->prog_type = t; + return 0; +} + /* Parse "=:,:,..." for * --prog-variants. helper_id 0 means the variant has no helper requirement * (the unconditional fallback). Variant order is selection priority. */ @@ -494,6 +559,12 @@ static int parse_args(int argc, char **argv, struct options *opts) { } continue; } + if (strcmp(argv[i], "--set-prog-type") == 0 && i + 1 < argc) { + if (add_prog_type_override(opts, argv[++i]) != 0) { + return -1; + } + continue; + } if (strcmp(argv[i], "--prog-variants") == 0 && i + 1 < argc) { if (add_prog_variant_group(opts, argv[++i]) != 0) { return -1; @@ -1667,6 +1738,34 @@ struct autotyped_prog { static struct autotyped_prog g_autotyped[MAX_AUTOTYPED_PROGS]; static int g_autotyped_count; +/* Apply manifest-declared program-type overrides: set each selected program's + * type explicitly. Runs before auto_type_programs so an explicit override wins + * and the program is no longer UNSPEC for the auto pass to touch. */ +static void apply_prog_type_overrides(struct options *opts, struct bpf_object *obj) { + for (int i = 0; i < opts->prog_type_override_count; i++) { + struct prog_type_override *o = &opts->prog_type_overrides[i]; + bool found = false; + struct bpf_program *prog; + bpf_object__for_each_program(prog, obj) { + const char *name = bpf_program__name(prog); + const char *section = bpf_program__section_name(prog); + if ((name && strcmp(name, o->selector) == 0) || + (section && strcmp(section, o->selector) == 0)) { + found = true; + if (bpf_program__set_type(prog, (enum bpf_prog_type)o->prog_type) == 0) { + snprintf(o->status, sizeof(o->status), "applied"); + } else { + snprintf(o->status, sizeof(o->status), "error"); + } + break; + } + } + if (!found) { + snprintf(o->status, sizeof(o->status), "program_not_found"); + } + } +} + static void auto_type_programs(struct bpf_object *obj) { struct bpf_program *prog; bpf_object__for_each_program(prog, obj) { @@ -1882,6 +1981,7 @@ static void run_libbpf_load(struct validator_result *res) { apply_prog_variants(&res->opts, obj); apply_map_fixups(&res->opts, obj, true); auto_size_maps(obj); + apply_prog_type_overrides(&res->opts, obj); auto_type_programs(obj); int err = bpf_object__load(obj); @@ -2003,6 +2103,16 @@ static int write_result_json(const struct validator_result *res) { fprintf(f, "\",\"prog_type\":%u}", g_autotyped[i].prog_type); } fprintf(f, "],\n"); + fprintf(f, " \"program_type_overrides\": ["); + for (int i = 0; i < res->opts.prog_type_override_count; i++) { + const struct prog_type_override *o = &res->opts.prog_type_overrides[i]; + fprintf(f, "%s{\"selector\":\"", i == 0 ? "" : ","); + escape_json_string(f, o->selector); + fprintf(f, "\",\"prog_type\":%u,\"status\":\"", o->prog_type); + escape_json_string(f, o->status); + fprintf(f, "\"}"); + } + fprintf(f, "],\n"); fprintf(f, " \"program_variants\": ["); for (int g = 0; g < res->opts.variant_group_count; g++) { const struct prog_variant_group *grp = &res->opts.variant_groups[g];