From 89ecca0f357f3bd3d8271d631e87c23f455800ac Mon Sep 17 00:00:00 2001 From: ErenAri Date: Sun, 21 Jun 2026 19:42:42 +0300 Subject: [PATCH] feat(validator): generic inner-map prototype fixup for map-in-map objects Add a manifest `maps[].inner_map` fixup that installs an inner-map template (type/key_size/value_size/max_entries) on a HASH_OF_MAPS / ARRAY_OF_MAPS before load. Objects whose own loader sets up the inner map at runtime (e.g. KubeArmor's kubearmor_visibility) previously failed a generic libbpf load with EINVAL on every kernel; declaring the prototype lets bpfcompat load them faithfully and produce a true per-kernel matrix. The prior fixup only supported an inner ringbuf. This threads a generic inner map through the full chain: manifest schema + validation, runner tuning and applied-note, validator result parsing, the VM command line (--set-map-inner-map =:::), and the C validator (bpf_map_create + bpf_map__set_inner_map_fd). Tests cover manifest validation and command-line construction. Verified end-to-end against KubeArmor system_monitor.bpf.o: loads across Ubuntu 5.4/5.15, Debian 6.1, Ubuntu 6.8, and AlmaLinux 8 (4.18). Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 8 +++ internal/manifest/manifest.go | 27 +++++++ internal/manifest/validate.go | 15 +++- internal/manifest/validate_test.go | 5 ++ internal/runner/runner.go | 14 +++- internal/runner/validator_result.go | 4 ++ internal/vm/qemu.go | 11 +++ internal/vm/qemu_test.go | 10 +++ validator/c-libbpf/src/main.c | 107 +++++++++++++++++++++++++++- 9 files changed, 196 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8856bd0..442b3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once a ## [Unreleased] ### Added +- 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 + sets up the inner map at runtime can be validated faithfully. Previously only + an inner *ringbuf* could be declared. Surfaced to the validator as + `--set-map-inner-map =:::`. Proven against + KubeArmor's `system_monitor.bpf.o` (`kubearmor_visibility`), which then loads + across Ubuntu 5.4/5.15, Debian 6.1, Ubuntu 6.8, and AlmaLinux 8 (4.18). - Supply-chain trust signals: GitHub CodeQL static analysis (`.github/workflows/codeql.yml`), OpenSSF Scorecard (`.github/workflows/scorecard.yml`), and Dependabot diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 64533aa..bf67556 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -86,6 +86,33 @@ type MapFixup struct { // InnerRingbufBytes creates a BPF_MAP_TYPE_RINGBUF of this byte size and // installs it as the inner-map prototype for an array-of-maps. InnerRingbufBytes uint32 `yaml:"inner_ringbuf_bytes,omitempty"` + // InnerMap installs a generic inner-map prototype on a map-in-map + // (BPF_MAP_TYPE_HASH_OF_MAPS / ARRAY_OF_MAPS) whose inner shape the + // artifact's own loader sets at runtime — e.g. KubeArmor's + // kubearmor_visibility, a per-namespace inner hash. + InnerMap *InnerMapSpec `yaml:"inner_map,omitempty"` +} + +// InnerMapSpec describes the inner-map prototype to create and install as the +// template for a map-in-map before the outer object is loaded. +type InnerMapSpec struct { + // Type is one of: hash, array, lru_hash, percpu_hash, percpu_array, + // lru_percpu_hash. + Type string `yaml:"type"` + KeySize uint32 `yaml:"key_size,omitempty"` + ValueSize uint32 `yaml:"value_size"` + MaxEntries uint32 `yaml:"max_entries"` +} + +// innerMapTypes is the set of inner-map type names the validator understands; +// the value is informational (whether the type requires a non-zero key size). +var innerMapTypes = map[string]bool{ + "hash": true, + "array": true, + "lru_hash": true, + "percpu_hash": true, + "percpu_array": true, + "lru_percpu_hash": true, } // EntriesValue accepts either a YAML integer or the string "cpus". diff --git a/internal/manifest/validate.go b/internal/manifest/validate.go index 4729e00..f2a018c 100644 --- a/internal/manifest/validate.go +++ b/internal/manifest/validate.go @@ -48,8 +48,8 @@ func Validate(m Manifest) error { return fmt.Errorf("duplicate map fixup %q", fixup.Name) } seenMaps[fixup.Name] = struct{}{} - if fixup.MaxEntries == "" && fixup.InnerRingbufBytes == 0 { - return fmt.Errorf("map fixup %q must set max_entries or inner_ringbuf_bytes", fixup.Name) + if fixup.MaxEntries == "" && fixup.InnerRingbufBytes == 0 && fixup.InnerMap == nil { + return fmt.Errorf("map fixup %q must set max_entries, inner_ringbuf_bytes, or inner_map", fixup.Name) } if fixup.MaxEntries != "" && fixup.MaxEntries != "cpus" { entries, err := strconv.ParseUint(string(fixup.MaxEntries), 10, 32) @@ -57,6 +57,17 @@ func Validate(m Manifest) error { return fmt.Errorf("map fixup %q max_entries must be a positive integer or \"cpus\"", fixup.Name) } } + if fixup.InnerMap != nil { + if !innerMapTypes[fixup.InnerMap.Type] { + return fmt.Errorf("map fixup %q inner_map.type %q must be one of hash, array, lru_hash, percpu_hash, percpu_array, lru_percpu_hash", fixup.Name, fixup.InnerMap.Type) + } + if fixup.InnerMap.ValueSize == 0 { + return fmt.Errorf("map fixup %q inner_map.value_size must be positive", fixup.Name) + } + if fixup.InnerMap.MaxEntries == 0 { + return fmt.Errorf("map fixup %q inner_map.max_entries must be positive", fixup.Name) + } + } } seenGroups := make(map[string]struct{}, len(m.ProgramVariants)) diff --git a/internal/manifest/validate_test.go b/internal/manifest/validate_test.go index 66fbccb..079f409 100644 --- a/internal/manifest/validate_test.go +++ b/internal/manifest/validate_test.go @@ -34,6 +34,11 @@ func TestValidateMapFixups(t *testing.T) { {"no settings", MapFixup{Name: "m"}, true}, {"zero entries", MapFixup{Name: "m", MaxEntries: "0"}, true}, {"non-numeric entries", MapFixup{Name: "m", MaxEntries: "lots"}, true}, + {"inner map hash", MapFixup{Name: "kubearmor_visibility", InnerMap: &InnerMapSpec{Type: "hash", KeySize: 4, ValueSize: 4, MaxEntries: 64}}, false}, + {"inner map array no key", MapFixup{Name: "m", InnerMap: &InnerMapSpec{Type: "array", ValueSize: 8, MaxEntries: 1}}, false}, + {"inner map bad type", MapFixup{Name: "m", InnerMap: &InnerMapSpec{Type: "queue", ValueSize: 4, MaxEntries: 1}}, true}, + {"inner map zero value_size", MapFixup{Name: "m", InnerMap: &InnerMapSpec{Type: "hash", KeySize: 4, MaxEntries: 1}}, true}, + {"inner map zero entries", MapFixup{Name: "m", InnerMap: &InnerMapSpec{Type: "hash", KeySize: 4, ValueSize: 4}}, true}, } for _, tc := range cases { err := Validate(Manifest{Maps: []MapFixup{tc.fixup}}) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index d4b15bc..93f20ad 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -943,6 +943,9 @@ func mapFixupNotes(vr validatorResult) []string { if fixup.InnerRingbufBytes > 0 { detail += fmt.Sprintf(" inner_ringbuf_bytes=%d", fixup.InnerRingbufBytes) } + if fixup.InnerMapType > 0 { + detail += fmt.Sprintf(" inner_map=type%d/%d/%d/%d", fixup.InnerMapType, fixup.InnerKeySize, fixup.InnerValueSize, fixup.InnerMaxEntries) + } notes = append(notes, fmt.Sprintf("map fixup applied: %s%s", fixup.Name, detail)) case "map_not_found": notes = append(notes, fmt.Sprintf("map fixup skipped: map %q not found in artifact", fixup.Name)) @@ -964,11 +967,18 @@ type validatorTuning struct { func validatorTuningFromManifest(mf manifest.Manifest) validatorTuning { var tuning validatorTuning for _, fixup := range mf.Maps { - tuning.mapFixups = append(tuning.mapFixups, vm.MapFixup{ + vmFixup := vm.MapFixup{ Name: fixup.Name, MaxEntries: string(fixup.MaxEntries), InnerRingbufBytes: fixup.InnerRingbufBytes, - }) + } + if fixup.InnerMap != nil { + vmFixup.InnerMapType = fixup.InnerMap.Type + vmFixup.InnerKeySize = fixup.InnerMap.KeySize + vmFixup.InnerValueSize = fixup.InnerMap.ValueSize + vmFixup.InnerMaxEntries = fixup.InnerMap.MaxEntries + } + tuning.mapFixups = append(tuning.mapFixups, vmFixup) } for _, group := range mf.ProgramVariants { vmGroup := vm.ProgVariantGroup{Group: group.Group} diff --git a/internal/runner/validator_result.go b/internal/runner/validator_result.go index 2f2b4a8..dfc2d11 100644 --- a/internal/runner/validator_result.go +++ b/internal/runner/validator_result.go @@ -80,6 +80,10 @@ type validatorResult struct { Name string `json:"name"` MaxEntries string `json:"max_entries"` InnerRingbufBytes uint32 `json:"inner_ringbuf_bytes"` + InnerMapType uint32 `json:"inner_map_type"` + InnerKeySize uint32 `json:"inner_key_size"` + InnerValueSize uint32 `json:"inner_value_size"` + InnerMaxEntries uint32 `json:"inner_max_entries"` Status string `json:"status"` Errno int `json:"errno"` AppliedEntries uint32 `json:"applied_entries"` diff --git a/internal/vm/qemu.go b/internal/vm/qemu.go index ba8c96c..8f03fa6 100644 --- a/internal/vm/qemu.go +++ b/internal/vm/qemu.go @@ -48,6 +48,13 @@ type MapFixup struct { Name string MaxEntries string InnerRingbufBytes uint32 + // Generic inner-map prototype for a map-in-map. InnerMapType is one of the + // names accepted by the validator (hash, array, lru_hash, ...) and is + // validated at manifest load, so it is shell-safe here. Empty = unset. + InnerMapType string + InnerKeySize uint32 + InnerValueSize uint32 + InnerMaxEntries uint32 } func mapFixupArgs(fixups []MapFixup) string { @@ -59,6 +66,10 @@ func mapFixupArgs(fixups []MapFixup) string { if fixup.InnerRingbufBytes > 0 { fmt.Fprintf(&b, " --set-map-inner-ringbuf %s=%d", fixup.Name, fixup.InnerRingbufBytes) } + if fixup.InnerMapType != "" { + fmt.Fprintf(&b, " --set-map-inner-map %s=%s:%d:%d:%d", fixup.Name, + fixup.InnerMapType, fixup.InnerKeySize, fixup.InnerValueSize, fixup.InnerMaxEntries) + } } return b.String() } diff --git a/internal/vm/qemu_test.go b/internal/vm/qemu_test.go index 4d773e2..42e1f2e 100644 --- a/internal/vm/qemu_test.go +++ b/internal/vm/qemu_test.go @@ -412,6 +412,16 @@ func TestMapFixupArgs(t *testing.T) { } } +func TestMapFixupArgsInnerMap(t *testing.T) { + fixups := []MapFixup{ + {Name: "kubearmor_visibility", InnerMapType: "hash", InnerKeySize: 4, InnerValueSize: 4, InnerMaxEntries: 64}, + } + want := " --set-map-inner-map kubearmor_visibility=hash:4:4:64" + if got := mapFixupArgs(fixups); got != want { + t.Fatalf("unexpected inner-map args:\n got %q\nwant %q", got, want) + } +} + func TestProgVariantArgs(t *testing.T) { groups := []ProgVariantGroup{{ Group: "recvmmsg_x", diff --git a/validator/c-libbpf/src/main.c b/validator/c-libbpf/src/main.c index bcda29e..f588b7c 100644 --- a/validator/c-libbpf/src/main.c +++ b/validator/c-libbpf/src/main.c @@ -33,11 +33,30 @@ struct map_fixup { char name[68]; char max_entries[16]; /* decimal or "cpus"; empty = unset */ unsigned int inner_ringbuf_bytes; /* 0 = unset */ + /* Generic inner-map prototype for HASH_OF_MAPS / ARRAY_OF_MAPS whose inner + * shape the artifact's loader sets at runtime (e.g. KubeArmor's + * kubearmor_visibility, an inner per-namespace hash). 0 type = unset. */ + unsigned int inner_map_type; /* BPF_MAP_TYPE_*; 0 = unset */ + unsigned int inner_key_size; + unsigned int inner_value_size; + unsigned int inner_max_entries; char status[32]; /* applied | map_not_found | error (main load only) */ int err; unsigned int applied_entries; }; +/* Resolve the inner-map type names accepted in manifests to numeric + * BPF_MAP_TYPE_* values. Returns 0 for an unknown name. */ +static unsigned int inner_map_type_from_name(const char *name) { + if (strcmp(name, "hash") == 0) return BPF_MAP_TYPE_HASH; + if (strcmp(name, "array") == 0) return BPF_MAP_TYPE_ARRAY; + if (strcmp(name, "lru_hash") == 0) return BPF_MAP_TYPE_LRU_HASH; + if (strcmp(name, "percpu_hash") == 0) return BPF_MAP_TYPE_PERCPU_HASH; + if (strcmp(name, "percpu_array") == 0) return BPF_MAP_TYPE_PERCPU_ARRAY; + if (strcmp(name, "lru_percpu_hash") == 0) return BPF_MAP_TYPE_LRU_PERCPU_HASH; + return 0; +} + #define MAX_VARIANT_GROUPS 16 #define MAX_VARIANTS_PER_GROUP 6 @@ -156,6 +175,7 @@ static void usage(const char *prog) { "[--functional-plan ] [--log-dir ] [--attach-mode ] " "[--probe-features ] [--set-map-max-entries =]... " "[--set-map-inner-ringbuf =]... " + "[--set-map-inner-map =:::]... " "[--prog-variants =:,...]... " "[--probe-companions ,,...]\n", prog); @@ -287,6 +307,63 @@ static int add_map_fixup(struct options *opts, const char *spec, bool inner_ring return 0; } +/* Parse "=:::" for + * --set-map-inner-map, installing a generic inner-map prototype on a + * map-in-map (e.g. kubearmor_visibility=hash:4:4:64). */ +static int add_inner_map_fixup(struct options *opts, const char *spec) { + const char *eq = spec ? strchr(spec, '=') : NULL; + if (!eq || eq == spec || !eq[1]) { + fprintf(stderr, "invalid inner-map spec (want =:::): %s\n", spec ? spec : ""); + return -1; + } + size_t name_len = (size_t)(eq - spec); + if (name_len >= sizeof(((struct map_fixup *)0)->name)) { + fprintf(stderr, "map fixup name too long: %s\n", spec); + return -1; + } + + char type_name[32]; + unsigned int key_size = 0, value_size = 0, max_entries = 0; + if (sscanf(eq + 1, "%31[^:]:%u:%u:%u", type_name, &key_size, &value_size, &max_entries) != 4) { + fprintf(stderr, "invalid inner-map spec (want :::): %s\n", spec); + return -1; + } + unsigned int type = inner_map_type_from_name(type_name); + if (type == 0) { + fprintf(stderr, "unknown inner-map type '%s' (want hash|array|lru_hash|percpu_hash|percpu_array|lru_percpu_hash): %s\n", type_name, spec); + return -1; + } + if (value_size == 0 || max_entries == 0) { + fprintf(stderr, "inner-map value_size and max_entries must be positive: %s\n", spec); + return -1; + } + + struct map_fixup *fx = NULL; + for (int i = 0; i < opts->map_fixup_count; i++) { + if (strncmp(opts->map_fixups[i].name, spec, name_len) == 0 && + opts->map_fixups[i].name[name_len] == '\0') { + fx = &opts->map_fixups[i]; + break; + } + } + if (!fx) { + if (opts->map_fixup_count >= MAX_MAP_FIXUPS) { + fprintf(stderr, "too many map fixups: max %d\n", MAX_MAP_FIXUPS); + return -1; + } + fx = &opts->map_fixups[opts->map_fixup_count++]; + memset(fx, 0, sizeof(*fx)); + memcpy(fx->name, spec, name_len); + fx->name[name_len] = '\0'; + } + + fx->inner_map_type = type; + fx->inner_key_size = key_size; + fx->inner_value_size = value_size; + fx->inner_max_entries = max_entries; + return 0; +} + /* Parse "=:,:,..." for * --prog-variants. helper_id 0 means the variant has no helper requirement * (the unconditional fallback). Variant order is selection priority. */ @@ -411,6 +488,12 @@ static int parse_args(int argc, char **argv, struct options *opts) { } continue; } + if (strcmp(argv[i], "--set-map-inner-map") == 0 && i + 1 < argc) { + if (add_inner_map_fixup(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; @@ -1482,6 +1565,25 @@ static void apply_map_fixups(struct options *opts, struct bpf_object *obj, bool } } + if (fx->inner_map_type > 0) { + int inner_fd = bpf_map_create((enum bpf_map_type)fx->inner_map_type, NULL, + fx->inner_key_size, fx->inner_value_size, + fx->inner_max_entries, NULL); + if (inner_fd < 0) { + record_fixup(fx, record, "error", inner_fd, entries); + continue; + } + int err = bpf_map__set_inner_map_fd(map, inner_fd); + if (err) { + close(inner_fd); + record_fixup(fx, record, "error", err, entries); + continue; + } + if (g_inner_map_fd_count < MAX_MAP_FIXUPS) { + g_inner_map_fds[g_inner_map_fd_count++] = inner_fd; + } + } + record_fixup(fx, record, "applied", 0, entries); } } @@ -1767,7 +1869,10 @@ static int write_result_json(const struct validator_result *res) { escape_json_string(f, fx->name); fprintf(f, "\",\"max_entries\":\""); escape_json_string(f, fx->max_entries); - fprintf(f, "\",\"inner_ringbuf_bytes\":%u,\"status\":\"", fx->inner_ringbuf_bytes); + fprintf(f, "\",\"inner_ringbuf_bytes\":%u", fx->inner_ringbuf_bytes); + fprintf(f, ",\"inner_map_type\":%u,\"inner_key_size\":%u,\"inner_value_size\":%u,\"inner_max_entries\":%u", + fx->inner_map_type, fx->inner_key_size, fx->inner_value_size, fx->inner_max_entries); + fprintf(f, ",\"status\":\""); escape_json_string(f, fx->status); fprintf(f, "\",\"errno\":%d,\"applied_entries\":%u}", fx->err, fx->applied_entries); }