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:
+49
-21
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user