fix: audit fixes — D-Bus sender validation, fp lifecycle, multi-monitor caching (v0.6.0)

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.
This commit is contained in:
2026-03-28 22:47:09 +01:00
parent 4026f6dafa
commit d11b6e634e
10 changed files with 176 additions and 55 deletions
+50 -12
View File
@@ -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: &gtk::Application,
unlock_callback: Rc<dyn Fn()>,
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
avatar_cache: &Rc<RefCell<Option<gdk::Texture>>>,
) -> 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<f32>) -> 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<f32>,
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
) -> 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<f32>) -
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: &gtk::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: &gtk::Image,
path: &Path,
cache: &Rc<RefCell<Option<gdk::Texture>>>,
) {
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: &gtk::Image, path: &Path) {
}
/// Load the default avatar SVG from GResources, tinted with the foreground color.
fn set_default_avatar(image: &gtk::Image, window: &gtk::ApplicationWindow) {
/// Stores the texture in the cache for reuse on additional monitors.
fn set_default_avatar(
image: &gtk::Image,
window: &gtk::ApplicationWindow,
cache: &Rc<RefCell<Option<gdk::Texture>>>,
) {
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: &gtk::Image, window: &gtk::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;
}
}