Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <name-or-section>, type: <bpf-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 <program|section>=<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
Expand Down
30 changes: 23 additions & 7 deletions docs/case-study-inspektor-gadget.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,38 @@ 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'
```

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

Expand Down
28 changes: 28 additions & 0 deletions internal/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions internal/manifest/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions internal/manifest/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
26 changes: 26 additions & 0 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ func executeTarget(
ManifestPath: stagedManifest,
FunctionalPlanPath: functionalPlanPath,
MapFixups: tuning.mapFixups,
ProgTypes: tuning.progTypes,
ProgVariants: tuning.progVariants,
ProbeCompanions: tuning.probeCompanions,
ValidatorBinary: validatorBinPath,
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions internal/runner/validator_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
19 changes: 18 additions & 1 deletion internal/vm/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ExecutionRequest struct {
ManifestPath string
FunctionalPlanPath string
MapFixups []MapFixup
ProgTypes []ProgTypeOverride
ProgVariants []ProgVariantGroup
ProbeCompanions []string
ValidatorBinary string
Expand Down Expand Up @@ -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 {
Expand All @@ -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, ",")
}
Expand Down
11 changes: 11 additions & 0 deletions internal/vm/qemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading
Loading