Skip to content

security: harden the D-Bus and daemon surface (Plan 06)#87

Open
tyvsmith wants to merge 5 commits into
mainfrom
sec/06-dbus-daemon-hardening
Open

security: harden the D-Bus and daemon surface (Plan 06)#87
tyvsmith wants to merge 5 commits into
mainfrom
sec/06-dbus-daemon-hardening

Conversation

@tyvsmith

@tyvsmith tyvsmith commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the info-leak, DoS, and inconsistent-authz issues on the D-Bus surface (Plan 06).

Preview UX regression fixed in-branch

The frame-authz-parity change initially stripped PreviewDetectFrame's jpeg_data for every non-root caller — but sudo can'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):

  • New interactive polkit action org.facelock.preview-frames (allow_active = auth_self_keep, allow_any/inactive = no), shipped in dbus/org.facelock.policy and installed by every packaging path (justfile, deb, rpm, PKGBUILDs, nix, CI).
  • The daemon checks 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; PreviewFrame stays root-only.
  • Verdicts are cached per caller connection (granted 120s, denied 15s), evicted on NameOwnerChanged so a grant can never outlive the connection. ipc_client reuses 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 warnings green.
  • Container (just test-arch-integration, real system bus + daemon): an unprivileged dbus-monitor / match rule receives no auth_attempted signal and the payload carries no score; a facelock-group non-root PreviewDetectFrame returns no raw jpeg_data; two concurrent Authenticate calls → the second is rejected with a busy error in milliseconds (deterministic race, never a 10s stall); an unprivileged RequestName on org.facelock.Daemon is denied by policy. polkitd runs in the Arch container so the 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.

Contract docs

docs/contracts.md + book/src/contracts.md (D-Bus interface: AuthAttempted payload shape, PreviewDetectFrame authz semantics) and docs/security.md §4 updated.

Closes #76
Closes #77
Closes #78
Closes #79

🤖 Generated with Claude Code

tyvsmith and others added 4 commits July 3, 2026 15:38
…, 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>
Copilot AI review requested due to automatic review settings July 4, 2026 20:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AuthAttempted D-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 PreviewDetectFrame raw 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.

Comment thread book/src/contracts.md Outdated
### 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment