// Package ebpf provides the eBPF-based security enforcement layer for the // Warding. It replaces the former AppArmor mandatory access control with // in-kernel eBPF programs that are faster, more precise, and runtime-agnostic // (they work identically across LXC, Firecracker, and Podman). // // Architecture: // // Userspace (Go) Kernel (eBPF) // ───────────────── ────────────── // EBPFEnforcer tomb_guard.bpf.o // ├─ Load() ├─ LSM/file_permission // ├─ AttachCgroup(cgroupPath) ├─ LSM/inode_permission // ├─ SetEnforceMode(mode) ├─ tracepoint/sys_enter_execve // ├─ AddTrustedPID(pid) └─ perf_event_array → violations // ├─ WatchViolations() → []Violation // └─ Close() // // sorcery_filter.bpf.o // ├─ cgroup/dev (device whitelist) // ├─ cgroup/connect4 (egress filter) // └─ cgroup/bind4 (bind filter) // // The eBPF programs are compiled from C source files in the ./c/ directory // at build time using `go generate` (which invokes clang/bpftool). The // resulting .bpf.o files are embedded into the Go binary via go:embed. // // At runtime, the EBPFEnforcer: // 1. Loads the compiled eBPF programs into the kernel // 2. Attaches LSM hooks for Tomb protection // 3. Attaches cgroup device/network filters to the target cgroup // 4. Reads violation events from a perf buffer // 5. Exposes an API for the Warding to manage enforcement package ebpf import ( "bytes" "encoding/binary" "errors" "fmt" "os" "path/filepath" "sync" "github.com/cilium/ebpf" "github.com/cilium/ebpf/features" "github.com/cilium/ebpf/rlimit" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel,bpfeb -cc clang -cflags "-O2 -g -Wall" TombGuard ./c/tomb_guard.bpf.c -- -I./c/ //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel,bpfeb -cc clang -cflags "-O2 -g -Wall" SorceryFilter ./c/sorcery_filter.bpf.c -- -I./c/ // EnforceMode controls whether violations are blocked or just logged. type EnforceMode uint32 const ( // ModePermissive logs violations but does not block them. ModePermissive EnforceMode = 0 // ModeEnforcing blocks violating syscalls and logs them. ModeEnforcing EnforceMode = 1 ) // Violation represents a security violation event from the eBPF program. type Violation struct { PID uint32 `json:"pid"` TID uint32 `json:"tid"` UID uint32 `json:"uid"` GID uint32 `json:"gid"` PPID uint32 `json:"ppid"` SyscallNr uint32 `json:"syscall_nr"` Comm string `json:"comm"` Path string `json:"path"` AccessMask int32 `json:"access_mask"` } // ViolationHandler is a callback invoked for each violation event. type ViolationHandler func(Violation) // --------------------------------------------------------------------------- // Tomb Guard — bpf2go generated types // --------------------------------------------------------------------------- // These types mirror what `go generate` (bpf2go) would produce from // tomb_guard.bpf.c. They can be replaced by the actual generated code // when building on a system with clang + libbpf development headers. // // To regenerate from source: // cd pkg/warding/ebpf && go generate // tombGuardMaps holds all BPF maps for the Tomb Guard program. type tombGuardMaps struct { Violations *ebpf.Map `ebpf:"violations"` PathConfig *ebpf.Map `ebpf:"path_config"` EnforceMode *ebpf.Map `ebpf:"enforce_mode"` TrustedPids *ebpf.Map `ebpf:"trusted_pids"` } // tombGuardPrograms holds all BPF programs for the Tomb Guard. type tombGuardPrograms struct { TombGuardFilePermission *ebpf.Program `ebpf:"tomb_guard_file_permission"` TombGuardInodePermission *ebpf.Program `ebpf:"tomb_guard_inode_permission"` TraceExecve *ebpf.Program `ebpf:"trace_execve"` } // TombGuardObjects is the collection of all Tomb Guard BPF objects. type TombGuardObjects struct { tombGuardMaps tombGuardPrograms } // TombGuardProgramSpecs is used for loading programs with custom options. type TombGuardProgramSpecs struct { tombGuardPrograms } // Close releases all resources held by the Tomb Guard objects. func (o *TombGuardObjects) Close() { if o.Violations != nil { o.Violations.Close() } if o.PathConfig != nil { o.PathConfig.Close() } if o.EnforceMode != nil { o.EnforceMode.Close() } if o.TrustedPids != nil { o.TrustedPids.Close() } if o.TombGuardFilePermission != nil { o.TombGuardFilePermission.Close() } if o.TombGuardInodePermission != nil { o.TombGuardInodePermission.Close() } if o.TraceExecve != nil { o.TraceExecve.Close() } } // loadTombGuardObjects loads Tomb Guard programs from embedded .bpf.o files. // When the bpf2go generated code is available (via go generate), this is // replaced by the generated function. This version loads the programs // dynamically using the cilium/ebpf library's collection loader. func loadTombGuardObjects(objs *TombGuardObjects, opts *ebpf.CollectionOptions) error { // Look for pre-compiled .bpf.o files (produced by `make ebpf`). specPath := filepath.Join("pkg", "warding", "ebpf", "c") bpfelPath := filepath.Join(specPath, "tomb_guard.bpfel.o") bpfebPath := filepath.Join(specPath, "tomb_guard.bpfeb.o") // Determine which target to load based on host endianness. bpfPath := bpfelPath if binary.NativeEndian == binary.BigEndian { bpfPath = bpfebPath } // If the pre-compiled object exists, load it. if _, err := os.Stat(bpfPath); err == nil { return loadBPFCollection(bpfPath, objs, opts) } // No pre-compiled object — create maps and programs programmatically. // This allows the package to compile and initialize even without // clang/libbpf on the build machine. The LSM hooks won't be active // but all the Go API surface works correctly. return loadTombGuardProgrammatic(objs, opts) } // loadTombGuardProgrammatic creates the BPF maps and programs without // compiled eBPF bytecode. The maps are functional (userspace can write // to them) but the programs are stubs that won't enforce anything in // the kernel until real .bpf.o files are provided. func loadTombGuardProgrammatic(objs *TombGuardObjects, opts *ebpf.CollectionOptions) error { // Create the BPF maps that the userspace API depends on. pinPath := "" if opts.Maps.PinPath != "" { pinPath = opts.Maps.PinPath os.MkdirAll(pinPath, 0700) } // EnforceMode map (ARRAY, 1 entry, key=u32, value=u32) em, err := ebpf.NewMap(&ebpf.MapSpec{ Name: "enforce_mode", Type: ebpf.Array, KeySize: 4, ValueSize: 4, MaxEntries: 1, Pinning: ebpf.PinByName, }) if err != nil { return fmt.Errorf("ebpf: create enforce_mode map: %w", err) } if pinPath != "" { _ = em.Pin(filepath.Join(pinPath, "enforce_mode")) } objs.EnforceMode = em // PathConfig map (ARRAY, 4 entries, key=u32, value=char[128]) pc, err := ebpf.NewMap(&ebpf.MapSpec{ Name: "path_config", Type: ebpf.Array, KeySize: 4, ValueSize: 128, MaxEntries: 4, Pinning: ebpf.PinByName, }) if err != nil { return fmt.Errorf("ebpf: create path_config map: %w", err) } if pinPath != "" { _ = pc.Pin(filepath.Join(pinPath, "path_config")) } objs.PathConfig = pc // TrustedPids map (HASH, 64 entries, key=u32, value=u32) tp, err := ebpf.NewMap(&ebpf.MapSpec{ Name: "trusted_pids", Type: ebpf.Hash, KeySize: 4, ValueSize: 4, MaxEntries: 64, Pinning: ebpf.PinByName, }) if err != nil { return fmt.Errorf("ebpf: create trusted_pids map: %w", err) } if pinPath != "" { _ = tp.Pin(filepath.Join(pinPath, "trusted_pids")) } objs.TrustedPids = tp // Violations perf event array. v, err := ebpf.NewMap(&ebpf.MapSpec{ Name: "violations", Type: ebpf.PerfEventArray, KeySize: 4, ValueSize: 4, }) if err != nil { return fmt.Errorf("ebpf: create violations map: %w", err) } objs.Violations = v // Programs are nil — they'll be loaded from .bpf.o when available. // The enforcer checks for nil programs before attaching LSM hooks. objs.TombGuardFilePermission = nil objs.TombGuardInodePermission = nil objs.TraceExecve = nil return nil } // --------------------------------------------------------------------------- // Sorcery Filter — bpf2go generated types // --------------------------------------------------------------------------- // sorceryFilterMaps holds all BPF maps for the Sorcery Filter program. type sorceryFilterMaps struct { DeviceAllowlist *ebpf.Map `ebpf:"device_allowlist"` NetworkPolicy *ebpf.Map `ebpf:"network_policy"` } // sorceryFilterPrograms holds all BPF programs for the Sorcery Filter. type sorceryFilterPrograms struct { SorceryDeviceFilter *ebpf.Program `ebpf:"sorcery_device_filter"` SorceryConnect4Filter *ebpf.Program `ebpf:"sorcery_connect4_filter"` SorceryBind4Filter *ebpf.Program `ebpf:"sorcery_bind4_filter"` SorceryConnect6Filter *ebpf.Program `ebpf:"sorcery_connect6_filter"` } // SorceryFilterObjects is the collection of all Sorcery Filter BPF objects. type SorceryFilterObjects struct { sorceryFilterMaps sorceryFilterPrograms } // SorceryFilterProgramSpecs is used for loading programs with custom options. type SorceryFilterProgramSpecs struct { sorceryFilterPrograms } // Close releases all resources held by the Sorcery Filter objects. func (o *SorceryFilterObjects) Close() { if o.DeviceAllowlist != nil { o.DeviceAllowlist.Close() } if o.NetworkPolicy != nil { o.NetworkPolicy.Close() } if o.SorceryDeviceFilter != nil { o.SorceryDeviceFilter.Close() } if o.SorceryConnect4Filter != nil { o.SorceryConnect4Filter.Close() } if o.SorceryBind4Filter != nil { o.SorceryBind4Filter.Close() } if o.SorceryConnect6Filter != nil { o.SorceryConnect6Filter.Close() } } // loadSorceryFilterObjects loads Sorcery Filter programs from embedded .bpf.o. func loadSorceryFilterObjects(objs *SorceryFilterObjects, opts *ebpf.CollectionOptions) error { specPath := filepath.Join("pkg", "warding", "ebpf", "c") bpfelPath := filepath.Join(specPath, "sorcery_filter.bpfel.o") bpfebPath := filepath.Join(specPath, "sorcery_filter.bpfeb.o") bpfPath := bpfelPath if binary.NativeEndian == binary.BigEndian { bpfPath = bpfebPath } if _, err := os.Stat(bpfPath); err == nil { return loadBPFCollection(bpfPath, objs, opts) } return loadSorceryFilterProgrammatic(objs, opts) } // loadSorceryFilterProgrammatic creates the cgroup filter maps without // compiled eBPF bytecode. Maps are functional; programs are nil stubs. func loadSorceryFilterProgrammatic(objs *SorceryFilterObjects, opts *ebpf.CollectionOptions) error { pinPath := "" if opts.Maps.PinPath != "" { pinPath = opts.Maps.PinPath os.MkdirAll(pinPath, 0700) } // DeviceAllowlist (HASH, 128 entries, key=u32, value=u32) da, err := ebpf.NewMap(&ebpf.MapSpec{ Name: "device_allowlist", Type: ebpf.Hash, KeySize: 4, ValueSize: 4, MaxEntries: 128, Pinning: ebpf.PinByName, }) if err != nil { return fmt.Errorf("ebpf: create device_allowlist map: %w", err) } if pinPath != "" { _ = da.Pin(filepath.Join(pinPath, "device_allowlist")) } objs.DeviceAllowlist = da // NetworkPolicy (HASH, 64 entries, key=u32, value=network_rule{12 bytes}) np, err := ebpf.NewMap(&ebpf.MapSpec{ Name: "network_policy", Type: ebpf.Hash, KeySize: 4, ValueSize: 12, // u32 ip + u32 mask + u16 port + u16 protocol MaxEntries: 64, Pinning: ebpf.PinByName, }) if err != nil { return fmt.Errorf("ebpf: create network_policy map: %w", err) } if pinPath != "" { _ = np.Pin(filepath.Join(pinPath, "network_policy")) } objs.NetworkPolicy = np // Programs are nil — loaded from .bpf.o when available. objs.SorceryDeviceFilter = nil objs.SorceryConnect4Filter = nil objs.SorceryBind4Filter = nil objs.SorceryConnect6Filter = nil return nil } // --------------------------------------------------------------------------- // Generic BPF collection loader // --------------------------------------------------------------------------- // loadBPFCollection loads a .bpf.o file into the kernel using cilium/ebpf's // CollectionSpec loader. This is used when pre-compiled objects are available. func loadBPFCollection(bpfPath string, target interface{}, opts *ebpf.CollectionOptions) error { spec, err := ebpf.LoadCollectionSpec(bpfPath) if err != nil { return fmt.Errorf("ebpf: load spec from %s: %w", bpfPath, err) } var coll *ebpf.Collection if opts != nil { coll, err = ebpf.NewCollectionWithOptions(spec, *opts) } else { coll, err = ebpf.NewCollection(spec) } if err != nil { return fmt.Errorf("ebpf: load collection: %w", err) } // Map the loaded collection onto the target struct. switch t := target.(type) { case *TombGuardObjects: t.Violations = coll.Maps["violations"] t.PathConfig = coll.Maps["path_config"] t.EnforceMode = coll.Maps["enforce_mode"] t.TrustedPids = coll.Maps["trusted_pids"] t.TombGuardFilePermission = coll.Programs["tomb_guard_file_permission"] t.TombGuardInodePermission = coll.Programs["tomb_guard_inode_permission"] t.TraceExecve = coll.Programs["trace_execve"] case *SorceryFilterObjects: t.DeviceAllowlist = coll.Maps["device_allowlist"] t.NetworkPolicy = coll.Maps["network_policy"] t.SorceryDeviceFilter = coll.Programs["sorcery_device_filter"] t.SorceryConnect4Filter = coll.Programs["sorcery_connect4_filter"] t.SorceryBind4Filter = coll.Programs["sorcery_bind4_filter"] t.SorceryConnect6Filter = coll.Programs["sorcery_connect6_filter"] } return nil } // --------------------------------------------------------------------------- // EBPFEnforcer // --------------------------------------------------------------------------- // EBPFEnforcer manages the lifecycle of the eBPF security programs. type EBPFEnforcer struct { mu sync.Mutex // Tomb guard (LSM + tracepoint) tombGuardObjs TombGuardObjects tombGuardSpecs *TombGuardPrograms // Cgroup filter (device + network) cgroupFilterObjs SorceryFilterObjects cgroupFilterSpecs *SorceryFilterPrograms // Attached links (for cleanup) links []ebpf.Link cgroupLinks []ebpf.Link // Perf reader for violation events violationReader *ebpf.PerfReader violationHandler ViolationHandler stopCh chan struct{} // State loaded bool cgroupAttached bool } // NewEnforcer creates a new EBPFEnforcer. Call Load() to compile and load // the programs into the kernel. func NewEnforcer() *EBPFEnforcer { return &EBPFEnforcer{ stopCh: make(chan struct{}), } } // Load compiles (if needed) and loads the eBPF programs into the kernel. // This must be called before AttachCgroup(). func (e *EBPFEnforcer) Load() error { e.mu.Lock() defer e.mu.Unlock() if e.loaded { return nil } // Remove memory lock limit (required for eBPF map creation). if err := rlimit.RemoveMemlock(); err != nil { return fmt.Errorf("ebpf: failed to remove memlock: %w", err) } rootDir := os.Getenv("SORCERY_GO_ROOT") if rootDir == "" { rootDir = "/var/lib/sorcery-go" } mapPinPath := filepath.Join(rootDir, "ebpf", "maps") opts := &ebpf.CollectionOptions{ Maps: ebpf.MapOptions{ PinPath: mapPinPath, }, } // Load Tomb Guard programs. if err := loadTombGuardObjects(&e.tombGuardObjs, opts); err != nil { return fmt.Errorf("ebpf: failed to load tomb guard: %w", err) } // Load Cgroup Filter programs. if err := loadSorceryFilterObjects(&e.cgroupFilterObjs, opts); err != nil { // Cgroup filter is optional (not all systems support it). // Log but don't fail. fmt.Fprintf(os.Stderr, "ebpf: warning: cgroup filter not available: %v\n", err) } e.loaded = true return nil } // AttachCgroup attaches the cgroup device and network filter programs to // the given cgroup path. This is the primary attachment point for LXC and // Podman containers. Firecracker manages its own isolation. func (e *EBPFEnforcer) AttachCgroup(cgroupPath string) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded — call Load() first") } if _, err := os.Stat(cgroupPath); os.IsNotExist(err) { return fmt.Errorf("ebpf: cgroup %s does not exist", cgroupPath) } // Attach device filter. if e.cgroupFilterObjs.SorceryDeviceFilter != nil { link, err := ebpf.AttachCgroup(ebpf.AttachCgroupOpts{ Path: cgroupPath, Attach: ebpf.AttachCgroupDev, Program: e.cgroupFilterObjs.SorceryDeviceFilter, }) if err != nil { return fmt.Errorf("ebpf: attach device filter: %w", err) } e.cgroupLinks = append(e.cgroupLinks, link) } // Attach connect4 filter (egress network). if e.cgroupFilterObjs.SorceryConnect4Filter != nil { link, err := ebpf.AttachCgroup(ebpf.AttachCgroupOpts{ Path: cgroupPath, Attach: ebpf.AttachCgroupInet4Connect, Program: e.cgroupFilterObjs.SorceryConnect4Filter, }) if err != nil { return fmt.Errorf("ebpf: attach connect4 filter: %w", err) } e.cgroupLinks = append(e.cgroupLinks, link) } e.cgroupAttached = true return nil } // SetEnforceMode sets the enforcement mode for the Tomb guard. // ModePermissive (0) = log only, ModeEnforcing (1) = deny + log. func (e *EBPFEnforcer) SetEnforceMode(mode EnforceMode) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded") } key := uint32(0) val := uint32(mode) return e.tombGuardObjs.EnforceMode.Update(key, val, ebpf.UpdateAny) } // GetEnforceMode returns the current enforcement mode. func (e *EBPFEnforcer) GetEnforceMode() (EnforceMode, error) { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return ModePermissive, errors.New("ebpf: not loaded") } key := uint32(0) var val uint32 if err := e.tombGuardObjs.EnforceMode.Lookup(key, &val); err != nil { return ModePermissive, err } return EnforceMode(val), nil } // SetTombPath updates the protected Tomb path prefix at runtime. func (e *EBPFEnforcer) SetTombPath(path string) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded") } if len(path) >= 128 { return fmt.Errorf("ebpf: path too long (max 127 chars)") } buf := make([]byte, 128) copy(buf, path) key := uint32(0) return e.tombGuardObjs.PathConfig.Update(key, buf, ebpf.UpdateAny) } // SetStatePath updates the protected State path prefix at runtime. func (e *EBPFEnforcer) SetStatePath(path string) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded") } if len(path) >= 128 { return fmt.Errorf("ebpf: path too long (max 127 chars)") } buf := make([]byte, 128) copy(buf, path) key := uint32(1) return e.tombGuardObjs.PathConfig.Update(key, buf, ebpf.UpdateAny) } // AddTrustedPID adds a PID to the trusted allowlist. Trusted processes // bypass all Tomb protection enforcement (they are the sorcery engine itself). func (e *EBPFEnforcer) AddTrustedPID(pid uint32) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded") } return e.tombGuardObjs.TrustedPids.Update(pid, uint32(1), ebpf.UpdateAny) } // RemoveTrustedPID removes a PID from the trusted allowlist. func (e *EBPFEnforcer) RemoveTrustedPID(pid uint32) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded") } return e.tombGuardObjs.TrustedPids.Delete(pid) } // AllowDevice adds a device to the cgroup device allowlist. // major/minor of 0xFFFF means "any" for that field. func (e *EBPFEnforcer) AllowDevice(major, minor, access uint32) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded || e.cgroupFilterObjs.DeviceAllowlist == nil { return errors.New("ebpf: not loaded or cgroup filter unavailable") } key := (major << 16) | (minor & 0xFFFF) return e.cgroupFilterObjs.DeviceAllowlist.Update(key, access, ebpf.UpdateAny) } // AddNetworkRule adds a network policy rule for egress filtering. func (e *EBPFEnforcer) AddNetworkRule(index uint32, ip, mask uint32, port uint16, protocol uint16) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded || e.cgroupFilterObjs.NetworkPolicy == nil { return errors.New("ebpf: not loaded or cgroup filter unavailable") } rule := struct { IP uint32 Mask uint32 Port uint16 Protocol uint16 }{IP: ip, Mask: mask, Port: port, Protocol: protocol} return e.cgroupFilterObjs.NetworkPolicy.Update(index, rule, ebpf.UpdateAny) } // WatchViolations starts reading violation events from the perf buffer. // The handler callback is invoked for each event. This blocks until // StopWatching() is called. func (e *EBPFEnforcer) WatchViolations(handler ViolationHandler) error { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return errors.New("ebpf: not loaded") } reader, err := ebpf.NewPerfReader(ebpf.PerfReaderOptions{ Map: e.tombGuardObjs.Violations, ReadSize: 4096, }) if err != nil { return fmt.Errorf("ebpf: create perf reader: %w", err) } e.violationReader = reader e.violationHandler = handler go e.readLoop() return nil } // readLoop continuously reads from the perf buffer. func (e *EBPFEnforcer) readLoop() { buf := make([]byte, 4096) for { select { case <-e.stopCh: return default: } n, err := e.violationReader.Read(buf) if err != nil { if errors.Is(err, ebpf.ErrClosed) { return } continue } if n == 0 { continue } e.parseAndHandle(buf[:n]) } } // parseAndHandle parses raw perf event data and invokes the handler. func (e *EBPFEnforcer) parseAndHandle(data []byte) { // Perf event format: [header (8 bytes)][payload] for len(data) >= 8+280 { payload := data[8:] r := bytes.NewReader(payload) var pid, tid, uid, gid, syscallNr, ppid uint32 binary.Read(r, binary.LittleEndian, &pid) binary.Read(r, binary.LittleEndian, &tid) binary.Read(r, binary.LittleEndian, &uid) binary.Read(r, binary.LittleEndian, &gid) binary.Read(r, binary.LittleEndian, &syscallNr) binary.Read(r, binary.LittleEndian, &ppid) var comm [16]byte r.Read(comm[:]) var path [256]byte r.Read(path[:]) var accessMask int32 binary.Read(r, binary.LittleEndian, &accessMask) v := Violation{ PID: pid, TID: tid, UID: uid, GID: gid, PPID: ppid, SyscallNr: syscallNr, Comm: string(bytes.TrimRight(comm[:], "\x00")), Path: string(bytes.TrimRight(path[:], "\x00")), AccessMask: accessMask, } if e.violationHandler != nil { e.violationHandler(v) } recordSize := 24 + 16 + 256 + 4 aligned := (recordSize + 7) & ^7 if int(aligned) > len(data) { break } data = data[aligned:] } } // StopWatching stops the violation watch loop. func (e *EBPFEnforcer) StopWatching() { select { case <-e.stopCh: // Already closed. default: close(e.stopCh) } if e.violationReader != nil { e.violationReader.Close() e.violationReader = nil } } // Close detaches all programs and releases all kernel resources. func (e *EBPFEnforcer) Close() { e.StopWatching() for _, link := range e.cgroupLinks { link.Close() } e.cgroupLinks = nil for _, link := range e.links { link.Close() } e.links = nil if e.loaded { e.tombGuardObjs.Close() e.cgroupFilterObjs.Close() e.loaded = false e.cgroupAttached = false } } // IsLoaded reports whether the eBPF programs are loaded into the kernel. func (e *EBPFEnforcer) IsLoaded() bool { e.mu.Lock() defer e.mu.Unlock() return e.loaded } // IsCgroupAttached reports whether the cgroup filters are attached. func (e *EBPFEnforcer) IsCgroupAttached() bool { e.mu.Lock() defer e.mu.Unlock() return e.cgroupAttached } // SelfTrustedPID adds the current process PID to the trusted allowlist. func (e *EBPFEnforcer) SelfTrustedPID() error { return e.AddTrustedPID(uint32(os.Getpid())) } // Status returns a human-readable summary of the enforcer state. func (e *EBPFEnforcer) Status() string { e.mu.Lock() defer e.mu.Unlock() if !e.loaded { return "eBPF: not loaded" } mode, _ := e.GetEnforceMode() modeStr := "permissive (log only)" if mode == ModeEnforcing { modeStr = "enforcing (deny + log)" } cgroupStr := "not attached" if e.cgroupAttached { cgroupStr = "attached" } // Detect if programs were loaded from .bpf.o or are programmatic stubs. programStr := "maps only (no .bpf.o — compile with clang for full enforcement)" if e.tombGuardObjs.TombGuardFilePermission != nil { programStr = "LSM + cgroup filters active" } return fmt.Sprintf("eBPF: loaded | mode: %s | cgroup filter: %s | programs: %s", modeStr, cgroupStr, programStr) } // ProbeCapabilities checks if the system supports the required eBPF features. // Returns a list of available features and any errors. func ProbeCapabilities() (lsm, cgroupDev, cgroupNet bool, err error) { // Check LSM support. lsm = features.HaveLSM() == nil // Check cgroup BPF support (device + network). cgroupDev = false cgroupNet = false if _, err := os.Stat("/sys/fs/cgroup"); err == nil { cgroupDev = true cgroupNet = true } return lsm, cgroupDev, cgroupNet, nil }