Testing and Safety
PAM, package lifecycle, service activation, enrollment, and authentication can change the machine or require real hardware. Do not exercise those paths on a workstation merely to validate documentation or a patch.
Safe local checks
These checks do not need root, a camera, installed models, or host PAM edits:
cargo test --workspace
cargo clippy --workspace -- -D warnings
just check
just check is broader than the first two commands: it includes formatting,
the RustSec audit, documentation/contracts checks, source-install lifecycle
tests, and package/release contract checks. See
Developer Commands for the generated inventory.
Ignored hardware tests need models and a camera and are not part of that safe baseline:
just link-models
cargo test --workspace -- --ignored
Container and guest tiers
The Arch PAM smoke container tests module loading and failure behavior without editing host PAM:
just test-arch-pam
The camera container recipes pass real devices through and perform live enrollment/authentication. Run them only when that hardware interaction is intended:
just test-arch-integration
just test-arch-oneshot
They default to a 90-second live-step timeout. A longer timeout uses
timeout(1) syntax:
FACELOCK_LIVE_TIMEOUT=5m just test-arch-integration
Synthetic camera
The same two scripts run against a synthetic camera, with no real device passed through and nobody in frame:
just test-arch-loopback
The camera is a v4l2loopback node fed by ffmpeg with a procedurally
rendered face sequence (test/loopback/NOTICE.md: drawn from arithmetic,
nobody's face). While it is fed, the node enumerates GREY only, so
facelock classifies it as IR by format evidence — the residual
Security describes — and the run keeps require_ir and
require_frame_variance at their product defaults, because the sequence
drifts frame to frame the way a person does. A second node fed YUYV is the
non-IR camera the require_ir refusal assertions need; without it they
report SKIP.
The tier needs two idle loopback nodes the calling user can write. Loading the module needs root; the recipe does not do it and exits 2 with this when the nodes are missing:
sudo modprobe v4l2loopback devices=2 video_nr=20,21 \
card_label=facelock-synth-mono,facelock-synth-color exclusive_caps=1,1
sudo udevadm settle && sudo chmod a+rw /dev/video20 /dev/video21
The settle matters: udev applies its own 0660 root:video mode when it
processes the add event, which can land after a chmod issued right behind
the modprobe.
If v4l2loopback is already loaded for something else, add nodes without
unloading it (v4l2loopback-utils, module 0.13 or later):
sudo v4l2loopback-ctl add -x 1 -n facelock-synth-mono /dev/video20
sudo v4l2loopback-ctl add -x 1 -n facelock-synth-color /dev/video21
While the tier feeds them, the nodes are visible to the host too. An idle
exclusive_caps=1 node enumerates no formats and auto-detection ignores it,
but a fed facelock-synth-mono node enumerates GREY only and classifies
as IR by format evidence, exactly as a real IR camera without a quirk entry
does; whichever of the two enumerates first wins a host auto-detect, so a
host face-auth attempt during the run (a sudo prompt, a lock screen) can
be judged against the synthetic face and fall through to the password. A
camera the quirks database knows still ranks above it. The labels carry no
ir token on purpose: auto-detection prefers a format-classified node whose
name says ir, and a synthetic node should never beat a real sensor on its
name (has_ir_name_token in crates/facelock-camera/src/device.rs). The
tier itself never reads the label. Pin device.path on the host or wait out
the run, which takes about two minutes.
FACELOCK_LOOPBACK_IR and FACELOCK_LOOPBACK_RGB pick other nodes
(FACELOCK_LOOPBACK_RGB=none runs without the twin). The script refuses a
node that has a parent device in sysfs — a real camera — or that another
process is already feeding, so it cannot open the host's webcam by mistake.
Only the loopback nodes are passed into the container.
It records the commit it passed at to .loopback-tier-verified, which
satisfies just release-preflight on its own, as the real-camera record
does. It is cheaper evidence, not the same evidence: it proves capture, IR
classification, the liveness gates, enrollment, the daemon and one-shot
paths and PAM end to end on a device the product treats as an IR sensor, and
it cannot prove that a real sensor's frames match a real face.
Container coverage is not proof that a booted package, display manager, or real login stack is safe. Use the evidence walkthrough in an explicitly marked disposable guest for those cases; its runner refuses ordinary hosts and does not provision a VM for you. See Testing Walkthrough.
Development configuration
dev/config.toml uses checkout models, oneshot mode, and temporary database,
key, snapshot, and audit paths. It is not rootless: the management CLI keeps
its normal privilege gate. Root also ignores FACELOCK_CONFIG, so pass the
configuration explicitly:
just build
just link-models
sudo target/debug/facelock --config "$PWD/dev/config.toml" devices
sudo target/debug/facelock --config "$PWD/dev/config.toml" enroll --skip-setup-check
sudo target/debug/facelock --config "$PWD/dev/config.toml" test
Do not run setup for this flow. Setup owns installed-system state, including
the fixed /etc/facelock/.setup-complete marker, and may offer systemd and PAM
changes. The explicit non-default configuration routes supported management
commands through direct access; it does not make a manually started daemon use
that backend.
facelock test returning zero is not proof of a match or even a scan. It also
returns zero when no usable enrollment exists and after a completed non-match.
Read its output.
Host PAM testing
Only test host PAM after the container and disposable-guest tiers are satisfactory.
- Open a separate root shell and keep it open.
- Optionally create and label your own emergency copy before Facelock touches
the service:
cp /etc/pam.d/sudo /root/sudo.pam.before-facelockfrom that root shell. - Add only the
sudoservice withfacelock pam add --service sudofrom the root shell. - Test a correct password and a wrong password in a new terminal, then test face authentication.
- If anything is wrong, run
facelock pam remove --service sudofrom the retained root shell.
Facelock-managed rollback files are versioned under
/var/lib/facelock/pam-backups/ with adjacent JSON provenance. They are not
the old /etc/pam.d/sudo.facelock-backup path. Never select the newest-looking
backup and copy it blindly: review its provenance and target state, or let the
CLI perform the validated removal. An adjacent
/etc/pam.d/sudo.facelock-backup exists only if an operator or an older
release created it; current Facelock does not create that emergency copy.
Do not begin with login, sshd, a display manager, or shared stacks such as
system-auth and common-auth. The CLI requires --allow-sensitive for these
targets because one error can affect many authentication paths.
If the retained root shell is unavailable, boot a recovery environment,
remount the root filesystem read-write, and remove the exact
pam_facelock.so rule or restore a separately reviewed operator copy. See
Troubleshooting.
Logging
Use global -v flags for privileged commands because they survive sudo's
environment filtering:
sudo facelock -v test
sudo facelock -vv daemon run
For target-specific filters, pass the environment through a trusted env
invocation:
sudo env RUST_LOG=facelock_camera=trace facelock devices