From d11b6e634e3f562035944c9983b96b316fb1e7fe Mon Sep 17 00:00:00 2001 From: nevaforget Date: Sat, 28 Mar 2026 22:47:09 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20audit=20fixes=20=E2=80=94=20D-Bus=20send?= =?UTF-8?q?er=20validation,=20fp=20lifecycle,=20multi-monitor=20caching=20?= =?UTF-8?q?(v0.6.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the only exploitable auth bypass: validate VerifyStatus signal sender against fprintd's unique bus name. Fix fingerprint D-Bus lifecycle so devices are properly released on verify-match and async restarts check the running flag between awaits. Security: num_msg guard in PAM callback, symlink rejection for background_path, peek icon disabled, TOML parse errors logged, panic hook before logging. Performance: blur and avatar textures cached across monitors, release profile with LTO/strip. --- CLAUDE.md | 14 ++++++---- Cargo.lock | 2 +- Cargo.toml | 7 ++++- DECISIONS.md | 7 +++++ src/auth.rs | 4 +++ src/config.rs | 35 +++++++++++++++++++---- src/fingerprint.rs | 70 ++++++++++++++++++++++++++++++++-------------- src/lockscreen.rs | 62 ++++++++++++++++++++++++++++++++-------- src/main.rs | 27 ++++++++++++++---- src/power.rs | 3 -- 10 files changed, 176 insertions(+), 55 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 26318c9..6b6e6f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,22 +38,24 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock ## Architektur - `auth.rs` — PAM-Authentifizierung via Raw FFI (unsafe extern "C" conv callback, msg_style-aware, Zeroizing>) -- `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, async fire-and-forget stop, on_exhausted callback after MAX_FP_ATTEMPTS +- `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 - `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, fingerprint_enabled als Option) + Wallpaper-Fallback -- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking, FP-Label/Start separat verdrahtet, Zeroizing für Passwort, Power-Confirm, GPU-Blur via GskBlurNode -- `main.rs` — Entry Point, Panic-Hook, Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor, systemd-Journal-Logging, async fprintd-Init nach window.present() +- `config.rs` — TOML-Config (background_path, background_blur, fingerprint_enabled als Option) + Wallpaper-Fallback + Symlink-Rejection für background_path + Parse-Error-Logging +- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking, FP-Label/Start separat verdrahtet, Zeroizing für Passwort, Power-Confirm, GPU-Blur via GskBlurNode, 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, async fprintd-Init nach window.present() ## Sicherheit - ext-session-lock-v1 garantiert: Compositor sperrt alle Surfaces bei lock() - Release-Build: Ohne ext-session-lock-v1 wird `exit(1)` aufgerufen — kein Fenster-Fallback -- Panic-Hook: Bei Crash wird geloggt, aber NIEMALS unlock() aufgerufen — Screen bleibt schwarz -- PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher +- Panic-Hook: Bei Crash wird geloggt, aber NIEMALS unlock() aufgerufen — Screen bleibt schwarz. Hook wird vor Logging installiert. +- 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) - Passwort: Zeroizing ab GTK-Entry-Extraktion, Zeroizing> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und CString werden nicht gezeroized — inhärente GTK/libc-Limitierung) - Root-Check: Exit mit Fehler wenn als root gestartet - Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv) - Kein Schließen per Escape/Alt-F4 — nur durch erfolgreiche PAM-Auth oder Fingerprint +- Kein Peek-Icon am Passwortfeld (Shoulder-Surfing-Schutz) - GResource-Bundle: CSS/Assets in der Binary kompiliert diff --git a/Cargo.lock b/Cargo.lock index 8dc56d8..2b7e2ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,7 +575,7 @@ dependencies = [ [[package]] name = "moonlock" -version = "0.5.0" +version = "0.6.0" dependencies = [ "gdk-pixbuf", "gdk4", diff --git a/Cargo.toml b/Cargo.toml index 6723937..100a516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moonlock" -version = "0.5.1" +version = "0.6.0" edition = "2024" description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support" license = "MIT" @@ -26,3 +26,8 @@ tempfile = "3" [build-dependencies] glib-build-tools = "0.22" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true diff --git a/DECISIONS.md b/DECISIONS.md index d7d5b60..cc156ce 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -2,6 +2,13 @@ Architectural and design decisions for Moonlock, in reverse chronological order. +## 2026-03-28 – Audit-driven security and lifecycle fixes (v0.6.0) + +- **Who**: Nyx, 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>` 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>` 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 diff --git a/src/auth.rs b/src/auth.rs index 960b9f3..9e47467 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -71,6 +71,10 @@ unsafe extern "C" fn pam_conv_callback( appdata_ptr: *mut libc::c_void, ) -> libc::c_int { unsafe { + if num_msg <= 0 { + return PAM_AUTH_ERR; + } + // Safety: appdata_ptr was set to a valid *const CString in authenticate() let password = appdata_ptr as *const CString; if password.is_null() { diff --git a/src/config.rs b/src/config.rs index a667914..022b9e8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -49,10 +49,15 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { let mut merged = Config::default(); for path in paths { if let Ok(content) = fs::read_to_string(path) { - if let Ok(parsed) = toml::from_str::(&content) { - if parsed.background_path.is_some() { merged.background_path = parsed.background_path; } - if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; } - if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; } + match toml::from_str::(&content) { + Ok(parsed) => { + if parsed.background_path.is_some() { merged.background_path = parsed.background_path; } + if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; } + if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; } + } + Err(e) => { + log::warn!("Failed to parse {}: {e}", path.display()); + } } } } @@ -66,7 +71,7 @@ pub fn resolve_background_path(config: &Config) -> PathBuf { pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> PathBuf { if let Some(ref bg) = config.background_path { let path = PathBuf::from(bg); - if path.is_file() { return path; } + if path.is_file() && !path.is_symlink() { return path; } } if moonarch_wallpaper.is_file() { return moonarch_wallpaper.to_path_buf(); } PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg")) @@ -120,4 +125,24 @@ mod tests { let r = resolve_background_path_with(&c, Path::new("/nonexistent")); assert!(r.to_str().unwrap().contains("moonlock")); } + #[test] fn toml_parse_error_returns_default() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moonlock.toml"); + fs::write(&conf, "this is not valid toml {{{{").unwrap(); + let c = load_config(Some(&[conf])); + assert!(c.fingerprint_enabled); + assert!(c.background_path.is_none()); + } + #[cfg(unix)] + #[test] fn symlink_rejected_for_background() { + let dir = tempfile::tempdir().unwrap(); + let real = dir.path().join("bg.jpg"); + let link = dir.path().join("link.jpg"); + fs::write(&real, "fake").unwrap(); + std::os::unix::fs::symlink(&real, &link).unwrap(); + let c = Config { background_path: Some(link.to_str().unwrap().to_string()), ..Config::default() }; + // Symlink should be rejected — falls through to moonarch wallpaper or gresource + let r = resolve_background_path_with(&c, Path::new("/nonexistent")); + assert_ne!(r, link); + } } diff --git a/src/fingerprint.rs b/src/fingerprint.rs index d494e4b..0f7dc92 100644 --- a/src/fingerprint.rs +++ b/src/fingerprint.rs @@ -3,7 +3,7 @@ use gio::prelude::*; use gtk4::gio; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint"; @@ -12,6 +12,7 @@ const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager"; const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device"; const MAX_FP_ATTEMPTS: u32 = 10; +const DBUS_TIMEOUT_MS: i32 = 3000; /// Retry-able statuses — finger not read properly, try again. const RETRY_STATUSES: &[&str] = &[ @@ -26,6 +27,8 @@ pub struct FingerprintListener { device_proxy: Option, signal_id: Option, running: bool, + /// Shared flag for async tasks to detect stop() between awaits. + running_flag: Rc>, failed_attempts: u32, on_success: Option>, on_failure: Option>, @@ -40,6 +43,7 @@ impl FingerprintListener { device_proxy: None, signal_id: None, running: false, + running_flag: Rc::new(Cell::new(false)), failed_attempts: 0, on_success: None, on_failure: None, @@ -68,7 +72,7 @@ impl FingerprintListener { // Call GetDefaultDevice let result = match manager - .call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, -1) + .call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await { Ok(r) => r, @@ -118,7 +122,7 @@ impl FingerprintListener { let args = glib::Variant::from((&username,)); match proxy - .call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, -1) + .call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await { Ok(result) => { @@ -168,7 +172,7 @@ impl FingerprintListener { // Claim the device let args = glib::Variant::from((&username,)); if let Err(e) = proxy - .call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, -1) + .call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await { log::error!("Failed to claim fingerprint device: {e}"); @@ -178,20 +182,34 @@ impl FingerprintListener { // Start verification let start_args = glib::Variant::from((&"any",)); if let Err(e) = proxy - .call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, -1) + .call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await { log::error!("Failed to start fingerprint verification: {e}"); let _ = proxy - .call_future("Release", None, gio::DBusCallFlags::NONE, -1) + .call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await; return; } + // Capture the unique bus name of fprintd for sender validation. + // D-Bus signals carry the sender's unique name (e.g. ":1.42"), not the + // well-known name. We validate this to prevent signal spoofing. + let expected_sender = proxy.name_owner(); + // Connect the g-signal handler on the proxy to dispatch VerifyStatus let listener_weak = Rc::downgrade(listener); let signal_id = proxy.connect_local("g-signal", false, move |values| { // g-signal arguments: (proxy, sender_name, signal_name, parameters) + let sender: String = match values[1].get() { + Ok(s) => s, + Err(_) => return None, + }; + if expected_sender.as_ref().map(|s| s.as_str()) != Some(sender.as_str()) { + log::warn!("Ignoring D-Bus signal from unexpected sender: {sender}"); + return None; + } + let signal_name: String = match values[2].get() { Ok(v) => v, Err(_) => return None, @@ -224,6 +242,7 @@ impl FingerprintListener { let mut inner = listener.borrow_mut(); inner.signal_id = Some(signal_id); inner.running = true; + inner.running_flag.set(true); } /// Process a VerifyStatus signal from fprintd. @@ -233,7 +252,7 @@ impl FingerprintListener { } if status == "verify-match" { - self.running = false; + self.cleanup_dbus(); if let Some(ref cb) = self.on_success { cb(); } @@ -270,17 +289,22 @@ impl FingerprintListener { } /// Restart fingerprint verification asynchronously after a completed attempt. + /// Checks running_flag after VerifyStop to avoid restarting on a released device. fn restart_verify_async(&self) { if let Some(ref proxy) = self.device_proxy { let proxy = proxy.clone(); + let running = self.running_flag.clone(); glib::spawn_future_local(async move { // VerifyStop before VerifyStart to avoid D-Bus errors let _ = proxy - .call_future("VerifyStop", None, gio::DBusCallFlags::NONE, -1) + .call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await; + if !running.get() { + return; + } let args = glib::Variant::from((&"any",)); if let Err(e) = proxy - .call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, -1) + .call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await { log::error!("Failed to restart fingerprint verification: {e}"); @@ -289,14 +313,12 @@ impl FingerprintListener { } } - /// Stop listening and release the device. + /// Disconnect the signal handler and send VerifyStop + Release to fprintd. /// Signal disconnect is synchronous to prevent further callbacks. - /// D-Bus cleanup (VerifyStop + Release) is fire-and-forget to avoid blocking the UI. - pub fn stop(&mut self) { - if !self.running { - return; - } + /// D-Bus cleanup is fire-and-forget to avoid blocking the UI. + fn cleanup_dbus(&mut self) { self.running = false; + self.running_flag.set(false); if let Some(ref proxy) = self.device_proxy { if let Some(id) = self.signal_id.take() { @@ -305,15 +327,23 @@ impl FingerprintListener { let proxy = proxy.clone(); glib::spawn_future_local(async move { let _ = proxy - .call_future("VerifyStop", None, gio::DBusCallFlags::NONE, 3000) + .call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await; let _ = proxy - .call_future("Release", None, gio::DBusCallFlags::NONE, 3000) + .call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await; }); } } + /// Stop listening and release the device. Idempotent — safe to call multiple times. + pub fn stop(&mut self) { + if !self.running { + return; + } + self.cleanup_dbus(); + } + } #[cfg(test)] @@ -335,21 +365,21 @@ mod tests { #[test] fn verify_match_sets_running_false_and_calls_success() { - use std::cell::Cell; let called = Rc::new(Cell::new(false)); let called_clone = called.clone(); let mut listener = FingerprintListener::new(); listener.running = true; + listener.running_flag.set(true); listener.on_success = Some(Box::new(move || { called_clone.set(true); })); listener.on_verify_status("verify-match", false); assert!(called.get()); assert!(!listener.running); + assert!(!listener.running_flag.get()); } #[test] fn verify_no_match_calls_failure_and_stays_running() { - use std::cell::Cell; let called = Rc::new(Cell::new(false)); let called_clone = called.clone(); let mut listener = FingerprintListener::new(); @@ -364,7 +394,6 @@ mod tests { #[test] fn max_attempts_stops_listener_and_calls_exhausted() { - use std::cell::Cell; let exhausted = Rc::new(Cell::new(false)); let exhausted_clone = exhausted.clone(); let mut listener = FingerprintListener::new(); @@ -382,7 +411,6 @@ mod tests { #[test] fn not_running_ignores_signals() { - use std::cell::Cell; let called = Rc::new(Cell::new(false)); let called_clone = called.clone(); let mut listener = FingerprintListener::new(); diff --git a/src/lockscreen.rs b/src/lockscreen.rs index 6741b9c..3c06086 100644 --- a/src/lockscreen.rs +++ b/src/lockscreen.rs @@ -41,11 +41,15 @@ struct LockscreenState { /// Create a lockscreen window for a single monitor. /// Fingerprint is not initialized here — use `wire_fingerprint()` after async init. +/// The `blur_cache` and `avatar_cache` are shared across monitors for multi-monitor +/// setups, avoiding redundant GPU renders and SVG rasterizations. pub fn create_lockscreen_window( bg_texture: &gdk::Texture, config: &Config, app: >k::Application, unlock_callback: Rc, + blur_cache: &Rc>>, + avatar_cache: &Rc>>, ) -> LockscreenHandles { let window = gtk::ApplicationWindow::builder() .application(app) @@ -83,7 +87,7 @@ pub fn create_lockscreen_window( window.set_child(Some(&overlay)); // Background wallpaper - let background = create_background_picture(bg_texture, config.background_blur); + let background = create_background_picture(bg_texture, config.background_blur, blur_cache); overlay.set_child(Some(&background)); // Centered vertical box @@ -109,12 +113,17 @@ pub fn create_lockscreen_window( avatar_frame.append(&avatar_image); login_box.append(&avatar_frame); - // Load avatar - let avatar_path = users::get_avatar_path(&user.home, &user.username); - if let Some(path) = avatar_path { - set_avatar_from_file(&avatar_image, &path); + // Load avatar — use shared cache to avoid redundant loading on multi-monitor setups. + // The cache is populated by the first monitor and reused by subsequent ones. + if let Some(ref cached) = *avatar_cache.borrow() { + avatar_image.set_paintable(Some(cached)); } else { - set_default_avatar(&avatar_image, &window); + let avatar_path = users::get_avatar_path(&user.home, &user.username); + if let Some(path) = avatar_path { + set_avatar_from_file(&avatar_image, &path, avatar_cache); + } else { + set_default_avatar(&avatar_image, &window, avatar_cache); + } } // Username label @@ -125,7 +134,7 @@ pub fn create_lockscreen_window( // Password entry let password_entry = gtk::PasswordEntry::builder() .placeholder_text(strings.password_placeholder) - .show_peek_icon(true) + .show_peek_icon(false) .hexpand(true) .build(); password_entry.add_css_class("password-entry"); @@ -361,12 +370,18 @@ pub fn start_fingerprint( let fp_label_fail = handles.fp_label.clone(); let unlock_cb_fp = handles.unlock_callback.clone(); + let fp_rc_success = fp_rc.clone(); let on_success = move || { let label = fp_label_success.clone(); let cb = unlock_cb_fp.clone(); + let fp = fp_rc_success.clone(); glib::idle_add_local_once(move || { - label.set_text(load_strings(None).fingerprint_success); + let strings = load_strings(None); + label.set_text(strings.fingerprint_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(); cb(); }); }; @@ -434,7 +449,13 @@ pub fn load_background_texture(bg_path: &Path) -> gdk::Texture { /// When `blur_radius` is `Some(sigma)` with sigma > 0, blur is applied via GPU /// (GskBlurNode). The blur is rendered to a concrete texture on `realize` (when /// the GPU renderer is available), avoiding lazy-render artifacts. -fn create_background_picture(texture: &gdk::Texture, blur_radius: Option) -> gtk::Picture { +/// The `blur_cache` is shared across monitors — the first to realize renders the +/// blur, subsequent monitors reuse the cached texture. +fn create_background_picture( + texture: &gdk::Texture, + blur_radius: Option, + blur_cache: &Rc>>, +) -> gtk::Picture { let background = gtk::Picture::for_paintable(texture); background.set_content_fit(gtk::ContentFit::Cover); background.set_hexpand(true); @@ -443,9 +464,15 @@ fn create_background_picture(texture: &gdk::Texture, blur_radius: Option) - if let Some(sigma) = blur_radius { if sigma > 0.0 { let texture = texture.clone(); + let cache = blur_cache.clone(); background.connect_realize(move |picture| { + if let Some(ref cached) = *cache.borrow() { + picture.set_paintable(Some(cached)); + return; + } if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) { picture.set_paintable(Some(&blurred)); + *cache.borrow_mut() = Some(blurred); } }); } @@ -477,12 +504,17 @@ fn render_blurred_texture( Some(renderer.render_texture(&node, None)) } -/// Load an image file and set it as the avatar. -fn set_avatar_from_file(image: >k::Image, path: &Path) { +/// Load an image file and set it as the avatar. Stores the texture in the cache. +fn set_avatar_from_file( + image: >k::Image, + path: &Path, + cache: &Rc>>, +) { match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), 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(_) => { image.set_icon_name(Some("avatar-default-symbolic")); @@ -491,7 +523,12 @@ fn set_avatar_from_file(image: >k::Image, path: &Path) { } /// Load the default avatar SVG from GResources, tinted with the foreground color. -fn set_default_avatar(image: >k::Image, window: >k::ApplicationWindow) { +/// Stores the texture in the cache for reuse on additional monitors. +fn set_default_avatar( + image: >k::Image, + window: >k::ApplicationWindow, + cache: &Rc>>, +) { let resource_path = users::get_default_avatar_path(); if let Ok(bytes) = gio::resources_lookup_data(&resource_path, gio::ResourceLookupFlags::NONE) @@ -513,6 +550,7 @@ fn set_default_avatar(image: >k::Image, window: >k::ApplicationWindow) { if let Some(pixbuf) = loader.pixbuf() { let texture = gdk::Texture::for_pixbuf(&pixbuf); image.set_paintable(Some(&texture)); + *cache.borrow_mut() = Some(texture); return; } } diff --git a/src/main.rs b/src/main.rs index fcfae7a..9361751 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,6 +86,10 @@ fn activate_with_session_lock( app_clone.quit(); }); + // Shared caches for multi-monitor — first monitor renders, rest reuse + let blur_cache: Rc>> = Rc::new(RefCell::new(None)); + let avatar_cache: Rc>> = Rc::new(RefCell::new(None)); + // Create all monitor windows immediately — no D-Bus calls here let mut all_handles = Vec::new(); let mut created_any = false; @@ -99,6 +103,8 @@ fn activate_with_session_lock( config, app, unlock_callback.clone(), + &blur_cache, + &avatar_cache, ); lock.assign_window_to_monitor(&handles.window, &monitor); handles.window.present(); @@ -160,11 +166,15 @@ fn activate_without_lock( app_clone.quit(); }); + let blur_cache = Rc::new(RefCell::new(None)); + let avatar_cache = Rc::new(RefCell::new(None)); let handles = lockscreen::create_lockscreen_window( bg_texture, config, app, unlock_callback, + &blur_cache, + &avatar_cache, ); handles.window.set_default_size(800, 600); handles.window.present(); @@ -176,10 +186,16 @@ fn activate_without_lock( } fn setup_logging() { - systemd_journal_logger::JournalLog::new() - .unwrap() - .install() - .unwrap(); + match systemd_journal_logger::JournalLog::new() { + Ok(logger) => { + if let Err(e) = logger.install() { + eprintln!("Failed to install journal logger: {e}"); + } + } + Err(e) => { + eprintln!("Failed to create journal logger: {e}"); + } + } log::set_max_level(log::LevelFilter::Info); } @@ -195,6 +211,7 @@ fn install_panic_hook() { } fn main() { + install_panic_hook(); setup_logging(); // Root check — moonlock should not run as root @@ -202,8 +219,6 @@ fn main() { log::error!("Moonlock should not run as root"); std::process::exit(1); } - - install_panic_hook(); log::info!("Moonlock starting"); // Register compiled GResources diff --git a/src/power.rs b/src/power.rs index fef7c0f..60202e4 100644 --- a/src/power.rs +++ b/src/power.rs @@ -7,14 +7,12 @@ use std::process::Command; #[derive(Debug)] pub enum PowerError { CommandFailed { action: &'static str, message: String }, - Timeout { action: &'static str }, } impl fmt::Display for PowerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PowerError::CommandFailed { action, message } => write!(f, "{action} failed: {message}"), - PowerError::Timeout { action } => write!(f, "{action} timed out"), } } } @@ -44,7 +42,6 @@ mod tests { use super::*; #[test] fn power_error_display() { assert_eq!(PowerError::CommandFailed { action: "reboot", message: "fail".into() }.to_string(), "reboot failed: fail"); } - #[test] fn timeout_display() { assert_eq!(PowerError::Timeout { action: "shutdown" }.to_string(), "shutdown timed out"); } #[test] fn missing_binary() { assert!(run_command("test", "nonexistent-xyz", &[]).is_err()); } #[test] fn nonzero_exit() { assert!(run_command("test", "false", &[]).is_err()); } #[test] fn success() { assert!(run_command("test", "true", &[]).is_ok()); }