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
+49 -21
View File
@@ -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<gio::DBusProxy>,
signal_id: Option<glib::SignalHandlerId>,
running: bool,
/// Shared flag for async tasks to detect stop() between awaits.
running_flag: Rc<Cell<bool>>,
failed_attempts: u32,
on_success: Option<Box<dyn Fn() + 'static>>,
on_failure: Option<Box<dyn Fn() + 'static>>,
@@ -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();