Do not make the trustlet re-read the whole template on every claim

Selecting a group makes the trustlet reload every template in it from storage,
and on this device that is 651276 bytes crossing the listener in chunks. Measured
over ten verifications: eleven claims, eleven reloads, fifty-four chunk reads,
twenty-six megabytes of round trips. All of it happens before the verify loop
starts, so all of it lands in the latency a user feels while being invisible to
the daemon's own timing -- which is how it survived this long.

It is also unnecessary. The trustlet holds templates in memory once loaded; a
successful authentication on stock produces no storage callbacks at all. A group
that is already active does not need selecting again, so a repeat claim now
answers from what the last real selection reported.

Enrolment forces a real selection, because it adds a template and the reload is
the point.

Measured after: two claims, one real selection, one skipped, and the chunk reads
drop from five per claim to two for the pair.
This commit is contained in:
Jorijn van der Graaf 2026-09-05 01:25:03 +02:00
commit 231a1860ac

View file

@ -1206,8 +1206,25 @@ public:
// Select a group and count what loads. A template reload needs the init
// chain to have run first -- Start() guarantees that.
int SetActiveGroup(std::uint32_t gid) {
// Selecting a group makes the trustlet RELOAD every template in it from
// storage, and on this device that is 651276 bytes crossing the listener in
// chunks -- about a megabyte of round trips per call. Measured 2026-09-05:
// ten verifications cost eleven reloads and 26 MB, all of it before the
// verify loop even starts, so all of it lands in the latency the user sees
// while being invisible to the daemon's own timing.
//
// It is also unnecessary. The trustlet holds templates in memory once
// loaded -- a successful authentication on stock produces no storage
// callbacks at all -- so a group that is already active does not need
// selecting again. `force` is for the cases that genuinely change the
// store: an enrolment, or a template removed underneath us.
int SetActiveGroup(std::uint32_t gid, bool force = false) {
namespace ta = fingerprintd::ta;
if (!force && gid == gid_ && templatesLoaded_ >= 0) {
std::println("SET_ACTIVE_GROUP gid={} skipped (already active, {} template(s))",
gid, templatesLoaded_);
return templatesLoaded_;
}
auto sag = ta::BuildSetActiveGroup(gid, g_groupPath);
std::println("SET_ACTIVE_GROUP gid={}", gid);
auto g = SendCommand(app_, ta::Cmd::SetActiveGroup, sag);
@ -1216,7 +1233,8 @@ public:
Report(ta::Cmd::Enumerate, e);
std::println(" templates loaded: {}", e.rc);
gid_ = gid;
return e.invoked ? e.rc : -1;
templatesLoaded_ = e.invoked ? e.rc : -1;
return templatesLoaded_;
}
// ---- Enrolment
@ -1244,7 +1262,8 @@ public:
out.why = "store is read-only or RPMB writes disabled";
return out;
}
if (gid != gid_) SetActiveGroup(gid);
// An enrolment adds a template, so this one reloads for real.
SetActiveGroup(gid, /*force*/ true);
// Stock's opening sequence. AUTHENTICATE is what arms the capture
// session; CANCEL and RESET_LOCKOUT bracket it.
@ -1755,6 +1774,9 @@ private:
Sensor sensor_;
fingerprintd::engine::Baseline baseline_;
std::uint32_t gid_ = 0xFFFFFFFF;
// 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;
// Set when a fold succeeded; cleared by the save.
bool templateDirty_ = false;
// Frames folded during the verify loop itself, carried into the harvest so