a template no name accounts for is removed, not kept in the way
The templates live on the persist partition and survive a reflash; the
finger-name map lives on the rootfs and does not. A reflashed phone comes
back with the trustlet holding every template it had and the daemon
knowing none of them by name -- and with five of them the group is full, so
every enrolment from Settings answered -207 "Up to the limit" (fennek182,
2026-10-13). No D-Bus client could get out of that: the list answered from
the map, delete removed only the fids the map knew, and ENUMERATE was sent
with an empty payload and read for its count alone.
A finger re-enrolled under its old name produced the same state one
template at a time: the map took the new fid and the old template stayed
in its slot.
ENUMERATE now asks with room for every slot the group can hold, and the
reply names the templates: one 8-byte entry per loaded one, its fid in the
second word. Verified live: the three fids the map names came back among
the four listed. With the list in hand:
- An enrolment removes the templates no name accounts for before ENROLL.
They can match nothing (a verify filters on the map's fids, standby
arms from them), and each is a slot the enrolment may need or the
duplicate the trustlet would refuse it for.
- Deleting every finger deletes them too, and the job is posted even
when the map is empty, which after a reflash it is.
- Re-enrolling a finger drops its name at EnrollStart, so the old
template is unnamed and goes under the first rule. The trustlet forces
the order: with duplicate checking on, a re-enrolment of a finger it
still holds does not complete (2026-09-04: one attempt 0 accepted of 7
presses, another stalled at sample 8 of 20).
- The claim log counts them: "4 template(s) loaded, 1 of them unnamed".
The map now decides what an enrolment keeps, so the Store module says so,
and three rules keep that from turning a damaged map into lost templates.
A line the decoder cannot use -- a typo in a hand rebuild, a finger named
twice -- makes the map untrusted, and no enrolment removes anything on its
account. A line it cannot read at all is written back, word for word, by
every save but a delete of every finger, so the distrust outlasts the next
enrolment; a line it can read but not seat, the repeated name or a sixth
finger, is kept as the finger and fid it names and goes with that finger's
delete, template included, instead of coming back as the finger's name;
and the finger named twice now answers to its FIRST line, where before
the last line's fid replaced the earlier one's. A file that will not open
or read, for any reason but its absence, refuses
enrolment and deletion outright and is never written over. And the map is
written with GLib's g_file_set_contents_full under its consistent and
durable flags, and a save that did not land is reported: the old map
stays, a re-enrolment whose name drop did not land is refused with the
name put back, a delete whose save did not land removes nothing, and an
enrolment whose name did not land is reported to the client as failed,
not completed. Of the daemon's own paths, deleting every finger is the
one removal of unnamed templates on the user's word rather than the
map's: in a managed group it takes every template, named or not. A delete is refused while the
claimant's enrolment or verify for that user is in progress, because the
running job's keep-list came from the claim's map; a standby verify for
that user is stopped and the delete goes ahead. Group 0 is never tidied:
every daemon path that removes a template the map does not name, and the
name drop before a re-enrolment, asks one predicate, and it says no for
group 0 -- root's,
which no session user is, and the group a stock Android install's primary
user also writes. Root re-enrolling a finger behaves as it did before.
The --probe-remove diagnostic removes the fid it is given in any group,
with no map involved, as before.
A result belongs to the operation that asked for it. EnrollStart and
VerifyStart number the operation, the job and every event it posts carry
the number, and a Stop or the claim's end clears it: a job still queued
under a cleared number exits at dequeue instead of running an operation
nobody wants, and a result still in flight is neither named nor reported
-- its template, unnamed, goes with the group's next enrolment. Without
this, a stop followed by a new start could name a late result under the
new finger and remove the just-saved template as unnamed. An enrolment is
also refused before anything changes when the store is read-only, and the
claim's map is re-read from disk at every EnrollStart, so a file rebuilt
by hand under a held claim counts.
The probe passes the loaded list as the named one. A diagnostic does not
tidy the group it is probing. fplearn.sh's header no longer says that
re-enrolment adds a template with no trustlet-side remove, and its `enrol`
refusal gives the protocol's own reason (a template with no history)
instead of the trustlet's duplicate refusal.
Tested on the dev phone. An earlier build of this change, before the
shared predicate and the damaged-map rule, removed the phone's one stray
template at an EnrollStart for a finger not in the map (fid 1313616972,
8 -> 6 containers), reloaded the group as the three named ones, got rc=0
from ENROLL and cancelled cleanly. A build of this series then ran the
same claim and EnrollStart on the group it left behind: three loaded, none
unnamed, nothing removed, ENROLL rc=0, cancelled cleanly. The build of this
series' tip then enrolled a fourth finger to completion (20 samples
accepted, one refused), which the map saved and listed, and deleted it
again under the same claim: its template removed, the group and the map
back to the three they started with.
This commit is contained in:
parent
60f31d4e66
commit
bae21dc7a8
6 changed files with 553 additions and 79 deletions
|
|
@ -1021,6 +1021,8 @@ struct CommandResult {
|
|||
std::uint32_t gid = 0;
|
||||
std::uint32_t fid = 0;
|
||||
std::int32_t samplesRemaining = -1;
|
||||
// Only for ENUMERATE: the fids of the templates the group holds.
|
||||
std::vector<std::uint32_t> fids;
|
||||
|
||||
bool Ok() const { return invoked && result == 0 && rc == 0; }
|
||||
};
|
||||
|
|
@ -1110,6 +1112,8 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd,
|
|||
out.fid = ta::MatchedFid(reqOut);
|
||||
out.samplesRemaining = ta::SamplesRemaining(reqOut);
|
||||
}
|
||||
if (cmd == ta::Cmd::Enumerate)
|
||||
out.fids = ta::EnumeratedFids(reqOut, out.rc);
|
||||
if (g_taLog) DumpTaLog(rspOut);
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1294,8 +1298,10 @@ public:
|
|||
// store: an enrolment, or a template removed underneath us.
|
||||
// Remove templates from the TRUSTLET, which is what makes a delete a
|
||||
// delete. Dropping the name from the map only stops the finger being
|
||||
// offered; the template keeps its slot, and with
|
||||
// enable_duplicated_finger_checking on, that slot is what refuses the
|
||||
// offered; the template keeps its slot until the group's next enrolment
|
||||
// or delete-all removes it as unnamed -- and in group 0, or under a map
|
||||
// the daemon does not trust, for good, where with
|
||||
// enable_duplicated_finger_checking on that slot refuses the
|
||||
// re-enrolment of the same finger.
|
||||
//
|
||||
// The group must be active first: the trustlet compares the gid against
|
||||
|
|
@ -1348,14 +1354,46 @@ public:
|
|||
std::println("SET_ACTIVE_GROUP gid={}", gid);
|
||||
auto g = SendCommand(app_, ta::Cmd::SetActiveGroup, sag);
|
||||
Report(ta::Cmd::SetActiveGroup, g);
|
||||
auto e = SendCommand(app_, ta::Cmd::Enumerate, {});
|
||||
// With room for every slot the group can hold, so the reply names the
|
||||
// templates and not only how many there are.
|
||||
std::vector<std::byte> en(ta::EnumeratePayloadSize);
|
||||
ta::BuildEnumeratePayload(en, fingerprintd::store::MaxFingersPerGroup);
|
||||
auto e = SendCommand(app_, ta::Cmd::Enumerate, en);
|
||||
Report(ta::Cmd::Enumerate, e);
|
||||
std::println(" templates loaded: {}", e.rc);
|
||||
std::println(" templates loaded: {}{}", e.rc, FidList(e.fids));
|
||||
gid_ = gid;
|
||||
templatesLoaded_ = e.invoked ? e.rc : -1;
|
||||
loadedFids_ = e.invoked ? e.fids : std::vector<std::uint32_t>{};
|
||||
return templatesLoaded_;
|
||||
}
|
||||
|
||||
// The loaded templates that none of `named` accounts for. A template
|
||||
// without a name is offered for nothing -- a verify filters on the map's
|
||||
// fids, standby arms from them -- but it still holds one of the group's
|
||||
// slots and, with duplicate-finger checking on, refuses the re-enrolment
|
||||
// of the finger it was taken from. How they arise: the map lives on the
|
||||
// rootfs and the templates on the persist partition, so a reflash keeps
|
||||
// the templates and loses every name (fennek182, 2026-10-13: five of
|
||||
// them, ENROLL answering -207); and a finger re-enrolled under its old
|
||||
// name left its old template behind until this release.
|
||||
std::vector<std::uint32_t> Unnamed(const std::vector<std::uint32_t>& named) const {
|
||||
std::vector<std::uint32_t> out;
|
||||
for (std::uint32_t fid : loadedFids_)
|
||||
if (fid != 0 && !std::ranges::contains(named, fid) && !std::ranges::contains(out, fid))
|
||||
out.push_back(fid);
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::vector<std::uint32_t>& LoadedFids() const { return loadedFids_; }
|
||||
|
||||
static std::string FidList(const std::vector<std::uint32_t>& fids) {
|
||||
if (fids.empty()) return {};
|
||||
std::string s = " (fid";
|
||||
for (std::size_t i = 0; i < fids.size(); i++)
|
||||
s += std::format("{}{}", i ? ", " : " ", fids[i]);
|
||||
return s + ")";
|
||||
}
|
||||
|
||||
// ---- Enrolment
|
||||
//
|
||||
// One sample per PRESS: touch on the rising edge, release on the falling
|
||||
|
|
@ -1369,7 +1407,8 @@ public:
|
|||
std::string why;
|
||||
};
|
||||
|
||||
EnrolOutcome Enrol(std::uint32_t gid, std::atomic<bool>& cancel,
|
||||
EnrolOutcome Enrol(std::uint32_t gid, const std::vector<std::uint32_t>& named, bool tidy,
|
||||
std::atomic<bool>& cancel,
|
||||
const std::function<void(int, int)>& onStage,
|
||||
const std::function<void()>& onRetry,
|
||||
int maxFrames) {
|
||||
|
|
@ -1383,6 +1422,23 @@ public:
|
|||
}
|
||||
// An enrolment adds a template, so this one reloads for real.
|
||||
SetActiveGroup(gid, /*force*/ true);
|
||||
// Templates no name accounts for go before the new one comes: they
|
||||
// can match nothing, and each one is a slot this enrolment may need
|
||||
// or the duplicate the trustlet would refuse it for. Not in group 0
|
||||
// (store::GroupIsManaged), and not on the word of a map the daemon
|
||||
// could not read in full (`tidy`): a removed template is gone for
|
||||
// good.
|
||||
auto stray = Unnamed(named);
|
||||
if (!stray.empty() && fingerprintd::store::GroupIsManaged(gid)) {
|
||||
if (tidy) {
|
||||
std::println("enrol: {} template(s) loaded that no finger name accounts for; "
|
||||
"removing them first", stray.size());
|
||||
RemoveTemplates(gid, stray);
|
||||
} else {
|
||||
std::println("enrol: {} template(s) loaded that no finger name accounts for, "
|
||||
"left in place: the map could not be read in full", stray.size());
|
||||
}
|
||||
}
|
||||
|
||||
// Stock's opening sequence. AUTHENTICATE is what arms the capture
|
||||
// session; CANCEL and RESET_LOCKOUT bracket it.
|
||||
|
|
@ -1523,12 +1579,11 @@ public:
|
|||
};
|
||||
|
||||
// `accept` is the set of fids that count as a match for THIS request. The
|
||||
// trustlet identifies against every template loaded in the group, and a
|
||||
// group accumulates them: re-enrolling a finger does NOT replace its
|
||||
// template, it adds one, because FF_CMD_TA_REMOVE is not implemented. So
|
||||
// trustlet identifies against every template loaded in the group, so
|
||||
// without this filter a verify answers for fingers the caller did not ask
|
||||
// about, and for stale templates no longer named by anything -- which is
|
||||
// both wrong by fprintd's contract and quietly ruins any measurement.
|
||||
// about, and for templates no name accounts for (one an enrolment has not
|
||||
// yet removed, or one in group 0) -- which is both wrong by fprintd's
|
||||
// contract and quietly ruins any measurement.
|
||||
VerifyOutcome Verify(std::uint32_t gid, std::atomic<bool>& cancel, int maxFrames,
|
||||
const std::vector<std::uint32_t>& accept) {
|
||||
namespace ta = fingerprintd::ta;
|
||||
|
|
@ -1928,6 +1983,7 @@ private:
|
|||
// What the last real SET_ACTIVE_GROUP reported, so a repeat claim can be
|
||||
// answered without making the trustlet re-read the store. -1 = unknown.
|
||||
int templatesLoaded_ = -1;
|
||||
std::vector<std::uint32_t> loadedFids_;
|
||||
// Set when a fold succeeded; cleared by the save.
|
||||
bool templateDirty_ = false;
|
||||
// Frames folded during the verify loop itself, carried into the harvest so
|
||||
|
|
@ -1948,7 +2004,12 @@ struct Job {
|
|||
std::string finger;
|
||||
std::vector<std::uint32_t> acceptFids; // Verify: which fids count
|
||||
// Remove: which fids to drop
|
||||
// Claim/Enroll: the fids the map names
|
||||
GDBusMethodInvocation* invocation = nullptr; // Claim/Remove reply asynchronously
|
||||
bool everything = false; // Remove: the unnamed templates too
|
||||
bool tidy = false; // Enroll: the map is trusted (read in
|
||||
// full), so it may say what is unnamed
|
||||
std::uint64_t seq = 0; // Enroll/Verify: the operation this job runs
|
||||
};
|
||||
|
||||
// Everything the worker sends back to the main thread. Delivered by g_idle_add
|
||||
|
|
@ -1961,6 +2022,8 @@ struct Event {
|
|||
std::string status;
|
||||
std::uint32_t fid = 0;
|
||||
int templates = 0;
|
||||
int unnamed = 0; // ClaimDone: of those, how many the map has no name for
|
||||
std::uint64_t seq = 0; // EnrollStatus/VerifyStatus: the operation it reports on
|
||||
GDBusMethodInvocation* invocation = nullptr;
|
||||
};
|
||||
|
||||
|
|
@ -1974,6 +2037,25 @@ void PostEvent(std::unique_ptr<Event> ev); // defined with the D-Bus code
|
|||
std::atomic<bool> g_standbyWanted{false};
|
||||
std::atomic<bool> g_standbyRunning{false};
|
||||
|
||||
// Which client operation the worker's jobs and events belong to. EnrollStart
|
||||
// and VerifyStart give the claim a fresh number, the job and every event it
|
||||
// posts carry it, and a Stop or the claim's end clears it. A job still
|
||||
// queued under a cleared number exits at dequeue instead of running an
|
||||
// operation nobody wants (the cancel flag is reset at dequeue, so it alone
|
||||
// would not stop it), and a result that arrives under one is not the
|
||||
// current operation's: it is neither named nor reported. Without this, a
|
||||
// stop followed by a new start could name a late result under the new
|
||||
// finger and remove the just-saved template as unnamed. The main thread
|
||||
// owns the counter; the worker reads the live number from the atomic.
|
||||
//
|
||||
// The same handshake as standby's `wanted`/`running`: a stop clears the
|
||||
// number BEFORE it cancels, and the worker resets its cancel flag BEFORE it
|
||||
// reads the number, so whichever side goes second sees the other -- a job
|
||||
// dequeued around a stop either reads the cleared number and exits, or
|
||||
// keeps the cancel that comes after its reset.
|
||||
std::uint64_t g_opSeqCounter = 0;
|
||||
std::atomic<std::uint64_t> g_liveOpSeq{0};
|
||||
|
||||
class Worker {
|
||||
public:
|
||||
void Start() {
|
||||
|
|
@ -2037,14 +2119,36 @@ private:
|
|||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::ClaimDone });
|
||||
ev->ok = n >= 0;
|
||||
ev->templates = n;
|
||||
ev->unnamed = static_cast<int>(session_.Unnamed(j.acceptFids).size());
|
||||
ev->invocation = j.invocation;
|
||||
PostEvent(std::move(ev));
|
||||
break;
|
||||
}
|
||||
case Job::Kind::Remove: {
|
||||
int n = session_.RemoveTemplates(j.uid, j.acceptFids);
|
||||
std::println("remove: {} of {} template(s) removed for uid {}",
|
||||
n < 0 ? 0 : n, j.acceptFids.size(), j.uid);
|
||||
std::vector<std::uint32_t> fids = j.acceptFids;
|
||||
// "Every finger" is every template in the group, the ones no
|
||||
// name accounts for included: after a reflash the map is empty
|
||||
// and they are all this user has to delete. On the user's
|
||||
// word, not the map's trust -- nothing is being kept on the
|
||||
// map's account here. Group 0 is left alone, as in Enrol.
|
||||
// Reloaded, not cached: the cached list can lack a template
|
||||
// enrolled since, and "every finger" means the group as it is.
|
||||
// A group that would not reload is a failure, not a delete
|
||||
// of nothing: the client must not hear "done" while the
|
||||
// templates stay.
|
||||
bool loaded = true;
|
||||
if (j.everything && fingerprintd::store::GroupIsManaged(j.uid)) {
|
||||
if (session_.SetActiveGroup(j.uid, /*force*/ true) >= 0)
|
||||
for (std::uint32_t fid : session_.Unnamed(fids)) fids.push_back(fid);
|
||||
else
|
||||
loaded = false;
|
||||
}
|
||||
int n = loaded ? session_.RemoveTemplates(j.uid, fids) : -1;
|
||||
if (loaded)
|
||||
std::println("remove: {} of {} template(s) removed for uid {}",
|
||||
n < 0 ? 0 : n, fids.size(), j.uid);
|
||||
else
|
||||
std::println(std::cerr, "remove: group {} would not load, nothing removed", j.uid);
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::RemoveDone });
|
||||
ev->ok = n >= 0;
|
||||
ev->templates = n;
|
||||
|
|
@ -2053,8 +2157,16 @@ private:
|
|||
break;
|
||||
}
|
||||
case Job::Kind::Enroll: {
|
||||
if (j.seq != g_liveOpSeq.load()) {
|
||||
// Stopped while it was queued.
|
||||
std::println("enrol: operation {} was stopped before it started", j.seq);
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::EnrollStatus });
|
||||
ev->seq = j.seq; ev->status = ""; ev->done = false;
|
||||
PostEvent(std::move(ev));
|
||||
break;
|
||||
}
|
||||
auto o = session_.Enrol(
|
||||
j.uid, cancel_,
|
||||
j.uid, j.acceptFids, j.tidy, cancel_,
|
||||
[&](int accepted, int total) {
|
||||
// Logged as well as signalled. A run whose refusals
|
||||
// exist only as D-Bus traffic cannot be counted from
|
||||
|
|
@ -2063,17 +2175,20 @@ private:
|
|||
// refused nothing", which was wrong.
|
||||
std::println(" enrol: sample ACCEPTED ({}/{})", accepted, total);
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::EnrollStatus });
|
||||
ev->seq = j.seq;
|
||||
ev->status = "enroll-stage-passed";
|
||||
PostEvent(std::move(ev));
|
||||
},
|
||||
[&] {
|
||||
std::println(" enrol: sample REFUSED (retry-scan)");
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::EnrollStatus });
|
||||
ev->seq = j.seq;
|
||||
ev->status = "enroll-retry-scan";
|
||||
PostEvent(std::move(ev));
|
||||
},
|
||||
/*maxFrames*/ 600);
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::EnrollStatus });
|
||||
ev->seq = j.seq;
|
||||
ev->done = true;
|
||||
ev->ok = o.saved;
|
||||
ev->fid = o.fid;
|
||||
|
|
@ -2136,8 +2251,17 @@ private:
|
|||
// timeout, and a claimant that vanishes from the bus has its
|
||||
// claim dropped -- and by VerifyStop, which is what sets
|
||||
// cancel_. See the idle-wait note in Verify().
|
||||
if (j.seq != g_liveOpSeq.load()) {
|
||||
// Stopped while it was queued; without this it would poll
|
||||
// until someone pressed.
|
||||
std::println("verify: operation {} was stopped before it started", j.seq);
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::VerifyStatus });
|
||||
ev->seq = j.seq; ev->status = ""; ev->done = false;
|
||||
PostEvent(std::move(ev));
|
||||
break;
|
||||
}
|
||||
auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
||||
while (!o.decided && !o.cancelled && o.presses == 0) {
|
||||
while (!o.decided && !o.cancelled && o.presses == 0 && j.seq == g_liveOpSeq.load()) {
|
||||
std::println("verify: {} frame(s), sensor never touched -- still waiting",
|
||||
o.frames);
|
||||
o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
||||
|
|
@ -2146,6 +2270,7 @@ private:
|
|||
o.cancelled ? "cancelled" : !o.decided ? "undecided"
|
||||
: o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames);
|
||||
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::VerifyStatus });
|
||||
ev->seq = j.seq;
|
||||
ev->done = true;
|
||||
ev->fid = o.fid;
|
||||
if (o.cancelled) { ev->status = ""; ev->done = false; }
|
||||
|
|
@ -2321,6 +2446,7 @@ struct Claim {
|
|||
std::uint32_t uid = 0;
|
||||
Op op = Op::None;
|
||||
std::string finger; // for the op in progress
|
||||
std::uint64_t opSeq = 0; // the op in progress, 0 = none (see g_liveOpSeq)
|
||||
fingerprintd::store::Map fingers;
|
||||
};
|
||||
Claim g_claim;
|
||||
|
|
@ -2335,6 +2461,7 @@ void DropClaim(const char* why) {
|
|||
g_dbus_connection_signal_unsubscribe(g_conn, g_claimWatch);
|
||||
g_claimWatch = 0;
|
||||
}
|
||||
g_liveOpSeq.store(0); // before the cancel: see g_liveOpSeq
|
||||
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);
|
||||
|
|
@ -2368,28 +2495,62 @@ std::string MapPath(std::uint32_t uid) {
|
|||
|
||||
fingerprintd::store::Map LoadMap(std::uint32_t uid, bool quiet = false) {
|
||||
std::string path = MapPath(uid);
|
||||
std::ifstream f(path);
|
||||
if (!f) {
|
||||
if (!quiet) std::println("map {}: {}", path, ::strerror(errno));
|
||||
return {};
|
||||
// Absent is "no fingers" and the map that says so is trusted. Any other
|
||||
// failure, at the open or during the read, is a map the daemon cannot
|
||||
// see: it must not decide which templates are unnamed on that account,
|
||||
// and it must never be written over. GLib's read reports a failed read
|
||||
// as an error, where a stream would hand back what it got so far and
|
||||
// make an EIO look like a short, clean map; and it tells an absent file
|
||||
// from every other failure, which is the one distinction this needs.
|
||||
gchar* contents = nullptr;
|
||||
gsize len = 0;
|
||||
GError* err = nullptr;
|
||||
if (!g_file_get_contents(path.c_str(), &contents, &len, &err)) {
|
||||
bool absent = g_error_matches(err, G_FILE_ERROR, G_FILE_ERROR_NOENT);
|
||||
if (!quiet) std::println("map {}: {}", path, err ? err->message : "unknown error");
|
||||
g_clear_error(&err);
|
||||
return absent ? fingerprintd::store::Map{} : fingerprintd::store::Map::Unreadable();
|
||||
}
|
||||
std::string text((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
std::string text(contents, len);
|
||||
g_free(contents);
|
||||
auto m = fingerprintd::store::Map::Decode(text);
|
||||
if (!quiet) std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size());
|
||||
if (!quiet)
|
||||
std::println("map {}: {} bytes, {} finger(s){}", path, text.size(), m.Size(),
|
||||
m.Skipped() ? std::format(", {} line(s) not understood, kept as they are",
|
||||
m.Skipped())
|
||||
: std::string());
|
||||
return m;
|
||||
}
|
||||
|
||||
void SaveMap(std::uint32_t uid, const fingerprintd::store::Map& m) {
|
||||
// Writes the map and says whether it landed. GLib writes a temp file in the
|
||||
// same directory, fsyncs it, renames it over the old file and fsyncs the
|
||||
// directory, so a crash leaves either the old map or the new one, never an
|
||||
// empty file that would read as a trusted map naming nothing. The map
|
||||
// decides which templates the next enrolment keeps, so a save that did not
|
||||
// reach the disk leaves the old map in place rather than replace it with a
|
||||
// shorter one, and the caller hears about it.
|
||||
bool SaveMap(std::uint32_t uid, const fingerprintd::store::Map& m) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(g_stateDir, ec);
|
||||
std::string path = MapPath(uid);
|
||||
std::string tmp = path + ".tmp";
|
||||
{
|
||||
std::ofstream f(tmp, std::ios::trunc);
|
||||
f << m.Encode();
|
||||
// A map the daemon could not read is not one it may replace: the file
|
||||
// may name fingers this object does not.
|
||||
if (!m.Readable()) {
|
||||
std::println(std::cerr, "map {}: not written: it could not be read, and the file stays as it is", path);
|
||||
return false;
|
||||
}
|
||||
::chmod(tmp.c_str(), 0600);
|
||||
std::filesystem::rename(tmp, path, ec);
|
||||
std::string enc = m.Encode();
|
||||
GError* err = nullptr;
|
||||
if (!g_file_set_contents_full(path.c_str(), enc.data(), static_cast<gssize>(enc.size()),
|
||||
static_cast<GFileSetContentsFlags>(G_FILE_SET_CONTENTS_CONSISTENT
|
||||
| G_FILE_SET_CONTENTS_DURABLE),
|
||||
0600, &err)) {
|
||||
std::println(std::cerr, "map {}: not written ({}); the old map stays", path,
|
||||
err ? err->message : "unknown error");
|
||||
g_clear_error(&err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmitDevice(const char* signal, GVariant* params) {
|
||||
|
|
@ -2753,8 +2914,10 @@ void PostEvent(std::unique_ptr<Event> ev) {
|
|||
case Event::Kind::ClaimDone:
|
||||
if (ev->invocation) {
|
||||
if (ev->ok) {
|
||||
std::println("claimed by {} for {} (uid {}, {} template(s) loaded)",
|
||||
g_claim.sender, g_claim.user, g_claim.uid, ev->templates);
|
||||
std::println("claimed by {} for {} (uid {}, {} template(s) loaded{})",
|
||||
g_claim.sender, g_claim.user, g_claim.uid, ev->templates,
|
||||
ev->unnamed ? std::format(", {} of them unnamed", ev->unnamed)
|
||||
: std::string());
|
||||
g_dbus_method_invocation_return_value(ev->invocation, nullptr);
|
||||
} else {
|
||||
DropClaim("group selection failed");
|
||||
|
|
@ -2773,32 +2936,59 @@ void PostEvent(std::unique_ptr<Event> ev) {
|
|||
} else {
|
||||
// The names are already gone from the map by this point.
|
||||
// Saying so is better than a bare failure: the fingers
|
||||
// will not be offered again, but their templates still
|
||||
// hold slots, which is what will refuse a re-enrolment.
|
||||
// will not be offered again, and their templates, now
|
||||
// unnamed, go with the group's next enrolment or
|
||||
// delete-all.
|
||||
ReturnError(ev->invocation, "Internal",
|
||||
"names removed, but the trustlet templates were not");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Event::Kind::EnrollStatus:
|
||||
if (!ev->status.empty())
|
||||
EmitDevice("EnrollStatus", g_variant_new("(sb)", ev->status.c_str(), ev->done ? TRUE : FALSE));
|
||||
case Event::Kind::EnrollStatus: {
|
||||
if (ev->seq != g_claim.opSeq) {
|
||||
// From an enrolment the client has since stopped, or a claim
|
||||
// since dropped: not this operation's result. A template it
|
||||
// saved is unnamed and goes with the group's next enrolment.
|
||||
if (ev->done && ev->ok && ev->fid != 0)
|
||||
std::println("enrol: a result of a stopped enrolment arrived (fid {}); it stays unnamed",
|
||||
ev->fid);
|
||||
break;
|
||||
}
|
||||
std::string status = ev->status;
|
||||
if (ev->done && ev->ok) {
|
||||
auto f = fingerprintd::store::FingerFromName(g_claim.finger);
|
||||
if (f && ev->fid != 0) {
|
||||
// The name is saved BEFORE the client hears "completed". A
|
||||
// save that did not land makes this an enrolment that
|
||||
// failed, with the claim's map put back -- not a success
|
||||
// whose finger is missing from the next list and never
|
||||
// offered.
|
||||
auto kept = g_claim.fingers;
|
||||
g_claim.fingers.Add(*f, ev->fid);
|
||||
SaveMap(g_claim.uid, g_claim.fingers);
|
||||
std::println("enrolled {} for uid {} as fid {}", g_claim.finger, g_claim.uid, ev->fid);
|
||||
if (SaveMap(g_claim.uid, g_claim.fingers)) {
|
||||
std::println("enrolled {} for uid {} as fid {}", g_claim.finger, g_claim.uid, ev->fid);
|
||||
} else {
|
||||
g_claim.fingers = kept;
|
||||
status = "enroll-failed";
|
||||
std::println(std::cerr,
|
||||
"enrolled {} for uid {} as fid {}, but its name could not be saved: "
|
||||
"reported as failed; the template is unnamed and goes with the group's "
|
||||
"next enrolment", g_claim.finger, g_claim.uid, ev->fid);
|
||||
}
|
||||
} else {
|
||||
std::println(std::cerr,
|
||||
"enrolled, but the trustlet reported no fid -- the finger cannot be "
|
||||
"named until it is seen in a verification");
|
||||
}
|
||||
}
|
||||
if (!status.empty())
|
||||
EmitDevice("EnrollStatus", g_variant_new("(sb)", status.c_str(), ev->done ? TRUE : FALSE));
|
||||
break;
|
||||
}
|
||||
case Event::Kind::VerifyStatus: {
|
||||
namespace ac = fingerprintd::actions;
|
||||
namespace store = fingerprintd::store;
|
||||
if (ev->seq != g_claim.opSeq) break; // a verify the client has since stopped
|
||||
std::string status = ev->status;
|
||||
const ac::Rule* rule = nullptr;
|
||||
std::optional<store::Finger> matched;
|
||||
|
|
@ -2894,7 +3084,8 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
g_pendingClaim = inv;
|
||||
WatchClaimant(g_claim.sender);
|
||||
StandbyCancel(); // the client's op comes first; standby returns with Release
|
||||
g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid, .invocation = inv });
|
||||
g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid,
|
||||
.acceptFids = g_claim.fingers.AllFids(), .invocation = inv });
|
||||
return;
|
||||
}
|
||||
if (method == "Release") {
|
||||
|
|
@ -2947,13 +3138,31 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
}
|
||||
}
|
||||
auto m = LoadMap(uid);
|
||||
if (!m.Readable()) {
|
||||
ReturnError(inv, "Internal", "the finger map could not be read; nothing is removed until it can be");
|
||||
return;
|
||||
}
|
||||
// An operation in progress for this user has the claim's own copy of
|
||||
// the map and a job on the worker whose keep-list came from it; a
|
||||
// delete underneath would remove what that job is about to name.
|
||||
if (g_claim.held && g_claim.uid == uid && g_claim.op != Op::None) {
|
||||
ReturnError(inv, "AlreadyInUse", "an operation is in progress for this user");
|
||||
return;
|
||||
}
|
||||
// The fids have to be read BEFORE the names go: the map is the only
|
||||
// place that remembers which template belongs to which finger.
|
||||
std::vector<std::uint32_t> fids;
|
||||
for (const auto& e : m.Entries())
|
||||
if (!one || e.finger == *one) fids.push_back(e.fid);
|
||||
std::vector<std::uint32_t> fids = one ? m.FidsNamed(*one) : m.AllFids();
|
||||
// Deleting every finger also deletes the templates the map has no
|
||||
// name for, which is why the job is posted even with no fids: after
|
||||
// a reflash the map is empty and the trustlet is full.
|
||||
bool everything = !one;
|
||||
if (one) m.Remove(*one); else m.Clear();
|
||||
SaveMap(uid, m);
|
||||
if (!SaveMap(uid, m)) {
|
||||
// The disk still names them; removing their templates now would
|
||||
// leave names that can never match.
|
||||
ReturnError(inv, "Internal", "the finger map could not be written; nothing is removed");
|
||||
return;
|
||||
}
|
||||
if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m;
|
||||
// A standby verify for this user holds the fids it was armed with and
|
||||
// keeps the worker busy; stop it so the removal below runs, and so it
|
||||
|
|
@ -2966,16 +3175,16 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
// purpose: a finger whose template survives a failed removal is a slot
|
||||
// leak, while a name that survives a successful one would keep
|
||||
// offering a finger that can no longer match.
|
||||
std::println("deleting finger name(s) for uid {}; removing {} trustlet template(s)",
|
||||
uid, fids.size());
|
||||
if (fids.empty()) {
|
||||
std::println("deleting finger name(s) for uid {}; removing {} named trustlet template(s){}",
|
||||
uid, fids.size(), everything ? " and every unnamed one" : "");
|
||||
if (fids.empty() && !everything) {
|
||||
g_dbus_method_invocation_return_value(inv, nullptr);
|
||||
return;
|
||||
}
|
||||
// Only the worker thread ever invokes the trustlet, so the reply waits
|
||||
// for it -- fprintd's Delete methods are synchronous to the client.
|
||||
g_worker->Post(Job{ .kind = Job::Kind::Remove, .uid = uid,
|
||||
.acceptFids = fids, .invocation = inv });
|
||||
.acceptFids = fids, .invocation = inv, .everything = everything });
|
||||
return;
|
||||
}
|
||||
if (method == "EnrollStart" || method == "VerifyStart") {
|
||||
|
|
@ -2996,11 +3205,51 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
ReturnError(inv, "InvalidFingername", "unknown finger");
|
||||
return;
|
||||
}
|
||||
// Nothing changes for an enrolment the store cannot take: the
|
||||
// worker would refuse it after the name below was already gone.
|
||||
if (g_sfsReadOnly || !g_rpmbWrite) {
|
||||
ReturnError(inv, "Internal", "the template store is read-only; nothing is enrolled");
|
||||
return;
|
||||
}
|
||||
// The claim's copy is re-read from disk: the file may have been
|
||||
// rebuilt by hand since the claim, and what it says now is what
|
||||
// decides this enrolment.
|
||||
g_claim.fingers = LoadMap(g_claim.uid);
|
||||
// An enrolment ends in a save, and a map the daemon could not
|
||||
// read is one it must not write over.
|
||||
if (!g_claim.fingers.Readable()) {
|
||||
ReturnError(inv, "Internal", "the finger map could not be read; nothing is enrolled until it can be");
|
||||
return;
|
||||
}
|
||||
if (g_claim.fingers.Full() && !g_claim.fingers.Has(*store::FingerFromName(finger))) {
|
||||
EmitDevice("EnrollStatus", g_variant_new("(sb)", "enroll-data-full", TRUE));
|
||||
g_dbus_method_invocation_return_value(inv, nullptr);
|
||||
return;
|
||||
}
|
||||
// Re-enrolling a finger replaces it, and the trustlet makes that
|
||||
// an order: with duplicate-finger checking on, a re-enrolment of a
|
||||
// finger it still holds does not complete (2026-09-04: one attempt
|
||||
// 0 accepted of 7 presses, another stalled at sample 8 of 20). The
|
||||
// name goes now and the template follows on the
|
||||
// worker, as one no name accounts for -- a name kept through a
|
||||
// cancelled enrolment would offer a finger that can no longer
|
||||
// match. Both halves under the one rule: a managed group, a map
|
||||
// the daemon trusts. Otherwise the name stays and the trustlet
|
||||
// refuses the duplicate, as it did before this release.
|
||||
auto f = *store::FingerFromName(finger);
|
||||
bool tidy = g_claim.fingers.Trusted();
|
||||
if (store::GroupIsManaged(g_claim.uid) && tidy && g_claim.fingers.Has(f)) {
|
||||
auto kept = g_claim.fingers;
|
||||
g_claim.fingers.Remove(f);
|
||||
if (!SaveMap(g_claim.uid, g_claim.fingers)) {
|
||||
// The drop did not land: the disk still names the template
|
||||
// the worker would remove. Keep both, and say so.
|
||||
g_claim.fingers = kept;
|
||||
ReturnError(inv, "Internal", "the finger map could not be written; nothing is enrolled or removed");
|
||||
return;
|
||||
}
|
||||
std::println("enrol: {} is being re-enrolled; its old template goes first", finger);
|
||||
}
|
||||
} else {
|
||||
if (finger != store::AnyFinger && !store::FingerFromName(finger)) {
|
||||
ReturnError(inv, "InvalidFingername", "unknown finger");
|
||||
|
|
@ -3013,6 +3262,8 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
}
|
||||
g_claim.op = enroll ? Op::Enroll : Op::Verify;
|
||||
g_claim.finger = finger;
|
||||
g_claim.opSeq = ++g_opSeqCounter;
|
||||
g_liveOpSeq.store(g_claim.opSeq);
|
||||
g_dbus_method_invocation_return_value(inv, nullptr);
|
||||
if (!enroll) {
|
||||
// The trustlet identifies against every template in the group, so
|
||||
|
|
@ -3025,7 +3276,11 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
// Which templates may answer this request: the named finger's fid, or
|
||||
// every named finger's for "any". A fid that no name maps to -- a
|
||||
// template left behind by an earlier enrolment -- answers for nothing.
|
||||
// For an enrolment the same list says which templates to KEEP: every
|
||||
// fid the map carries, the shadowed lines' included -- a template the
|
||||
// map names is never "unnamed", whatever the trust gate then decides.
|
||||
std::vector<std::uint32_t> accept;
|
||||
if (enroll) accept = g_claim.fingers.AllFids();
|
||||
if (!enroll) {
|
||||
for (const auto& e : g_claim.fingers.Entries())
|
||||
if (finger == store::AnyFinger || store::NameOf(e.finger) == finger)
|
||||
|
|
@ -3037,7 +3292,8 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
}
|
||||
g_worker->Post(Job{ .kind = enroll ? Job::Kind::Enroll : Job::Kind::Verify,
|
||||
.uid = g_claim.uid, .finger = finger,
|
||||
.acceptFids = std::move(accept) });
|
||||
.acceptFids = std::move(accept),
|
||||
.tidy = g_claim.fingers.Trusted(), .seq = g_claim.opSeq });
|
||||
return;
|
||||
}
|
||||
if (method == "EnrollStop" || method == "VerifyStop") {
|
||||
|
|
@ -3055,9 +3311,12 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
|||
// it, and clearing the op ourselves on `done` made every one of them
|
||||
// fail with NoActionInProgress. Cancelling a loop that already ended
|
||||
// is harmless.
|
||||
g_worker->CancelOp();
|
||||
// The number is cleared before the cancel: see g_liveOpSeq.
|
||||
g_claim.op = Op::None;
|
||||
g_claim.finger.clear();
|
||||
g_claim.opSeq = 0;
|
||||
g_liveOpSeq.store(0);
|
||||
g_worker->CancelOp();
|
||||
g_dbus_method_invocation_return_value(inv, nullptr);
|
||||
return;
|
||||
}
|
||||
|
|
@ -3207,7 +3466,9 @@ int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, bool doLearnProbe,
|
|||
std::atomic<bool> cancel{false};
|
||||
if (doEnrol) {
|
||||
for (int c = 3; c > 0; c--) { std::println("*** press and LIFT, repeatedly, in {}... ***", c); std::this_thread::sleep_for(std::chrono::seconds(1)); }
|
||||
auto o = s.Enrol(gid, cancel,
|
||||
// The probe has no name map, so every loaded template counts as
|
||||
// named: a diagnostic does not tidy the group it is probing.
|
||||
auto o = s.Enrol(gid, s.LoadedFids(), /*tidy*/ false, cancel,
|
||||
[](int a, int t) { std::println(" [{}/{}] accepted -- LIFT, then press again", a, t); },
|
||||
[] { std::println(" press rejected -- LIFT, shift the finger, press again"); },
|
||||
frames);
|
||||
|
|
|
|||
Loading…
Reference in a new issue