Skip to content

fix(enforce): stop advertising bpf_lsm once detached; pin guard for restart survival#128

Draft
gnanirahulnutakki wants to merge 1 commit into
devfrom
fix/epicA-121-124-failopen
Draft

fix(enforce): stop advertising bpf_lsm once detached; pin guard for restart survival#128
gnanirahulnutakki wants to merge 1 commit into
devfrom
fix/epicA-121-124-failopen

Conversation

@gnanirahulnutakki

Copy link
Copy Markdown
Member

Summary

Epic A holistic review (#125), issues #121 and #124.

#121 (HIGH) — fail-open on guard-consumer death. 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". Health kept reporting enforcement that no longer existed.

Fixed:

  • The goroutine awaiting runGuardConsumer now calls degradeGuardTier on any non-shutdown exit (whether err is nil or not — both mean the guard is no longer attached).
  • degradeGuardTier 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 (_tamper/tamper_audit.jsonl), not just a log line that can be missed.
  • activeTier reads/writes are now consistently mu-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 (which kernel-enforce.yml already runs).
  • Deliberately does not attempt to retroactively stand up the seccomp fallback tier — that tier's supervisor/socket lifecycle is wired only at startup, and retrofitting a second start site would need its own careful lifecycle handling for a case this fix already reports correctly. "Loudly signal degradation," per the issue's own acceptance framing.

#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_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 (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 exercises getActiveTier/setActiveTier/degradeGuardTier under -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 (tryLoadPinnedGuardState failing cleanly at the ENOENT level, before ever touching BPF-LSM) — runs on real Linux in the bpf-generate CI job, no privileged VM needed.
  • ardur-guard-smoke gained a third scenario, restart_survival_scenario.go (runs in kernel-smoke's virtme-ng VM): applies OP_EXEC:DENY through 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 darwin
  • go 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-compiled
  • ardur-guard-smoke binary 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)
  • Existing test suites (daemon, kernelcapture, guard-smoke build) all still green — no regressions

Refs: #121, #124, Epic A #63.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant