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.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 22:39:04 +02:00
commit 32fe0ac665

View file

@ -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<Event> 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;
}