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

@ -276,6 +276,19 @@ int main() {
Check(Get32(payload, RespFidOff) == FidPoison, "poison written at +0x10");
Check(Classify(0, Get32(payload, RespFidOff)) == Verdict::MatcherNeverRan,
"an untouched poisoned payload classifies as never-ran");
// PoisonFid takes the PAYLOAD and offsets internally. Handing it a
// span already offset by ResponsePayloadOff double-counts and poisons
// payload+0x20, leaving the real fid field zero -- which makes every
// released finger read as a rejection. That shipped once.
Check(RespFidOff == ResponsePayloadOff,
"the two offsets are equal, which is exactly why double-applying is silent");
std::vector<std::byte> wrong(64);
PoisonFid(std::span(wrong).subspan(ResponsePayloadOff));
Check(Get32(wrong, RespFidOff) != FidPoison,
"double-offsetting leaves the fid field unpoisoned");
Check(Classify(0, Get32(wrong, RespFidOff)) == Verdict::Rejected,
"and an unpoisoned release is then misread as a rejection");
}
// ---- Init chain