feat(enforce): seccomp user-notify enforcement tier (E4)#103
Merged
Conversation
Implements the seccomp user-notify fallback for hosts where BPF-LSM never loads (stock distros that ship CONFIG_BPF_LSM=y but don't put "bpf" in the boot lsm= list). ardur-exec-shim installs a connect(2)- scoped SECCOMP_RET_USER_NOTIF filter and hands the listener fd to ardur-kernelcaptured over a dedicated Unix socket via SCM_RIGHTS; the daemon supervises it using the same policy tables and net_allow matching as the BPF-LSM tier, with TOCTOU-safe re-validation around each /proc/pid/mem read and fail-closed-on-ambiguity semantics. The daemon now decides at startup whether BPF-LSM actually loads and falls back to seccomp when it doesn't, advertising the active tier on health responses so a launcher knows whether ardur-exec-shim is needed. apply_policy keeps the seccomp policy store in sync regardless of which tier is active, and reports genuine success (not degraded) when the active tier fully covers the request. Real-kernel verification (go/cmd/ardur-seccomp-smoke, wired into CI) caught two bugs no pure-Go test could: the shim's own connect() to the daemon's handoff socket deadlocking against its own just-installed filter, and a positive-vs-negative errno sign bug in the kernel response struct that silently let denied connects through. Claim boundary: seccomp user-notify is weaker than an in-kernel LSM against a racing multithreaded adversary — documented in seccomp_policy.go rather than left implicit.
GitHub-hosted runners (and Docker Desktop's kernel) boot with "bpf" in the active LSM list, so the daemon's tier selection preferred BPF-LSM and the seccomp-smoke job — which exercises the seccomp user-notify path specifically — asserted tier==seccomp and failed on every real CI run. Add a --disable-bpf-lsm daemon flag that skips the BPF-LSM guard tier (exec/exit observation still runs) and forces the seccomp fallback, and pass it from ardur-seccomp-smoke so the seccomp path is genuinely exercised regardless of the runner kernel's BPF-LSM availability. Verified on a real BPF-LSM kernel: denied connect() -> EPERM, allowed connect() -> reaches kernel (ECONNREFUSED).
gnanirahulnutakki
added a commit
that referenced
this pull request
Jul 3, 2026
gnanirahulnutakki
added a commit
that referenced
this pull request
Jul 3, 2026
…dence verifier (#113) * docs(demo): ardur run --enforce BPF-LSM end-to-end demo + offline verifier Adds a reproducible, privileged-Linux demo that drives the whole Epic A enforcement stack together (detect + apply_policy + BPF-LSM EPERM + attest) and proves the cycle end to end: - docs/demo/enforce-e2e.md — exact build/run commands, expected output, an enforce-vs-permissive A/B, the STRICT/file_open-vs-bprm mechanism, and offline verification. Validated live on Docker Desktop's LinuxKit kernel (lsm=capability,bpf,landlock): a governed agent's execve is refused with EPERM under --enforce and logged-but-allowed under permissive. - docs/demo/enforce-e2e/{Dockerfile,agent.py,run.sh} — build daemon + verifier from source, run the flow, verify the evidence. - go/cmd/enforce-verify — offline evidence verifier over enforce_events.jsonl using the shipped kernelcapture.VerifyEnforceReceiptChain (no kernel, daemon, or root). Confirms hash-chain integrity and that a session attestation's kernel_enforcement.chain_digest commits to the log head. With tests. * docs(site): regenerate source-backed Hugo mirror for enforce-e2e demo Runs site/scripts/sync_source_docs.py to mirror the new docs/demo/ pages into site/content/source/. Also fills a pre-existing gap: the kernel-enforce.yml workflow (added in #101) was never mirrored to site/static/repo/ because no docs/site-touching PR had re-run the sync check since. * docs(site): regenerate source mirror for consolidated dev (#102/#103/#112)
This was referenced Jul 3, 2026
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
Implements E4: the seccomp user-notify enforcement tier, the common-case fallback for hosts where BPF-LSM never loads — stock distros ship
CONFIG_BPF_LSM=ybut don't putbpfin the bootlsm=list, so the BPF-LSM tier (#101, merged) silently isn't active. Part of Epic A (#63). Builds on the merged E1 (#101, privileged CI), E2 (#96, apply_policy wiring), and E3 (#100, enforce_events correlation hardening).ardur-exec-shim(new binary): installs aconnect(2)-scopedSECCOMP_RET_USER_NOTIFfilter, hands the notification listener fd to the daemon over a dedicated Unix socket viaSCM_RIGHTS, thenexecve()s into the target — the filter (installed before exec, inherited across it) covers the whole process tree from there with no per-child re-registration.daemon_seccomp_linux.go): services notifications using the same policy tables andnet_allowCIDR matching as the BPF-LSM tier (SeccompPolicyStoremirrorsbpf_policy_apply.go's schema), re-validates the notification id viaSECCOMP_IOCTL_NOTIF_ID_VALIDboth before and after each/proc/pid/memread (the standard seccomp-notify TOCTOU mitigation), and fails closed (EPERM) on any ambiguous or error case —SECCOMP_USER_NOTIF_FLAG_CONTINUEis only ever set on a confirmed, policy-resolved ALLOW.bpf_lsm/seccomp/none) on health responses, so a launcher knows whether routing throughardur-exec-shimis necessary.apply_policykeeps the seccomp policy store in sync regardless of which tier is active, and reports genuine success (applied_seccomp_tier, not degraded) when the active tier fully covers the request.processEnforceEventnow takes an explicittierparameter (previously derived internally, alwaysbpf_lsm:*) so seccomp-tier events are correctly labeledseccomp:enforce/seccomp:permissiveinTierCoverage, sharing the same hash-chained receipt format.seccomp_policy.go's header comment, not a theoretically-airtight guarantee against an adversarial multithreaded target.Two real bugs found only by kernel-in-loop verification
Neither pure-Go tests nor
go vetcould catch these — both were found by actually running the shim + daemon against a real kernel:connect()to the daemon's handoff socket was trapped by the filter it had just installed (seccomp intercepts by syscall number only, with no socket-family awareness) — since nothing was servicing that listener yet, the connect blocked forever. Fixed by dialing the handoff connection before installing the filter (seeardur-exec-shim/main.go'srun()comment).SendSeccompNotifRespwrote a positiveerrno(e.g.unix.EPERM=+1) into the kernel'sseccomp_notif_resp.errorfield, which requires the raw syscall-return convention (-errno). A non-negative raw return is treated as success by the kernel — so a policy-deniedconnect()was silently let through with a nonsense return value instead of being blocked. Fixed by negating internally inbuildSeccompNotifResp, with a regression test (TestBuildSeccompNotifResp_NegatesErrno) and a permanent kernel-in-loop smoke test (see below) guarding it going forward.Testing
seccomp_sockaddr_test.go,seccomp_policy_test.go— sockaddr decoding, policy store semantics (ALLOW/DENY/ALLOWLIST, CIDR + bare-IP matching, enforce vs permissive, fail-closed on unknown action, nil-store handling), tier-refactor coverage indaemon_enforce_test.go/daemon_apply_policy_test.go.seccomp_notify_linux_test.go— ioctl number pinning against known runc/containerd values,sock_fprogkernel-ABI layout, and a small classic-BPF interpreter that runs the actual assembled filter program against syntheticseccomp_datato prove its three-way decision logic (arch mismatch → kill, matching arch + wrong nr → allow, matching arch + connect's nr → user-notify).go build/go vet/go test -race ./...pass on darwin and Linux (verified in a Docker container matching an unprivileged CI runner — no--privileged, no extra capabilities,CONFIG_BPF_LSMpresent butbpfnot in the active LSM list, exactly the scenario this tier targets).seccomp-smokeinkernel-enforce.yml): runsardur-seccomp-smoke, a permanent kernel-in-loop harness that starts a real daemon (BPF-LSM unavailable → seccomp fallback engages), runs the real shim against a real target process, and asserts a policy-deniedconnect(2)getsEPERMwhile a policy-allowed one reaches the kernel's real connect handling (observed asECONNREFUSED, proving the syscall wasn't blocked). Unlikekernel-smoke(BPF-LSM tier), this needs no KVM/virtme-ng custom kernel boot — confirmed empirically thatseccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)works under an ordinaryPR_SET_NO_NEW_PRIVS-only process, so it runs on a plainubuntu-24.04runner. Set as a required check (not soft-gated likekernel-smoke) since it needs no unproven VM-boot machinery.Honest gaps / what's not in this PR
seccomp_policy.go) — a seccomp filter can trapexecve, but by the time aSECCOMP_RET_USER_NOTIFnotification is serviced the new program image hasn't loaded yet, andSECCOMP_USER_NOTIF_FLAG_CONTINUEfor execve carries TOCTOU hazards beyond what connect's fixed-size sockaddr argument has. BPF-LSM remains the only tier that governs exec and file ops.ardur run/ launcher-side wiring to actually invokeardur-exec-shimbased on the daemon's advertised tier — that's launcher-side follow-on work, not part of this daemon/shim-side PR.Ref: #63