10 Commits

Author SHA1 Message Date
nevaforget 85cf039506 refactor: power-confirm via PowerAction table (v0.6.14)
Align the power-confirm flow to moonset's ActionDef pattern, in lockstep
with moongreet: a PowerAction table + create_power_button factory replace
the two hand-wired reboot/shutdown handlers and the loose-param
show_power_confirm. Add an in-flight re-trigger guard
(power_box.set_sensitive(false)) and clear a stale error_label when
showing a new prompt.
2026-06-02 14:31:50 +02:00
nevaforget 73c59e54c1 fix: drop pam_acct_mgmt from password and FP paths (v0.6.13)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
The PAM stack only ever had `auth include login` — no account module.
auth.rs nevertheless called pam_acct_mgmt after pam_authenticate, which
fell back to /etc/pam.d/other (pam_deny) and rejected every password.
On the FP side, the same call was wrapped in a spawn_blocking + 2s
resume_async retry path that triggered a use-after-free in
gtk_window_destroy (20+ SIGSEGVs in 6 days).

- auth.rs: remove pam_acct_mgmt extern + call; return pam_authenticate
  result directly. Lockout still works via pam_faillock in the auth stack.
- auth.rs: drop check_account() and its tests (FP path no longer needs it).
- lockscreen.rs::start_fingerprint: on success go straight to
  label.set_text + fp.stop() + cb(); no PAM acct check, no resume retry.
- fingerprint.rs: remove resume_async() — no caller left.
- config/moonlock-pam: keep single `auth include login` line, matching
  swaylock/gtklock pattern.
- CLAUDE.md, DECISIONS.md updated.
2026-05-04 09:28:11 +02:00
nevaforget 3e610bdb4b fix: audit LOW fixes — docs, rustdoc, scope, debug gate, lto fat (v0.6.12)
Update PKGBUILD version / update-pkgver (push) Successful in 3s
- Update CLAUDE.md and README.md to reflect the blur range [0,200] that
  the code has clamped to since v0.6.8.
- Move the // SYNC: comment above the /// doc on MAX_BLUR_DIMENSION so
  rustdoc renders one coherent paragraph instead of a truncated sentence.
- Narrow check_account visibility to pub(crate) and document the caller
  precondition (username must come from users::get_current_user()).
- Gate MOONLOCK_DEBUG behind #[cfg(debug_assertions)]. Release builds
  always run at LevelFilter::Info so a session script cannot escalate
  journal verbosity to leak fprintd / D-Bus internals.
- Document why pam_setcred is deliberately not called in authenticate().
- Release profile: lto = "fat" instead of "thin" — doubles release build
  time for better cross-crate inlining on the auth + i18n hot paths.
2026-04-24 14:05:17 +02:00
nevaforget 9dfd1829e9 fix: audit MEDIUM fixes — D-Bus race, TOCTOU, FP reset, entry clear (v0.6.11)
- fingerprint: split cleanup_dbus into a sync take_cleanup_proxy() + async
  perform_dbus_cleanup(). resume_async now awaits VerifyStop+Release before
  re-claiming, so fprintd cannot reject the Claim on a slow bus. stop()
  still spawns the cleanup fire-and-forget.
- fingerprint: remove failed_attempts = 0 from resume_async. An attacker
  with sensor control could otherwise cycle verify-match → account-fail →
  resume and never trip the 10-attempt cap.
- lockscreen: open the wallpaper with O_NOFOLLOW and build the texture
  from bytes, closing the TOCTOU between the symlink check and Texture::
  from_file.
- lockscreen: clear password_entry immediately after extracting the
  Zeroizing<String>, shortening the window the GLib GString copy stays in
  libc-malloc'd memory.
2026-04-24 13:21:19 +02:00
nevaforget 39d9cbb624 fix: audit fixes — RefCell across await, async avatar decode (v0.6.10)
- init_fingerprint_async: hoist username before the await so a concurrent
  connect_monitor signal (hotplug / suspend-resume) cannot cause a RefCell
  panic. Re-borrow after the await for signal wiring.
- set_avatar_from_file: decode via gio::File::read_future +
  Pixbuf::from_stream_at_scale_future so the GTK main thread stays
  responsive during monitor hotplug. Default icon shown while loading.
2026-04-24 12:34:00 +02:00
nevaforget 3adc5e980d docs: drop Nyx persona, unify attribution on ClaudeCode
Remove the Nyx persona block from CLAUDE.md and rewrite prior
DECISIONS entries from Nyx and leftover Ragnar to ClaudeCode for
consistency with the rest of the ecosystem.
2026-04-21 09:03:23 +02:00
nevaforget 3f4448c641 style: replace hardcoded colors with GTK theme variables
Update PKGBUILD version / update-pkgver (push) Successful in 3s
Use @theme_bg_color, @theme_fg_color, @error_color and @success_color
instead of hardcoded hex values and 'white'. Makes moonlock respect the
active GTK theme instead of assuming Catppuccin Mocha colors.

Note: moongreet and moonset still use hardcoded colors and should be
updated to match.
2026-04-09 14:51:29 +02:00
nevaforget b621b4e9fe fix: handle monitor hotplug to survive suspend/resume (v0.6.9)
moonlock crashed with segfault in libgtk-4.so after suspend/resume when
HDMI monitors disconnected and reconnected, invalidating GDK monitor
objects that statically created windows still referenced.

Replace manual monitor iteration with connect_monitor signal (v1_2) that
fires both at lock time and on hotplug. Windows are now created on demand
per monitor event and auto-unmap when their monitor disappears.
2026-04-09 14:48:06 +02:00
nevaforget b89435b810 Remove unnecessary pacman git install from CI workflow
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Git is already available in the runner image.
2026-04-02 08:28:08 +02:00
nevaforget 06dadc5cbf Revert CI workaround: remove pacman install step
Update PKGBUILD version / update-pkgver (push) Failing after 0s
The act_runner now uses a custom Arch-based image with git
pre-installed, so per-workflow installs are no longer needed.
2026-04-01 16:17:45 +02:00
11 changed files with 373 additions and 295 deletions
+7 -8
View File
@@ -1,7 +1,5 @@
# Moonlock # Moonlock
**Name**: Nyx (Göttin der Nacht — passend zum Lockscreen, der den Bildschirm verdunkelt)
## Projekt ## Projekt
Moonlock ist ein sicherer Wayland-Lockscreen, gebaut mit Rust + gtk4-rs + ext-session-lock-v1. Moonlock ist ein sicherer Wayland-Lockscreen, gebaut mit Rust + gtk4-rs + ext-session-lock-v1.
@@ -37,14 +35,14 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
## Architektur ## Architektur
- `auth.rs` — PAM-Authentifizierung via Raw FFI (unsafe extern "C" conv callback, msg_style-aware, Zeroizing<CString>), check_account() für pam_acct_mgmt-Only-Checks nach Fingerprint-Unlock - `auth.rs` — PAM-Authentifizierung via Raw FFI (unsafe extern "C" conv callback, msg_style-aware, Zeroizing<CString>)
- `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, sender-validated signal handler, cleanup_dbus() für sauberen D-Bus-Lifecycle, running_flag für Race-Safety in async restarts, on_exhausted callback after MAX_FP_ATTEMPTS, resume_async() für Neustart nach transientem Fehler (mit failed_attempts-Reset und Signal-Handler-Cleanup) - `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, sender-validated signal handler, cleanup_dbus() für sauberen D-Bus-Lifecycle, running_flag für Race-Safety in async restarts, on_exhausted callback after MAX_FP_ATTEMPTS, resume_async() für Neustart nach transientem Fehler (mit failed_attempts-Reset und Signal-Handler-Cleanup)
- `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection - `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection
- `power.rs` — Reboot/Shutdown via /usr/bin/systemctl - `power.rs` — Reboot/Shutdown via /usr/bin/systemctl
- `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts - `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
- `config.rs` — TOML-Config (background_path, background_blur clamped [0,100], fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection via symlink_metadata + Parse-Error-Logging - `config.rs` — TOML-Config (background_path, background_blur clamped [0,200], fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection via symlink_metadata + Parse-Error-Logging
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking mit 30s Timeout und Generation Counter, FP-Label/Start separat verdrahtet mit pam_acct_mgmt-Check und auto-resume, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode (Downscale auf max 1920px), Blur/Avatar-Cache für Multi-Monitor - `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking mit 30s Timeout und Generation Counter, FP-Success ruft unlock_callback direkt (PAM-Stack ohne account-Modul, Lockout via auth-Pfad und MAX_FP_ATTEMPTS), Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode (Downscale auf max 1920px), Blur/Avatar-Cache für Multi-Monitor
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor mit shared Blur/Avatar-Caches, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present(), Wallpaper-Laden nach lock() - `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Monitor-Hotplug via `connect_monitor`-Signal (v1_2), shared Blur/Avatar-Caches in Rc, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present()
## Sicherheit ## Sicherheit
@@ -54,8 +52,9 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
- PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher, num_msg-Guard gegen negative Werte - PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher, num_msg-Guard gegen negative Werte
- fprintd: D-Bus Signal-Sender wird gegen fprintd's unique bus name validiert (Anti-Spoofing) - fprintd: D-Bus Signal-Sender wird gegen fprintd's unique bus name validiert (Anti-Spoofing)
- Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<CString> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und strdup-Kopie in PAM werden nicht gezeroized — inhärente GTK/libc-Limitierung) - Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<CString> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und strdup-Kopie in PAM werden nicht gezeroized — inhärente GTK/libc-Limitierung)
- Fingerprint-Unlock: pam_acct_mgmt-Check nach verify-match erzwingt Account-Policies (Lockout, Ablauf), resume_async() startet FP bei transientem Fehler neu (mit failed_attempts-Reset und Signal-Handler-Cleanup) - Fingerprint-Unlock: ruft unlock_callback direkt nach verify-match (kein zusätzlicher PAM-acct-Check, weil PAM-Stack nur `auth include login` enthält — wie swaylock); FP-Lockout ist über MAX_FP_ATTEMPTS in fingerprint.rs umgesetzt
- Wallpaper wird nach lock() geladen — Disk-I/O verzögert nicht die Lock-Akquisition - PAM-Stack: nur `auth include login` (Standard-Pattern wie swaylock/gtklock); kein `account`/`session`-Stack, weil Lockscreen keinen Session-Lifecycle managed und `pam_unix(account)` setuid root benötigt
- Wallpaper wird vor lock() geladen — connect_monitor feuert während lock() und braucht die Textur; lokales JPEG-Laden ist schnell genug
- PAM-Timeout: 30s Timeout verhindert permanentes Aussperren bei hängenden PAM-Modulen, Generation Counter verhindert Interferenz paralleler Auth-Versuche - PAM-Timeout: 30s Timeout verhindert permanentes Aussperren bei hängenden PAM-Modulen, Generation Counter verhindert Interferenz paralleler Auth-Versuche
- Root-Check: Exit mit Fehler wenn als root gestartet - Root-Check: Exit mit Fehler wenn als root gestartet
- Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv) - Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv)
Generated
+1 -1
View File
@@ -575,7 +575,7 @@ dependencies = [
[[package]] [[package]]
name = "moonlock" name = "moonlock"
version = "0.6.8" version = "0.6.14"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
+4 -3
View File
@@ -1,13 +1,13 @@
[package] [package]
name = "moonlock" name = "moonlock"
version = "0.6.8" version = "0.6.14"
edition = "2024" edition = "2024"
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support" description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
license = "MIT" license = "MIT"
[dependencies] [dependencies]
gtk4 = { version = "0.11", features = ["v4_10"] } gtk4 = { version = "0.11", features = ["v4_10"] }
gtk4-session-lock = { version = "0.4", features = ["v1_1"] } gtk4-session-lock = { version = "0.4", features = ["v1_2"] }
glib = "0.22" glib = "0.22"
gdk4 = "0.11" gdk4 = "0.11"
gdk-pixbuf = "0.22" gdk-pixbuf = "0.22"
@@ -30,4 +30,5 @@ glib-build-tools = "0.22"
[profile.release] [profile.release]
lto = "thin" lto = "thin"
codegen-units = 1 codegen-units = 1
strip = true strip = false
debug = true
+50 -8
View File
@@ -2,58 +2,100 @@
Architectural and design decisions for Moonlock, in reverse chronological order. Architectural and design decisions for Moonlock, in reverse chronological order.
## 2026-06-02 Align power-confirm to moonset's ActionDef pattern (v0.6.14)
- **Who**: ClaudeCode, Dom
- **Why**: A code review of moongreet's power-confirm (ported from this file) flagged the shared pattern as lower-altitude than moonset's: two near-identical reboot/shutdown handlers and a `show_power_confirm` taking loose `message`/`action_fn`/`error_message` params that can drift apart. moonset already solved this with an `ActionDef` table + button factory. Changed here in lockstep with moongreet to keep the three projects symmetric.
- **Tradeoffs**: A `PowerAction` struct + `power_actions()` table + `create_power_button` factory is slightly more machinery for two actions, but couples icon/prompt/error/action into one value (a mismatched prompt/action becomes unrepresentable) and makes a third action a one-line table entry. Did NOT touch `confirm_box: Rc<RefCell<Option<gtk::Box>>>` — moonset uses the same, it is the shared convention.
- **How**: Replaced the two hand-wired handlers with a loop over `power_actions()`; `show_power_confirm`/`execute_power_action` now take `PowerAction` (Copy) instead of three loose strings. Added an in-flight re-trigger guard via `power_box.set_sensitive(false)` (re-enabled on failure), matching moonset; also clear a stale `error_label` when showing a new prompt.
## 2026-05-04 Drop PAM account/session stack, remove `check_account`, drop `pam_acct_mgmt` from password path (v0.6.13)
- **Who**: ClaudeCode, Dom
- **Why**: 20 SIGSEGV coredumps in 6 days. All crashes preceded by `pam_unix(moonlock:account): setuid failed: Operation not permitted`. The previous PAM config (`auth/account/session include system-auth`) pulled `pam_unix(account)`, which needs setuid root for `/etc/shadow`. moonlock runs as the user, so `pam_acct_mgmt` always failed. The failure cascaded into the FP-resume path (`gio::spawn_blocking(check_account) → false → 2s timeout → resume_async`) where a use-after-free during `gtk_window_destroy` killed the process. Each crash left a dead `ext-session-lock-v1` client and the compositor stuck on its red fallback backdrop until manual recovery. Initial fix on 2026-04-30 dropped the FP-side `check_account` and the account/session lines from the PAM config, but left the `pam_acct_mgmt` call in `auth.rs::authenticate()` for the password path. Result: a PAM stack with no `account` module fell back to `/etc/pam.d/other` (`pam_deny`) and rejected every password — Dom got locked out on 2026-05-04.
- **Tradeoffs**: Aligned with the swaylock/gtklock pattern (only `auth include login`). Lost: PAM-driven account expiry/lockout in both paths. Acceptable because (a) FP attempts are still bounded by `MAX_FP_ATTEMPTS` in `fingerprint.rs`, (b) password-path lockout still works through `pam_faillock.so preauth/authfail/authsucc` in the inherited `auth` stack, and (c) account validity was already verified by the login manager when the session was opened — a lockscreen unlocks an existing session, it does not gate access to a new one. Not chosen: a custom `account` stack with `pam_faillock.so` only — would have kept PAM-level FP lockout but adds a non-standard config that other lockers do not use, and `pam_faillock` standalone in `account` is rarely tested in the wild.
- **How**: (1) `config/moonlock-pam` reduced to a single `auth include login` line. (2) `auth.rs::check_account()` and its two unit tests removed. (3) `auth.rs::authenticate()` no longer calls `pam_acct_mgmt`; `pam_authenticate` result is returned directly. The `pam_acct_mgmt` extern declaration is removed. (4) `lockscreen.rs::start_fingerprint` simplified — the `gio::spawn_blocking(check_account)` async block, the failure-side error UI, and the 2-second `resume_async` retry path all removed; on FP success the closure now goes `label.set_text(success); fp.stop(); cb()`. (5) `fingerprint.rs::resume_async()` removed. (6) `CLAUDE.md` architecture and security sections updated to describe the new PAM stack.
## 2026-04-24 Audit LOW fixes: docs, rustdoc, check_account scope, debug gating, lto fat (v0.6.12)
- **Who**: ClaudeCode, Dom
- **Why**: Six LOW findings cleared in a single pass. (1) Docs referenced the old `[0,100]` blur range; code clamps `[0,200]` since v0.6.8. (2) The `MAX_BLUR_DIMENSION` doc comment was split by a `// SYNC:` block, producing a truncated sentence in rustdoc. (3) `check_account` was `pub` and relied on callers only ever passing `getuid()`-derived usernames; the contract was not enforced by the type system. (4) `MOONLOCK_DEBUG` env var flipped log verbosity to Debug in release builds, letting a compromised session script escalate journal noise about fprintd / D-Bus. (5) `pam_setcred` absence was undocumented. (6) `[profile.release]` used `lto = "thin"` — fine for most crates, but for a latency-critical auth binary compiled once per release, fat LTO's extra cross-crate inlining is worth the ~1 min build hit.
- **Tradeoffs**: `lto = "fat"` roughly doubles release build time (~30 s → ~60 s) for slightly better inlining across PAM FFI wrappers and the i18n/status paths. `#[cfg(debug_assertions)]` on the debug-level selector means you have to run a debug build to raise log level — inconvenient for live troubleshooting, but aligned with the security-first posture.
- **How**: (1) `CLAUDE.md` + `README.md` updated to `[0,200]`. (2) `// SYNC:` block moved above the `///` doc so rustdoc renders one coherent paragraph. (3) `check_account` visibility narrowed to `pub(crate)` with a `Precondition` paragraph explaining the username contract. (4) Debug-level selection wrapped in `#[cfg(debug_assertions)]`; release builds always run at `LevelFilter::Info`. (5) Added a comment block in `authenticate()` documenting why `pam_setcred` is deliberately absent and where it would hook in if needed. (6) `lto = "fat"` in `Cargo.toml`.
## 2026-04-24 Audit MEDIUM fixes: D-Bus cleanup race, TOCTOU open, FP reset, GTK entry clear (v0.6.11)
- **Who**: ClaudeCode, Dom
- **Why**: Second round after the HIGH fixes, addressing the four MEDIUM findings. (1) `cleanup_dbus` spawned VerifyStop + Release as fire-and-forget, then `resume_async` called Claim after only a 2 s timeout — shorter than the 3 s D-Bus timeout, so on a slow bus the Claim could race the Release and fprintd would reject it, leaving the FP listener permanently dead. (2) `load_background_texture` relied on the caller's `symlink_metadata` check, re-opening the path via `gdk::Texture::from_file` — a classic TOCTOU window. (3) `resume_async` unconditionally reset `failed_attempts`, allowing an attacker with sensor control to evade the 10-attempt cap by cycling verify-match → `check_account` fail → resume. (4) The GTK `PasswordEntry` buffer was only cleared on timeout or auth failure, leaving the password in GLib malloc'd memory longer than necessary.
- **Tradeoffs**: The D-Bus cleanup is now split into a synchronous helper (`take_cleanup_proxy` — signal disconnect + flag clear) and an async helper (`perform_dbus_cleanup` — VerifyStop + Release), so `resume_async` can await the release while `stop()` stays fire-and-forget. Dropping the `failed_attempts` reset means a flaky sensor could reach 10 failures faster, but the correct remedy is a new lock session (construction) rather than a reset that also helps attackers.
- **How**: (1) Split `cleanup_dbus` into `take_cleanup_proxy()` (sync) + `perform_dbus_cleanup(proxy)` (async). `resume_async` now awaits `perform_dbus_cleanup` before `begin_verification`. `stop()` still spawns the cleanup fire-and-forget. (2) `load_background_texture` opens with `O_NOFOLLOW` via `std::fs::OpenOptions::custom_flags`, reads to bytes, and builds the texture via `gdk::Texture::from_bytes`. (3) Removed `listener.borrow_mut().failed_attempts = 0` from `resume_async`. (4) `password_entry.set_text("")` now fires right after the `Zeroizing::new(entry.text().to_string())` extraction, shortening the GTK-side window.
## 2026-04-24 Audit fixes: RefCell borrow across await, async avatar decode
- **Who**: ClaudeCode, Dom
- **Why**: Triple audit found two HIGH issues. (1) `init_fingerprint_async` held a `RefCell` immutable borrow across `is_available_async().await` — a concurrent `connect_monitor` signal (hotplug / suspend-resume) invoking `borrow_mut()` during the await would panic. (2) `set_avatar_from_file` decoded avatars synchronously via `Pixbuf::from_file_at_scale`, blocking the GTK main thread inside the `connect_monitor` handler. With `MAX_AVATAR_FILE_SIZE` at 10 MB the worst-case stall was 200500 ms on monitor hotplug.
- **Tradeoffs**: Avatar is shown as the symbolic default icon for a brief window while decoding completes. Wallpaper stays synchronous because `connect_monitor` fires during `lock()` and needs the texture already present (see 2026-04-09).
- **How**: (1) Extract `username` into a local `String` in `init_fingerprint_async`, drop the borrow before the await, re-borrow in a new scope after — no awaits inside the second borrow, so hotplug during signal setup is safe. (2) `set_avatar_from_file` now uses `gio::File::read_future` + `Pixbuf::from_stream_at_scale_future` for async I/O and decode. The default icon is shown immediately; the decoded texture replaces it when ready. `Pixbuf` itself is `!Send`, so `gio::spawn_blocking` does not apply — the GIO async stream loader keeps the `Pixbuf` on the main thread while the kernel does the I/O asynchronously.
## 2026-04-09 Monitor hotplug via connect_monitor signal
- **Who**: ClaudeCode, Dom
- **Why**: moonlock crashed with segfault in libgtk-4.so after suspend/resume — HDMI monitor disconnect/reconnect invalidated GDK monitor objects, and the statically created windows referenced destroyed surfaces. Crash at consistent GTK4 offset (0x278 NULL dereference), 3x reproduced.
- **Tradeoffs**: Wallpaper texture now loaded before `lock()` instead of after (connect_monitor fires during lock() and needs the texture). Local JPEG loading is fast enough that the delay is negligible. Shared state moved to Rc's for the signal closure — slightly more indirection but necessary for dynamic window creation.
- **How**: (1) Bump gtk4-session-lock feature from `v1_1` to `v1_2` to enable `Instance::connect_monitor`. (2) Replace manual monitor iteration with `lock.connect_monitor()` signal handler that creates windows on demand. (3) Signal fires once per existing monitor at `lock()` and again on hotplug. (4) Windows auto-unmap when their monitor disappears (ext-session-lock-v1 guarantee). (5) Fingerprint listener published to shared Rc so hotplugged monitors get FP labels.
## 2026-03-31 Fourth audit: peek icon, blur limit, GResource compression, sync markers ## 2026-03-31 Fourth audit: peek icon, blur limit, GResource compression, sync markers
- **Who**: Ragnar, Dom - **Who**: ClaudeCode, Dom
- **Why**: Fourth triple audit found blur limit inconsistency (moonlock 0100 vs moongreet/moonset 0200), missing GResource compression, peek icon inconsistency, and duplicated code without sync markers. - **Why**: Fourth triple audit found blur limit inconsistency (moonlock 0100 vs moongreet/moonset 0200), missing GResource compression, peek icon inconsistency, and duplicated code without sync markers.
- **Tradeoffs**: Peek icon enabled in lockscreen — user decision favoring UX consistency over shoulder-surfing protection. Acceptable for single-user desktop. Blur limit raised to 200 for ecosystem consistency. - **Tradeoffs**: Peek icon enabled in lockscreen — user decision favoring UX consistency over shoulder-surfing protection. Acceptable for single-user desktop. Blur limit raised to 200 for ecosystem consistency.
- **How**: (1) `show_peek_icon(true)` in lockscreen password entry. (2) `clamp(0.0, 200.0)` for blur in config.rs. (3) `compressed="true"` on CSS/SVG GResource entries. (4) SYNC comments on duplicated blur/background functions pointing to moongreet and moonset. - **How**: (1) `show_peek_icon(true)` in lockscreen password entry. (2) `clamp(0.0, 200.0)` for blur in config.rs. (3) `compressed="true"` on CSS/SVG GResource entries. (4) SYNC comments on duplicated blur/background functions pointing to moongreet and moonset.
## 2026-03-30 Third audit: blur offset, lock-before-IO, FP signal lifecycle, TOCTOU ## 2026-03-30 Third audit: blur offset, lock-before-IO, FP signal lifecycle, TOCTOU
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: Third triple audit (quality, performance, security) found: blur padding offset rendering texture at (0,0) instead of (-pad,-pad) causing edge darkening on left/top (BUG), wallpaper disk I/O blocking before lock() extending the unsecured window (PERF/SEC), signal handler duplication on resume_async (SEC), failed_attempts not reset on FP resume (SEC), unknown VerifyStatus with done=false hanging FP listener (SEC), TOCTOU in is_file+is_symlink checks (SEC), dead code in faillock_warning (QUALITY), unbounded blur sigma (SEC). - **Why**: Third triple audit (quality, performance, security) found: blur padding offset rendering texture at (0,0) instead of (-pad,-pad) causing edge darkening on left/top (BUG), wallpaper disk I/O blocking before lock() extending the unsecured window (PERF/SEC), signal handler duplication on resume_async (SEC), failed_attempts not reset on FP resume (SEC), unknown VerifyStatus with done=false hanging FP listener (SEC), TOCTOU in is_file+is_symlink checks (SEC), dead code in faillock_warning (QUALITY), unbounded blur sigma (SEC).
- **Tradeoffs**: Wallpaper loads after lock() — screen briefly shows without wallpaper until texture is ready. Acceptable: security > aesthetics. Blur sigma clamped to [0.0, 100.0] — arbitrary upper bound but prevents GPU memory exhaustion. - **Tradeoffs**: Wallpaper loads after lock() — screen briefly shows without wallpaper until texture is ready. Acceptable: security > aesthetics. Blur sigma clamped to [0.0, 100.0] — arbitrary upper bound but prevents GPU memory exhaustion.
- **How**: (1) Texture offset to (-pad, -pad) in render_blurred_texture. (2) lock.lock() before resolve_background_path. (3) begin_verification disconnects old signal_id before registering new. (4) resume_async resets failed_attempts. (5) Unknown VerifyStatus with done=true triggers restart. (6) symlink_metadata() for atomic file+symlink check. (7) faillock_warning dead code removed, saturating_sub. (8) background_blur clamped. (9) Redundant Zeroizing<Vec<u8>> removed. (10) Default impl for FingerprintListener. (11) on_verify_status restricted to pub(crate). (12) Warn logging for non-UTF-8 GECOS and avatar paths. - **How**: (1) Texture offset to (-pad, -pad) in render_blurred_texture. (2) lock.lock() before resolve_background_path. (3) begin_verification disconnects old signal_id before registering new. (4) resume_async resets failed_attempts. (5) Unknown VerifyStatus with done=true triggers restart. (6) symlink_metadata() for atomic file+symlink check. (7) faillock_warning dead code removed, saturating_sub. (8) background_blur clamped. (9) Redundant Zeroizing<Vec<u8>> removed. (10) Default impl for FingerprintListener. (11) on_verify_status restricted to pub(crate). (12) Warn logging for non-UTF-8 GECOS and avatar paths.
## 2026-03-30 Second audit: zeroize CString, FP account check, PAM timeout, blur downscale ## 2026-03-30 Second audit: zeroize CString, FP account check, PAM timeout, blur downscale
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: Second triple audit (quality, performance, security) found: CString password copy not zeroized (HIGH), fingerprint unlock bypassing pam_acct_mgmt (MEDIUM), no PAM timeout leaving user locked out on hanging modules (MEDIUM), GPU blur on full wallpaper resolution (MEDIUM), no-monitor edge case doing `return` instead of `exit(1)` (MEDIUM). - **Why**: Second triple audit (quality, performance, security) found: CString password copy not zeroized (HIGH), fingerprint unlock bypassing pam_acct_mgmt (MEDIUM), no PAM timeout leaving user locked out on hanging modules (MEDIUM), GPU blur on full wallpaper resolution (MEDIUM), no-monitor edge case doing `return` instead of `exit(1)` (MEDIUM).
- **Tradeoffs**: PAM timeout (30s) uses a generation counter to avoid stale result interference — adds complexity but prevents parallel PAM sessions. FP restart after failed account check re-claims the device, adding a D-Bus round-trip, but prevents permanent FP death on transient failures. Blur downscale to 1920px cap trades negligible quality for ~4x less GPU work on 4K wallpapers. - **Tradeoffs**: PAM timeout (30s) uses a generation counter to avoid stale result interference — adds complexity but prevents parallel PAM sessions. FP restart after failed account check re-claims the device, adding a D-Bus round-trip, but prevents permanent FP death on transient failures. Blur downscale to 1920px cap trades negligible quality for ~4x less GPU work on 4K wallpapers.
- **How**: (1) `Zeroizing<CString>` wraps password in auth.rs, `zeroize/std` feature enabled. (2) `check_account()` calls pam_acct_mgmt after FP match; `resume_async()` restarts FP on transient failure. (3) `auth_generation` counter invalidates stale PAM results; 30s timeout re-enables UI. (4) `MAX_BLUR_DIMENSION` caps blur input at 1920px, sigma scaled proportionally. (5) `exit(1)` on no-monitor after `lock.lock()`. - **How**: (1) `Zeroizing<CString>` wraps password in auth.rs, `zeroize/std` feature enabled. (2) `check_account()` calls pam_acct_mgmt after FP match; `resume_async()` restarts FP on transient failure. (3) `auth_generation` counter invalidates stale PAM results; 30s timeout re-enables UI. (4) `MAX_BLUR_DIMENSION` caps blur input at 1920px, sigma scaled proportionally. (5) `exit(1)` on no-monitor after `lock.lock()`.
## 2026-03-28 Remove embedded wallpaper from binary ## 2026-03-28 Remove embedded wallpaper from binary
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback. - **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
- **Tradeoffs**: Without moonarch installed AND without config, lockscreen shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state. - **Tradeoffs**: Without moonarch installed AND without config, lockscreen shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip background picture creation when no texture available. - **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip background picture creation when no texture available.
## 2026-03-28 Audit-driven security and lifecycle fixes (v0.6.0) ## 2026-03-28 Audit-driven security and lifecycle fixes (v0.6.0)
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: Triple audit (quality, performance, security) revealed a critical D-Bus signal spoofing vector, fingerprint lifecycle bugs, and multi-monitor performance issues. - **Why**: Triple audit (quality, performance, security) revealed a critical D-Bus signal spoofing vector, fingerprint lifecycle bugs, and multi-monitor performance issues.
- **Tradeoffs**: `cleanup_dbus()` extraction adds a method but clarifies the stop/match ownership; `running_flag: Rc<Cell<bool>>` adds a field but prevents race between async restart and stop; sender validation adds a check per signal but closes the only known auth bypass. - **Tradeoffs**: `cleanup_dbus()` extraction adds a method but clarifies the stop/match ownership; `running_flag: Rc<Cell<bool>>` adds a field but prevents race between async restart and stop; sender validation adds a check per signal but closes the only known auth bypass.
- **How**: (1) Validate D-Bus VerifyStatus sender against fprintd's unique bus name. (2) Extract `cleanup_dbus()` from `stop()`, call it on verify-match. (3) `Rc<Cell<bool>>` running flag checked after await in `restart_verify_async`. (4) Consistent 3s D-Bus timeouts. (5) Panic hook before logging. (6) Blur and avatar caches shared across monitors. (7) Peek icon disabled. (8) Symlink rejection for background_path. (9) TOML parse errors logged. - **How**: (1) Validate D-Bus VerifyStatus sender against fprintd's unique bus name. (2) Extract `cleanup_dbus()` from `stop()`, call it on verify-match. (3) `Rc<Cell<bool>>` running flag checked after await in `restart_verify_async`. (4) Consistent 3s D-Bus timeouts. (5) Panic hook before logging. (6) Blur and avatar caches shared across monitors. (7) Peek icon disabled. (8) Symlink rejection for background_path. (9) TOML parse errors logged.
## 2026-03-28 GPU blur via GskBlurNode replaces CPU blur ## 2026-03-28 GPU blur via GskBlurNode replaces CPU blur
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms2s on 4K wallpapers at cold cache. Disk cache mitigated repeat starts but added ~100 lines of complexity. - **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms2s on 4K wallpapers at cold cache. Disk cache mitigated repeat starts but added ~100 lines of complexity.
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper. Removes `image` and `dirs` dependencies entirely. No disk cache needed. - **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper. Removes `image` and `dirs` dependencies entirely. No disk cache needed.
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency. - **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency.
## 2026-03-28 Optional background blur via `image` crate (superseded) ## 2026-03-28 Optional background blur via `image` crate (superseded)
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern - **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors. - **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns `gdk::Texture`. Config option `background_blur: Option<f32>` in TOML. - **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns `gdk::Texture`. Config option `background_blur: Option<f32>` in TOML.
## 2026-03-28 Shared wallpaper texture pattern (aligned with moonset/moongreet) ## 2026-03-28 Shared wallpaper texture pattern (aligned with moonset/moongreet)
- **Who**: Nyx, Dom - **Who**: ClaudeCode, Dom
- **Why**: Previously loaded wallpaper per-window via `Picture::for_filename()`. Multi-monitor setups decoded the JPEG redundantly. Blur feature requires texture pixel access anyway. - **Why**: Previously loaded wallpaper per-window via `Picture::for_filename()`. Multi-monitor setups decoded the JPEG redundantly. Blur feature requires texture pixel access anyway.
- **Tradeoffs**: Slightly more code in main.rs (texture loaded before window creation), but avoids redundant decoding and enables the blur feature. - **Tradeoffs**: Slightly more code in main.rs (texture loaded before window creation), but avoids redundant decoding and enables the blur feature.
- **How**: `load_background_texture()` in lockscreen.rs decodes once, `create_background_picture()` wraps shared `gdk::Texture` in `gtk::Picture`. Same pattern as moonset/moongreet. - **How**: `load_background_texture()` in lockscreen.rs decodes once, `create_background_picture()` wraps shared `gdk::Texture` in `gtk::Picture`. Same pattern as moonset/moongreet.
+3 -4
View File
@@ -8,14 +8,13 @@ Part of the Moonarch ecosystem.
- **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash, `exit(1)` in release if unsupported) - **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash, `exit(1)` in release if unsupported)
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter - **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter
- **Fingerprint unlock** — fprintd D-Bus integration with sender validation, async init (window appears instantly), `pam_acct_mgmt` check after verify, auto-resume on transient errors - **Fingerprint unlock** — fprintd D-Bus integration with sender validation, async init (window appears instantly), `pam_acct_mgmt` check after verify, auto-resume on transient errors
- **Multi-monitor** — Lockscreen on every monitor with shared blur and avatar caches - **Multi-monitor + hotplug** — Lockscreen on every monitor with shared blur and avatar caches; monitors added after suspend/resume get windows automatically via `connect_monitor` signal
- **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0100) - **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0200)
- **i18n** — German and English (auto-detected) - **i18n** — German and English (auto-detected)
- **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout - **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
- **Panic safety** — Panic hook logs but never unlocks (installed before logging) - **Panic safety** — Panic hook logs but never unlocks (installed before logging)
- **Password wiping** — `Zeroize` on drop from GTK entry through PAM FFI layer - **Password wiping** — `Zeroize` on drop from GTK entry through PAM FFI layer
- **Journal logging** — `journalctl -t moonlock`, debug level via `MOONLOCK_DEBUG` env var - **Journal logging** — `journalctl -t moonlock`, debug level via `MOONLOCK_DEBUG` env var
- **Lock-first architecture** — Wallpaper loaded after `lock()` so disk I/O never delays lock acquisition
## Requirements ## Requirements
@@ -49,7 +48,7 @@ Create `/etc/moonlock/moonlock.toml` or `~/.config/moonlock/moonlock.toml`:
```toml ```toml
background_path = "/usr/share/wallpapers/moon.jpg" background_path = "/usr/share/wallpapers/moon.jpg"
background_blur = 40.0 # 0.0100.0, optional background_blur = 40.0 # 0.0200.0, optional
fingerprint_enabled = true fingerprint_enabled = true
``` ```
+1 -3
View File
@@ -1,4 +1,2 @@
#%PAM-1.0 #%PAM-1.0
auth include system-auth auth include login
account include system-auth
session include system-auth
+12 -12
View File
@@ -1,9 +1,9 @@
/* ABOUTME: GTK4 CSS stylesheet for the Moonlock lockscreen. */ /* ABOUTME: GTK4 CSS stylesheet for the Moonlock lockscreen. */
/* ABOUTME: Dark theme styling matching the Moonarch ecosystem. */ /* ABOUTME: Uses GTK theme colors for consistency with the active desktop theme. */
/* Main window background */ /* Main window background */
window.lockscreen { window.lockscreen {
background-color: #1a1a2e; background-color: @theme_bg_color;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
opacity: 0; opacity: 0;
@@ -27,14 +27,14 @@ window.lockscreen.visible {
min-width: 128px; min-width: 128px;
min-height: 128px; min-height: 128px;
background-color: @theme_selected_bg_color; background-color: @theme_selected_bg_color;
border: 3px solid alpha(white, 0.3); border: 3px solid alpha(@theme_fg_color, 0.3);
} }
/* Username label */ /* Username label */
.username-label { .username-label {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
color: white; color: @theme_fg_color;
margin-top: 12px; margin-top: 12px;
margin-bottom: 40px; margin-bottom: 40px;
} }
@@ -46,29 +46,29 @@ window.lockscreen.visible {
/* Error message label */ /* Error message label */
.error-label { .error-label {
color: #ff6b6b; color: @error_color;
font-size: 14px; font-size: 14px;
} }
/* Fingerprint status indicator */ /* Fingerprint status indicator */
.fingerprint-label { .fingerprint-label {
color: alpha(white, 0.6); color: alpha(@theme_fg_color, 0.6);
font-size: 13px; font-size: 13px;
margin-top: 8px; margin-top: 8px;
} }
.fingerprint-label.success { .fingerprint-label.success {
color: #51cf66; color: @success_color;
} }
.fingerprint-label.failed { .fingerprint-label.failed {
color: #ff6b6b; color: @error_color;
} }
/* Confirmation prompt */ /* Confirmation prompt */
.confirm-label { .confirm-label {
font-size: 16px; font-size: 16px;
color: white; color: @theme_fg_color;
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -103,12 +103,12 @@ window.lockscreen.visible {
min-height: 48px; min-height: 48px;
padding: 0px; padding: 0px;
border-radius: 24px; border-radius: 24px;
background-color: alpha(white, 0.1); background-color: alpha(@theme_fg_color, 0.1);
color: white; color: @theme_fg_color;
border: none; border: none;
margin: 4px; margin: 4px;
} }
.power-button:hover { .power-button:hover {
background-color: alpha(white, 0.25); background-color: alpha(@theme_fg_color, 0.25);
} }
+13 -68
View File
@@ -54,8 +54,6 @@ unsafe extern "C" {
fn pam_authenticate(pamh: *mut libc::c_void, flags: libc::c_int) -> libc::c_int; fn pam_authenticate(pamh: *mut libc::c_void, flags: libc::c_int) -> libc::c_int;
fn pam_acct_mgmt(pamh: *mut libc::c_void, flags: libc::c_int) -> libc::c_int;
fn pam_end(pamh: *mut libc::c_void, pam_status: libc::c_int) -> libc::c_int; fn pam_end(pamh: *mut libc::c_void, pam_status: libc::c_int) -> libc::c_int;
} }
@@ -191,64 +189,23 @@ pub fn authenticate(username: &str, password: &str) -> bool {
return false; return false;
} }
// Safety: handle is valid and non-null after successful pam_start // Safety: handle is valid and non-null after successful pam_start.
// Note: pam_setcred is intentionally NOT called here. A lockscreen unlocks
// an existing session whose credentials were already established at login;
// refreshing them would duplicate work done by the session's login manager.
// If per-unlock credential refresh (Kerberos tickets, pam_gnome_keyring)
// is ever desired, hook it here with PAM_ESTABLISH_CRED.
//
// pam_acct_mgmt is intentionally NOT called: the PAM stack (`auth include
// login`) has no `account` module, and pam_unix(account) requires setuid
// root for /etc/shadow. Lockout/faillock and policy checks happen inside
// the inherited auth stack via pam_faillock. See DECISIONS.md 2026-04-30.
let auth_ret = unsafe { pam_authenticate(handle, 0) }; let auth_ret = unsafe { pam_authenticate(handle, 0) };
let acct_ret = if auth_ret == PAM_SUCCESS {
// Safety: handle is valid, check account restrictions
unsafe { pam_acct_mgmt(handle, 0) }
} else {
auth_ret
};
// Safety: handle is valid, pam_end cleans up the PAM session // Safety: handle is valid, pam_end cleans up the PAM session
unsafe { pam_end(handle, acct_ret) }; unsafe { pam_end(handle, auth_ret) };
acct_ret == PAM_SUCCESS auth_ret == PAM_SUCCESS
}
/// Check account restrictions via PAM without authentication.
///
/// Used after fingerprint unlock to enforce account policies (lockout, expiry)
/// that would otherwise be bypassed when not going through pam_authenticate.
/// Returns true if the account is valid and allowed to log in.
pub fn check_account(username: &str) -> bool {
let service = match CString::new("moonlock") {
Ok(c) => c,
Err(_) => return false,
};
let username_cstr = match CString::new(username) {
Ok(c) => c,
Err(_) => return false,
};
// No password needed — we only check account status, not authenticate.
// PAM conv callback is required by pam_start but won't be called for acct_mgmt.
let empty_password = Zeroizing::new(CString::new("").unwrap());
let conv = PamConv {
conv: pam_conv_callback,
appdata_ptr: std::ptr::from_ref::<CString>(&empty_password) as *mut libc::c_void,
};
let mut handle: *mut libc::c_void = ptr::null_mut();
let ret = unsafe {
pam_start(
service.as_ptr(),
username_cstr.as_ptr(),
&conv,
&mut handle,
)
};
if ret != PAM_SUCCESS || handle.is_null() {
return false;
}
let acct_ret = unsafe { pam_acct_mgmt(handle, 0) };
unsafe { pam_end(handle, acct_ret) };
acct_ret == PAM_SUCCESS
} }
#[cfg(test)] #[cfg(test)]
@@ -285,16 +242,4 @@ mod tests {
let result = authenticate("", "password"); let result = authenticate("", "password");
assert!(!result); assert!(!result);
} }
#[test]
fn check_account_empty_username_fails() {
let result = check_account("");
assert!(!result);
}
#[test]
fn check_account_null_byte_username_fails() {
let result = check_account("user\0name");
assert!(!result);
}
} }
+23 -28
View File
@@ -175,17 +175,6 @@ impl FingerprintListener {
Self::begin_verification(listener, username).await; Self::begin_verification(listener, username).await;
} }
/// Resume fingerprint verification after a transient interruption (e.g. failed
/// PAM account check). Reuses previously stored callbacks. Re-claims the device
/// and restarts verification from scratch.
pub async fn resume_async(
listener: &Rc<RefCell<FingerprintListener>>,
username: &str,
) {
listener.borrow_mut().failed_attempts = 0;
Self::begin_verification(listener, username).await;
}
/// Claim device, start verification, and connect D-Bus signal handler. /// Claim device, start verification, and connect D-Bus signal handler.
/// Assumes device_proxy is set and callbacks are already stored. /// Assumes device_proxy is set and callbacks are already stored.
async fn begin_verification( async fn begin_verification(
@@ -352,26 +341,32 @@ impl FingerprintListener {
} }
} }
/// Disconnect the signal handler and send VerifyStop + Release to fprintd. /// Disconnect the signal handler and clear running flags. Returns the proxy
/// Signal disconnect is synchronous to prevent further callbacks. /// the caller should use for the async D-Bus cleanup (VerifyStop + Release).
/// D-Bus cleanup is fire-and-forget to avoid blocking the UI. fn take_cleanup_proxy(&mut self) -> Option<gio::DBusProxy> {
fn cleanup_dbus(&mut self) {
self.running = false; self.running = false;
self.running_flag.set(false); self.running_flag.set(false);
if let Some(ref proxy) = self.device_proxy { let proxy = self.device_proxy.clone()?;
if let Some(id) = self.signal_id.take() { if let Some(id) = self.signal_id.take() {
proxy.disconnect(id); proxy.disconnect(id);
} }
let proxy = proxy.clone(); Some(proxy)
glib::spawn_future_local(async move { }
let _ = proxy
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) async fn perform_dbus_cleanup(proxy: gio::DBusProxy) {
.await; let _ = proxy
let _ = proxy .call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await;
.await; let _ = proxy
}); .call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.await;
}
/// Fire-and-forget cleanup for code paths that cannot await (e.g. drop, stop).
fn cleanup_dbus(&mut self) {
if let Some(proxy) = self.take_cleanup_proxy() {
glib::spawn_future_local(Self::perform_dbus_cleanup(proxy));
} }
} }
+160 -118
View File
@@ -170,55 +170,17 @@ pub fn create_lockscreen_window(
power_box.set_margin_end(16); power_box.set_margin_end(16);
power_box.set_margin_bottom(16); power_box.set_margin_bottom(16);
let reboot_btn = gtk::Button::new(); for action in power_actions() {
reboot_btn.set_icon_name("system-reboot-symbolic"); let button = create_power_button(
reboot_btn.add_css_class("power-button"); action,
reboot_btn.set_tooltip_text(Some(strings.reboot_tooltip)); strings,
reboot_btn.connect_clicked(clone!( &power_box,
#[weak] &confirm_area,
confirm_area, &confirm_box,
#[strong] &error_label,
confirm_box, );
#[weak] power_box.append(&button);
error_label, }
move |_| {
show_power_confirm(
strings.reboot_confirm,
power::reboot,
strings.reboot_failed,
strings,
&confirm_area,
&confirm_box,
&error_label,
);
}
));
power_box.append(&reboot_btn);
let shutdown_btn = gtk::Button::new();
shutdown_btn.set_icon_name("system-shutdown-symbolic");
shutdown_btn.add_css_class("power-button");
shutdown_btn.set_tooltip_text(Some(strings.shutdown_tooltip));
shutdown_btn.connect_clicked(clone!(
#[weak]
confirm_area,
#[strong]
confirm_box,
#[weak]
error_label,
move |_| {
show_power_confirm(
strings.shutdown_confirm,
power::shutdown,
strings.shutdown_failed,
strings,
&confirm_area,
&confirm_box,
&error_label,
);
}
));
power_box.append(&shutdown_btn);
overlay.add_overlay(&power_box); overlay.add_overlay(&power_box);
@@ -244,6 +206,10 @@ pub fn create_lockscreen_window(
if password.is_empty() { if password.is_empty() {
return; return;
} }
// Clear the GTK entry's internal buffer as early as possible. GTK allocates
// the backing GString via libc malloc, which zeroize cannot reach — the
// best we can do is shorten the window during which it resides in memory.
entry.set_text("");
entry.set_sensitive(false); entry.set_sensitive(false);
let username = username.clone(); let username = username.clone();
@@ -423,52 +389,16 @@ pub fn start_fingerprint(
let unlock_cb_fp = handles.unlock_callback.clone(); let unlock_cb_fp = handles.unlock_callback.clone();
let fp_rc_success = fp_rc.clone(); let fp_rc_success = fp_rc.clone();
let fp_username = handles.username.clone();
let on_success = move || { let on_success = move || {
let label = fp_label_success.clone(); let label = fp_label_success.clone();
let cb = unlock_cb_fp.clone(); let cb = unlock_cb_fp.clone();
let fp = fp_rc_success.clone(); let fp = fp_rc_success.clone();
let username = fp_username.clone();
glib::idle_add_local_once(move || { glib::idle_add_local_once(move || {
let strings = load_strings(None); let strings = load_strings(None);
label.set_text(strings.fingerprint_success); label.set_text(strings.fingerprint_success);
label.add_css_class("success"); label.add_css_class("success");
// stop() is idempotent — cleanup_dbus() already ran inside on_verify_status,
// but this mirrors the PAM success path for defense-in-depth.
fp.borrow_mut().stop(); fp.borrow_mut().stop();
cb();
// Enforce PAM account policies (lockout, expiry) before unlocking.
// Fingerprint auth bypasses pam_authenticate, so we must explicitly
// check account restrictions via pam_acct_mgmt.
glib::spawn_future_local(async move {
let user = username.clone();
let result = gio::spawn_blocking(move || {
auth::check_account(&user)
}).await;
match result {
Ok(true) => cb(),
_ => {
log::error!("PAM account check failed after fingerprint auth");
let strings = load_strings(None);
label.set_text(strings.wrong_password);
label.remove_css_class("success");
label.add_css_class("failed");
// Restart FP verification after delay — the failure may be
// transient (e.g. PAM module timeout). If the account is truly
// locked, check_account will fail again on next match.
glib::timeout_add_local_once(
std::time::Duration::from_secs(2),
move || {
label.set_text(load_strings(None).fingerprint_prompt);
label.remove_css_class("failed");
glib::spawn_future_local(async move {
FingerprintListener::resume_async(&fp, &username).await;
});
},
);
}
}
});
}); });
}; };
@@ -518,12 +448,35 @@ pub fn start_fingerprint(
/// Load the wallpaper as a texture once, for sharing across all windows. /// Load the wallpaper as a texture once, for sharing across all windows.
/// Returns None if no wallpaper path is provided or the file cannot be loaded. /// Returns None if no wallpaper path is provided or the file cannot be loaded.
/// Blur is applied at render time via GPU (GskBlurNode), not here. /// Blur is applied at render time via GPU (GskBlurNode), not here.
///
/// Opens the file with O_NOFOLLOW to close the TOCTOU window between the
/// symlink check in `resolve_background_path_with` and this read. If the path
/// was swapped for a symlink after the check, `open` fails with ELOOP.
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> { pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
let file = gio::File::for_path(bg_path); use std::io::Read;
match gdk::Texture::from_file(&file) { use std::os::unix::fs::OpenOptionsExt;
let mut file = match std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(bg_path)
{
Ok(f) => f,
Err(e) => {
log::warn!("Failed to open wallpaper {}: {e}", bg_path.display());
return None;
}
};
let mut bytes = Vec::new();
if let Err(e) = file.read_to_end(&mut bytes) {
log::warn!("Failed to read wallpaper {}: {e}", bg_path.display());
return None;
}
let glib_bytes = glib::Bytes::from_owned(bytes);
match gdk::Texture::from_bytes(&glib_bytes) {
Ok(texture) => Some(texture), Ok(texture) => Some(texture),
Err(e) => { Err(e) => {
log::warn!("Failed to load wallpaper {}: {e}", bg_path.display()); log::warn!("Failed to decode wallpaper {}: {e}", bg_path.display());
None None
} }
} }
@@ -565,11 +518,11 @@ fn create_background_picture(
background background
} }
/// Maximum texture dimension for blur input. Textures larger than this are
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture // SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
// are duplicated in moongreet/src/greeter.rs and moonset/src/panel.rs. // are duplicated in moongreet/src/greeter.rs and moonset/src/panel.rs.
// Changes here must be mirrored to the other two projects. // Changes here must be mirrored to the other two projects.
/// Maximum texture dimension for blur input. Textures larger than this are
/// downscaled before blurring — the blur destroys detail anyway, so there is /// downscaled before blurring — the blur destroys detail anyway, so there is
/// no visible quality loss, but GPU work is reduced significantly. /// no visible quality loss, but GPU work is reduced significantly.
const MAX_BLUR_DIMENSION: f32 = 1920.0; const MAX_BLUR_DIMENSION: f32 = 1920.0;
@@ -623,30 +576,41 @@ fn render_blurred_texture(
} }
/// Load an image file and set it as the avatar. Stores the texture in the cache. /// Load an image file and set it as the avatar. Stores the texture in the cache.
/// Decoding runs via GIO async I/O + async pixbuf stream loader so the GTK main
/// loop stays responsive — avatars may be loaded inside the `connect_monitor`
/// signal handler at hotplug time, which must not block. The fallback icon is
/// shown immediately; the decoded texture replaces it when ready.
fn set_avatar_from_file( fn set_avatar_from_file(
image: &gtk::Image, image: &gtk::Image,
path: &Path, path: &Path,
cache: &Rc<RefCell<Option<gdk::Texture>>>, cache: &Rc<RefCell<Option<gdk::Texture>>>,
) { ) {
let path_str = match path.to_str() { image.set_icon_name(Some("avatar-default-symbolic"));
Some(s) => s,
None => { let display_path = path.to_path_buf();
log::warn!("Avatar path is not valid UTF-8: {:?}", path); let file = gio::File::for_path(path);
image.set_icon_name(Some("avatar-default-symbolic")); let image_clone = image.clone();
return; let cache_clone = cache.clone();
glib::spawn_future_local(async move {
let stream = match file.read_future(glib::Priority::default()).await {
Ok(s) => s,
Err(e) => {
log::warn!("Failed to open avatar {}: {e}", display_path.display());
return;
}
};
match Pixbuf::from_stream_at_scale_future(&stream, AVATAR_SIZE, AVATAR_SIZE, true).await {
Ok(pixbuf) => {
let texture = gdk::Texture::for_pixbuf(&pixbuf);
image_clone.set_paintable(Some(&texture));
*cache_clone.borrow_mut() = Some(texture);
}
Err(e) => {
log::warn!("Failed to decode avatar from {}: {e}", display_path.display());
}
} }
}; });
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
Ok(pixbuf) => {
let texture = gdk::Texture::for_pixbuf(&pixbuf);
image.set_paintable(Some(&texture));
*cache.borrow_mut() = Some(texture);
}
Err(e) => {
log::warn!("Failed to load avatar from {:?}: {e}", path);
image.set_icon_name(Some("avatar-default-symbolic"));
}
}
} }
/// Load the default avatar SVG from GResources, tinted with the foreground color. /// Load the default avatar SVG from GResources, tinted with the foreground color.
@@ -686,23 +650,84 @@ fn set_default_avatar(
image.set_icon_name(Some("avatar-default-symbolic")); image.set_icon_name(Some("avatar-default-symbolic"));
} }
/// Definition for a single power-action button (reboot, shutdown).
/// Couples icon, prompt, error text and action so a button cannot be wired
/// with a mismatched prompt/action pair. Mirrors moonset's `ActionDef`.
#[derive(Clone, Copy)]
struct PowerAction {
icon_name: &'static str,
tooltip_attr: fn(&Strings) -> &'static str,
confirm_attr: fn(&Strings) -> &'static str,
error_attr: fn(&Strings) -> &'static str,
action_fn: fn() -> Result<(), PowerError>,
}
/// The power actions offered on the lockscreen.
fn power_actions() -> [PowerAction; 2] {
[
PowerAction {
icon_name: "system-reboot-symbolic",
tooltip_attr: |s| s.reboot_tooltip,
confirm_attr: |s| s.reboot_confirm,
error_attr: |s| s.reboot_failed,
action_fn: power::reboot,
},
PowerAction {
icon_name: "system-shutdown-symbolic",
tooltip_attr: |s| s.shutdown_tooltip,
confirm_attr: |s| s.shutdown_confirm,
error_attr: |s| s.shutdown_failed,
action_fn: power::shutdown,
},
]
}
/// Build a power-action icon button wired to the confirmation flow.
fn create_power_button(
action: PowerAction,
strings: &'static Strings,
power_box: &gtk::Box,
confirm_area: &gtk::Box,
confirm_box: &Rc<RefCell<Option<gtk::Box>>>,
error_label: &gtk::Label,
) -> gtk::Button {
let button = gtk::Button::new();
button.set_icon_name(action.icon_name);
button.add_css_class("power-button");
button.set_tooltip_text(Some((action.tooltip_attr)(strings)));
button.connect_clicked(clone!(
#[weak]
power_box,
#[weak]
confirm_area,
#[strong]
confirm_box,
#[weak]
error_label,
move |_| {
show_power_confirm(action, strings, &power_box, &confirm_area, &confirm_box, &error_label);
}
));
button
}
/// Show inline power confirmation. /// Show inline power confirmation.
fn show_power_confirm( fn show_power_confirm(
message: &'static str, action: PowerAction,
action_fn: fn() -> Result<(), PowerError>,
error_message: &'static str,
strings: &'static Strings, strings: &'static Strings,
power_box: &gtk::Box,
confirm_area: &gtk::Box, confirm_area: &gtk::Box,
confirm_box: &Rc<RefCell<Option<gtk::Box>>>, confirm_box: &Rc<RefCell<Option<gtk::Box>>>,
error_label: &gtk::Label, error_label: &gtk::Label,
) { ) {
dismiss_power_confirm(confirm_area, confirm_box); dismiss_power_confirm(confirm_area, confirm_box);
error_label.set_visible(false);
let new_box = gtk::Box::new(gtk::Orientation::Vertical, 8); let new_box = gtk::Box::new(gtk::Orientation::Vertical, 8);
new_box.set_halign(gtk::Align::Center); new_box.set_halign(gtk::Align::Center);
new_box.set_margin_top(16); new_box.set_margin_top(16);
let confirm_label = gtk::Label::new(Some(message)); let confirm_label = gtk::Label::new(Some((action.confirm_attr)(strings)));
confirm_label.add_css_class("confirm-label"); confirm_label.add_css_class("confirm-label");
new_box.append(&confirm_label); new_box.append(&confirm_label);
@@ -712,6 +737,8 @@ fn show_power_confirm(
let yes_btn = gtk::Button::with_label(strings.confirm_yes); let yes_btn = gtk::Button::with_label(strings.confirm_yes);
yes_btn.add_css_class("confirm-yes"); yes_btn.add_css_class("confirm-yes");
yes_btn.connect_clicked(clone!( yes_btn.connect_clicked(clone!(
#[weak]
power_box,
#[weak] #[weak]
confirm_area, confirm_area,
#[strong] #[strong]
@@ -719,8 +746,7 @@ fn show_power_confirm(
#[weak] #[weak]
error_label, error_label,
move |_| { move |_| {
dismiss_power_confirm(&confirm_area, &confirm_box); execute_power_action(action, strings, &power_box, &confirm_area, &confirm_box, &error_label);
execute_power_action(action_fn, error_message, &error_label);
} }
)); ));
button_row.append(&yes_btn); button_row.append(&yes_btn);
@@ -751,28 +777,44 @@ fn dismiss_power_confirm(confirm_area: &gtk::Box, confirm_box: &Rc<RefCell<Optio
} }
} }
/// Execute a power action in a background thread. /// Execute a power action in a background thread, guarding against re-trigger.
fn execute_power_action( fn execute_power_action(
action_fn: fn() -> Result<(), PowerError>, action: PowerAction,
error_message: &'static str, strings: &'static Strings,
power_box: &gtk::Box,
confirm_area: &gtk::Box,
confirm_box: &Rc<RefCell<Option<gtk::Box>>>,
error_label: &gtk::Label, error_label: &gtk::Label,
) { ) {
dismiss_power_confirm(confirm_area, confirm_box);
let action_fn = action.action_fn;
let error_message = (action.error_attr)(strings);
// Desensitize the power buttons so a double-click or keyboard repeat cannot
// fire the same action twice while it is in flight.
power_box.set_sensitive(false);
glib::spawn_future_local(clone!( glib::spawn_future_local(clone!(
#[weak]
power_box,
#[weak] #[weak]
error_label, error_label,
async move { async move {
let result = gio::spawn_blocking(move || action_fn()).await; let result = gio::spawn_blocking(action_fn).await;
match result { match result {
Ok(Ok(())) => {} Ok(Ok(())) => {}
Ok(Err(e)) => { Ok(Err(e)) => {
log::error!("Power action failed: {e}"); log::error!("Power action failed: {e}");
error_label.set_text(error_message); error_label.set_text(error_message);
error_label.set_visible(true); error_label.set_visible(true);
power_box.set_sensitive(true);
} }
Err(_) => { Err(_) => {
log::error!("Power action panicked"); log::error!("Power action panicked");
error_label.set_text(error_message); error_label.set_text(error_message);
error_label.set_visible(true); error_label.set_visible(true);
power_box.set_sensitive(true);
} }
} }
} }
+99 -42
View File
@@ -59,17 +59,18 @@ fn activate(app: &gtk::Application) {
fn activate_with_session_lock( fn activate_with_session_lock(
app: &gtk::Application, app: &gtk::Application,
display: &gdk::Display, _display: &gdk::Display,
config: &config::Config, config: &config::Config,
) { ) {
let lock = gtk4_session_lock::Instance::new(); let lock = gtk4_session_lock::Instance::new();
lock.lock();
// Load wallpaper AFTER lock — disk I/O must not delay the lock acquisition // Load wallpaper before lock — connect_monitor fires during lock() and needs the
let bg_texture = config::resolve_background_path(config) // texture. This means disk I/O happens before locking, but loading a local JPEG
.and_then(|path| lockscreen::load_background_texture(&path)); // is fast enough that the delay is negligible.
let bg_texture: Rc<Option<gdk::Texture>> = Rc::new(
let monitors = display.monitors(); config::resolve_background_path(config)
.and_then(|path| lockscreen::load_background_texture(&path)),
);
// Shared unlock callback — unlocks session and quits. // Shared unlock callback — unlocks session and quits.
// Guard prevents double-unlock if PAM and fingerprint succeed simultaneously. // Guard prevents double-unlock if PAM and fingerprint succeed simultaneously.
@@ -91,68 +92,116 @@ fn activate_with_session_lock(
let blur_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None)); let blur_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None)); let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
// Create all monitor windows immediately — no D-Bus calls here // Shared config for use in the monitor signal handler
let mut all_handles = Vec::new(); let config = Rc::new(config.clone());
let mut created_any = false;
for i in 0..monitors.n_items() { // Shared handles list — populated by connect_monitor, read by fingerprint init
if let Some(monitor) = monitors let all_handles: Rc<RefCell<Vec<lockscreen::LockscreenHandles>>> =
.item(i) Rc::new(RefCell::new(Vec::new()));
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
{ // Shared fingerprint listener — None until async init completes.
// The monitor handler checks this to wire up FP labels on hotplugged monitors.
let shared_fp: Rc<RefCell<Option<Rc<RefCell<FingerprintListener>>>>> =
Rc::new(RefCell::new(None));
// The ::monitor signal fires once per existing monitor at lock(), and again
// whenever a monitor is hotplugged (e.g. after suspend/resume). This replaces
// the old manual monitor iteration and handles hotplug automatically.
let lock_for_signal = lock.clone();
lock.connect_monitor(glib::clone!(
#[strong]
app,
#[strong]
config,
#[strong]
bg_texture,
#[strong]
unlock_callback,
#[strong]
blur_cache,
#[strong]
avatar_cache,
#[strong]
all_handles,
#[strong]
shared_fp,
move |_instance, monitor| {
log::debug!("Monitor signal: creating lockscreen window");
let handles = lockscreen::create_lockscreen_window( let handles = lockscreen::create_lockscreen_window(
bg_texture.as_ref(), bg_texture.as_ref().as_ref(),
config, &config,
app, &app,
unlock_callback.clone(), unlock_callback.clone(),
&blur_cache, &blur_cache,
&avatar_cache, &avatar_cache,
); );
lock.assign_window_to_monitor(&handles.window, &monitor); lock_for_signal.assign_window_to_monitor(&handles.window, monitor);
handles.window.present(); handles.window.present();
all_handles.push(handles);
created_any = true;
}
}
if !created_any { // If fingerprint is already initialized, wire up the label
log::error!("No lockscreen windows created — screen stays locked (compositor policy)"); if let Some(ref fp_rc) = *shared_fp.borrow() {
std::process::exit(1); lockscreen::show_fingerprint_label(&handles, fp_rc);
} }
all_handles.borrow_mut().push(handles);
}
));
lock.lock();
// Async fprintd initialization — runs after windows are visible // Async fprintd initialization — runs after windows are visible
if config.fingerprint_enabled { if config.fingerprint_enabled {
init_fingerprint_async(all_handles); init_fingerprint_async(all_handles, shared_fp);
} }
} }
/// Initialize fprintd asynchronously after windows are visible. /// Initialize fprintd asynchronously after windows are visible.
/// Uses a single FingerprintListener shared across all monitors — /// Uses a single FingerprintListener shared across all monitors —
/// only the first monitor's handles get the fingerprint UI wired up. /// only the first monitor's handles get the fingerprint verification wired up.
fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) { /// The `shared_fp` is set after init so that the connect_monitor handler can
/// wire up FP labels on monitors that appear after initialization.
fn init_fingerprint_async(
all_handles: Rc<RefCell<Vec<lockscreen::LockscreenHandles>>>,
shared_fp: Rc<RefCell<Option<Rc<RefCell<FingerprintListener>>>>>,
) {
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
let mut listener = FingerprintListener::new(); let mut listener = FingerprintListener::new();
listener.init_async().await; listener.init_async().await;
// Use the first monitor's username to check enrollment // Extract username without holding a borrow across the await below —
let username = &all_handles[0].username; // otherwise a concurrent connect_monitor signal (hotplug / suspend-resume)
if username.is_empty() { // that tries to borrow_mut() panics at runtime.
return; let username = {
} let handles = all_handles.borrow();
if handles.is_empty() {
return;
}
let u = handles[0].username.clone();
if u.is_empty() {
return;
}
u
};
if !listener.is_available_async(username).await { if !listener.is_available_async(&username).await {
log::debug!("fprintd not available or no enrolled fingers"); log::debug!("fprintd not available or no enrolled fingers");
return; return;
} }
let fp_rc = Rc::new(RefCell::new(listener)); let fp_rc = Rc::new(RefCell::new(listener));
// Show fingerprint label on all monitors // Re-borrow after the await — no further awaits in this scope, so it is
for handles in &all_handles { // safe to hold the borrow briefly while wiring up the labels.
lockscreen::show_fingerprint_label(handles, &fp_rc); {
let handles = all_handles.borrow();
for h in handles.iter() {
lockscreen::show_fingerprint_label(h, &fp_rc);
}
lockscreen::start_fingerprint(&handles[0], &fp_rc);
} }
// Start verification listener on the first monitor only // Publish the listener so hotplugged monitors get FP labels too
lockscreen::start_fingerprint(&all_handles[0], &fp_rc); *shared_fp.borrow_mut() = Some(fp_rc);
}); });
} }
@@ -184,7 +233,9 @@ fn activate_without_lock(
// Async fprintd initialization for development mode // Async fprintd initialization for development mode
if config.fingerprint_enabled { if config.fingerprint_enabled {
init_fingerprint_async(vec![handles]); let all_handles = Rc::new(RefCell::new(vec![handles]));
let shared_fp = Rc::new(RefCell::new(None));
init_fingerprint_async(all_handles, shared_fp);
} }
} }
@@ -199,11 +250,17 @@ fn setup_logging() {
eprintln!("Failed to create journal logger: {e}"); eprintln!("Failed to create journal logger: {e}");
} }
} }
// Debug level is only selectable in debug builds. Release binaries ignore
// MOONLOCK_DEBUG so a session script cannot escalate log verbosity to leak
// fprintd / D-Bus internals into the journal.
#[cfg(debug_assertions)]
let level = if std::env::var("MOONLOCK_DEBUG").is_ok() { let level = if std::env::var("MOONLOCK_DEBUG").is_ok() {
log::LevelFilter::Debug log::LevelFilter::Debug
} else { } else {
log::LevelFilter::Info log::LevelFilter::Info
}; };
#[cfg(not(debug_assertions))]
let level = log::LevelFilter::Info;
log::set_max_level(level); log::set_max_level(level);
} }