From 32fe0ac665046b6680208b955ffc8ea228301be1 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 22:39:04 +0200 Subject: [PATCH] Drop a claim when its client leaves the bus A claim is held by a bus connection. If that connection goes away -- the client crashed, was killed, or never called Release -- the claim has to go with it, or the device is AlreadyInUse for everyone until the daemon restarts. fprintd watches the claimant's name for exactly this reason. Found the hard way: a Claim issued from one busctl invocation, which exits the moment it returns, left the device permanently claimed by a connection that no longer existed. The daemon now subscribes to NameOwnerChanged for the claimant's unique name and, when it loses its owner, cancels any running operation and clears the claim. Release goes through the same path so the subscription is torn down either way. Verified with the stock client killed mid-verify under timeout: the claim dropped, the verify cancelled, and fprintd-list worked immediately after. --- implementations/main.cpp | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index be86315..607e764 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -1589,6 +1589,38 @@ struct Claim { }; Claim g_claim; GDBusMethodInvocation* g_pendingClaim = nullptr; +guint g_claimWatch = 0; + +void DropClaim(const char* why) { + if (g_claimWatch) { + g_dbus_connection_signal_unsubscribe(g_conn, g_claimWatch); + g_claimWatch = 0; + } + if (g_claim.op != Op::None && g_worker) g_worker->CancelOp(); + if (g_claim.held) + std::println("claim by {} for {} dropped: {}", g_claim.sender, g_claim.user, why); + g_claim = {}; +} + +// A claim is held by a bus connection. If that connection goes away -- the +// client crashed, was killed, or simply never called Release -- the claim +// must go with it, or the device is wedged for everyone until the daemon +// restarts. fprintd watches the claimant's name for exactly this reason. Seen +// the hard way: a Claim from one busctl invocation, which exits immediately, +// left the device permanently "AlreadyInUse". +void WatchClaimant(const std::string& sender) { + g_claimWatch = g_dbus_connection_signal_subscribe( + g_conn, "org.freedesktop.DBus", "org.freedesktop.DBus", "NameOwnerChanged", + "/org/freedesktop/DBus", sender.c_str(), G_DBUS_SIGNAL_FLAGS_NONE, + [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar*, + GVariant* params, gpointer) { + const gchar* name = nullptr; const gchar* oldOwner = nullptr; const gchar* newOwner = nullptr; + g_variant_get(params, "(&s&s&s)", &name, &oldOwner, &newOwner); + if (g_claim.held && name && g_claim.sender == name && newOwner && *newOwner == '\0') + DropClaim("client left the bus"); + }, + nullptr, nullptr); +} std::string MapPath(std::uint32_t uid) { return fingerprintd::store::PathForUid(g_stateDir, uid); @@ -1685,7 +1717,7 @@ void PostEvent(std::unique_ptr ev) { g_claim.sender, g_claim.user, g_claim.uid, ev->templates); g_dbus_method_invocation_return_value(ev->invocation, nullptr); } else { - g_claim = {}; + DropClaim("group selection failed"); ReturnError(ev->invocation, "Internal", "could not select the user's group"); } g_pendingClaim = nullptr; @@ -1763,6 +1795,7 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant* g_claim.uid = who->second; g_claim.fingers = LoadMap(g_claim.uid); g_pendingClaim = inv; + WatchClaimant(g_claim.sender); g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid, .invocation = inv }); return; } @@ -1771,8 +1804,7 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant* ReturnError(inv, "ClaimDevice", "device is not claimed by you"); return; } - if (g_claim.op != Op::None) g_worker->CancelOp(); - g_claim = {}; + DropClaim("released"); g_dbus_method_invocation_return_value(inv, nullptr); return; }