Skip to content

fix(enforce): wire ardur run to actually invoke the seccomp shim (#104)#129

Draft
gnanirahulnutakki wants to merge 1 commit into
devfrom
fix/epicA-104-seccomp-runpath
Draft

fix(enforce): wire ardur run to actually invoke the seccomp shim (#104)#129
gnanirahulnutakki wants to merge 1 commit into
devfrom
fix/epicA-104-seccomp-runpath

Conversation

@gnanirahulnutakki

Copy link
Copy Markdown
Member

Summary

Closes #104 — Epic A #63. Fixes the false-success bug the holistic integration review found: on a seccomp-only host (no bpf in the boot lsm= list — the majority case, per E4/#103's own rationale), ardur run --enforce could report kernel policy as applied while nothing actually wrapped the agent.

The false-success path (before this fix)

  • run_bridge.py never inspected the daemon's advertised enforcement_tier, and never invoked ardur-exec-shim (feat(enforce): seccomp user-notify enforcement tier (E4) #103's seccomp on-ramp) under any circumstance.
  • On a seccomp-tier host, apply_policy could genuinely return status="applied_seccomp_tier" (the daemon's in-memory seccomp policy store synced correctly) for a mission whose policy happened to be fully seccomp-coverable.
  • _apply_kernel_policy treated any non-exception apply_policy response as applied=True — with no process ever wrapped in a seccomp filter, no syscall was ever actually trapped or enforced. --enforce would report success for a run that enforced nothing.

The fix

  • Tier detection before spawn. run_governed now calls the daemon's health method before launching the agent. When the active tier is seccomp, it resolves ardur-exec-shim (kernel_correlation.exec_shim_path()PATH, then the systemd-packaged install location) and wraps the launch command with it, so the filter actually installs on the agent before it execs.
  • Verified, not assumed. A new SeccompListenerAttached field on session_status (Go: daemon.seccompListenerAttached, populated from the existing seccompListeners map) lets the launcher confirm a listener is genuinely supervising the session — not just that the tier is theoretically active or that apply_policy synced its store. _apply_kernel_policy now polls this after a seccomp-tier apply succeeds; if the shim was never resolved or its listener never attaches, the result downgrades to applied=False, and under --enforce the run aborts loudly (KernelPolicyEnforcementError) instead of reporting success.

Two more bugs, found only by running the real shim through the real CLI (not by any unit test)

  1. Startup race. The shim's one-shot daemon handoff (it can't be retried once the seccomp filter is installed — retrying would risk the self-deadlock feat(enforce): seccomp user-notify enforcement tier (E4) #103 already found and fixed) could lose to the launcher's own register_session call, which runs concurrently. Fixed with a file-based readiness handshake: the launcher creates a marker file right after register_session succeeds; the shim waits for it before attempting the handoff. Two alternatives were tried and rejected first — a daemon round-trip (session_status enforces exact-PID peer ownership, so the shim, a different process than the launcher, can never pass it) and a signal-based handshake (a signal arriving before the handler is installed would kill the shim outright, since most signals default to terminate — a race with a fatal failure mode, not just a slow one).
  2. Every mission's default file scope makes the seccomp tier uncoverable. run_governed always attaches a cwd-based file resource_scope (OP_FILE_READ/OP_FILE_WRITE) regardless of mission content, but the seccomp tier's "fully covered" gate (seccompFullyCoversPolicy, from feat(enforce): seccomp user-notify enforcement tier (E4) #103) requires every op to be OP_NET_CONNECT — so no real mission could ever reach applied_seccomp_tier through the CLI, only through hand-constructed test policies. Added an opt-in --no-resource-scope flag (and run_governed(no_resource_scope=...)) for missions that are genuinely network-only.

Real end-to-end verification (not just unit tests)

Directly exercised the real Go daemon + real ardur-exec-shim + real ardur run CLI in a privileged Linux container (--disable-bpf-lsm to force the seccomp tier regardless of the host kernel's own LSM list):

  • ardur run --forbidden-tools fetch --no-resource-scope --enforce on a seccomp-only host: the agent's connect() gets a genuine kernel EPERM, the hash-chained enforce_events.jsonl receipt reflects the denial, and the attestation's kernel_enforcement.chain_digest verifies offline (enforce-verify, no kernel/daemon/root needed).
  • Permissive control: the same connect reaches the kernel's real handling (ECONNREFUSED, not blocked), logged but not denied.
  • Abort paths: shim binary unresolvable, and shim resolved but its listener never attaches (simulated) — both correctly raise KernelPolicyEnforcementError under --enforce against the real daemon.
  • The existing BPF-LSM run.sh demo still passes unmodified — no regression on that tier.

CI

New job ardur-run-e2e-seccomp (required, no VM needed — confirmed empirically that the seccomp install syscall needs no special kernel boot): builds the docs/demo/enforce-e2e Docker image and runs the new run-seccomp.sh in both enforce and permissive mode, asserting the EPERM/receipt/attestation chain described above. This is the exact test class that would have caught #104 — piecewise ardur-seccomp-smoke drives the shim directly and would never have caught run_bridge.py failing to invoke it at all.

The existing kernel-smoke job (virtme-ng, BPF-LSM tier) now also runs the full ardur run --enforce CLI via run.sh, not just the piecewise ardur-guard-smoke tool — this specific step is unverified against live GitHub-hosted virtme-ng as of this PR (I have no way to test virtme-ng locally); it inherits kernel-smoke's existing continue-on-error: true soft-gate for exactly this reason, the same position kernel-smoke itself started from before its own multi-round-trip burn-in.

Testing

  • Go: new SeccompListenerAttached tests (daemon-level, session_status/health scoping); go build/go vet/go test -race ./... green on darwin + Linux.
  • Python: new tests for exec_shim_path/seccomp_handoff_socket_path resolution, _plan_seccomp_shim tier detection, _wrap_command_with_seccomp_shim argv shape, _verify_seccomp_listener_attached (true/timeout/unreachable), and full run_governed integration tests for the success path, both abort paths, and the permissive degrade — all against a real daemon protocol (mocked socket, not the CLI). Full suite (1206 tests, including a real biscuit-python install) green.

Ref: #104, #63

Closes the false-success bug: on a seccomp-only host (the majority —
no "bpf" in the boot lsm= list), apply_policy could report
"applied_seccomp_tier" while run_bridge never invoked ardur-exec-shim
or checked the daemon's advertised tier, so nothing actually wrapped
the agent. ardur run now detects the active tier before spawning the
agent and, when it's seccomp, launches through ardur-exec-shim; under
--enforce, if the tier can't actually be wired (shim missing, or its
listener never attaches — confirmed via a new session_status field),
the run aborts loudly instead of reporting success.

Two more bugs surfaced only by running the real shim through the real
CLI, not by any unit test: a startup race where the shim's one-shot
daemon handoff could lose to register_session (fixed with a file-based
readiness handshake — a daemon round-trip doesn't work here since
session_status enforces exact-PID peer ownership, and a signal-based
handshake risks killing the shim if it arrives before the handler is
installed); and every mission's default cwd-based file resource_scope
making a mission un-enforceable on the seccomp tier regardless of its
network scope, since that tier only ever covers OP_NET_CONNECT (fixed
with an opt-in --no-resource-scope flag for network-only missions).

Adds a real ardur run --enforce full-flow e2e for both tiers: a new
CI job builds the enforce-e2e demo image and runs it against a
seccomp-only host (--disable-bpf-lsm), and the existing BPF-LSM
virtme-ng smoke test now also runs the full CLI flow, not just the
piecewise guard-smoke tool. Both assert the denied syscall actually
fails and the hash-chained receipt + attestation verify offline —
the exact test class that would have caught this.
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.

[A] E4 — seccomp user-notify enforcement tier for bpf_lower's tier2_ops

1 participant