Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
// lint-disable-file fixed-width-types
|
|
|
|
|
/*
|
|
|
|
|
Fingerprintd:Ta — the focal64 trustlet's command surface.
|
|
|
|
|
|
|
|
|
|
Requests and responses only: build a payload, read a response, name an error.
|
|
|
|
|
No TEE, no transport. The daemon shell invokes; this module decides what bytes
|
|
|
|
|
go in and what the bytes coming back mean.
|
|
|
|
|
|
|
|
|
|
Almost nothing here is guessable. The layouts were read out of the stock HAL
|
|
|
|
|
(`fingerprint.default.so`, unoptimised and unstripped) and out of the trustlet
|
|
|
|
|
itself, and three of the fields were found only because QTEE recorded a fault
|
|
|
|
|
naming the instruction that read them. Each one is annotated with where it came
|
|
|
|
|
from, because "we tried values until it worked" is exactly what did not work.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export module Fingerprintd:Ta;
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
export namespace fingerprintd::ta {
|
|
|
|
|
|
|
|
|
|
// ---- Commands ---------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
enum class Cmd : std::uint32_t {
|
|
|
|
|
TaInit = 0x1004,
|
|
|
|
|
InitSpi = 0x1006,
|
|
|
|
|
ProbeDevice = 0x100a,
|
|
|
|
|
InitDevice = 0x100b,
|
|
|
|
|
SyncConfig = 0x100d,
|
|
|
|
|
SyncStatistics = 0x100e,
|
|
|
|
|
StartScanning = 0x1012,
|
|
|
|
|
CaptureImage = 0x1013,
|
Add the authentication loop
Arms a scan session and drives the frame loop: capture, decide finger from the
calibrated floor, report the touch edges, classify the verdict.
It needs no writes of any kind -- no SAVE_DATA, no RPMB write, no SFS write --
so it runs safely against an existing template with the store read-only. That
is what makes it the right thing to try before enrolment rather than after.
Verified armed on the phone: the template loads, the floor calibrates, and
AUTHENTICATE returns rc=0, which also proves the gid agrees with the one
SET_ACTIVE_GROUP used (a mismatch answers -200). With no finger present the
loop correctly reports nothing: no touch edge, no event, no terminal frame.
The fid field is poisoned before every REPORT_EVENT, because a zero-initialised
buffer cannot distinguish "the matcher never ran" from "the matcher ran and
rejected" -- the failure path writes zero there too.
The tally reports terminal frames as the denominator and presses separately,
so a run cannot be read as having rejections it did not have.
2026-09-02 18:48:44 +02:00
|
|
|
QueryEventStatus = 0x101d,
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
SaveData = 0x1014,
|
UPDATE_TEMPLATE: the command stock learns with, and the field that was killing it
Stock rewrites the stored template on every successful press. Its post-match
loop is QUERY_FINGER_STATUS, CAPTURE_IMAGE, 0x1015 UPDATE_TEMPLATE while the
finger stays down, with no REPORT_EVENT in it -- so the matcher does not re-run
and the verdict cannot change. Forty-six of those against eighty-six captures in
one reference session, and the stored body measurably grows: 333278 bytes at
enrolment, 360822 at the next session's load, 371734 after one authentication
session. This daemon sent none of them.
The command shares REPORT_EVENT's event context. The stock wrapper memsets 732
bytes and writes six fields: a zero byte at +0x2a4, the scan-slot count, a zero
word, the count of frames folded so far in this press, a flags word of
0x00080000 with bit 6 set on the frame whose event was FingerTouched, and a zero
at +0x2d8. Declared length 0x2dc.
+0x2d8 is the one that matters, and it matters by staying zero. The dispatcher
stub reads it after the handler returns and only if it is non-zero does it read
+0x2dc and make that the response length. Every earlier attempt in this project
set both fields and varied the declared length across 0x2e0, 0x400 and 0x1000;
all of them answered -90, the trustlet gone, and the conclusion recorded was
"do not retry until a template is loaded". A loaded template was necessary but
not sufficient. The stock HAL sets neither field.
Measured on the device with two templates loaded and no finger: both branches
answer rc=0 and ENUMERATE still reports 2, so the app did not fault.
Those fields are little endian, assembled low-address-first by the handler. An
earlier reading called them big endian, off the bfi order, and was wrong.
Bit 6 selects which algorithm entry runs: clear takes libfp_template_x_update,
set takes the other, which also reads the scan-slot count.
2026-09-03 17:45:27 +02:00
|
|
|
UpdateTemplate = 0x1015,
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
ReportEvent = 0x1018,
|
|
|
|
|
WorkMode = 0x1020,
|
|
|
|
|
|
|
|
|
|
PreEnroll = 0x2000,
|
|
|
|
|
Enroll = 0x2001,
|
|
|
|
|
PostEnroll = 0x2002,
|
|
|
|
|
Cancel = 0x2004,
|
Add enrolment, and let it choose its own namespace
Enrolment is the first thing here that writes: template containers through the
gpfile listener and counter records through RPMB. It refuses to run unless both
--sfs-writable and --rpmb-write are given, and it refuses to call SAVE_DATA if
the sample count did not reach zero, because a partial template is worse than
none.
The sequence is stock's: cancel, reset-lockout, authenticate, cancel,
reset-lockout, PRE_ENROLL, authenticate, cancel, ENROLL, the sample loop,
POST_ENROLL, SAVE_DATA with bit 30 set. AUTHENTICATE is what arms the capture
session, which is why it appears in an enrolment at all.
Enrolment takes one sample per PRESS: touch on the rising edge, release on the
falling one, nothing in between. Stock's entire enrolment trace contains no
image-ready event, and feeding every held frame gives the algorithm
near-duplicate images from a single press.
Two things named honestly. The ENROLL payload's u32 at +69 was recorded here as
a "timeout"; the trustlet reports it back as the GROUP ID, and filling a
mislabelled field with a plausible number is the entire provenance of gid 60.
It is the gid now, so an enrolment can choose its own group.
And --group-path exposes the namespace key the trustlet hashes into the group's
directory name. It defaults to Android's, which is where this device's existing
store lives and how that template is readable. But SAVE_DATA rewrites the
group's index container, and an index QTEE later fails to verify takes every
template listed in it -- so enrolling into a DIFFERENT namespace is complete
isolation from a store we did not write.
2026-09-02 20:12:24 +02:00
|
|
|
ResetLockout = 0x200a,
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
Enumerate = 0x2005,
|
|
|
|
|
SetActiveGroup = 0x2007,
|
|
|
|
|
Authenticate = 0x2008,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The device init chain, in order. Every step returns rc=0 on a healthy
|
|
|
|
|
// sensor, ending in the trustlet's own "TA is successfully initialized."
|
|
|
|
|
//
|
|
|
|
|
// SyncStatistics is not optional and is not cosmetic. `g_statistics` is
|
|
|
|
|
// statically NULL and command 0x100e is its only writer; do_enroll stores a
|
|
|
|
|
// timestamp through it without a null check, so the first enrol frame that
|
|
|
|
|
// ever gets that far takes a data abort at TA offset 0x15f74. The trustlet
|
|
|
|
|
// dies, every later command answers -90, and nothing in the log says why —
|
|
|
|
|
// it was found in QTEE's fault ring, not by varying inputs.
|
|
|
|
|
inline constexpr std::array<Cmd, 6> InitChain = {
|
|
|
|
|
Cmd::InitSpi, Cmd::ProbeDevice, Cmd::InitDevice,
|
|
|
|
|
Cmd::TaInit, Cmd::WorkMode, Cmd::SyncStatistics,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A reload needs the same chain. Without it the per-slot enroll-template
|
|
|
|
|
// array is never allocated and FtInitEnrollTplData writes through a NULL
|
|
|
|
|
// at TA 0xceb70 — the same shape of bug as g_statistics, found the same
|
|
|
|
|
// way, and harmless right up until a template is actually reachable.
|
|
|
|
|
inline constexpr std::size_t SyncStatisticsPayloadSize = 560;
|
|
|
|
|
|
|
|
|
|
// ---- Work modes and events -------------------------------------------
|
|
|
|
|
|
|
|
|
|
enum class WorkMode : std::uint32_t {
|
|
|
|
|
Idle = 0, WaitTouch = 1, WaitLeave = 2, Gesture = 6,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Event ids for REPORT_EVENT. The trustlet never polls for a finger: the
|
|
|
|
|
// normal world takes the sensor IRQ and tells it what happened.
|
|
|
|
|
enum class Event : std::uint32_t {
|
|
|
|
|
FingerTouched = 5,
|
|
|
|
|
FingerReleased = 6,
|
|
|
|
|
ImageReady = 7,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Event 7 reaches the matcher unconditionally; event 5 only when
|
|
|
|
|
// device+0x10a8 is 1 or 2. Enrolment mirrors stock by sending 5 on touch
|
|
|
|
|
// and 6 on release — stock's whole enrolment trace contains no event 7,
|
|
|
|
|
// and sending it on every held frame feeds the algorithm near-duplicate
|
|
|
|
|
// images from one press.
|
|
|
|
|
inline constexpr Event EnrolTouchEvent = Event::FingerTouched;
|
|
|
|
|
inline constexpr Event AuthEvent = Event::ImageReady;
|
|
|
|
|
|
|
|
|
|
// ---- The event context -----------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// ff_trustlet_event_context_t, the REPORT_EVENT payload. The stock HAL
|
|
|
|
|
// memsets 732 bytes and writes exactly six fields
|
|
|
|
|
// (fingerprint.default.so, device_irq_event_thread 0xb82f8-0xb8374).
|
|
|
|
|
inline constexpr std::size_t EventContextSize = 740;
|
|
|
|
|
inline constexpr std::size_t EventDeclaredLen = 732;
|
|
|
|
|
|
|
|
|
|
inline constexpr std::size_t EvEventOff = 4;
|
|
|
|
|
inline constexpr std::size_t EvScanSlotsOff = 712;
|
|
|
|
|
inline constexpr std::size_t EvZeroAOff = 716;
|
|
|
|
|
inline constexpr std::size_t EvSlotIndexOff = 720;
|
|
|
|
|
inline constexpr std::size_t EvFlagsOff = 724;
|
|
|
|
|
inline constexpr std::size_t EvZeroBOff = 728;
|
|
|
|
|
|
|
|
|
|
inline constexpr std::uint32_t EvDefaultFlags = 0x08080000;
|
|
|
|
|
inline constexpr std::uint32_t EvDefaultScanSlots = 1;
|
|
|
|
|
|
|
|
|
|
// The scan-slot count is load-bearing and was zero in every run this
|
|
|
|
|
// project made for weeks. do_enroll assembles it bytewise and
|
|
|
|
|
// `cbz w9, 0x162e8` jumps past the entire slot loop — past the enrol call
|
|
|
|
|
// and past every flag read — straight to the "scan slot %u: groups->%s"
|
|
|
|
|
// log with both buffers still zeroed. So an all-zero payload printed
|
|
|
|
|
// `groups->, results->`, which reads exactly like a gate failing deep in
|
|
|
|
|
// the trustlet and is really zero iterations.
|
|
|
|
|
|
|
|
|
|
struct EventContext {
|
|
|
|
|
Event event = Event::ImageReady;
|
|
|
|
|
std::uint32_t scanSlots = EvDefaultScanSlots;
|
|
|
|
|
std::uint32_t slotIndex = 0;
|
|
|
|
|
std::uint32_t flags = EvDefaultFlags;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
namespace detail {
|
|
|
|
|
inline void StoreU32(std::span<std::byte> b, std::size_t off, std::uint32_t v) {
|
|
|
|
|
for (std::size_t i = 0; i < 4; i++)
|
|
|
|
|
b[off + i] = static_cast<std::byte>((v >> (8 * i)) & 0xFF);
|
|
|
|
|
}
|
|
|
|
|
inline std::uint32_t LoadU32(std::span<const std::byte> b, std::size_t off) {
|
|
|
|
|
std::uint32_t v = 0;
|
|
|
|
|
for (std::size_t i = 0; i < 4; i++)
|
|
|
|
|
v |= static_cast<std::uint32_t>(std::to_integer<unsigned>(b[off + i])) << (8 * i);
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build the event payload. The event id is LITTLE endian: the trustlet
|
|
|
|
|
// assembles it as p[4] | p[5]<<8 | ... at 0x152c0. Written big endian,
|
|
|
|
|
// event 7 arrives as 0x07000000, fails the 5..14 bound check at 0x152e4,
|
|
|
|
|
// and silently takes the default path — returning rc=0 while doing nothing.
|
|
|
|
|
inline void BuildEventContext(std::span<std::byte> out, const EventContext& ev) {
|
|
|
|
|
std::ranges::fill(out.first(EventContextSize), std::byte{0});
|
|
|
|
|
detail::StoreU32(out, EvEventOff, static_cast<std::uint32_t>(ev.event));
|
|
|
|
|
detail::StoreU32(out, EvScanSlotsOff, ev.scanSlots);
|
|
|
|
|
detail::StoreU32(out, EvZeroAOff, 0);
|
|
|
|
|
detail::StoreU32(out, EvSlotIndexOff, ev.slotIndex);
|
|
|
|
|
detail::StoreU32(out, EvFlagsOff, ev.flags);
|
|
|
|
|
detail::StoreU32(out, EvZeroBOff, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Capture ----------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// CAPTURE_IMAGE's flags word at payload+0x18. With neither bit 1 nor bit
|
|
|
|
|
// 30 set, `0x14b18: tst w8, #0x40000002 / b.eq 0x14d9c` skips image
|
|
|
|
|
// preprocessing, the frame classifier and the enrol grouper entirely — and
|
|
|
|
|
// the grouper is the only thing that ever writes the slot flag do_enroll
|
|
|
|
|
// gates on. So the trustlet returns rc=0 having done nothing but a raw
|
|
|
|
|
// scan, whatever is on the sensor.
|
|
|
|
|
//
|
|
|
|
|
// Bit 0 means "the caller appends the frame to the request". We do not, so
|
|
|
|
|
// it must stay clear.
|
|
|
|
|
inline constexpr std::size_t CaptureFlagsOff = 0x18;
|
|
|
|
|
inline constexpr std::uint32_t CaptureFlagsUseCallerFrame = 0x1;
|
|
|
|
|
// What the stock HAL's enrol path builds: |= 0xC0040000 then |= 2
|
|
|
|
|
// (fingerprint.default.so 0xb7d7c / 0xb7dbc).
|
|
|
|
|
inline constexpr std::uint32_t CaptureFlagsEnrol = 0xC0040002;
|
|
|
|
|
static_assert((CaptureFlagsEnrol & CaptureFlagsUseCallerFrame) == 0,
|
|
|
|
|
"bit 0 would promise the trustlet a frame we do not append");
|
|
|
|
|
static_assert((CaptureFlagsEnrol & 0x40000002) != 0,
|
|
|
|
|
"without bit 1 or bit 30 the capture does no preprocessing");
|
|
|
|
|
|
|
|
|
|
// CAPTURE_IMAGE's declared payload length is range-checked to exactly
|
|
|
|
|
// 0x14, and the trustlet reads the flags at +0x18 regardless — so the word
|
|
|
|
|
// is written past the declared length on purpose. 0x20 gives -201.
|
|
|
|
|
inline constexpr std::uint32_t CaptureDeclaredLen = 0x14;
|
|
|
|
|
|
2026-09-02 18:27:35 +02:00
|
|
|
// Two fields inside that payload which an all-zero request leaves unset.
|
|
|
|
|
//
|
|
|
|
|
// +0x0c frame count how many frames this capture takes
|
|
|
|
|
// +0x10 branch selector which capture path runs; 0 returns metric 0
|
|
|
|
|
//
|
|
|
|
|
// Both matter for reading the result as much as for getting one: the
|
|
|
|
|
// metric is PER FRAME, so a count of 4 reads roughly four times a count of
|
|
|
|
|
// 1 and a threshold calibrated at one count is meaningless at another.
|
|
|
|
|
// Sending zeros gets -201.
|
|
|
|
|
inline constexpr std::size_t CaptureFrameCountOff = 0x0c;
|
|
|
|
|
inline constexpr std::size_t CaptureSelectorOff = 0x10;
|
|
|
|
|
inline constexpr std::uint32_t CaptureFrameCountDefault = 1;
|
|
|
|
|
inline constexpr std::uint32_t CaptureSelectorDefault = 1;
|
|
|
|
|
|
|
|
|
|
inline void BuildCapturePayload(std::span<std::byte> out,
|
|
|
|
|
std::uint32_t frames = CaptureFrameCountDefault,
|
|
|
|
|
std::uint32_t selector = CaptureSelectorDefault) {
|
|
|
|
|
std::ranges::fill(out.first(CaptureDeclaredLen), std::byte{0});
|
|
|
|
|
detail::StoreU32(out, CaptureFrameCountOff, frames);
|
|
|
|
|
detail::StoreU32(out, CaptureSelectorOff, selector);
|
|
|
|
|
}
|
|
|
|
|
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
// ---- SAVE_DATA --------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// payload+0x00 is a bitmask and the handler's first test is
|
|
|
|
|
// `0x139ec: tbz w22, #30`: bit 30 clear takes the calibration path, set
|
|
|
|
|
// falls through toward libfp_template_export / ff_template_save.
|
|
|
|
|
inline constexpr std::uint32_t SaveMaskTemplate = 0x40000000;
|
|
|
|
|
inline constexpr std::uint32_t SaveMaskCalibration = 0x80000000;
|
|
|
|
|
static_assert((SaveMaskTemplate & (1u << 30)) != 0);
|
|
|
|
|
static_assert((SaveMaskCalibration & (1u << 30)) == 0);
|
|
|
|
|
|
UPDATE_TEMPLATE: the command stock learns with, and the field that was killing it
Stock rewrites the stored template on every successful press. Its post-match
loop is QUERY_FINGER_STATUS, CAPTURE_IMAGE, 0x1015 UPDATE_TEMPLATE while the
finger stays down, with no REPORT_EVENT in it -- so the matcher does not re-run
and the verdict cannot change. Forty-six of those against eighty-six captures in
one reference session, and the stored body measurably grows: 333278 bytes at
enrolment, 360822 at the next session's load, 371734 after one authentication
session. This daemon sent none of them.
The command shares REPORT_EVENT's event context. The stock wrapper memsets 732
bytes and writes six fields: a zero byte at +0x2a4, the scan-slot count, a zero
word, the count of frames folded so far in this press, a flags word of
0x00080000 with bit 6 set on the frame whose event was FingerTouched, and a zero
at +0x2d8. Declared length 0x2dc.
+0x2d8 is the one that matters, and it matters by staying zero. The dispatcher
stub reads it after the handler returns and only if it is non-zero does it read
+0x2dc and make that the response length. Every earlier attempt in this project
set both fields and varied the declared length across 0x2e0, 0x400 and 0x1000;
all of them answered -90, the trustlet gone, and the conclusion recorded was
"do not retry until a template is loaded". A loaded template was necessary but
not sufficient. The stock HAL sets neither field.
Measured on the device with two templates loaded and no finger: both branches
answer rc=0 and ENUMERATE still reports 2, so the app did not fault.
Those fields are little endian, assembled low-address-first by the handler. An
earlier reading called them big endian, off the bfi order, and was wrong.
Bit 6 selects which algorithm entry runs: clear takes libfp_template_x_update,
set takes the other, which also reads the scan-slot count.
2026-09-03 17:45:27 +02:00
|
|
|
// ---- UPDATE_TEMPLATE --------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// TEMPLATE LEARNING. Stock folds the frames of a successful press back
|
|
|
|
|
// into the stored template and the template GROWS as a result: on the
|
|
|
|
|
// reference device ff_template_0_0.bin went 333278 bytes at enrolment ->
|
|
|
|
|
// 360822 at the next session's load -> 371734 after one authentication
|
|
|
|
|
// session. Over the same session the HAL issued 46 of these against 86
|
|
|
|
|
// captures. This daemon issued none, so every rate this project has ever
|
|
|
|
|
// measured was against a day-zero template that no stock user lives with.
|
|
|
|
|
//
|
|
|
|
|
// The command shares REPORT_EVENT's ff_trustlet_event_context_t. The stock
|
|
|
|
|
// wrapper (fingerprint.default.so 0xb8d84, "checking the template...")
|
|
|
|
|
// memsets 732 bytes and writes exactly six fields:
|
|
|
|
|
//
|
|
|
|
|
// +0x2a4 (676) u8 0
|
|
|
|
|
// +0x2c8 (712) u32 scan-slot count (as REPORT_EVENT)
|
|
|
|
|
// +0x2cc (716) u32 0
|
|
|
|
|
// +0x2d0 (720) u32 slot index = frames folded so far in this press
|
|
|
|
|
// +0x2d4 (724) u32 0x00080000, |0x40 when the frame's event was 5
|
|
|
|
|
// +0x2d8 (728) u32 0
|
|
|
|
|
//
|
|
|
|
|
// and calls it with a declared length of 0x2dc (0xbefec).
|
|
|
|
|
//
|
|
|
|
|
// +0x2d8 IS THE FIELD THAT MATTERS, and it matters by staying zero. The
|
|
|
|
|
// dispatcher stub reads it AFTER the handler returns (0x9ee0-0x9f00) and
|
|
|
|
|
// only if it is non-zero does it read +0x2dc and compute the response
|
|
|
|
|
// length as that value + 0x2dc. Every previous attempt in this project set
|
|
|
|
|
// both fields and varied the declared length (0x2e0 / 0x400 / 0x1000);
|
|
|
|
|
// all of them answered -90, i.e. the trustlet was gone. The stock HAL
|
|
|
|
|
// never sets either one.
|
|
|
|
|
//
|
|
|
|
|
// Endianness: the handler assembles these bytewise low-address-first
|
|
|
|
|
// (0x1300c-0x13028), so they are LITTLE endian. An earlier ledger entry
|
|
|
|
|
// called them big endian; it was read off the bfi order and was wrong.
|
|
|
|
|
inline constexpr std::size_t UpdateTemplatePayloadSize = 0x2dc; // 732
|
|
|
|
|
inline constexpr std::size_t UpdZeroByteOff = 0x2a4;
|
|
|
|
|
inline constexpr std::size_t UpdScanSlotsOff = EvScanSlotsOff; // 712
|
|
|
|
|
inline constexpr std::size_t UpdZeroAOff = EvZeroAOff; // 716
|
|
|
|
|
inline constexpr std::size_t UpdSlotIndexOff = EvSlotIndexOff; // 720
|
|
|
|
|
inline constexpr std::size_t UpdFlagsOff = EvFlagsOff; // 724
|
|
|
|
|
inline constexpr std::size_t UpdRespLenOff = EvZeroBOff; // 728
|
|
|
|
|
|
|
|
|
|
// The flags word. Note it is NOT the event context's 0x08080000: the
|
|
|
|
|
// update wrapper builds its own value from scratch.
|
|
|
|
|
inline constexpr std::uint32_t UpdFlagsBase = 0x00080000;
|
|
|
|
|
// Bit 6 selects which of the algorithm's two update entries runs: clear
|
|
|
|
|
// takes libfp_template_x_update (0x20bf4), set takes 0x212e0, which also
|
|
|
|
|
// reads the scan-slot count. Stock sets it on the frame whose event was
|
|
|
|
|
// FingerTouched, i.e. the first frame of a press.
|
|
|
|
|
inline constexpr std::uint32_t UpdFlagsTouchFrame = 0x40;
|
|
|
|
|
|
|
|
|
|
inline void BuildUpdateTemplate(std::span<std::byte> out, std::uint32_t slotIndex,
|
|
|
|
|
bool touchFrame,
|
|
|
|
|
std::uint32_t scanSlots = EvDefaultScanSlots) {
|
|
|
|
|
std::ranges::fill(out.first(UpdateTemplatePayloadSize), std::byte{0});
|
|
|
|
|
out[UpdZeroByteOff] = std::byte{0};
|
|
|
|
|
detail::StoreU32(out, UpdScanSlotsOff, scanSlots);
|
|
|
|
|
detail::StoreU32(out, UpdZeroAOff, 0);
|
|
|
|
|
detail::StoreU32(out, UpdSlotIndexOff, slotIndex);
|
|
|
|
|
detail::StoreU32(out, UpdFlagsOff,
|
|
|
|
|
UpdFlagsBase | (touchFrame ? UpdFlagsTouchFrame : 0u));
|
|
|
|
|
// Left zero deliberately. See above: a non-zero value here sends the
|
|
|
|
|
// stub off to compute a response length from +0x2dc.
|
|
|
|
|
detail::StoreU32(out, UpdRespLenOff, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static_assert(UpdRespLenOff + 4 == UpdateTemplatePayloadSize,
|
|
|
|
|
"the response-length trigger is the last word of the payload");
|
|
|
|
|
static_assert((UpdFlagsBase & UpdFlagsTouchFrame) == 0,
|
|
|
|
|
"the touch bit must not already be in the base value");
|
|
|
|
|
static_assert(UpdFlagsTouchFrame == (1u << 6),
|
|
|
|
|
"the handler tests bit 6 of the LOW byte at +0x2d4");
|
|
|
|
|
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
// ---- ENROLL / AUTHENTICATE payloads -----------------------------------
|
|
|
|
|
|
Add enrolment, and let it choose its own namespace
Enrolment is the first thing here that writes: template containers through the
gpfile listener and counter records through RPMB. It refuses to run unless both
--sfs-writable and --rpmb-write are given, and it refuses to call SAVE_DATA if
the sample count did not reach zero, because a partial template is worse than
none.
The sequence is stock's: cancel, reset-lockout, authenticate, cancel,
reset-lockout, PRE_ENROLL, authenticate, cancel, ENROLL, the sample loop,
POST_ENROLL, SAVE_DATA with bit 30 set. AUTHENTICATE is what arms the capture
session, which is why it appears in an enrolment at all.
Enrolment takes one sample per PRESS: touch on the rising edge, release on the
falling one, nothing in between. Stock's entire enrolment trace contains no
image-ready event, and feeding every held frame gives the algorithm
near-duplicate images from a single press.
Two things named honestly. The ENROLL payload's u32 at +69 was recorded here as
a "timeout"; the trustlet reports it back as the GROUP ID, and filling a
mislabelled field with a plausible number is the entire provenance of gid 60.
It is the gid now, so an enrolment can choose its own group.
And --group-path exposes the namespace key the trustlet hashes into the group's
directory name. It defaults to Android's, which is where this device's existing
store lives and how that template is readable. But SAVE_DATA rewrites the
group's index container, and an index QTEE later fails to verify takes every
template listed in it -- so enrolling into a DIFFERENT namespace is complete
isolation from a store we did not write.
2026-09-02 20:12:24 +02:00
|
|
|
// ENROLL takes a 69-byte hw_auth_token, a u32 at +69 and a u8 flag at +73
|
|
|
|
|
// (stub 0xa0c8).
|
|
|
|
|
//
|
|
|
|
|
// The u32 at +69 was recorded in this project as a "timeout". It is not:
|
|
|
|
|
// the trustlet reports it back as the GROUP ID. Setting it to 60 is where
|
|
|
|
|
// `gid = 60` came from, and the whole gid-60 store exists because a
|
|
|
|
|
// mislabelled field was filled with a plausible-looking number. Naming it
|
|
|
|
|
// honestly is what makes an enrolment able to choose its own group.
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
inline constexpr std::size_t EnrollPayloadSize = 74;
|
|
|
|
|
inline constexpr std::size_t EnrollTokenSize = 69;
|
Add enrolment, and let it choose its own namespace
Enrolment is the first thing here that writes: template containers through the
gpfile listener and counter records through RPMB. It refuses to run unless both
--sfs-writable and --rpmb-write are given, and it refuses to call SAVE_DATA if
the sample count did not reach zero, because a partial template is worse than
none.
The sequence is stock's: cancel, reset-lockout, authenticate, cancel,
reset-lockout, PRE_ENROLL, authenticate, cancel, ENROLL, the sample loop,
POST_ENROLL, SAVE_DATA with bit 30 set. AUTHENTICATE is what arms the capture
session, which is why it appears in an enrolment at all.
Enrolment takes one sample per PRESS: touch on the rising edge, release on the
falling one, nothing in between. Stock's entire enrolment trace contains no
image-ready event, and feeding every held frame gives the algorithm
near-duplicate images from a single press.
Two things named honestly. The ENROLL payload's u32 at +69 was recorded here as
a "timeout"; the trustlet reports it back as the GROUP ID, and filling a
mislabelled field with a plausible number is the entire provenance of gid 60.
It is the gid now, so an enrolment can choose its own group.
And --group-path exposes the namespace key the trustlet hashes into the group's
directory name. It defaults to Android's, which is where this device's existing
store lives and how that template is readable. But SAVE_DATA rewrites the
group's index container, and an index QTEE later fails to verify takes every
template listed in it -- so enrolling into a DIFFERENT namespace is complete
isolation from a store we did not write.
2026-09-02 20:12:24 +02:00
|
|
|
inline constexpr std::size_t EnrollGidOff = 69;
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
|
|
|
|
|
// No Gatekeeper is needed. ff_trustlet_enroll reads config
|
|
|
|
|
// trustlet.enable_trusted_enrollment and, when false, skips the version
|
|
|
|
|
// check, the PRE_ENROLL challenge compare and the token HMAC verify
|
|
|
|
|
// outright (0xce34 tbz -> 0xd198), so an all-zero token is accepted.
|
|
|
|
|
// pmOS has no Gatekeeper to mint one and nothing there verifies auth
|
|
|
|
|
// tokens anyway.
|
Add enrolment, and let it choose its own namespace
Enrolment is the first thing here that writes: template containers through the
gpfile listener and counter records through RPMB. It refuses to run unless both
--sfs-writable and --rpmb-write are given, and it refuses to call SAVE_DATA if
the sample count did not reach zero, because a partial template is worse than
none.
The sequence is stock's: cancel, reset-lockout, authenticate, cancel,
reset-lockout, PRE_ENROLL, authenticate, cancel, ENROLL, the sample loop,
POST_ENROLL, SAVE_DATA with bit 30 set. AUTHENTICATE is what arms the capture
session, which is why it appears in an enrolment at all.
Enrolment takes one sample per PRESS: touch on the rising edge, release on the
falling one, nothing in between. Stock's entire enrolment trace contains no
image-ready event, and feeding every held frame gives the algorithm
near-duplicate images from a single press.
Two things named honestly. The ENROLL payload's u32 at +69 was recorded here as
a "timeout"; the trustlet reports it back as the GROUP ID, and filling a
mislabelled field with a plausible number is the entire provenance of gid 60.
It is the gid now, so an enrolment can choose its own group.
And --group-path exposes the namespace key the trustlet hashes into the group's
directory name. It defaults to Android's, which is where this device's existing
store lives and how that template is readable. But SAVE_DATA rewrites the
group's index container, and an index QTEE later fails to verify takes every
template listed in it -- so enrolling into a DIFFERENT namespace is complete
isolation from a store we did not write.
2026-09-02 20:12:24 +02:00
|
|
|
inline void BuildEnrollPayload(std::span<std::byte> out, std::uint32_t gid) {
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
std::ranges::fill(out.first(EnrollPayloadSize), std::byte{0});
|
Add enrolment, and let it choose its own namespace
Enrolment is the first thing here that writes: template containers through the
gpfile listener and counter records through RPMB. It refuses to run unless both
--sfs-writable and --rpmb-write are given, and it refuses to call SAVE_DATA if
the sample count did not reach zero, because a partial template is worse than
none.
The sequence is stock's: cancel, reset-lockout, authenticate, cancel,
reset-lockout, PRE_ENROLL, authenticate, cancel, ENROLL, the sample loop,
POST_ENROLL, SAVE_DATA with bit 30 set. AUTHENTICATE is what arms the capture
session, which is why it appears in an enrolment at all.
Enrolment takes one sample per PRESS: touch on the rising edge, release on the
falling one, nothing in between. Stock's entire enrolment trace contains no
image-ready event, and feeding every held frame gives the algorithm
near-duplicate images from a single press.
Two things named honestly. The ENROLL payload's u32 at +69 was recorded here as
a "timeout"; the trustlet reports it back as the GROUP ID, and filling a
mislabelled field with a plausible number is the entire provenance of gid 60.
It is the gid now, so an enrolment can choose its own group.
And --group-path exposes the namespace key the trustlet hashes into the group's
directory name. It defaults to Android's, which is where this device's existing
store lives and how that template is readable. But SAVE_DATA rewrites the
group's index container, and an index QTEE later fails to verify takes every
template listed in it -- so enrolling into a DIFFERENT namespace is complete
isolation from a store we did not write.
2026-09-02 20:12:24 +02:00
|
|
|
detail::StoreU32(out, EnrollGidOff, gid);
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AUTHENTICATE (TA 0xea88 takes these as x0/w1/w2/w3):
|
|
|
|
|
// +0x00 u64 operation_id
|
|
|
|
|
// +0x08 u32 gid
|
|
|
|
|
// +0x0c u8 b_relight
|
|
|
|
|
// +0x0d u8 b_covered
|
|
|
|
|
// Declared length 0x0e.
|
|
|
|
|
inline constexpr std::size_t AuthPayloadSize = 0x0e;
|
|
|
|
|
inline constexpr std::size_t AuthGidOff = 0x08;
|
|
|
|
|
inline constexpr std::size_t AuthRelightOff = 0x0c;
|
|
|
|
|
inline constexpr std::size_t AuthCoveredOff = 0x0d;
|
|
|
|
|
|
|
|
|
|
// With both flags zero the trustlet calls ff_trustlet_query_finger_status
|
|
|
|
|
// first and starts mode 1 or 2 from the answer; 1,1 skips that and starts
|
|
|
|
|
// mode 1 directly, which is what the query returns with no finger down.
|
|
|
|
|
inline void BuildAuthPayload(std::span<std::byte> out, std::uint64_t operationId,
|
|
|
|
|
std::uint32_t gid, bool relight = true, bool covered = true) {
|
|
|
|
|
std::ranges::fill(out.first(AuthPayloadSize), std::byte{0});
|
|
|
|
|
for (std::size_t i = 0; i < 8; i++)
|
|
|
|
|
out[i] = static_cast<std::byte>((operationId >> (8 * i)) & 0xFF);
|
|
|
|
|
detail::StoreU32(out, AuthGidOff, gid);
|
|
|
|
|
out[AuthRelightOff] = static_cast<std::byte>(relight ? 1 : 0);
|
|
|
|
|
out[AuthCoveredOff] = static_cast<std::byte>(covered ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SET_ACTIVE_GROUP writes its gid to device+0x30 and AUTHENTICATE compares
|
|
|
|
|
// its own against the same field (0xeb08), logging
|
|
|
|
|
// "templates with gid(%u != %u) hasn't been loaded." and returning -200 on
|
|
|
|
|
// a mismatch. So the two only have to agree with each other — the value
|
|
|
|
|
// itself is the caller's to choose.
|
Serve QTEE's storage: the enrolled template loads
The whole storage path now works from the daemon. On the phone, against the
real store:
listener 0x7000 sb=516096 -> result=0 REGISTERED
listener 0x2000 sb=25600 -> result=0 REGISTERED
SET_ACTIVE_GROUP gid=60 path='/data/vendor_de/0/fpdata'
gpfile READ .../1lPrxAL0vXRvWPeDkW2c off=4096 len=252114
...
CMD 0x2005 -> result=0 rc=1
templates loaded: 1
QTEE read a 252114-byte enrolled template through our gpfile listener, verified
it, and loaded it. Since QTEE unlinks any container whose keyed integrity tag
fails, a load is proof the framing is right -- the read/write offset split, the
container chunking, and the RPMB anti-rollback read that has to succeed before
QTEE will trust any of it.
RPMB is served too: SECURITY PROTOCOL IN/OUT against the RPMB well-known LUN,
retrying the unit attention the LUN raises once after a reset. Writes are
refused unless asked for, because they advance a counter that cannot be moved
back, and key programming is refused unconditionally.
The store was served READ-ONLY throughout, which is the point. A listener that
serves bytes at the wrong offset does not merely fail: QTEE deletes the
container it cannot verify, and that is an enrolled fingerprint gone. Read-only
makes a wrong build harmless, so it is the default and writing is opt-in.
Two ordering facts, both of which produce -2 with no storage read at all --
indistinguishable from a broken listener:
* a template reload needs the device init chain to have run FIRST, because
that chain allocates the per-slot array the reload writes through;
* SET_ACTIVE_GROUP's second field is a NAMESPACE path, not a filesystem one
and not the gid again. The trustlet hashes it into the group's directory
name, so it has to match what the store was written under.
Also: a positive rc is not an error code. ENUMERATE returns the template count
there, and running that through the error table printed "unknown" for a good
answer.
2026-09-02 18:42:20 +02:00
|
|
|
//
|
|
|
|
|
// Its payload is {u32 gid; char path[]} and the path is NOT a filesystem
|
|
|
|
|
// path we control: the trustlet hashes it into the SFS group's directory
|
|
|
|
|
// name, so it is a namespace key and it has to match whatever the store
|
|
|
|
|
// was written under. The store on this device was written by the Android
|
|
|
|
|
// stack under its data directory, and every group in it derives from that
|
|
|
|
|
// string. Passing anything else resolves a different group, finds nothing
|
|
|
|
|
// and answers -2 -- with no storage read at all, which reads like a
|
|
|
|
|
// listener failure and is not one.
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
inline constexpr std::size_t SetActiveGroupGidOff = 0;
|
Serve QTEE's storage: the enrolled template loads
The whole storage path now works from the daemon. On the phone, against the
real store:
listener 0x7000 sb=516096 -> result=0 REGISTERED
listener 0x2000 sb=25600 -> result=0 REGISTERED
SET_ACTIVE_GROUP gid=60 path='/data/vendor_de/0/fpdata'
gpfile READ .../1lPrxAL0vXRvWPeDkW2c off=4096 len=252114
...
CMD 0x2005 -> result=0 rc=1
templates loaded: 1
QTEE read a 252114-byte enrolled template through our gpfile listener, verified
it, and loaded it. Since QTEE unlinks any container whose keyed integrity tag
fails, a load is proof the framing is right -- the read/write offset split, the
container chunking, and the RPMB anti-rollback read that has to succeed before
QTEE will trust any of it.
RPMB is served too: SECURITY PROTOCOL IN/OUT against the RPMB well-known LUN,
retrying the unit attention the LUN raises once after a reset. Writes are
refused unless asked for, because they advance a counter that cannot be moved
back, and key programming is refused unconditionally.
The store was served READ-ONLY throughout, which is the point. A listener that
serves bytes at the wrong offset does not merely fail: QTEE deletes the
container it cannot verify, and that is an enrolled fingerprint gone. Read-only
makes a wrong build harmless, so it is the default and writing is opt-in.
Two ordering facts, both of which produce -2 with no storage read at all --
indistinguishable from a broken listener:
* a template reload needs the device init chain to have run FIRST, because
that chain allocates the per-slot array the reload writes through;
* SET_ACTIVE_GROUP's second field is a NAMESPACE path, not a filesystem one
and not the gid again. The trustlet hashes it into the group's directory
name, so it has to match what the store was written under.
Also: a positive rc is not an error code. ENUMERATE returns the template count
there, and running that through the error table printed "unknown" for a good
answer.
2026-09-02 18:42:20 +02:00
|
|
|
inline constexpr std::size_t SetActiveGroupPathOff = 4;
|
|
|
|
|
inline constexpr std::string_view GroupNamespacePath = "/data/vendor_de/0/fpdata";
|
|
|
|
|
|
|
|
|
|
inline std::vector<std::byte> BuildSetActiveGroup(
|
|
|
|
|
std::uint32_t gid, std::string_view path = GroupNamespacePath) {
|
|
|
|
|
std::vector<std::byte> out(SetActiveGroupPathOff + path.size() + 1, std::byte{0});
|
|
|
|
|
detail::StoreU32(out, SetActiveGroupGidOff, gid);
|
|
|
|
|
for (std::size_t i = 0; i < path.size(); i++)
|
|
|
|
|
out[SetActiveGroupPathOff + i] = static_cast<std::byte>(path[i]);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
|
2026-09-02 18:19:26 +02:00
|
|
|
// ---- The request/response envelope ------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// sendRequest carries two buffers in and two back. The request is:
|
|
|
|
|
//
|
|
|
|
|
// +0x00 u32 command id
|
|
|
|
|
// +0x04 u32 declared payload length
|
|
|
|
|
// +0x10 the payload
|
|
|
|
|
//
|
|
|
|
|
// and the returned copy of it carries the trustlet's own return code and
|
|
|
|
|
// the capture metric in the header, ahead of the payload:
|
|
|
|
|
//
|
|
|
|
|
// +0x08 i32 rc the trustlet's result, distinct from QTEE's
|
|
|
|
|
// +0x0c i32 metric CAPTURE_IMAGE's finger signal
|
|
|
|
|
//
|
|
|
|
|
// The metric is a HEADER field. It has been called "payload+12" in this
|
|
|
|
|
// project's notes and it is not; it tracks the finger reproducibly and
|
|
|
|
|
// every recorded number depends on reading it here.
|
|
|
|
|
inline constexpr std::size_t ReqCmdOff = 0x00;
|
|
|
|
|
inline constexpr std::size_t ReqLenOff = 0x04;
|
|
|
|
|
inline constexpr std::size_t ReqPayloadOff = 0x10;
|
|
|
|
|
inline constexpr std::size_t RespRcOff = 0x08;
|
|
|
|
|
inline constexpr std::size_t RespMetricOff = 0x0c;
|
|
|
|
|
|
|
|
|
|
inline void BuildRequest(std::span<std::byte> req, Cmd cmd,
|
|
|
|
|
std::span<const std::byte> payload) {
|
|
|
|
|
std::ranges::fill(req, std::byte{0});
|
|
|
|
|
detail::StoreU32(req, ReqCmdOff, static_cast<std::uint32_t>(cmd));
|
|
|
|
|
if (!payload.empty()) {
|
|
|
|
|
detail::StoreU32(req, ReqLenOff, static_cast<std::uint32_t>(payload.size()));
|
|
|
|
|
std::ranges::copy(payload, req.begin() + static_cast<std::ptrdiff_t>(ReqPayloadOff));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline std::int32_t ResultCode(std::span<const std::byte> reqOut) {
|
|
|
|
|
return static_cast<std::int32_t>(detail::LoadU32(reqOut, RespRcOff));
|
|
|
|
|
}
|
|
|
|
|
inline std::int32_t CaptureMetric(std::span<const std::byte> reqOut) {
|
|
|
|
|
return static_cast<std::int32_t>(detail::LoadU32(reqOut, RespMetricOff));
|
|
|
|
|
}
|
|
|
|
|
|
Port the trustlet command surface, and pin the counting rule to recorded runs
Fingerprintd:Ta is the second core module: request payloads, response fields,
the error table, and the rule that decides what a frame meant. Payload building
and response reading only -- no TEE, no transport.
Very little of this is guessable, so each constant carries where it came from.
Three were found only because QTEE recorded a fault naming the instruction that
read them:
* the event context's scan-slot count at +712, which do_enroll branches on to
skip the entire slot loop -- an all-zero payload logged "groups->,
results->" and read exactly like a gate failing deep in the trustlet, when
it was zero iterations;
* CAPTURE_IMAGE's flags at payload+0x18, without which preprocessing, the
classifier and the enrol grouper never run at all, whatever is on the
sensor;
* SYNC_STATISTICS, whose absence leaves g_statistics NULL so the first enrol
frame that gets far enough takes a data abort and every later command
answers -90.
The verdict rule gets the most attention because it was mislabelled three times
before the comparison producing it was read. A frame is one of three things and
only the third is a verdict: the poison intact means the matcher never ran,
rc=-11 means not identified yet with attempts remaining, and only rc=0 carries
a match or a rejection. The poison exists because a zero-initialised buffer
cannot tell a released finger from a rejected one.
The tests are in two halves that cannot prop each other up. Explicit wire
conditions pin the classifier; three recorded runs pin the counting policy,
which is what actually went wrong. In the stock-budget run 31 of 48 frames
answered "not identified yet" and every frame that carried an image matched --
counting those 31 as attempts turns 8-for-8 into 8-of-39 and reads as a flaky
sensor. The wrong-finger control pins zero false accepts.
Fixtures are verdict-line excerpts, not the 40 KB transcripts, which are thick
with the device's SFS container names the test has no use for.
Verified by mutation: classifying -11 as a rejection, dropping SYNC_STATISTICS
from the init chain, and forgetting the +0x10 response payload offset each fail
the suite.
2026-09-02 16:46:00 +02:00
|
|
|
// ---- Responses --------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// THE TRAP. The buffer that comes back is the whole REQUEST, and the
|
|
|
|
|
// payload starts at +0x10 — proved by reading back the event id we sent at
|
|
|
|
|
// reqo+0x14. Reading a payload field at its payload offset directly gives
|
|
|
|
|
// a confident, wrong zero, which is what happened to "samples remaining"
|
|
|
|
|
// for a whole session.
|
|
|
|
|
inline constexpr std::size_t ResponsePayloadOff = 0x10;
|
|
|
|
|
|
|
|
|
|
// Enrolment progress, without needing the trustlet's log: do_enroll copies
|
|
|
|
|
// g_context+56 (samples remaining) into the response payload at +36
|
|
|
|
|
// bytewise on the common path, whether or not the sample was accepted.
|
|
|
|
|
// That matters because the log starves exactly when a frame is accepted.
|
|
|
|
|
inline constexpr std::size_t RespSamplesRemainingOff = 36;
|
|
|
|
|
|
|
|
|
|
// The match result. ff_trustlet_event's success path writes the gid to
|
|
|
|
|
// payload+0x0c and the matched fid to payload+0x10 one byte at a time
|
|
|
|
|
// (0x1642c / 0x16454), and "authentication failed." explicitly zeroes the
|
|
|
|
|
// fid (0x164a4). So a non-zero fid there can only have come from the path
|
|
|
|
|
// that logged a match.
|
|
|
|
|
inline constexpr std::size_t RespGidOff = 0x0c;
|
|
|
|
|
inline constexpr std::size_t RespFidOff = 0x10;
|
|
|
|
|
|
|
|
|
|
inline std::int32_t SamplesRemaining(std::span<const std::byte> response) {
|
|
|
|
|
return static_cast<std::int32_t>(
|
|
|
|
|
detail::LoadU32(response, ResponsePayloadOff + RespSamplesRemainingOff));
|
|
|
|
|
}
|
|
|
|
|
inline std::uint32_t MatchedGid(std::span<const std::byte> response) {
|
|
|
|
|
return detail::LoadU32(response, ResponsePayloadOff + RespGidOff);
|
|
|
|
|
}
|
|
|
|
|
inline std::uint32_t MatchedFid(std::span<const std::byte> response) {
|
|
|
|
|
return detail::LoadU32(response, ResponsePayloadOff + RespFidOff);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- The verdict ------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// A frame is one of three things and only the third is a verdict. This was
|
|
|
|
|
// mislabelled three separate times before the comparison producing it was
|
|
|
|
|
// actually read, and each mistake invented rejections that never happened
|
|
|
|
|
// and made the sensor look flaky.
|
|
|
|
|
//
|
|
|
|
|
// 16a0c: ldr w9, [x12, #0x8c] ; common.max_authentication_rescan_times
|
|
|
|
|
// 16a30: cmp w8, w9 ; w8 = counter at x20+0x2d0
|
|
|
|
|
// 16a34: mov w8, #-0xb
|
|
|
|
|
// 16a38: csel w8, w8, wzr, lo ; below the limit -> -11, else terminal
|
|
|
|
|
//
|
|
|
|
|
// Proven with no finger on the sensor: at the default budget those frames
|
|
|
|
|
// return -11; with the key set to 0 the same frames return rc=0 with the
|
|
|
|
|
// fid zeroed, which is a real rejection.
|
|
|
|
|
enum class Verdict {
|
|
|
|
|
MatcherNeverRan, // finger released; the poison is intact
|
|
|
|
|
NotIdentifiedYet, // rc=-11: ran, attempts remain. NOT a rejection
|
|
|
|
|
Match,
|
|
|
|
|
Rejected,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The caller poisons the fid field before the call, because a
|
|
|
|
|
// zero-initialised buffer cannot distinguish "the matcher never ran" from
|
|
|
|
|
// "the matcher ran and rejected the finger" — both leave zero there.
|
|
|
|
|
inline constexpr std::uint32_t FidPoison = 0xAAAAAAAA;
|
|
|
|
|
inline constexpr std::int32_t RcTryAgain = -11;
|
|
|
|
|
|
|
|
|
|
inline void PoisonFid(std::span<std::byte> payload) {
|
|
|
|
|
detail::StoreU32(payload, RespFidOff, FidPoison);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline Verdict Classify(std::int32_t rc, std::uint32_t fid) {
|
|
|
|
|
if (fid == FidPoison) return Verdict::MatcherNeverRan;
|
|
|
|
|
if (rc == RcTryAgain) return Verdict::NotIdentifiedYet;
|
|
|
|
|
if (rc != 0) return Verdict::NotIdentifiedYet;
|
|
|
|
|
return fid != 0 ? Verdict::Match : Verdict::Rejected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only a terminal verdict counts toward an accept/reject rate. Counting
|
|
|
|
|
// NotIdentifiedYet as a rejection is the specific error above.
|
|
|
|
|
inline bool IsTerminal(Verdict v) {
|
|
|
|
|
return v == Verdict::Match || v == Verdict::Rejected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Errors -----------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// ff_strerror (TA 0x7218) is two jump-table ranges. Every error path in
|
|
|
|
|
// the trustlet passes its result through this, so every named error in
|
|
|
|
|
// every log we hold converts back to a number and vice versa.
|
|
|
|
|
inline constexpr std::string_view StrError(std::int32_t rc) {
|
|
|
|
|
switch (rc) {
|
|
|
|
|
case 0: return "Success";
|
|
|
|
|
case -1: return "Internal error";
|
|
|
|
|
case -2: return "No such file or directory";
|
|
|
|
|
case -4: return "Interrupted";
|
|
|
|
|
case -5: return "I/O error";
|
|
|
|
|
case -11: return "Try again";
|
|
|
|
|
case -12: return "Out of memory";
|
|
|
|
|
case -16: return "Resource busy/Timeout";
|
|
|
|
|
case -200: return "Bad parameter(s)";
|
|
|
|
|
case -201: return "Null pointer";
|
|
|
|
|
case -202: return "Buffer overflow";
|
|
|
|
|
case -203: return "Bad protocol";
|
|
|
|
|
case -204: return "Wrong sensor dimension";
|
|
|
|
|
case -205: return "Device not found";
|
|
|
|
|
case -206: return "Device is dead";
|
|
|
|
|
case -207: return "Up to the limit";
|
|
|
|
|
case -208: return "Untrusted enrollment";
|
|
|
|
|
case -209: return "Template store in REE";
|
|
|
|
|
default: return "unknown";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -90 is not a trustlet error at all: it is QTEE's "the app is gone",
|
|
|
|
|
// which is what every command answers once the trustlet has taken a fault.
|
|
|
|
|
// A -90 yields no trustlet log, so the fault ring is the only instrument.
|
|
|
|
|
inline constexpr std::int32_t QteeAppGone = -90;
|
|
|
|
|
|
|
|
|
|
// -205 is what a second TA init in one sensor power cycle returns. One
|
|
|
|
|
// sensor reset buys exactly one init, which is why the process that powers
|
|
|
|
|
// the sensor has to be the process that holds the session.
|
|
|
|
|
inline constexpr std::int32_t RcDeviceNotFound = -205;
|
|
|
|
|
}
|