Learn from a matched press, as stock does
Template learning, the half of the algorithm this daemon never ran. Every match rate measured against this device -- 1/10, 2/10, 3/10, 7/10 -- was measured against a day-zero template that no stock user lives with, because a stock template is rewritten on every successful press and ours never moved a byte. HarvestTemplate mirrors stock's post-match loop: while the finger is still on the sensor, capture and fold, sending no event so the matcher cannot re-run and revise a verdict the client already has. A quick tap pays almost nothing, since the finger is gone by the time a verdict lands and the first capture reads the idle floor; a held press contributes the frames it was actually held for. That last property is why this subsumes the enrolment-tuning thread. The working hypothesis after the strict-threshold and 30-sample failures was that enrolment conditions have to resemble verification conditions. Learning is exactly that, done by the algorithm from real unlock presses instead of by coaching a user into positions they never use. FlushTemplate issues the SAVE_DATA, and the worker calls it after the verdict is posted rather than before. Persisting a learned template is roughly 350 ms of gpfile and RPMB traffic and it does not belong on an unlock path; stock defers it the same way, with a lazy-updater timer. --learn=0 turns the whole thing off, which is the only way the comparison is single-variable: learning is cumulative, so an A/B needs one binary and one template lineage rather than two builds. --learn-frames bounds what one press may contribute, because the template body moves as a single gpfile op against a 516084-byte listener buffer and a 30-sample body already measured 386402. --probe-learn sends one UPDATE_TEMPLATE with no finger and then ENUMERATEs to prove the trustlet survived it, which is the part a bare return code cannot say.
This commit is contained in:
parent
e03ce4ae5a
commit
e2033ed26d
1 changed files with 149 additions and 2 deletions
|
|
@ -101,6 +101,19 @@ bool g_undecidedIsNoMatch = false;
|
|||
bool g_irqObserve = false;
|
||||
bool g_edgeWake = false;
|
||||
|
||||
// TEMPLATE LEARNING: fold the frames of a successful press back into the
|
||||
// stored template, as stock does. On by default because stock does it and
|
||||
// because every match rate this project has measured was taken against a
|
||||
// day-zero template; `--learn=0` turns it off so the two can be compared on
|
||||
// one binary without a rebuild, which is the only way the comparison is
|
||||
// single-variable.
|
||||
bool g_learn = true;
|
||||
// How many frames one press may contribute. Stock has no explicit bound -- it
|
||||
// harvests until the finger lifts -- but a finger left resting on the sensor
|
||||
// should not grow the template without limit, and the template body has a hard
|
||||
// ceiling: it moves as a SINGLE gpfile op against a 516084-byte listener
|
||||
// buffer, and a 30-sample body already measured 386402.
|
||||
int g_learnMaxFrames = 8;
|
||||
|
||||
// The namespace key the trustlet hashes into the SFS group's directory name.
|
||||
// It defaults to Android's because that is what this device's existing store
|
||||
|
|
@ -1453,9 +1466,95 @@ public:
|
|||
SendCommand(app_, ta::Cmd::Cancel, {});
|
||||
out.cancelled = true;
|
||||
}
|
||||
// A press that matched is the only material template learning gets.
|
||||
if (out.matched && g_learn) HarvestTemplate(g_learnMaxFrames);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Template learning
|
||||
//
|
||||
// Stock's post-match harvest, and the whole reason a stock template grows
|
||||
// over its life: while the finger is STILL on the sensor after a match,
|
||||
// keep capturing and fold each frame into the loaded template. Stock's
|
||||
// loop is {QUERY_FINGER_STATUS, CAPTURE_IMAGE, UPDATE_TEMPLATE} and it
|
||||
// sends NO event -- so the matcher does not run again and this cannot
|
||||
// revise the verdict the client has already been given.
|
||||
//
|
||||
// A quick tap pays almost nothing: the finger is gone by the time a
|
||||
// verdict lands, so the first capture reads the idle floor and the loop
|
||||
// stops. A held press contributes the frames it was actually held for.
|
||||
// Which means the frames learned from are exactly the frames a real
|
||||
// unlock produces -- the "enrolment must resemble verification" problem,
|
||||
// solved by the algorithm instead of by coaching the user.
|
||||
int HarvestTemplate(int maxFrames) {
|
||||
namespace ta = fingerprintd::ta;
|
||||
int folded = 0;
|
||||
for (int i = 0; i < maxFrames; i++) {
|
||||
std::vector<std::byte> cap(ta::CaptureDeclaredLen);
|
||||
ta::BuildCapturePayload(cap);
|
||||
auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap);
|
||||
if (!c.invoked || c.result != 0) break;
|
||||
if (!baseline_.IsFinger(c.metric)) {
|
||||
if (g_verbose)
|
||||
std::println(" learn: finger gone (metric={}), {} frame(s) folded",
|
||||
c.metric, folded);
|
||||
break;
|
||||
}
|
||||
std::vector<std::byte> up(ta::UpdateTemplatePayloadSize);
|
||||
// Bit 6 on the first frame, mirroring stock: it marks the frame
|
||||
// whose reported event was FingerTouched, and it selects which of
|
||||
// the algorithm's two update entries runs.
|
||||
ta::BuildUpdateTemplate(up, static_cast<std::uint32_t>(folded), folded == 0);
|
||||
auto u = SendCommand(app_, ta::Cmd::UpdateTemplate, up);
|
||||
if (!u.Ok()) {
|
||||
std::println(" learn: UPDATE_TEMPLATE rc={} ({}) result={} -- stopping",
|
||||
u.rc, ta::StrError(u.rc), static_cast<int>(u.result));
|
||||
break;
|
||||
}
|
||||
folded++;
|
||||
if (g_verbose)
|
||||
std::println(" learn: frame {} folded in (metric={})", folded, c.metric);
|
||||
}
|
||||
if (folded > 0) templateDirty_ = true;
|
||||
std::println(" learn: {} frame(s) folded into the template{}", folded,
|
||||
templateDirty_ ? ", save pending" : "");
|
||||
return folded;
|
||||
}
|
||||
|
||||
// Persist what the harvest folded in. Stock does this lazily -- it arms a
|
||||
// timer on the match ("lazy data updater activated") and the SAVE_DATA
|
||||
// lands at the next authenticate, taking 356 ms and rewriting the template
|
||||
// plus the share template plus the chip calibration. Deferring it matters
|
||||
// for the same reason it does on stock: the client already has its answer,
|
||||
// and a third of a second of RPMB traffic does not belong on the unlock
|
||||
// path. Here the worker calls this after it has posted the verdict.
|
||||
bool FlushTemplate() {
|
||||
namespace ta = fingerprintd::ta;
|
||||
if (!templateDirty_) return true;
|
||||
if (g_sfsReadOnly || !g_rpmbWrite) {
|
||||
std::println(" learn: learned frames DISCARDED -- the store is read-only");
|
||||
templateDirty_ = false;
|
||||
return false;
|
||||
}
|
||||
std::vector<std::byte> sd(0x10, std::byte{0});
|
||||
for (std::size_t k = 0; k < 4; k++)
|
||||
sd[k] = static_cast<std::byte>((ta::SaveMaskTemplate >> (8 * k)) & 0xFF);
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
auto sv = SendCommand(app_, ta::Cmd::SaveData, sd);
|
||||
Report(ta::Cmd::SaveData, sv);
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
templateDirty_ = false;
|
||||
if (!sv.Ok()) {
|
||||
std::println(" learn: the learned template did NOT persist (rc={})", sv.rc);
|
||||
return false;
|
||||
}
|
||||
std::println(" learn: template saved in {} ms", ms);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TemplateDirty() const { return templateDirty_; }
|
||||
|
||||
bool FingerPresent() const { return fingerPresent_.load(); }
|
||||
// Before SyncConfig has run there is no answer yet; stock's value is the
|
||||
// only honest stand-in, and a client asking this early gets it.
|
||||
|
|
@ -1556,6 +1655,8 @@ private:
|
|||
Sensor sensor_;
|
||||
fingerprintd::engine::Baseline baseline_;
|
||||
std::uint32_t gid_ = 0xFFFFFFFF;
|
||||
// Set when a harvest folded at least one frame in; cleared by the save.
|
||||
bool templateDirty_ = false;
|
||||
std::atomic<bool> fingerPresent_{false};
|
||||
std::thread irqThread_;
|
||||
std::atomic<bool> irqQuit_{false};
|
||||
|
|
@ -1699,6 +1800,11 @@ private:
|
|||
else if (o.matched) ev->status = "verify-match";
|
||||
else ev->status = "verify-no-match";
|
||||
PostEvent(std::move(ev));
|
||||
// AFTER the verdict is on its way to the client, never before:
|
||||
// persisting a learned template is ~350 ms of gpfile and RPMB
|
||||
// traffic and it must not sit on the unlock path. Stock defers
|
||||
// it the same way, with a timer.
|
||||
session_.FlushTemplate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -2207,13 +2313,43 @@ int RunDaemon() {
|
|||
// -----------------------------------------------------------------------------
|
||||
// Diagnostic modes: the probe flow, kept for a phone with no bus client at hand.
|
||||
// -----------------------------------------------------------------------------
|
||||
int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, std::uint32_t gid, int frames) {
|
||||
int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, bool doLearnProbe,
|
||||
std::uint32_t gid, int frames) {
|
||||
namespace ta = fingerprintd::ta;
|
||||
Session s;
|
||||
if (!s.Start()) return 1;
|
||||
int n = s.SetActiveGroup(gid);
|
||||
(void)n;
|
||||
|
||||
// Does the trustlet accept UPDATE_TEMPLATE at all? NO FINGER NEEDED, and
|
||||
// that is the point: this project's record says "DO NOT RETRY 0x1015 until
|
||||
// a template is loaded" because every earlier shape answered -90, i.e. the
|
||||
// app was gone. One template is loaded now, so this asks the question for
|
||||
// the cost of one command -- and ENUMERATE afterwards proves whether the
|
||||
// trustlet survived it, which is the part a bare rc cannot tell you.
|
||||
if (doLearnProbe) {
|
||||
if (n <= 0)
|
||||
std::println("\n*** no template loaded (ENUMERATE={}) -- 0x1015 reads the "
|
||||
"template AFTER the update call and faults without one. "
|
||||
"Refusing to probe. ***", n);
|
||||
else {
|
||||
std::println("\n=== UPDATE_TEMPLATE (no finger; {} template(s) loaded) ===", n);
|
||||
std::vector<std::byte> up(ta::UpdateTemplatePayloadSize);
|
||||
ta::BuildUpdateTemplate(up, 0, /*touchFrame*/ true);
|
||||
Report(ta::Cmd::UpdateTemplate,
|
||||
SendCommand(s.App(), ta::Cmd::UpdateTemplate, up));
|
||||
std::println(" and again as a held frame (bit 6 clear, the x_update path):");
|
||||
ta::BuildUpdateTemplate(up, 1, /*touchFrame*/ false);
|
||||
Report(ta::Cmd::UpdateTemplate,
|
||||
SendCommand(s.App(), ta::Cmd::UpdateTemplate, up));
|
||||
// The liveness probe. -90 here means the trustlet took a fault.
|
||||
auto e = SendCommand(s.App(), ta::Cmd::Enumerate, {});
|
||||
Report(ta::Cmd::Enumerate, e);
|
||||
std::println(" trustlet {} (ENUMERATE rc={})",
|
||||
e.invoked && e.rc >= 0 ? "SURVIVED" : "IS GONE", e.rc);
|
||||
}
|
||||
}
|
||||
|
||||
if (doCalSave) {
|
||||
if (g_sfsReadOnly) { std::println(std::cerr, "a calibration save writes; pass --sfs-writable"); return 1; }
|
||||
std::vector<std::byte> sd(0x10, std::byte{0});
|
||||
|
|
@ -2235,6 +2371,10 @@ int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, std::uint32_t gid, int f
|
|||
for (int c = 3; c > 0; c--) { std::println("*** press your finger in {}... ***", c); std::this_thread::sleep_for(std::chrono::seconds(1)); }
|
||||
auto o = s.Verify(gid, cancel, frames, {}); // probe: any template counts
|
||||
std::println("verify: decided={} matched={} fid={}", o.decided, o.matched, o.fid);
|
||||
// The probe is the cheapest end-to-end test of template learning:
|
||||
// Verify harvests, this persists, and the container on disk should
|
||||
// come back larger than it went in.
|
||||
s.FlushTemplate();
|
||||
}
|
||||
s.Stop();
|
||||
return 0;
|
||||
|
|
@ -2287,6 +2427,7 @@ int RunProbeTaLoad(const std::string& path) {
|
|||
int main(int argc, char** argv) {
|
||||
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
||||
bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false;
|
||||
bool doLearnProbe = false;
|
||||
std::string probeTa;
|
||||
std::uint32_t gid = 0;
|
||||
int frames = 120;
|
||||
|
|
@ -2305,6 +2446,7 @@ int main(int argc, char** argv) {
|
|||
if (a == "--auth") { doAuth = true; probe = true; }
|
||||
if (a == "--enrol") { doEnrol = true; probe = true; }
|
||||
if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = true; }
|
||||
if (a == "--probe-learn") { doLearnProbe = true; probe = true; g_verbose = true; }
|
||||
if (a.starts_with("--frames=")) frames = std::stoi(std::string(a.substr(9)));
|
||||
if (a.starts_with("--frame-gap=")) g_frameGapMs = std::stoi(std::string(a.substr(12)));
|
||||
if (a == "--undecided=nomatch") g_undecidedIsNoMatch = true;
|
||||
|
|
@ -2317,13 +2459,15 @@ int main(int argc, char** argv) {
|
|||
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
|
||||
if (a.starts_with("--group-path=")) g_groupPath = a.substr(13);
|
||||
if (a.starts_with("--samples=")) { g_samples = std::stoi(std::string(a.substr(10))); g_samplesForced = true; }
|
||||
if (a.starts_with("--learn=")) g_learn = a.substr(8) != "0";
|
||||
if (a.starts_with("--learn-frames=")) g_learnMaxFrames = std::stoi(std::string(a.substr(15)));
|
||||
if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11);
|
||||
if (a.starts_with("--gid=")) gid = static_cast<std::uint32_t>(std::stoul(std::string(a.substr(6))));
|
||||
}
|
||||
StartTranscript(g_logDir);
|
||||
if (!probeTa.empty()) return RunProbeTaLoad(probeTa);
|
||||
if (daemon) return RunDaemon();
|
||||
if (probe) return RunProbe(doAuth, doEnrol, doCalSave, gid, frames);
|
||||
if (probe) return RunProbe(doAuth, doEnrol, doCalSave, doLearnProbe, gid, frames);
|
||||
|
||||
std::println(std::cerr,
|
||||
"fingerprintd {}\n"
|
||||
|
|
@ -2331,6 +2475,9 @@ int main(int argc, char** argv) {
|
|||
" --probe-tee [--gid=N] bring the session up and report\n"
|
||||
" --probe-ta-load=PATH load one TA image and report the loader result\n"
|
||||
" --auth | --enrol | --cal-save diagnostic loops (see README)\n"
|
||||
" --probe-learn send one UPDATE_TEMPLATE, no finger needed\n"
|
||||
" --learn=0|1 [--learn-frames=N] fold a matched press back into the\n"
|
||||
" template, as stock does (default on, 8)\n"
|
||||
" --sfs-root=DIR --sfs-writable --rpmb-write storage policy",
|
||||
Version);
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue