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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <map>=<type>:<key>:<value>:<entries>`. 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
Expand Down
27 changes: 27 additions & 0 deletions internal/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
15 changes: 13 additions & 2 deletions internal/manifest/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,26 @@ 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)
if err != nil || entries == 0 {
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))
Expand Down
5 changes: 5 additions & 0 deletions internal/manifest/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}})
Expand Down
14 changes: 12 additions & 2 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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}
Expand Down
4 changes: 4 additions & 0 deletions internal/runner/validator_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
11 changes: 11 additions & 0 deletions internal/vm/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}
Expand Down
10 changes: 10 additions & 0 deletions internal/vm/qemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
107 changes: 106 additions & 1 deletion validator/c-libbpf/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -156,6 +175,7 @@ static void usage(const char *prog) {
"[--functional-plan <path>] [--log-dir <dir>] [--attach-mode <mode>] "
"[--probe-features <bool>] [--set-map-max-entries <map>=<n|cpus>]... "
"[--set-map-inner-ringbuf <map>=<bytes>]... "
"[--set-map-inner-map <map>=<type>:<key>:<value>:<entries>]... "
"[--prog-variants <group>=<prog>:<helper_id|trial>,...]... "
"[--probe-companions <prog>,<prog>,...]\n",
prog);
Expand Down Expand Up @@ -287,6 +307,63 @@ static int add_map_fixup(struct options *opts, const char *spec, bool inner_ring
return 0;
}

/* Parse "<map>=<type>:<key_size>:<value_size>:<max_entries>" 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 <map>=<type>:<key>:<value>:<entries>): %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 <type>:<key>:<value>:<entries>): %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 "<group>=<prog>:<helper_id>,<prog>:<helper_id>,..." for
* --prog-variants. helper_id 0 means the variant has no helper requirement
* (the unconditional fallback). Variant order is selection priority. */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading