fix(enforce): stop advertising bpf_lsm once detached; pin guard for restart survival#128
Draft
gnanirahulnutakki wants to merge 1 commit into
Draft
fix(enforce): stop advertising bpf_lsm once detached; pin guard for restart survival#128gnanirahulnutakki wants to merge 1 commit into
gnanirahulnutakki wants to merge 1 commit into
Conversation
…estart survival Epic A holistic review (#125), issues #121 and #124. #121 — fail-open on guard-consumer death (HIGH). If runGuardConsumer returned mid-run (a real error, or a clean ringbuf close caused by something other than the daemon's own shutdown watcher -- e.g. an external force-detach), its defer cleared d.policyMaps but nothing ever moved activeTier off "bpf_lsm", so health kept reporting enforcement that no longer existed. Fixed: the goroutine awaiting runGuardConsumer now calls degradeGuardTier on any non-shutdown exit, which atomically moves activeTier to "none" and records the transition as a hash-chained tamper-audit entry (the same auditable evidence trail RunTamperAudit's periodic ticks already write to) -- never just a log line that could be missed. activeTier reads/writes are now consistently mu-guarded (getActiveTier/setActiveTier), since this adds a second concurrent writer to a field previously only ever set once at startup. #124 — guard unpinned, so a daemon restart detached the three LSM hooks and rebuilt every policy map empty, leaving a governed agent completely unenforced from process exit until (if ever) apply_policy was called again. Fixed: LoadAndAttachProcessGuardEBPFPinned mirrors #99's tracepoint pinning -- pins the three LSM links and the six policy-state maps (+ the enforce_events ringbuf) under /sys/fs/bpf/ardur/, all-or- nothing. A restart that finds every pin re-attaches to kernel state that was never actually detached: the LSM hooks kept enforcing the whole time, so there is nothing to "re-apply". If pinning is unavailable, it falls back to a fresh load (same accepted degradation #99 already has). Tests: daemon_guard_degrade_test.go proves health reflects the true tier after a simulated consumer death (no real kernel needed -- pure state-transition logic), plus a race-detector test since activeTier now has concurrent writers. bpf_policy_apply_pinned_linux_test.go covers the pin-path logic and the no-pins-yet fallback on real Linux (runs in bpf-generate CI, no privileged VM needed). ardur-guard-smoke gained a third scenario (restart_survival_scenario.go, kernel-smoke CI): applies an OP_EXEC:DENY policy through a pinned load, simulates a restart (Close, then load again from the same pins), and asserts execve is still denied with no re-apply -- a regression here fails loudly (execve would unexpectedly succeed), not ambiguously. All verified with go build/vet/test -race on both darwin and a real Linux container (golang:1.26); the new pinned-path unit tests actually executed and passed there, not just cross-compiled.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Epic A holistic review (#125), issues #121 and #124.
#121 (HIGH) — fail-open on guard-consumer death. If
runGuardConsumerreturned mid-run — a real error, or a clean ringbuf close caused by something other than the daemon's own shutdown watcher (e.g. an external force-detach) — itsdeferclearedd.policyMaps, but nothing ever movedactiveTieroff"bpf_lsm". Health kept reporting enforcement that no longer existed.Fixed:
runGuardConsumernow callsdegradeGuardTieron any non-shutdown exit (whethererris nil or not — both mean the guard is no longer attached).degradeGuardTieratomically movesactiveTierto"none"and records the transition as a hash-chained tamper-audit entry — the same auditable evidence trailRunTamperAudit's periodic ticks already write to (_tamper/tamper_audit.jsonl), not just a log line that can be missed.activeTierreads/writes are now consistentlymu-guarded (getActiveTier/setActiveTier) — this change adds a second concurrent writer to a field that was previously only ever set once at startup, so the existing unsynchronized access needed fixing to stay race-free under-race(whichkernel-enforce.ymlalready runs).#124 (MED) — guard unpinned. A daemon restart detached the three LSM hooks and rebuilt every policy map empty (
cgroup_managed,cgroup_op_policy, etc.), so a governed agent ran completely unenforced from process exit until (if ever)apply_policywas called again.Fixed:
LoadAndAttachProcessGuardEBPFPinnedmirrors #99's tracepoint pinning — pins the three LSM links and the six policy-state maps (+ theenforce_eventsringbuf) under/sys/fs/bpf/ardur/, all-or-nothing (never a partial reuse). A restart that finds every pin re-attaches to kernel state that was never actually detached: the LSM hooks kept enforcing the whole time, so there is nothing to "re-apply" — the pinned maps' contents are exactly what was last applied. If pinning is unavailable (bpffs not mounted, etc.), it falls back to a fresh load, the same accepted degradation #99 already has for the tracepoint.Tests
daemon_guard_degrade_test.go— proves health reflects the true tier after a simulated consumer death (pure state-transition logic, no real kernel needed): tier moves to none, the transition is hash-chained and appended to evidence, the no-op case (guard never actually reached bpf_lsm) doesn't spuriously fire, and a concurrent-access test exercisesgetActiveTier/setActiveTier/degradeGuardTierunder-race.bpf_policy_apply_pinned_linux_test.go— pin-path logic (all 10 paths distinct, under the ardur bpffs namespace) and the "no pins exist yet" fallback (tryLoadPinnedGuardStatefailing cleanly at theENOENTlevel, before ever touching BPF-LSM) — runs on real Linux in thebpf-generateCI job, no privileged VM needed.ardur-guard-smokegained a third scenario,restart_survival_scenario.go(runs inkernel-smoke's virtme-ng VM): appliesOP_EXEC:DENYthrough a pinned load, confirms EPERM, simulates a daemon restart (Close, then load again from the same pins), and asserts execve is still denied with no re-apply call in between. This can't pass by accident — a regression here (silent fallback to a fresh, unpinned attach) shows up as execve unexpectedly succeeding post-restart, not an ambiguous error.Verification
go build/vet/test -race ./...green on darwingo build/vet/test -race ./...green on a real Linux container (golang:1.26) — the new pinned-path tests actually executed there (confirmed via-v), not just cross-compiledardur-guard-smokebinary builds and runs on real Linux, failing exactly where expected outside a privileged BPF-LSM kernel (confirms the control flow reaches the real loader correctly)Refs: #121, #124, Epic A #63.