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_irqObserve = false;
|
||||||
bool g_edgeWake = 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.
|
// 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
|
// It defaults to Android's because that is what this device's existing store
|
||||||
|
|
@ -1453,9 +1466,95 @@ public:
|
||||||
SendCommand(app_, ta::Cmd::Cancel, {});
|
SendCommand(app_, ta::Cmd::Cancel, {});
|
||||||
out.cancelled = true;
|
out.cancelled = true;
|
||||||
}
|
}
|
||||||
|
// A press that matched is the only material template learning gets.
|
||||||
|
if (out.matched && g_learn) HarvestTemplate(g_learnMaxFrames);
|
||||||
return out;
|
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(); }
|
bool FingerPresent() const { return fingerPresent_.load(); }
|
||||||
// Before SyncConfig has run there is no answer yet; stock's value is the
|
// 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.
|
// only honest stand-in, and a client asking this early gets it.
|
||||||
|
|
@ -1556,6 +1655,8 @@ private:
|
||||||
Sensor sensor_;
|
Sensor sensor_;
|
||||||
fingerprintd::engine::Baseline baseline_;
|
fingerprintd::engine::Baseline baseline_;
|
||||||
std::uint32_t gid_ = 0xFFFFFFFF;
|
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::atomic<bool> fingerPresent_{false};
|
||||||
std::thread irqThread_;
|
std::thread irqThread_;
|
||||||
std::atomic<bool> irqQuit_{false};
|
std::atomic<bool> irqQuit_{false};
|
||||||
|
|
@ -1699,6 +1800,11 @@ private:
|
||||||
else if (o.matched) ev->status = "verify-match";
|
else if (o.matched) ev->status = "verify-match";
|
||||||
else ev->status = "verify-no-match";
|
else ev->status = "verify-no-match";
|
||||||
PostEvent(std::move(ev));
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2207,13 +2313,43 @@ int RunDaemon() {
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Diagnostic modes: the probe flow, kept for a phone with no bus client at hand.
|
// 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;
|
namespace ta = fingerprintd::ta;
|
||||||
Session s;
|
Session s;
|
||||||
if (!s.Start()) return 1;
|
if (!s.Start()) return 1;
|
||||||
int n = s.SetActiveGroup(gid);
|
int n = s.SetActiveGroup(gid);
|
||||||
(void)n;
|
(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 (doCalSave) {
|
||||||
if (g_sfsReadOnly) { std::println(std::cerr, "a calibration save writes; pass --sfs-writable"); return 1; }
|
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});
|
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)); }
|
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
|
auto o = s.Verify(gid, cancel, frames, {}); // probe: any template counts
|
||||||
std::println("verify: decided={} matched={} fid={}", o.decided, o.matched, o.fid);
|
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();
|
s.Stop();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -2287,6 +2427,7 @@ int RunProbeTaLoad(const std::string& path) {
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
||||||
bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false;
|
bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false;
|
||||||
|
bool doLearnProbe = false;
|
||||||
std::string probeTa;
|
std::string probeTa;
|
||||||
std::uint32_t gid = 0;
|
std::uint32_t gid = 0;
|
||||||
int frames = 120;
|
int frames = 120;
|
||||||
|
|
@ -2305,6 +2446,7 @@ int main(int argc, char** argv) {
|
||||||
if (a == "--auth") { doAuth = true; probe = true; }
|
if (a == "--auth") { doAuth = true; probe = true; }
|
||||||
if (a == "--enrol") { doEnrol = true; probe = true; }
|
if (a == "--enrol") { doEnrol = true; probe = true; }
|
||||||
if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = 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("--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.starts_with("--frame-gap=")) g_frameGapMs = std::stoi(std::string(a.substr(12)));
|
||||||
if (a == "--undecided=nomatch") g_undecidedIsNoMatch = true;
|
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("--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("--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("--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("--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))));
|
if (a.starts_with("--gid=")) gid = static_cast<std::uint32_t>(std::stoul(std::string(a.substr(6))));
|
||||||
}
|
}
|
||||||
StartTranscript(g_logDir);
|
StartTranscript(g_logDir);
|
||||||
if (!probeTa.empty()) return RunProbeTaLoad(probeTa);
|
if (!probeTa.empty()) return RunProbeTaLoad(probeTa);
|
||||||
if (daemon) return RunDaemon();
|
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,
|
std::println(std::cerr,
|
||||||
"fingerprintd {}\n"
|
"fingerprintd {}\n"
|
||||||
|
|
@ -2331,6 +2475,9 @@ int main(int argc, char** argv) {
|
||||||
" --probe-tee [--gid=N] bring the session up and report\n"
|
" --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"
|
" --probe-ta-load=PATH load one TA image and report the loader result\n"
|
||||||
" --auth | --enrol | --cal-save diagnostic loops (see README)\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",
|
" --sfs-root=DIR --sfs-writable --rpmb-write storage policy",
|
||||||
Version);
|
Version);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue