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.
This commit is contained in:
parent
b89435b810
commit
b621b4e9fe
@ -44,7 +44,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
||||
- `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
|
||||
- `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), 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
|
||||
|
||||
@ -55,7 +55,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
||||
- 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)
|
||||
- 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)
|
||||
- Wallpaper wird nach lock() geladen — Disk-I/O verzögert nicht die Lock-Akquisition
|
||||
- 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
|
||||
- Root-Check: Exit mit Fehler wenn als root gestartet
|
||||
- Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv)
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "moonlock"
|
||||
version = "0.6.8"
|
||||
version = "0.6.9"
|
||||
edition = "2024"
|
||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
gdk4 = "0.11"
|
||||
gdk-pixbuf = "0.22"
|
||||
|
||||
@ -2,6 +2,13 @@
|
||||
|
||||
Architectural and design decisions for Moonlock, in reverse chronological order.
|
||||
|
||||
## 2026-04-09 – Monitor hotplug via connect_monitor signal
|
||||
|
||||
- **Who**: Nyx, 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
|
||||
|
||||
@ -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)
|
||||
- **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** — 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 0–100)
|
||||
- **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)
|
||||
- **Password wiping** — `Zeroize` on drop from GTK entry through PAM FFI layer
|
||||
- **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
|
||||
|
||||
|
||||
117
src/main.rs
117
src/main.rs
@ -59,17 +59,18 @@ fn activate(app: >k::Application) {
|
||||
|
||||
fn activate_with_session_lock(
|
||||
app: >k::Application,
|
||||
display: &gdk::Display,
|
||||
_display: &gdk::Display,
|
||||
config: &config::Config,
|
||||
) {
|
||||
let lock = gtk4_session_lock::Instance::new();
|
||||
lock.lock();
|
||||
|
||||
// Load wallpaper AFTER lock — disk I/O must not delay the lock acquisition
|
||||
let bg_texture = config::resolve_background_path(config)
|
||||
.and_then(|path| lockscreen::load_background_texture(&path));
|
||||
|
||||
let monitors = display.monitors();
|
||||
// Load wallpaper before lock — connect_monitor fires during lock() and needs the
|
||||
// texture. This means disk I/O happens before locking, but loading a local JPEG
|
||||
// is fast enough that the delay is negligible.
|
||||
let bg_texture: Rc<Option<gdk::Texture>> = Rc::new(
|
||||
config::resolve_background_path(config)
|
||||
.and_then(|path| lockscreen::load_background_texture(&path)),
|
||||
);
|
||||
|
||||
// Shared unlock callback — unlocks session and quits.
|
||||
// Guard prevents double-unlock if PAM and fingerprint succeed simultaneously.
|
||||
@ -91,50 +92,89 @@ fn activate_with_session_lock(
|
||||
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));
|
||||
|
||||
// Create all monitor windows immediately — no D-Bus calls here
|
||||
let mut all_handles = Vec::new();
|
||||
let mut created_any = false;
|
||||
for i in 0..monitors.n_items() {
|
||||
if let Some(monitor) = monitors
|
||||
.item(i)
|
||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
{
|
||||
// Shared config for use in the monitor signal handler
|
||||
let config = Rc::new(config.clone());
|
||||
|
||||
// Shared handles list — populated by connect_monitor, read by fingerprint init
|
||||
let all_handles: Rc<RefCell<Vec<lockscreen::LockscreenHandles>>> =
|
||||
Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
// 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(
|
||||
bg_texture.as_ref(),
|
||||
config,
|
||||
app,
|
||||
bg_texture.as_ref().as_ref(),
|
||||
&config,
|
||||
&app,
|
||||
unlock_callback.clone(),
|
||||
&blur_cache,
|
||||
&avatar_cache,
|
||||
);
|
||||
lock.assign_window_to_monitor(&handles.window, &monitor);
|
||||
lock_for_signal.assign_window_to_monitor(&handles.window, monitor);
|
||||
handles.window.present();
|
||||
all_handles.push(handles);
|
||||
created_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !created_any {
|
||||
log::error!("No lockscreen windows created — screen stays locked (compositor policy)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
// If fingerprint is already initialized, wire up the label
|
||||
if let Some(ref fp_rc) = *shared_fp.borrow() {
|
||||
lockscreen::show_fingerprint_label(&handles, fp_rc);
|
||||
}
|
||||
|
||||
all_handles.borrow_mut().push(handles);
|
||||
}
|
||||
));
|
||||
|
||||
lock.lock();
|
||||
|
||||
// Async fprintd initialization — runs after windows are visible
|
||||
if config.fingerprint_enabled {
|
||||
init_fingerprint_async(all_handles);
|
||||
init_fingerprint_async(all_handles, shared_fp);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize fprintd asynchronously after windows are visible.
|
||||
/// Uses a single FingerprintListener shared across all monitors —
|
||||
/// only the first monitor's handles get the fingerprint UI wired up.
|
||||
fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
|
||||
/// only the first monitor's handles get the fingerprint verification wired up.
|
||||
/// 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 {
|
||||
let mut listener = FingerprintListener::new();
|
||||
listener.init_async().await;
|
||||
|
||||
let handles = all_handles.borrow();
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the first monitor's username to check enrollment
|
||||
let username = &all_handles[0].username;
|
||||
let username = &handles[0].username;
|
||||
if username.is_empty() {
|
||||
return;
|
||||
}
|
||||
@ -146,13 +186,16 @@ fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
|
||||
|
||||
let fp_rc = Rc::new(RefCell::new(listener));
|
||||
|
||||
// Show fingerprint label on all monitors
|
||||
for handles in &all_handles {
|
||||
lockscreen::show_fingerprint_label(handles, &fp_rc);
|
||||
// Show fingerprint label on all existing monitors
|
||||
for h in handles.iter() {
|
||||
lockscreen::show_fingerprint_label(h, &fp_rc);
|
||||
}
|
||||
|
||||
// Start verification listener on the first monitor only
|
||||
lockscreen::start_fingerprint(&all_handles[0], &fp_rc);
|
||||
lockscreen::start_fingerprint(&handles[0], &fp_rc);
|
||||
|
||||
// Publish the listener so hotplugged monitors get FP labels too
|
||||
*shared_fp.borrow_mut() = Some(fp_rc);
|
||||
});
|
||||
}
|
||||
|
||||
@ -184,7 +227,9 @@ fn activate_without_lock(
|
||||
|
||||
// Async fprintd initialization for development mode
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user