Fix the poison offset: a released finger was reading as a rejection

PoisonFid takes the payload and offsets to the fid field internally. It was
being handed a span already offset by the payload offset, so the poison landed
at payload+0x20 and the real fid field stayed zero. A frame where the matcher
never ran then looks exactly like a frame where it ran and rejected -- which is
the specific failure this project has recorded three times and is precisely
what the poison exists to prevent.

Visible in a real run: the frames marked REJECTED were 138, 138, 133, 137, 134
against a floor of 136, i.e. every one of them was a finger-RELEASE frame with
nothing on the sensor. Five rejections that never happened.

The two offsets are numerically equal, which is why double-applying is silent,
so the test now pins both directions: poisoning the payload marks the fid
field, and poisoning an already-offset span leaves it zero and misclassifies.

Also adds --rescan=N, which patches common.max_authentication_rescan_times into
the config. The stock budget lets a whole run end with no terminal verdict --
correct for shipping, useless as a measurement, because a wrong-finger control
that never reaches a verdict has not demonstrated a rejection. Forcing 0 makes
every frame terminal. It prints MEASUREMENT ONLY because a rate taken that way
is a per-frame figure with the retry mechanism disabled, and is not a shipping
reject rate.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 19:16:50 +02:00
commit afb7496ef6
2 changed files with 45 additions and 1 deletions

View file

@ -57,6 +57,7 @@ bool g_auth = false;
int g_frames = 40;
int g_frameGapMs = 500;
std::string g_logDir = "/var/log/fingerprintd";
int g_rescan = -1; // -1 = leave the config's value alone
std::uint32_t g_gid = 0;
std::string g_taPath = "/lib/firmware/focal64.mbn";
std::string g_cfgPath = "/lib/firmware/fingerprintd.json";
@ -944,6 +945,29 @@ int Probe() {
}
std::string json((std::istreambuf_iterator<char>(cf)),
std::istreambuf_iterator<char>());
// common.max_authentication_rescan_times bounds how many frames the
// matcher may answer "not identified yet" before it has to produce a
// verdict. At the stock default a whole run can end undecided, which is
// the right shipping behaviour and useless as a measurement: a
// wrong-finger control that never reaches a verdict has not demonstrated
// a rejection. Setting it to 0 forces every frame terminal.
//
// MEASUREMENT ONLY. A rate measured this way is a per-frame figure taken
// with the retry mechanism disabled and is not a shipping reject rate.
if (g_rescan >= 0) {
auto at = json.find("\"common\":{");
if (at == std::string::npos) at = json.find("\"common\": {");
if (at == std::string::npos) {
std::println(std::cerr, "config has no \"common\" object to patch");
return 1;
}
auto brace = json.find('{', at);
json.insert(brace + 1,
std::format("\"max_authentication_rescan_times\":{},", g_rescan));
std::println("forcing max_authentication_rescan_times={} (MEASUREMENT ONLY)",
g_rescan);
}
// The trustlet wants the terminating NUL counted.
std::vector<std::byte> cfg(json.size() + 1, std::byte{0});
for (std::size_t i = 0; i < json.size(); i++)
@ -1100,7 +1124,13 @@ int Probe() {
// buffer cannot tell "the matcher never ran" from "the matcher
// ran and rejected the finger" -- the failure path writes zero
// there too, so zero is ambiguous and 0xAAAAAAAA is not.
ta::PoisonFid(std::span(evbuf).subspan(ta::ResponsePayloadOff));
// PoisonFid already writes at RespFidOff within the PAYLOAD.
// Handing it a span that is itself already offset by the
// payload offset double-counts and poisons payload+0x20, so
// the real fid field stays zero -- and a released finger then
// classifies as a REJECTION, inventing failures that never
// happened.
ta::PoisonFid(evbuf);
auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf);
if (!r.invoked) continue;
@ -1162,6 +1192,7 @@ int main(int argc, char** argv) {
if (a == "--auth") { g_auth = true; g_listeners = true; }
if (a.starts_with("--frames=")) g_frames = std::stoi(std::string(a.substr(9)));
if (a.starts_with("--log-dir=")) g_logDir = a.substr(10);
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11);
if (a.starts_with("--gid=")) g_gid = static_cast<std::uint32_t>(
std::stoul(std::string(a.substr(6))));