Skip to content

Config Struct Alignment

Critical Importance

Misalignment is Silent and Catastrophic

If the struct config in C (ebpf/headers/config.h) does not match the struct config generated by bpf2go in Go byte-for-byte, the BPF program reads corrupted configuration values. All thresholds, feature flags, and scoring values will be garbage. The program will appear to load and run correctly — there is no error or warning. Detection will be completely broken.

How Alignment Works

The config is passed from Go userspace to the BPF kernel program via a single-entry BPF ARRAY map (config_map). The Go loader writes a struct using bpf_map_update_elem, and the BPF program reads it with bpf_map_lookup_elem.

Both sides must interpret the same byte layout identically:

Go struct → bpf_map_update_elem(MAP, &key0, &cfgStruct) → ARRAY map

BPF reads: cfg = bpf_map_lookup_elem(&config_map, &key0) → struct config

bpf2go-Generated Types vs Hand-Written Structs

When bpf2go processes the BPF C source, it:

  1. Compiles openshield.bpf.c to BPF object code (ELF)
  2. Extracts BTF type information from the ELF file
  3. Generates Go struct definitions in bpf_bpfel.go that match the BTF types

For example, the C-side:

c
struct config {
    u32 pps_threshold;
    u32 bps_threshold;
    u8  enable_static_mitigation;
    u8  _pad[3];
};

Becomes the Go-side (generated by bpf2go):

go
type config struct {
    PpsThreshold             uint32
    BpsThreshold             uint32
    EnableStaticMitigation   uint8
    Pad                      [3]uint8
}

The Go config struct in config/config.go is separate from the generated BPF types. It's the user-facing YAML config, not the BPF map struct. The loader's writeConfig() function translates between the two:

go
// loader.go — translating Go config to BPF map struct
func (l *Loader) writeConfig(cfg *config.Config) error {
    bpfc := bpf.Config{
        PpsThreshold:           uint32(cfg.Static.PPSThreshold),
        BpsThreshold:           uint32(cfg.Static.BPSThreshold),
        EnableStaticMitigation: boolToU8(cfg.Static.Enabled),
        // ...
    }
    return l.maps.ConfigMap.Put(uint32(0), bpfc)
}

Padding and Alignment Rules

  1. Natural alignment: u32 = 4-byte aligned, u64 = 8-byte aligned
  2. Explicit padding: After a u8 field, add _pad[3] to reach the next 4-byte boundary
  3. Struct final padding: Add _reserved or _pad fields to align the total struct size to 8 bytes
  4. Arrays of u8: MAC address arrays u8 mac[8][6] need _mac_pad[2] to reach 8-byte alignment
  5. No bitfields: Every boolean flag is a u8 — no C bitfields (unreliable across compilers/architectures)

How to Add a New Config Field End-to-End

Step 1: Add to BPF Config (C)

In ebpf/headers/config.h, add your field maintaining alignment:

c
struct config {
    // ... existing fields ...
    u32 event_rate_limit;   // ← last field before your addition
    u32 _erl_pad;           // Existing padding

    /* ─── Your New Section ─── */
    u8  your_feature_enabled;
    u8  _yf_pad[3];                     /* Align to 4 bytes */
    u32 your_feature_threshold;
    u32 _yf_pad2;                       /* Align to 8 bytes (if needed) */
};

Step 2: Regenerate Go Bindings

bash
make generate

This re-runs bpf2go and produces updated bpf_bpfel.go with the new fields in the generated BPF config struct.

Step 3: Add to User-Facing Go Config

In userspace/internal/config/config.go:

go
type DynamicConfig struct {
    // ... existing fields ...
    YourFeatureEnabled   bool `yaml:"your_feature_enabled"`
    YourFeatureThreshold int  `yaml:"your_feature_threshold"`
}

In userspace/internal/config/defaults.go:

go
Dynamic: DynamicConfig{
    // ...
    YourFeatureEnabled:   true,
    YourFeatureThreshold: 100,
},

Step 4: Wire in Loader Config Write

In userspace/internal/bpf/loader.go, in the writeConfig() method:

go
cfgStruct := bpf.Config{
    // ... existing fields ...
    YourFeatureEnabled:   boolToU8(cfg.Dynamic.YourFeatureEnabled),
    YourFeatureThreshold: uint32(cfg.Dynamic.YourFeatureThreshold),
}

Step 5: Register Metadata

In userspace/internal/config/metadata.go:

go
// Add to runtimeFields (or readOnlyFields if it requires restart)
{
    Name: "dynamic.your_feature_enabled",
    Display: "Your Feature",
    Category: "Dynamic",
    Type: FieldBool,
    Description: "Enable your detection feature",
    RuntimeSafe: true,
    GetFunc: func(c *Config) interface{} { return c.Dynamic.YourFeatureEnabled },
    SetFunc: func(c *Config, v interface{}) error {
        b, ok := v.(bool)
        if !ok { return fmt.Errorf("expected bool") }
        c.Dynamic.YourFeatureEnabled = b
        return nil
    },
},

Step 6: Verify Alignment

bash
# Dump the BPF struct layout from BTF
bpftool btf dump file ebpf/openshield.bpf.o | grep -A 60 "'struct config'"

# Check the Go BPF config struct (generated by bpf2go)
grep -A 100 "type config struct" userspace/internal/bpf/bpf_bpfel.go

# Verify: every field offset must match between the two

Debugging Misalignment

If you suspect alignment issues:

bash
# Dump the live config from the BPF map
sudo bpftool map dump name config_map

# Expected: every field value matches what's in /etc/openshield/openshield.yaml
# If pps_threshold shows garbage (e.g., 0 or huge number), alignment is broken

Common causes:

  • Forgetting to regenerate after C-side changes (make generate not run)
  • Adding a field without adding corresponding padding
  • Different field ordering between C and generated Go (shouldn't happen with bpf2go, but possible if manually editing generated code)
  • Feature flag mismatch: If C compiles with -DOPENSHIELD_FEATURE but bpf2go runs without it, the structs have different layouts