5 Commits

Author SHA1 Message Date
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
nevaforget 2a9cc52223 fix: audit fixes — peek icon, blur limit, GResource compression, sync markers (v0.6.8)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- Enable peek icon on password entry (consistent with moongreet)
- Raise blur limit from 100 to 200 (consistent with moongreet/moonset)
- Add compressed="true" to GResource CSS/SVG entries
- Add SYNC comments to duplicated blur/background functions
2026-03-31 11:08:36 +02:00
nevaforget 102520d15f docs: update README features and fix build.rs comment
README was missing features added since v0.6.1 (GPU blur, journal
logging, lock-first architecture, PAM timeout, fprintd sender
validation, progressive faillock). build.rs comment still referenced
removed wallpaper.jpg.
2026-03-31 09:34:06 +02:00
10 changed files with 120 additions and 54 deletions
+3 -3
View File
@@ -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 - `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,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 - `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 ## Sicherheit
@@ -55,10 +55,10 @@ 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) - 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: 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 - 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)
- Kein Schließen per Escape/Alt-F4 — nur durch erfolgreiche PAM-Auth oder Fingerprint - Kein Schließen per Escape/Alt-F4 — nur durch erfolgreiche PAM-Auth oder Fingerprint
- Kein Peek-Icon am Passwortfeld (Shoulder-Surfing-Schutz) - Peek-Icon am Passwortfeld aktiv (UX-Entscheidung, konsistent mit moongreet)
- GResource-Bundle: CSS/Assets in der Binary kompiliert - GResource-Bundle: CSS/Assets in der Binary kompiliert
Generated
+1 -1
View File
@@ -575,7 +575,7 @@ dependencies = [
[[package]] [[package]]
name = "moonlock" name = "moonlock"
version = "0.6.7" version = "0.6.8"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
+2 -2
View File
@@ -1,13 +1,13 @@
[package] [package]
name = "moonlock" name = "moonlock"
version = "0.6.7" version = "0.6.9"
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"
+14
View File
@@ -2,6 +2,20 @@
Architectural and design decisions for Moonlock, in reverse chronological order. 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
- **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.
- **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**: Nyx, Dom
+10 -7
View File
@@ -5,14 +5,16 @@ Part of the Moonarch ecosystem.
## Features ## Features
- **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash) - **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`) - **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter
- **Fingerprint unlock** — fprintd D-Bus integration, async init (optional, window appears instantly) - **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, single shared fingerprint listener - **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)
- **i18n** — German and English (auto-detected) - **i18n** — German and English (auto-detected)
- **Faillock warning** — UI counter + system pam_faillock - **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
- **Panic safety** — Panic hook logs but never unlocks - **Panic safety** — Panic hook logs but never unlocks (installed before logging)
- **Password wiping** — Zeroize on drop - **Password wiping** — `Zeroize` on drop from GTK entry through PAM FFI layer
- **Journal logging** — `journalctl -t moonlock`, debug level via `MOONLOCK_DEBUG` env var
## Requirements ## Requirements
@@ -46,6 +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
fingerprint_enabled = true fingerprint_enabled = true
``` ```
+1 -1
View File
@@ -1,5 +1,5 @@
// ABOUTME: Build script for compiling GResource bundle. // ABOUTME: Build script for compiling GResource bundle.
// ABOUTME: Bundles style.css, wallpaper.jpg, and default-avatar.svg into the binary. // ABOUTME: Bundles style.css and default-avatar.svg into the binary.
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <gresources>
<gresource prefix="/dev/moonarch/moonlock"> <gresource prefix="/dev/moonarch/moonlock">
<file>style.css</file> <file compressed="true">style.css</file>
<file>default-avatar.svg</file> <file compressed="true">default-avatar.svg</file>
</gresource> </gresource>
</gresources> </gresources>
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
Ok(parsed) => { Ok(parsed) => {
if parsed.background_path.is_some() { merged.background_path = parsed.background_path; } if parsed.background_path.is_some() { merged.background_path = parsed.background_path; }
if let Some(blur) = parsed.background_blur { if let Some(blur) = parsed.background_blur {
merged.background_blur = Some(blur.clamp(0.0, 100.0)); merged.background_blur = Some(blur.clamp(0.0, 200.0));
} }
if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; } if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; }
} }
+5 -1
View File
@@ -137,7 +137,7 @@ pub fn create_lockscreen_window(
// Password entry // Password entry
let password_entry = gtk::PasswordEntry::builder() let password_entry = gtk::PasswordEntry::builder()
.placeholder_text(strings.password_placeholder) .placeholder_text(strings.password_placeholder)
.show_peek_icon(false) .show_peek_icon(true)
.hexpand(true) .hexpand(true)
.build(); .build();
password_entry.add_css_class("password-entry"); password_entry.add_css_class("password-entry");
@@ -566,6 +566,10 @@ fn create_background_picture(
} }
/// Maximum texture dimension for blur input. Textures larger than this are /// 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.
/// 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;
+81 -36
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,50 +92,89 @@ 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;
let handles = all_handles.borrow();
if handles.is_empty() {
return;
}
// Use the first monitor's username to check enrollment // Use the first monitor's username to check enrollment
let username = &all_handles[0].username; let username = &handles[0].username;
if username.is_empty() { if username.is_empty() {
return; return;
} }
@@ -146,13 +186,16 @@ fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
let fp_rc = Rc::new(RefCell::new(listener)); let fp_rc = Rc::new(RefCell::new(listener));
// Show fingerprint label on all monitors // Show fingerprint label on all existing monitors
for handles in &all_handles { for h in handles.iter() {
lockscreen::show_fingerprint_label(handles, &fp_rc); lockscreen::show_fingerprint_label(h, &fp_rc);
} }
// Start verification listener on the first monitor only // 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 // 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);
} }
} }