fix: audit MEDIUM fixes — D-Bus race, TOCTOU, FP reset, entry clear (v0.6.11)

- fingerprint: split cleanup_dbus into a sync take_cleanup_proxy() + async
  perform_dbus_cleanup(). resume_async now awaits VerifyStop+Release before
  re-claiming, so fprintd cannot reject the Claim on a slow bus. stop()
  still spawns the cleanup fire-and-forget.
- fingerprint: remove failed_attempts = 0 from resume_async. An attacker
  with sensor control could otherwise cycle verify-match → account-fail →
  resume and never trip the 10-attempt cap.
- lockscreen: open the wallpaper with O_NOFOLLOW and build the texture
  from bytes, closing the TOCTOU between the symlink check and Texture::
  from_file.
- lockscreen: clear password_entry immediately after extracting the
  Zeroizing<String>, shortening the window the GLib GString copy stays in
  libc-malloc'd memory.
This commit is contained in:
2026-04-24 13:21:19 +02:00
parent 39d9cbb624
commit 9dfd1829e9
5 changed files with 81 additions and 24 deletions
+42 -19
View File
@@ -177,12 +177,24 @@ impl FingerprintListener {
/// Resume fingerprint verification after a transient interruption (e.g. failed
/// PAM account check). Reuses previously stored callbacks. Re-claims the device
/// and restarts verification from scratch.
/// and restarts verification from scratch. Awaits any in-flight VerifyStop +
/// Release before re-claiming the device so fprintd does not reject the Claim
/// while the previous session is still being torn down.
pub async fn resume_async(
listener: &Rc<RefCell<FingerprintListener>>,
username: &str,
) {
listener.borrow_mut().failed_attempts = 0;
// Drain in-flight cleanup so the device is actually released before Claim.
// Without this, a fast resume after on_verify_status's fire-and-forget
// cleanup races the Release call and fprintd returns "already claimed".
let proxy = listener.borrow_mut().take_cleanup_proxy();
if let Some(proxy) = proxy {
Self::perform_dbus_cleanup(proxy).await;
}
// Deliberately do NOT reset failed_attempts here. An attacker with sensor
// control could otherwise cycle verify-match → check_account fail → resume,
// and the 10-attempt cap would never trigger. The counter decays only via
// a fresh lock session (listener construction).
Self::begin_verification(listener, username).await;
}
@@ -352,26 +364,37 @@ impl FingerprintListener {
}
}
/// Disconnect the signal handler and send VerifyStop + Release to fprintd.
/// Signal disconnect is synchronous to prevent further callbacks.
/// D-Bus cleanup is fire-and-forget to avoid blocking the UI.
fn cleanup_dbus(&mut self) {
/// Disconnect the signal handler and clear running flags. Returns the proxy
/// the caller should use for the async D-Bus cleanup (VerifyStop + Release).
///
/// Split into a sync part (signal disconnect, flags) and an async part
/// (`perform_dbus_cleanup`) so callers can either spawn the async work
/// fire-and-forget (via `cleanup_dbus`) or await it to serialize with a
/// subsequent Claim (via `resume_async`).
fn take_cleanup_proxy(&mut self) -> Option<gio::DBusProxy> {
self.running = false;
self.running_flag.set(false);
if let Some(ref proxy) = self.device_proxy {
if let Some(id) = self.signal_id.take() {
proxy.disconnect(id);
}
let proxy = proxy.clone();
glib::spawn_future_local(async move {
let _ = proxy
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.await;
let _ = proxy
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.await;
});
let proxy = self.device_proxy.clone()?;
if let Some(id) = self.signal_id.take() {
proxy.disconnect(id);
}
Some(proxy)
}
async fn perform_dbus_cleanup(proxy: gio::DBusProxy) {
let _ = proxy
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.await;
let _ = proxy
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.await;
}
/// Fire-and-forget cleanup for code paths that cannot await (e.g. drop, stop).
fn cleanup_dbus(&mut self) {
if let Some(proxy) = self.take_cleanup_proxy() {
glib::spawn_future_local(Self::perform_dbus_cleanup(proxy));
}
}