Compare commits
4 Commits
3f4448c641
...
3e610bdb4b
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e610bdb4b | |||
| 9dfd1829e9 | |||
| 39d9cbb624 | |||
| 3adc5e980d |
@ -1,7 +1,5 @@
|
||||
# Moonlock
|
||||
|
||||
**Name**: Nyx (Göttin der Nacht — passend zum Lockscreen, der den Bildschirm verdunkelt)
|
||||
|
||||
## Projekt
|
||||
|
||||
Moonlock ist ein sicherer Wayland-Lockscreen, gebaut mit Rust + gtk4-rs + ext-session-lock-v1.
|
||||
@ -42,7 +40,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
||||
- `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection
|
||||
- `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
|
||||
- `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
|
||||
- `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()
|
||||
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -575,7 +575,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moonlock"
|
||||
version = "0.6.8"
|
||||
version = "0.6.12"
|
||||
dependencies = [
|
||||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "moonlock"
|
||||
version = "0.6.9"
|
||||
version = "0.6.12"
|
||||
edition = "2024"
|
||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||
license = "MIT"
|
||||
@ -28,6 +28,6 @@ tempfile = "3"
|
||||
glib-build-tools = "0.22"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
39
DECISIONS.md
39
DECISIONS.md
@ -2,65 +2,86 @@
|
||||
|
||||
Architectural and design decisions for Moonlock, in reverse chronological order.
|
||||
|
||||
## 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 200–500 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**: Nyx, Dom
|
||||
- **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
|
||||
|
||||
- **Who**: Ragnar, Dom
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **Why**: Fourth triple audit found blur limit inconsistency (moonlock 0–100 vs moongreet/moonset 0–200), 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.
|
||||
- **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
|
||||
|
||||
- **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).
|
||||
- **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.
|
||||
|
||||
## 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).
|
||||
- **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()`.
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
## 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 500ms–2s 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.
|
||||
- **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)
|
||||
|
||||
- **Who**: Nyx, Dom
|
||||
- **Who**: ClaudeCode, Dom
|
||||
- **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.
|
||||
- **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)
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@ -9,7 +9,7 @@ Part of the Moonarch ecosystem.
|
||||
- **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
|
||||
- **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 0–100)
|
||||
- **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0–200)
|
||||
- **i18n** — German and English (auto-detected)
|
||||
- **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
|
||||
- **Panic safety** — Panic hook logs but never unlocks (installed before logging)
|
||||
@ -48,7 +48,7 @@ Create `/etc/moonlock/moonlock.toml` or `~/.config/moonlock/moonlock.toml`:
|
||||
|
||||
```toml
|
||||
background_path = "/usr/share/wallpapers/moon.jpg"
|
||||
background_blur = 40.0 # 0.0–100.0, optional
|
||||
background_blur = 40.0 # 0.0–200.0, optional
|
||||
fingerprint_enabled = true
|
||||
```
|
||||
|
||||
|
||||
14
src/auth.rs
14
src/auth.rs
@ -191,7 +191,12 @@ pub fn authenticate(username: &str, password: &str) -> bool {
|
||||
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.
|
||||
let auth_ret = unsafe { pam_authenticate(handle, 0) };
|
||||
let acct_ret = if auth_ret == PAM_SUCCESS {
|
||||
// Safety: handle is valid, check account restrictions
|
||||
@ -211,7 +216,12 @@ pub fn authenticate(username: &str, password: &str) -> bool {
|
||||
/// 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 {
|
||||
///
|
||||
/// **Precondition**: `username` must be the authenticated system user, derived
|
||||
/// via `users::get_current_user()` (which reads `getuid()`). Calling this with
|
||||
/// an attacker-controlled username is unsafe — `pam_acct_mgmt` returns SUCCESS
|
||||
/// for any valid unlocked account, giving a trivial unlock bypass.
|
||||
pub(crate) fn check_account(username: &str) -> bool {
|
||||
let service = match CString::new("moonlock") {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
|
||||
@ -177,12 +177,24 @@ impl FingerprintListener {
|
||||
|
||||
/// 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.
|
||||
/// and restarts verification from scratch. Awaits any in-flight VerifyStop +
|
||||
/// Release before re-claiming the device so fprintd does not reject the Claim
|
||||
/// while the previous session is still being torn down.
|
||||
pub async fn resume_async(
|
||||
listener: &Rc<RefCell<FingerprintListener>>,
|
||||
username: &str,
|
||||
) {
|
||||
listener.borrow_mut().failed_attempts = 0;
|
||||
// Drain in-flight cleanup so the device is actually released before Claim.
|
||||
// Without this, a fast resume after on_verify_status's fire-and-forget
|
||||
// cleanup races the Release call and fprintd returns "already claimed".
|
||||
let proxy = listener.borrow_mut().take_cleanup_proxy();
|
||||
if let Some(proxy) = proxy {
|
||||
Self::perform_dbus_cleanup(proxy).await;
|
||||
}
|
||||
// Deliberately do NOT reset failed_attempts here. An attacker with sensor
|
||||
// control could otherwise cycle verify-match → check_account fail → resume,
|
||||
// and the 10-attempt cap would never trigger. The counter decays only via
|
||||
// a fresh lock session (listener construction).
|
||||
Self::begin_verification(listener, username).await;
|
||||
}
|
||||
|
||||
@ -352,26 +364,37 @@ impl FingerprintListener {
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect the signal handler and send VerifyStop + Release to fprintd.
|
||||
/// Signal disconnect is synchronous to prevent further callbacks.
|
||||
/// D-Bus cleanup is fire-and-forget to avoid blocking the UI.
|
||||
fn cleanup_dbus(&mut self) {
|
||||
/// Disconnect the signal handler and clear running flags. Returns the proxy
|
||||
/// the caller should use for the async D-Bus cleanup (VerifyStop + Release).
|
||||
///
|
||||
/// Split into a sync part (signal disconnect, flags) and an async part
|
||||
/// (`perform_dbus_cleanup`) so callers can either spawn the async work
|
||||
/// fire-and-forget (via `cleanup_dbus`) or await it to serialize with a
|
||||
/// subsequent Claim (via `resume_async`).
|
||||
fn take_cleanup_proxy(&mut self) -> Option<gio::DBusProxy> {
|
||||
self.running = false;
|
||||
self.running_flag.set(false);
|
||||
|
||||
if let Some(ref proxy) = self.device_proxy {
|
||||
if let Some(id) = self.signal_id.take() {
|
||||
proxy.disconnect(id);
|
||||
}
|
||||
let proxy = proxy.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let _ = proxy
|
||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await;
|
||||
let _ = proxy
|
||||
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await;
|
||||
});
|
||||
let proxy = self.device_proxy.clone()?;
|
||||
if let Some(id) = self.signal_id.take() {
|
||||
proxy.disconnect(id);
|
||||
}
|
||||
Some(proxy)
|
||||
}
|
||||
|
||||
async fn perform_dbus_cleanup(proxy: gio::DBusProxy) {
|
||||
let _ = proxy
|
||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -244,6 +244,10 @@ pub fn create_lockscreen_window(
|
||||
if password.is_empty() {
|
||||
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);
|
||||
let username = username.clone();
|
||||
@ -518,12 +522,35 @@ pub fn start_fingerprint(
|
||||
/// 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.
|
||||
/// 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> {
|
||||
let file = gio::File::for_path(bg_path);
|
||||
match gdk::Texture::from_file(&file) {
|
||||
use std::io::Read;
|
||||
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),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load wallpaper {}: {e}", bg_path.display());
|
||||
log::warn!("Failed to decode wallpaper {}: {e}", bg_path.display());
|
||||
None
|
||||
}
|
||||
}
|
||||
@ -565,11 +592,11 @@ fn create_background_picture(
|
||||
background
|
||||
}
|
||||
|
||||
/// Maximum texture dimension for blur input. Textures larger than this are
|
||||
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
|
||||
// are duplicated in moongreet/src/greeter.rs and moonset/src/panel.rs.
|
||||
// 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
|
||||
/// no visible quality loss, but GPU work is reduced significantly.
|
||||
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
||||
@ -623,30 +650,41 @@ fn render_blurred_texture(
|
||||
}
|
||||
|
||||
/// 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(
|
||||
image: >k::Image,
|
||||
path: &Path,
|
||||
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
) {
|
||||
let path_str = match path.to_str() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
log::warn!("Avatar path is not valid UTF-8: {:?}", path);
|
||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||
return;
|
||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||
|
||||
let display_path = path.to_path_buf();
|
||||
let file = gio::File::for_path(path);
|
||||
let image_clone = image.clone();
|
||||
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.
|
||||
|
||||
46
src/main.rs
46
src/main.rs
@ -168,32 +168,38 @@ fn init_fingerprint_async(
|
||||
let mut listener = FingerprintListener::new();
|
||||
listener.init_async().await;
|
||||
|
||||
let handles = all_handles.borrow();
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Extract username without holding a borrow across the await below —
|
||||
// otherwise a concurrent connect_monitor signal (hotplug / suspend-resume)
|
||||
// that tries to borrow_mut() panics at runtime.
|
||||
let username = {
|
||||
let handles = all_handles.borrow();
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
let u = handles[0].username.clone();
|
||||
if u.is_empty() {
|
||||
return;
|
||||
}
|
||||
u
|
||||
};
|
||||
|
||||
// Use the first monitor's username to check enrollment
|
||||
let username = &handles[0].username;
|
||||
if username.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !listener.is_available_async(username).await {
|
||||
if !listener.is_available_async(&username).await {
|
||||
log::debug!("fprintd not available or no enrolled fingers");
|
||||
return;
|
||||
}
|
||||
|
||||
let fp_rc = Rc::new(RefCell::new(listener));
|
||||
|
||||
// Show fingerprint label on all existing monitors
|
||||
for h in handles.iter() {
|
||||
lockscreen::show_fingerprint_label(h, &fp_rc);
|
||||
// Re-borrow after the await — no further awaits in this scope, so it is
|
||||
// safe to hold the borrow briefly while wiring up the labels.
|
||||
{
|
||||
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
|
||||
lockscreen::start_fingerprint(&handles[0], &fp_rc);
|
||||
|
||||
// Publish the listener so hotplugged monitors get FP labels too
|
||||
*shared_fp.borrow_mut() = Some(fp_rc);
|
||||
});
|
||||
@ -244,11 +250,17 @@ fn setup_logging() {
|
||||
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() {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
#[cfg(not(debug_assertions))]
|
||||
let level = log::LevelFilter::Info;
|
||||
log::set_max_level(level);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user