security: harden the D-Bus and daemon surface (Plan 06)#87
Open
tyvsmith wants to merge 6 commits into
Open
Conversation
…, capture DoS guard Plan 06 (D-Bus & daemon hardening): - AuthAttempted signal no longer carries the similarity score (spoof-tuning oracle); payload is now (user: s, matched: b). Bus policy denies signal reception in the default context — only root and the facelock group may receive auth-attempt broadcasts. - PreviewDetectFrame returns empty jpeg_data to non-root callers: raw camera/IR frames are root-only across both preview variants; non-root callers keep detection/recognition metadata for enroll feedback. Wayland preview renders a metadata-only placeholder for stripped frames. - New in-flight CaptureSlot guard checked before the handler mutex: concurrent Authenticate/Enroll/PreviewFrame/PreviewDetectFrame calls fail immediately with a "daemon busy" error instead of queueing up to the 10s handler-lock timeout (local DoS). PAM treats busy as a daemon error and degrades to password — never a lockout. Per-user rate limiting unchanged. - Explicit deny own="org.facelock.Daemon" in the default policy context (self-contained name-squatting protection). - Container E2E: new assertions in test/run-integration-tests.sh for the signal policy/payload, frame stripping, and busy rejection (camera). - Contract updates: docs/contracts.md, book/src/contracts.md (D-Bus interface), docs/security.md section 4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… face-independent - New assertion: an unprivileged user's RequestName on org.facelock.Daemon is denied by the bus policy (explicit default-context deny own). - The post-busy sequential auth check now asserts the security property directly: the auth runs a full capture (match or no-match) and is never rejected with a busy error. Whether the face matches depends on the human in front of the camera and is already covered by the earlier tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uard Race two simultaneous Authenticate calls instead of sleeping into the first capture window (which a fast face match made flaky): exactly one call wins the capture slot and the other must be rejected with a busy error in milliseconds, never queued toward the 10s handler-lock timeout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ectFrame Plan 06's frame-authz parity stripped PreviewDetectFrame's jpeg_data for every non-root caller, and sudo can't reach the user's Wayland compositor — leaving no working GUI preview path at all (hardware-verified). Restore the preview UX without reopening the silent-frame-exfiltration hole, per the approved Option B: - New polkit action org.facelock.preview-frames (allow_active= auth_self_keep, allow_any/inactive=no) shipped in dbus/org.facelock.policy and installed to /usr/share/polkit-1/actions/ by every packaging path (justfile, deb, rpm, PKGBUILDs, nix, CI build-deb). - Daemon checks CheckAuthorization (AllowUserInteraction=true, subject = caller's unique bus name) in a background task — never blocking the reply or the capture slot — and FAILS CLOSED (metadata-only frames) on denial, pending prompt, timeout, or any polkit/D-Bus error. Root keeps frames unconditionally; PreviewFrame stays root-only. - Verdicts cached per caller connection: granted 120s, denied 15s, evicted on NameOwnerChanged so a grant can never outlive the connection. - ipc_client now reuses one bus connection per process so a preview session keeps a stable unique name for its grant. - Preview window message now says to authorize in the system prompt instead of "run as root". - Integration tests: polkitd runs in the Arch container; fail-closed stripping is re-asserted against real polkit, and a rules.d override proves the authorized path serves frames; pkg tests assert the .policy file is installed. Contracts and security docs updated (PreviewDetectFrame authz semantics, Plan 06 frame-parity section). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Hardens Facelock’s D-Bus/daemon surface to reduce information leakage, enforce consistent authorization for preview frames, and mitigate capture-path DoS by rejecting concurrent capture requests immediately. It also introduces a polkit action to preserve the non-root preview UX while keeping raw frame access fail-closed by default.
Changes:
- Remove similarity score from the
AuthAttemptedD-Bus signal payload and restrict signal reception via bus policy. - Add an in-flight capture guard to immediately reject concurrent capture operations with a “daemon busy” error.
- Gate
PreviewDetectFrameraw frame bytes behind interactive polkit authorization (org.facelock.preview-frames) with per-connection caching and disconnect eviction; update packaging/tests/docs accordingly.
Reviewed changes
Copilot reviewed 15 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
test/run-integration-tests.sh |
Starts polkitd in-container and adds end-to-end D-Bus hardening assertions (signals, own-deny, frame stripping, busy guard). |
test/pkg-validate.sh |
Validates the polkit action policy file exists and is valid XML in package installs. |
test/Containerfile |
Adds polkit dependency to the Arch test container image. |
justfile |
Installs/uninstalls the new polkit action policy during local install-files flows. |
docs/security.md |
Documents the new D-Bus signal hygiene, frame authorization model, and capture contention guard. |
docs/contracts.md |
Updates the formal D-Bus contract for signal payload shape, PreviewDetectFrame authz semantics, and busy behavior. |
dist/PKGBUILD-git |
Packages the polkit action policy into Arch -git package output. |
dist/PKGBUILD-bin |
Packages the polkit action policy into Arch -bin package output. |
dist/PKGBUILD |
Packages the polkit action policy into the standard Arch package output. |
dist/nix/module.nix |
Enables polkit and links /share/polkit-1 so NixOS discovers the action policy. |
dist/nix/default.nix |
Installs the polkit action policy into the Nix derivation output. |
dist/facelock.spec |
Installs and lists the polkit action policy for RPM packaging. |
dist/debian/rules |
Installs the polkit action policy for Debian packaging. |
dbus/org.facelock.policy |
New polkit action definition for org.facelock.preview-frames. |
dbus/org.facelock.Daemon.conf |
Tightens D-Bus policy: deny default signal reception and explicit deny own for defense-in-depth. |
crates/facelock-cli/src/ipc_client.rs |
Reuses a process-wide D-Bus proxy/connection to preserve stable unique bus name across preview frames. |
crates/facelock-cli/src/commands/preview/wayland_preview.rs |
Renders metadata-only output when the daemon strips jpeg_data pending authorization. |
crates/facelock-cli/src/commands/daemon.rs |
Implements capture slot guard, removes similarity from signal, and adds polkit-backed per-connection frame authorization cache. |
book/src/contracts.md |
Updates the book’s contract summary for signal/busy behavior (needs alignment for polkit-authorized frames). |
.github/workflows/scripts/validate-rpm.sh |
CI RPM validation now checks for the polkit action policy file. |
.github/workflows/scripts/validate-deb.sh |
CI DEB validation now checks for the polkit action policy file. |
.github/workflows/scripts/build-deb.sh |
DEB build script now installs the polkit action policy into the package tree. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### Methods | ||
| `Authenticate`, `Enroll`, `ListModels`, `RemoveModel`, `ClearModels`, `PreviewFrame`, `PreviewDetectFrame`, `ListDevices`, `ReleaseCamera`, `Ping`, `Shutdown` | ||
|
|
||
| Raw camera frames are root-only: `PreviewDetectFrame` returns an **empty** `jpeg_data` payload to non-root callers — they receive detection and recognition metadata only. |
book/src/contracts.md still described raw preview frames as strictly root-only, contradicting docs/contracts.md's description of the polkit-authorized non-root path (org.facelock.preview-frames, fail-closed) added in this branch. Mirror that wording here.
…PreviewDetectFrame (A3) The detect-frame response returns per-face similarity metadata to non-root callers for enroll-quality feedback. This is a bounded, accepted residual: PreviewDetectFrame is authorized only for root or the caller's own Unix user, so the score is self-scoped and does not provide a cross-user spoof-tuning oracle (the auth-path oracle is separately closed in A2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
Closes the info-leak, DoS, and inconsistent-authz issues on the D-Bus surface (Plan 06).
AuthAttemptedsignal no longer carries the similarity score (a spoof-tuning oracle); payload is now(user: s, matched: b). The bus policy denies signal reception in the default context — only root and thefacelockgroup may receive auth-attempt broadcasts.PreviewDetectFramereturns emptyjpeg_datato non-root callers; raw camera/IR frames are root-only across both preview variants, while non-root callers keep detection/recognition metadata for enroll feedback.CaptureSlotguard is checked before the handler mutex — concurrentAuthenticate/Enroll/PreviewFrame/PreviewDetectFramecalls fail immediately with a "daemon busy" error instead of queueing to the 10s handler-lock timeout. PAM treats busy as a daemon error and degrades to password — never a lockout. Per-user rate limiting unchanged.own(perf: speed up container tests ~20x by eliminating redundant build #20): explicitdeny own="org.facelock.Daemon"in the default policy context (self-contained name-squatting protection).Preview UX regression fixed in-branch
The frame-authz-parity change initially stripped
PreviewDetectFrame'sjpeg_datafor every non-root caller — butsudocan't reach the user's Wayland compositor, leaving no working GUI preview path at all (hardware-verified). Restored the preview UX without reopening the silent-frame-exfiltration hole (approved Option B):org.facelock.preview-frames(allow_active = auth_self_keep,allow_any/inactive = no), shipped indbus/org.facelock.policyand installed by every packaging path (justfile, deb, rpm, PKGBUILDs, nix, CI).CheckAuthorization(subject = caller's unique bus name) in a background task — never blocking the reply or the capture slot — and fails closed (metadata-only frames) on denial, pending prompt, timeout, or any polkit/D-Bus error. Root keeps frames unconditionally;PreviewFramestays root-only.NameOwnerChangedso a grant can never outlive the connection.ipc_clientreuses one bus connection per process so a preview session keeps a stable unique name for its grant.Verification
cargo build/cargo test/cargo clippy -D warningsgreen.just test-arch-integration, real system bus + daemon): an unprivilegeddbus-monitor/ match rule receives noauth_attemptedsignal and the payload carries no score; afacelock-group non-rootPreviewDetectFramereturns no rawjpeg_data; two concurrentAuthenticatecalls → the second is rejected with a busy error in milliseconds (deterministic race, never a 10s stall); an unprivilegedRequestNameonorg.facelock.Daemonis denied by policy. polkitd runs in the Arch container so the fail-closed stripping is re-asserted against real polkit and arules.doverride proves the authorized path serves frames; pkg tests assert the.policyfile is installed.Contract docs
docs/contracts.md+book/src/contracts.md(D-Bus interface:AuthAttemptedpayload shape,PreviewDetectFrameauthz semantics) anddocs/security.md§4 updated.Closes #76
Closes #77
Closes #78
Closes #79
🤖 Generated with Claude Code