From 6c4622afff29058d15d295cca86b4beef69da2aa Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 18:27:35 +0200 Subject: [PATCH] Capture works: idle floor 133, matching the reference measurement The finger-free path is complete. On the phone, from a cold start: client env -> loader -> trustlet -> config -> sensor rail -> init chain calibrating the idle floor (5 samples) idle 1/5: rc=-11 metric=133 ... idle floor = 133, finger threshold = 266 133 is the number the journal records for this sensor, so the port reproduces the reference measurement rather than merely producing one. Two things had to be right at once, and the first attempt had neither. The memory region: CAPTURE_IMAGE reads an output-buffer pointer out of payload+0x00, and QTEE only patches an address there if the location is named in embeddedBufOffsets and the region handed over in an object slot. The instrumented dump shows it working -- payload+0x00 came back holding 0x088db98000 -- which is what made the remaining failure legible instead of mysterious. And two fields inside the capture payload that an all-zero request leaves unset: a frame count at +0x0c and a branch selector at +0x10. Selector 0 returns metric 0. Sending zeros gets -201 with the region correctly attached, which reads exactly like a broken region and is not one. They are named constants now, with the note that the metric is PER FRAME so a threshold calibrated at one frame count means nothing at another. The flags word at payload+0x18 stays past the declared length of 0x14 on purpose: the trustlet range-checks that length to exactly 0x14 and reads the flags anyway. --verbose keeps the region and reqOut dumps, which is what turned this from guesswork into reading. --- implementations/main.cpp | 88 ++++++++++++++++++++++++++++++--- interfaces/Fingerprintd-Ta.cppm | 22 +++++++++ tests/Ta/main.cpp | 17 +++++++ 3 files changed, 120 insertions(+), 7 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index a0c3693..15dcb20 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -49,6 +49,7 @@ namespace { constexpr const char* Version = "0.0.3"; +bool g_verbose = false; std::string g_taPath = "/lib/firmware/focal64.mbn"; std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; @@ -396,12 +397,24 @@ struct CommandResult { bool invoked = false; qcomtee_result_t result = 0; std::i CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, std::span payload) { namespace ta = fingerprintd::ta; + namespace tee = fingerprintd::tee; static std::vector req(8192), rsp(16384), reqOut(8192), rspOut(16384); std::ranges::fill(rsp, std::byte{0}); std::ranges::fill(reqOut, std::byte{0}); std::ranges::fill(rspOut, std::byte{0}); ta::BuildRequest(req, cmd, payload); + // CAPTURE_IMAGE's flags word sits at payload+0x18, PAST the declared + // length of 0x14 -- the trustlet range-checks the length to exactly that + // and reads the flags anyway. Without bit 1 or bit 30 it skips + // preprocessing, the classifier and the enrol grouper entirely and returns + // success having done nothing but a raw scan. + if (cmd == ta::Cmd::CaptureImage) { + for (std::size_t i = 0; i < 4; i++) + req[ta::ReqPayloadOff + ta::CaptureFlagsOff + i] = + static_cast((ta::CaptureFlagsEnrol >> (8 * i)) & 0xFF); + } + std::uint32_t is64 = 1; qcomtee_param p[10] = {}; p[0].attr = QCOMTEE_UBUF_INPUT; p[0].ubuf.addr = req.data(); p[0].ubuf.size = req.size(); @@ -415,12 +428,52 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, p[i].object = QCOMTEE_OBJECT_NULL; } + // A capture needs a real shared memory REGION or the trustlet answers + // -201: it reads an output-buffer pointer out of payload+0x00, and QTEE + // only patches an address in there if we name the location in + // embeddedBufOffsets (IB2) and hand it the region in an object slot. + // Without that the pointer is NULL. This is the whole difference between a + // flat metric and a real scan. + // + // Two traps: the offsets array applies to EVERY command in a run, so it is + // scoped to this one command -- patching a pointer into SYNC_CONFIG's + // request breaks it. And an invoke CONSUMES its input objects, so the + // region is allocated fresh each time. + qcomtee_object* region = QCOMTEE_OBJECT_NULL; + std::uint32_t offsets = tee::EmbeddedBufOffsetValue; + if (cmd == static_cast(tee::RegionScopedToCommand)) { + if (qcomtee_memory_object_alloc(tee::CaptureRegionSize, g_root, ®ion)) { + std::println(std::cerr, " memory region alloc failed"); + region = QCOMTEE_OBJECT_NULL; + } else { + void* addr = qcomtee_memory_object_addr(region); + std::size_t sz = qcomtee_memory_object_size(region); + if (g_verbose) + std::println(" region: addr={} size={} offsets=[0x{:x}] slot=IO0", + addr, sz, offsets); + std::memset(addr, 0, sz); + p[2].ubuf.addr = &offsets; + p[2].ubuf.size = sizeof(offsets); + p[6].object = region; + } + } + CommandResult out; - if (qcomtee_object_invoke(app, fingerprintd::tee::AppSendRequestOp, p, 10, &out.result)) + if (qcomtee_object_invoke(app, tee::AppSendRequestOp, p, 10, &out.result)) { + if (region != QCOMTEE_OBJECT_NULL) + qcomtee_memory_object_release(region); return out; + } out.invoked = true; out.rc = ta::ResultCode(reqOut); out.metric = ta::CaptureMetric(reqOut); + if (g_verbose && cmd == ta::Cmd::CaptureImage) { + std::string hex; + for (std::size_t i = 0; i < 0x30; i++) + hex += std::format("{:02x}{}", std::to_integer(reqOut[i]), + (i % 16 == 15) ? "\n " : " "); + std::println(" reqOut[0x00..0x2f]:\n {}", hex); + } return out; } @@ -548,12 +601,32 @@ int Probe() { return 1; } - // With the sensor initialised, a capture returns a real metric. No finger - // is needed to see the idle floor. - std::vector cap(fingerprintd::ta::CaptureDeclaredLen, std::byte{0}); - auto c1 = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap); - Report(fingerprintd::ta::Cmd::CaptureImage, c1); - std::println(" idle capture metric = {}", c1.metric); + // With the sensor initialised and a region supplied, a capture returns a + // real metric. No finger is needed to establish the idle floor, and the + // floor is the only meaningful reference: the metric is per frame and + // drifts, so a fixed threshold is wrong by construction. + fingerprintd::engine::Baseline baseline; + std::println("calibrating the idle floor ({} samples)", + fingerprintd::engine::Baseline::DefaultSamples); + for (std::size_t i = 0; i < fingerprintd::engine::Baseline::DefaultSamples; i++) { + std::vector cap(fingerprintd::ta::CaptureDeclaredLen); + fingerprintd::ta::BuildCapturePayload(cap); + auto c = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap); + if (!c.invoked || c.result != 0) { + Report(fingerprintd::ta::Cmd::CaptureImage, c); + std::println(std::cerr, "capture failed during calibration"); + return 1; + } + std::println(" idle {}/{}: rc={} metric={}", i + 1, + fingerprintd::engine::Baseline::DefaultSamples, c.rc, c.metric); + baseline.Observe(c.metric); + } + if (!baseline.Ready()) { + std::println(std::cerr, "baseline did not calibrate (floor stayed 0)"); + return 1; + } + std::println("idle floor = {}, finger threshold = {}", baseline.Floor(), + baseline.Threshold()); std::println("\ntrustlet initialised against a powered sensor."); pthread_cancel(th); @@ -574,6 +647,7 @@ int main(int argc, char** argv) { if (a == "--probe-tee") probe = true; if (a.starts_with("--ta=")) g_taPath = a.substr(5); if (a.starts_with("--config=")) g_cfgPath = a.substr(9); + if (a == "--verbose") g_verbose = true; } if (probe) return Probe(); diff --git a/interfaces/Fingerprintd-Ta.cppm b/interfaces/Fingerprintd-Ta.cppm index ff4cd81..f2325ec 100644 --- a/interfaces/Fingerprintd-Ta.cppm +++ b/interfaces/Fingerprintd-Ta.cppm @@ -173,6 +173,28 @@ export namespace fingerprintd::ta { // is written past the declared length on purpose. 0x20 gives -201. inline constexpr std::uint32_t CaptureDeclaredLen = 0x14; + // 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 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); + } + // ---- SAVE_DATA -------------------------------------------------------- // // payload+0x00 is a bitmask and the handler's first test is diff --git a/tests/Ta/main.cpp b/tests/Ta/main.cpp index 7b0f583..ed0d1b8 100644 --- a/tests/Ta/main.cpp +++ b/tests/Ta/main.cpp @@ -153,6 +153,23 @@ int main() { Check(CaptureFlagsOff == 0x18 && CaptureDeclaredLen == 0x14, "the flags word sits past the declared length on purpose"); + // ---- The capture payload's two fields + { + std::vector cap(CaptureDeclaredLen); + BuildCapturePayload(cap); + Check(Get32(cap, CaptureFrameCountOff) == 1, "frame count defaults to 1"); + Check(Get32(cap, CaptureSelectorOff) == 1, "selector defaults to 1"); + Check(Get32(cap, 0) == 0, "payload+0 is left for QTEE to patch the region into"); + // An all-zero payload is what -201 looks like on the wire. + std::vector zero(CaptureDeclaredLen, std::byte{0}); + Check(Get32(zero, CaptureSelectorOff) == 0, "selector 0 returns metric 0"); + // The fields must fit inside the declared length. + Check(CaptureSelectorOff + 4 <= CaptureDeclaredLen, "selector fits the payload"); + Check(CaptureFrameCountOff < CaptureSelectorOff, "count precedes selector"); + // ...while the flags word deliberately does not. + Check(CaptureFlagsOff >= CaptureDeclaredLen, "the flags word sits past it"); + } + // ---- SAVE_DATA masks: bit 30 is the whole discriminator Check((SaveMaskTemplate & (1u << 30)) != 0, "template save sets bit 30"); Check((SaveMaskCalibration & (1u << 30)) == 0, "calibration save clears bit 30");