From 50617f994f17fdd9513042e4ad05344014dc30f1 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 22:24:12 +0200 Subject: [PATCH] Judge verification per press, and log every frame of it A wrong finger against the daemon was a silent wait: the loop exited only on a terminal verdict, and at the stock rescan budget a wrong finger never produces one -- it answers "not identified yet" on every frame until the right finger shows up. fprintd's PAM module needs a verify-no-match to deny or retry, so that is a client that hangs. The unit of decision is now a press. Within one press the matcher may reject early frames and match a later one, so a press is judged when the finger lifts: any match wins, only rejections is no-match, no terminal frame at all is undecided and scanning continues. A press that matches is reported the moment it does rather than making the user hold for a release. Undecided presses are exactly why the rescan budget matters here. At 0 every frame is terminal and every press decides; at the stock budget seven of nineteen correct-finger presses ended undecided in the last measurement, which under a press rule would read as rejections. So the daemon is being run with rescan forced to 0 while that trade-off is measured on the new template rather than assumed. Every frame is logged under --verbose and every press always, so the next wrong-finger control is visible in the transcript instead of absent from it. --- implementations/main.cpp | 66 ++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index b06423a..d8abc3c 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -1200,14 +1200,27 @@ public: // ---- Verification // - // Runs until the first terminal verdict or a cancel. A frame is one of - // three things and only the third is a verdict: release (poison intact), - // rescan (rc=-11), or match/reject. + // The unit of decision is a PRESS, not a frame. A frame is one of three + // things -- release (poison intact), rescan (rc=-11), match/reject -- and + // within one press the matcher may reject early frames and match a later + // one, so a press is judged when the finger LIFTS: any match wins; only + // rejections means no-match; no terminal frame at all means undecided, + // and scanning continues into the next press. + // + // That last case is why the rescan budget matters here. At the stock + // budget a wrong finger answers "not identified yet" on every frame and + // never yields a terminal rejection, so its presses are all undecided and + // a client waits forever -- fprintd's PAM module needs a verify-no-match + // to deny or retry. With max_authentication_rescan_times at 0 every frame + // is terminal and every press decides. A wrong finger stops being a + // silent wait. struct VerifyOutcome { bool decided = false; bool matched = false; bool cancelled = false; std::uint32_t fid = 0; + int presses = 0; + int frames = 0; }; VerifyOutcome Verify(std::uint32_t gid, std::atomic& cancel, int maxFrames) { @@ -1226,6 +1239,10 @@ public: if (!a.Ok()) return out; en::TouchTracker tracker; + bool inPress = false, pressMatched = false, pressRejected = false; + int pressFrames = 0, rescans = 0; + std::uint32_t pressFid = 0; + // The reference frame loop is {QUERY, CAPTURE, REPORT, QUERY, REPORT}. // The trailing query acknowledges the trustlet's event state; without // it, after the first verdict every later frame answers "not @@ -1239,8 +1256,15 @@ public: auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap); bool finger = baseline_.IsFinger(c.metric); fingerPresent_.store(finger); + out.frames++; + std::string note; for (ta::Event ev : tracker.Observe(finger, en::Mode::Authenticate)) { + if (ev == ta::Event::FingerTouched) { + inPress = true; pressMatched = false; pressRejected = false; + pressFrames = 0; rescans = 0; pressFid = 0; + out.presses++; + } std::vector evbuf(ta::EventContextSize); ta::BuildEventContext(evbuf, { .event = ev }); // A zero-initialised buffer cannot tell "the matcher never @@ -1250,14 +1274,35 @@ public: auto r = SendCommand(app_, ta::Cmd::ReportEvent, evbuf); if (!r.invoked) continue; ta::Verdict v = ta::Classify(r.rc, r.fid); - if (ta::IsTerminal(v)) { - out.decided = true; - out.matched = (v == ta::Verdict::Match); - out.fid = out.matched ? r.fid : 0; - std::println(" verdict: {} fid={}", out.matched ? "MATCH" : "REJECTED", out.fid); - break; + switch (v) { + case ta::Verdict::Match: pressMatched = true; pressFid = r.fid; note += " MATCH"; break; + case ta::Verdict::Rejected: pressRejected = true; note += " rej"; break; + case ta::Verdict::NotIdentifiedYet: rescans++; note += " -11"; break; + case ta::Verdict::MatcherNeverRan: break; + } + if (ev == ta::Event::FingerReleased && inPress) { + // The press is over: judge it. + inPress = false; + if (pressMatched) { + out.decided = true; out.matched = true; out.fid = pressFid; + } else if (pressRejected) { + out.decided = true; out.matched = false; + } + std::println(" press {}: {} frames, {} rescans -> {}", out.presses, pressFrames, + rescans, pressMatched ? std::format("MATCH fid={}", pressFid) + : pressRejected ? "NO MATCH" : "undecided"); } } + if (finger) pressFrames++; + // A press that matched is decided the moment it does; do not make + // the user keep holding for a release. + if (pressMatched && !out.decided) { + out.decided = true; out.matched = true; out.fid = pressFid; + std::println(" press {}: {} frames -> MATCH fid={}", out.presses, pressFrames, pressFid); + } + if (g_verbose) + std::println(" frame {:3}: metric={:<4}{}{}", i + 1, c.metric, + finger ? " FINGER" : " ", note); SendCommand(app_, ta::Cmd::QueryEventStatus, q); std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); } @@ -1434,6 +1479,9 @@ private: } case Job::Kind::Verify: { auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600); + std::println("verify: {} over {} press(es), {} frame(s)", + o.cancelled ? "cancelled" : !o.decided ? "undecided" + : o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames); auto ev = std::make_unique(Event{ .kind = Event::Kind::VerifyStatus }); ev->done = true; ev->fid = o.fid;