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:
parent
a8c989857a
commit
afb7496ef6
2 changed files with 45 additions and 1 deletions
|
|
@ -57,6 +57,7 @@ bool g_auth = false;
|
||||||
int g_frames = 40;
|
int g_frames = 40;
|
||||||
int g_frameGapMs = 500;
|
int g_frameGapMs = 500;
|
||||||
std::string g_logDir = "/var/log/fingerprintd";
|
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::uint32_t g_gid = 0;
|
||||||
std::string g_taPath = "/lib/firmware/focal64.mbn";
|
std::string g_taPath = "/lib/firmware/focal64.mbn";
|
||||||
std::string g_cfgPath = "/lib/firmware/fingerprintd.json";
|
std::string g_cfgPath = "/lib/firmware/fingerprintd.json";
|
||||||
|
|
@ -944,6 +945,29 @@ int Probe() {
|
||||||
}
|
}
|
||||||
std::string json((std::istreambuf_iterator<char>(cf)),
|
std::string json((std::istreambuf_iterator<char>(cf)),
|
||||||
std::istreambuf_iterator<char>());
|
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.
|
// The trustlet wants the terminating NUL counted.
|
||||||
std::vector<std::byte> cfg(json.size() + 1, std::byte{0});
|
std::vector<std::byte> cfg(json.size() + 1, std::byte{0});
|
||||||
for (std::size_t i = 0; i < json.size(); i++)
|
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
|
// buffer cannot tell "the matcher never ran" from "the matcher
|
||||||
// ran and rejected the finger" -- the failure path writes zero
|
// ran and rejected the finger" -- the failure path writes zero
|
||||||
// there too, so zero is ambiguous and 0xAAAAAAAA is not.
|
// 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);
|
auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf);
|
||||||
if (!r.invoked) continue;
|
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 == "--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("--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("--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("--sfs-root=")) g_sfsRoot = a.substr(11);
|
||||||
if (a.starts_with("--gid=")) g_gid = static_cast<std::uint32_t>(
|
if (a.starts_with("--gid=")) g_gid = static_cast<std::uint32_t>(
|
||||||
std::stoul(std::string(a.substr(6))));
|
std::stoul(std::string(a.substr(6))));
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,19 @@ int main() {
|
||||||
Check(Get32(payload, RespFidOff) == FidPoison, "poison written at +0x10");
|
Check(Get32(payload, RespFidOff) == FidPoison, "poison written at +0x10");
|
||||||
Check(Classify(0, Get32(payload, RespFidOff)) == Verdict::MatcherNeverRan,
|
Check(Classify(0, Get32(payload, RespFidOff)) == Verdict::MatcherNeverRan,
|
||||||
"an untouched poisoned payload classifies as never-ran");
|
"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
|
// ---- Init chain
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue