An enrolment cannot be ended by a finger release

A three-tap enrolment declared itself complete. The transcript says why:

    frame 2: metric=308  FINGER  ev5 rem=10
    frame 3: metric=187          ev6 rem=0
    samples: 10 of 10 accepted

The release event never enters do_enroll, so its response leaves
samples-remaining untouched at 0 -- which is indistinguishable from "none
remaining, you are finished". The session believed it, stopped after one press,
and called SAVE_DATA on an algorithm holding no template. That answered -1 and
wrote nothing, so the store was undamaged, but only by luck: the guard meant to
prevent a partial save was itself satisfied by the bogus count.

A reading is only meaningful when it came from the event that runs the enrol
path, and nothing about the value says so -- the caller has to. Observe now
takes that as an argument. Two further guards: a FIRST reading of 0 is an
unpopulated field rather than a finished enrolment, and the count only ever
falls, so an increase is noise.

Verified by mutation: trusting the release event's count, and accepting a
leading zero, each fail the suite.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 20:40:16 +02:00
commit c785aad653
3 changed files with 70 additions and 10 deletions

View file

@ -119,11 +119,32 @@ export namespace fingerprintd::engine {
// remaining into the response payload on the common path whether or not
// the sample was accepted, and the log starves exactly when a frame is
// accepted. `rem` counting down is the only reliable progress signal.
//
// "The common path" means do_enroll's common path -- and a finger-RELEASE
// event never enters do_enroll at all. Its response leaves the field
// untouched, so it reads 0, which is indistinguishable from "no samples
// remaining, you are finished". Taking that at face value ends an
// enrolment after one press and then calls SAVE_DATA on an algorithm
// holding no template, which answers -1.
//
// So a reading is only meaningful when it came from the event that runs
// the enrol path. The caller has to say so; there is no way to tell from
// the value.
class EnrolSession {
public:
void Observe(std::int32_t remaining) {
if (remaining < 0) return; // not populated by this command
if (!started_) { total_ = remaining; started_ = true; }
void Observe(std::int32_t remaining, bool fromEnrolPath) {
if (!fromEnrolPath) return; // a release reports nothing
if (remaining < 0) return; // not populated at all
if (!started_) {
// A first reading of 0 is an unpopulated field, not a finished
// enrolment: the count starts at max_enrolling_samples.
if (remaining == 0) return;
total_ = remaining;
remaining_ = remaining;
started_ = true;
return;
}
if (remaining > remaining_) return; // the count only ever falls
remaining_ = remaining;
}
bool Started() const { return started_; }
@ -139,7 +160,7 @@ export namespace fingerprintd::engine {
private:
bool started_ = false;
std::int32_t total_ = 0;
std::int32_t remaining_ = 0;
std::int32_t remaining_ = -1;
};
// ---- Authentication accounting ----------------------------------------