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 configbpf2go-Generated Types vs Hand-Written Structs
When bpf2go processes the BPF C source, it:
- Compiles
openshield.bpf.cto BPF object code (ELF) - Extracts BTF type information from the ELF file
- Generates Go struct definitions in
bpf_bpfel.gothat match the BTF types
For example, the C-side:
struct config {
u32 pps_threshold;
u32 bps_threshold;
u8 enable_static_mitigation;
u8 _pad[3];
};Becomes the Go-side (generated by bpf2go):
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:
// 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
- Natural alignment:
u32= 4-byte aligned,u64= 8-byte aligned - Explicit padding: After a
u8field, add_pad[3]to reach the next 4-byte boundary - Struct final padding: Add
_reservedor_padfields to align the total struct size to 8 bytes - Arrays of u8: MAC address arrays
u8 mac[8][6]need_mac_pad[2]to reach 8-byte alignment - 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:
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
make generateThis 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:
type DynamicConfig struct {
// ... existing fields ...
YourFeatureEnabled bool `yaml:"your_feature_enabled"`
YourFeatureThreshold int `yaml:"your_feature_threshold"`
}In userspace/internal/config/defaults.go:
Dynamic: DynamicConfig{
// ...
YourFeatureEnabled: true,
YourFeatureThreshold: 100,
},Step 4: Wire in Loader Config Write
In userspace/internal/bpf/loader.go, in the writeConfig() method:
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:
// 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
# 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 twoDebugging Misalignment
If you suspect alignment issues:
# 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 brokenCommon causes:
- Forgetting to regenerate after C-side changes (
make generatenot 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_FEATUREbutbpf2goruns without it, the structs have different layouts
Related Pages
- Developer Guide — Full architecture and build system
- Adding a Detection Module — End-to-end module creation
- BPF Development Patterns — Common BPF code patterns
