diff --git a/implementations/main.cpp b/implementations/main.cpp index 7c99736..a0c3693 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -32,7 +32,9 @@ extern "C" { #include } +#include #include +#include #include #include #include @@ -215,6 +217,111 @@ qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) { return p[1].object; } +// ---- The sensor rail +// +// GPIO v2 chardev ioctls directly: libgpiod is not on the phone and this is +// three lines. The chip is found by LABEL, never by index -- /dev/gpiochipN +// ordering is not stable and driving the wrong controller's pins is the kind +// of mistake that is not recoverable over ssh. +class Sensor { +public: + ~Sensor() { PowerOff(); } + + bool Open() { + namespace sn = fingerprintd::sensor; + chip_ = FindChip(sn::ChipLabel); + if (chip_ < 0) { + std::println(std::cerr, "no gpiochip labelled '{}'", sn::ChipLabel); + return false; + } + power_ = RequestLine(sn::PowerLine, GPIO_V2_LINE_FLAG_OUTPUT, "fpd-pwr"); + reset_ = RequestLine(sn::ResetLine, GPIO_V2_LINE_FLAG_OUTPUT, "fpd-rst"); + irq_ = RequestLine(sn::IrqLine, GPIO_V2_LINE_FLAG_INPUT, "fpd-irq"); + return power_ >= 0 && reset_ >= 0 && irq_ >= 0; + } + + // Rail up, settle, release reset, settle. Both lines are driven low first + // so a warm restart starts where a cold one does. + bool PowerOn() { + namespace sn = fingerprintd::sensor; + if (!Set(power_, 0) || !Set(reset_, 0)) return false; + if (!Set(power_, 1)) return false; + std::this_thread::sleep_for(sn::PowerSettle); + if (!Set(reset_, 1)) return false; + std::this_thread::sleep_for(sn::ResetSettle); + on_ = true; + return true; + } + + void PowerOff() { + if (!on_) return; + Set(reset_, 0); + Set(power_, 0); + on_ = false; + } + + std::optional ReadIrq() const { return Get(irq_); } + +private: + static int FindChip(std::string_view label) { + for (int i = 0; i < 32; i++) { + std::string path = std::format("/dev/gpiochip{}", i); + int fd = ::open(path.c_str(), O_RDWR | O_CLOEXEC); + if (fd < 0) continue; + gpiochip_info info{}; + if (::ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info) == 0 + && label == info.label + && info.lines >= fingerprintd::sensor::MinChipLines) { + std::println("gpiochip '{}' is {} ({} lines)", info.label, path, + info.lines); + return fd; + } + ::close(fd); + } + return -1; + } + + int RequestLine(unsigned line, std::uint64_t flags, const char* consumer) { + // The guard, enforced where the line is actually opened rather than + // only asserted in the core. gpio8-11 are XPU-protected and touching + // one is an immediate SError, not an error return. + if (!fingerprintd::sensor::IsSafeLine(line)) { + std::println(std::cerr, + "REFUSING to open gpio{}: XPU-protected fingerprint SPI", line); + return -1; + } + gpio_v2_line_request req{}; + req.offsets[0] = line; + req.num_lines = 1; + req.config.flags = flags; + std::snprintf(req.consumer, sizeof(req.consumer), "%s", consumer); + if (::ioctl(chip_, GPIO_V2_GET_LINE_IOCTL, &req) < 0) { + std::println(std::cerr, "gpio{} request failed: {}", line, ::strerror(errno)); + return -1; + } + return req.fd; + } + + static bool Set(int fd, int v) { + if (fd < 0) return false; + gpio_v2_line_values vals{}; + vals.mask = 1; + vals.bits = v ? 1 : 0; + return ::ioctl(fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) == 0; + } + + static std::optional Get(int fd) { + if (fd < 0) return std::nullopt; + gpio_v2_line_values vals{}; + vals.mask = 1; + if (::ioctl(fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) return std::nullopt; + return static_cast(vals.bits & 1); + } + + int chip_ = -1, power_ = -1, reset_ = -1, irq_ = -1; + bool on_ = false; +}; + // ---- The trustlet // // The loader is IQSEEComCompatAppLoader (UID 122): op 1 loadFromBuffer, op 2 @@ -388,11 +495,67 @@ int Probe() { } // A storage read needs no sensor. It exercises the whole SFS listener path - // if listeners are registered, and answers -2 when they are not. + // if listeners are registered, and answers with no templates when they are + // not. auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {}); Report(fingerprintd::ta::Cmd::Enumerate, e); - std::println("\ntrustlet is up and configured. Sensor not powered yet."); + // ---- The sensor, and the init chain that needs it powered + Sensor sensor; + if (!sensor.Open()) { + std::println(std::cerr, "sensor lines unavailable; stopping before init"); + return 1; + } + if (!sensor.PowerOn()) { + std::println(std::cerr, "sensor power-up failed"); + return 1; + } + auto irq = sensor.ReadIrq(); + std::println("sensor powered, reset released, irq={}", + irq ? std::to_string(*irq) : std::string("?")); + + // The chain, in order. Every step answers rc=0 on a healthy sensor and the + // last one is not optional: without SYNC_STATISTICS the trustlet's + // g_statistics stays NULL and the first enrol frame that gets far enough + // writes through it. + // + // One reset buys one init. If this fails, the rail has to go down and come + // back up -- re-running the chain answers -205. + bool ok = true; + for (fingerprintd::ta::Cmd c : fingerprintd::ta::InitChain) { + std::vector payload; + if (c == fingerprintd::ta::Cmd::WorkMode) { + // WORK_MODE takes a u32 mode; 1 = WAIT_TOUCH. + payload.assign(0x10, std::byte{0}); + payload[0] = static_cast( + static_cast(fingerprintd::ta::WorkMode::WaitTouch)); + } else if (c == fingerprintd::ta::Cmd::SyncStatistics) { + payload.assign(fingerprintd::ta::SyncStatisticsPayloadSize, std::byte{0}); + } + auto ir = SendCommand(app, c, payload); + Report(c, ir); + if (!ir.invoked || ir.result != 0 || ir.rc != 0) { + ok = false; + if (ir.rc == fingerprintd::sensor::RcDeviceNotFound) + std::println(std::cerr, + " -205: a second init in one power cycle. " + "Power-cycle the rail, do not retry."); + break; + } + } + if (!ok) { + std::println(std::cerr, "init chain did not complete"); + 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); + + std::println("\ntrustlet initialised against a powered sensor."); pthread_cancel(th); pthread_join(th, nullptr); return 0; diff --git a/interfaces/Fingerprintd-Sensor.cppm b/interfaces/Fingerprintd-Sensor.cppm new file mode 100644 index 0000000..ba3700c --- /dev/null +++ b/interfaces/Fingerprintd-Sensor.cppm @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// lint-disable-file fixed-width-types +/* +Fingerprintd:Sensor — which pins the sensor hangs off, and which must never be +touched. + +The FT9391's SPI bus belongs to TrustZone. The normal world drives only the +sideband: a load-switch enable, a reset, and an interrupt. That is the same +division of labour the downstream driver uses — it owns power, reset and the +IRQ while the trustlet owns SPI. + +Pin numbers and the pad configuration come from the stock device tree, which +names this node `focalfp_ft9362` even though the part is an FT9391 (vendor +copy-paste; the trustlet registers four chip drivers and selects ft9391). + +No I/O here: the line numbers, the timings, and the guard. The shell opens the +chip. +*/ + +export module Fingerprintd:Sensor; +import std; + +export namespace fingerprintd::sensor { + + // The TLMM pin controller. Resolved BY LABEL, never by /dev/gpiochipN -- + // the index is not stable across kernels and getting it wrong means + // driving someone else's pins. + // + // The label is the DT node's unit address, not the driver's name: the node + // is pinctrl@f100000, so the chardev reports "f100000.pinctrl". "tlmm" is + // what the binding and every DT reference call it and it is NOT what the + // chip advertises -- measured on the phone, where the seven chips are five + // PMIC gpio banks plus this one and the 23-line 3440000.pinctrl. + inline constexpr std::string_view ChipLabel = "f100000.pinctrl"; + + // A second, independent check on the chip: TLMM has 168 lines, and the + // other pinctrl on this SoC has 23. Matching the label alone would be + // enough today, but a chip that cannot even contain our highest line is + // never the right one, and driving line 75 of the wrong controller is not + // recoverable over ssh. + inline constexpr unsigned MinChipLines = 76; // must contain IrqLine + + // From the stock DT node: vdd-gpio = <&tlmm 29>, reset-gpio = <&tlmm 74>, + // irq-gpio = <&tlmm 75>, interrupts = <75 1> = IRQ_TYPE_EDGE_RISING. + // No clocks, no supplies, no SPI phandle -- pure GPIO plus an interrupt. + inline constexpr unsigned PowerLine = 29; // FP_3P3_EN, an ETA5053 load switch + inline constexpr unsigned ResetLine = 74; // FPS_RESET_N + inline constexpr unsigned IrqLine = 75; // FPS_INT_N + + // Despite the _N in the vendor's name, the line IDLES LOW and pulses high: + // the DT says edge-rising. A reader expecting active-low sees the finger + // backwards. + inline constexpr bool IrqIsActiveHigh = true; + + // ---- The hazard ------------------------------------------------------- + // + // gpio8-11 are the fingerprint SPI pads and they are XPU-protected. + // Touching one from the normal world is not an error return: it is an + // immediate SError and the phone reboots on the spot. That was paid for + // once, with a module load that took the machine down mid-insmod. + // + // Mainline's DTS reserves them (`gpio-reserved-ranges = <8 4>`), but a + // userspace chardev request does not consult that, so the guard has to be + // ours and it has to be checked on every line we open. + inline constexpr unsigned ReservedSpiFirst = 8; + inline constexpr unsigned ReservedSpiCount = 4; + + inline constexpr bool IsReserved(unsigned line) { + return line >= ReservedSpiFirst && line < ReservedSpiFirst + ReservedSpiCount; + } + inline constexpr bool IsSafeLine(unsigned line) { return !IsReserved(line); } + + // The only lines this daemon ever opens. + inline constexpr std::array OwnedLines = { PowerLine, ResetLine, IrqLine }; + static_assert(IsSafeLine(PowerLine) && IsSafeLine(ResetLine) && IsSafeLine(IrqLine), + "a line this daemon opens is XPU-protected"); + + // ---- Power sequencing ------------------------------------------------- + // + // Rail up, settle, release reset, settle. The delays are the ones the + // working reference used; the sensor answers its ready pulse about a + // millisecond after reset is released. + inline constexpr std::chrono::milliseconds PowerSettle{250}; + inline constexpr std::chrono::milliseconds ResetSettle{50}; + + // Both lines are driven low before the rail comes up, so a warm restart + // starts from the same state as a cold one. + inline constexpr bool AssertResetBeforePower = true; + + // ---- Why the daemon owns this ---------------------------------------- + // + // One sensor reset buys exactly ONE trustlet init. A second init in the + // same power cycle answers -205 "Device not found". So a session that + // fails cannot be recovered by re-running the init chain: the rail has to + // go down and come back up first, and that means the process holding the + // session must also be the process holding the rail. + // + // The research harness split them -- a Python script held the rail for a + // fixed number of seconds while a separate binary drove the trustlet -- and + // every run had to restart the holder first or fail. + inline constexpr int RcDeviceNotFound = -205; + + enum class Power { Off, On }; +} diff --git a/interfaces/Fingerprintd.cppm b/interfaces/Fingerprintd.cppm index 0521742..1d5d63e 100644 --- a/interfaces/Fingerprintd.cppm +++ b/interfaces/Fingerprintd.cppm @@ -18,3 +18,4 @@ export import :Ta; export import :Engine; export import :Store; export import :Tee; +export import :Sensor; diff --git a/project.cpp b/project.cpp index effe973..a6219ac 100644 --- a/project.cpp +++ b/project.cpp @@ -49,7 +49,7 @@ extern "C" Configuration CrafterBuildProject(std::span a ApplyStandardArgs(*Core, args); Core->type = ConfigurationType::LibraryStatic; { - std::array ifaces = { + std::array ifaces = { "interfaces/Fingerprintd", "interfaces/Fingerprintd-Sfs", "interfaces/Fingerprintd-Rpmb", @@ -57,6 +57,7 @@ extern "C" Configuration CrafterBuildProject(std::span a "interfaces/Fingerprintd-Engine", "interfaces/Fingerprintd-Store", "interfaces/Fingerprintd-Tee", + "interfaces/Fingerprintd-Sensor", }; std::array impls = {}; Core->GetInterfacesAndImplementations(ifaces, impls); @@ -86,6 +87,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.AddTest("Engine").Dependencies({ Core.get() }); cfg.AddTest("Store").Dependencies({ Core.get() }); cfg.AddTest("Tee").Dependencies({ Core.get() }); + cfg.AddTest("Sensor").Dependencies({ Core.get() }); ProjectLint::AddProjectLintRules(cfg); diff --git a/tests/Sensor/main.cpp b/tests/Sensor/main.cpp new file mode 100644 index 0000000..e582b9c --- /dev/null +++ b/tests/Sensor/main.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// lint-disable-file fixed-width-types +/* +Fingerprintd:Sensor unit tests. + +Mostly one thing. gpio8-11 are the fingerprint SPI pads, they are +XPU-protected, and touching one from the normal world is not an error return -- +it is an immediate SError and the phone reboots where it stands. Mainline's DTS +reserves the range but a userspace chardev request never consults that, so the +guard is ours and it is worth a test that spells out every pin. +*/ +import std; +import Fingerprintd; + +using namespace fingerprintd::sensor; + +namespace { + int Failures = 0; + void Check(bool cond, std::string_view msg) { + if (!cond) { + std::println(std::cerr, "FAIL: {}", msg); + ++Failures; + } + } +} + +int main() { + // ---- The XPU-protected range, pin by pin + Check(IsReserved(8) && IsReserved(9) && IsReserved(10) && IsReserved(11), + "gpio8-11 are all reserved"); + Check(!IsReserved(7), "gpio7 is below the range"); + Check(!IsReserved(12), "gpio12 is above it"); + Check(ReservedSpiFirst == 8 && ReservedSpiCount == 4, + "the range matches the DTS gpio-reserved-ranges = <8 4>"); + + for (unsigned l = 8; l <= 11; l++) + Check(!IsSafeLine(l), std::format("gpio{} must never be opened", l)); + + // ---- Every line we own is outside it + for (unsigned l : OwnedLines) + Check(IsSafeLine(l), std::format("owned line gpio{} is safe", l)); + Check(OwnedLines.size() == 3, "three lines: power, reset, irq"); + + // ---- The pins themselves, from the stock device tree + Check(PowerLine == 29, "vdd-gpio = <&tlmm 29>"); + Check(ResetLine == 74, "reset-gpio = <&tlmm 74>"); + Check(IrqLine == 75, "irq-gpio = <&tlmm 75>"); + Check(PowerLine != ResetLine && ResetLine != IrqLine && PowerLine != IrqLine, + "the three are distinct"); + + // The vendor calls it FPS_INT_N but the DT says edge-rising: it idles low + // and pulses high. Reading it as active-low sees the finger backwards. + Check(IrqIsActiveHigh, "the interrupt is active high despite the _N"); + + // ---- The chip is found by label + // + // The chardev reports the DT unit address, not the binding's name. "tlmm" + // is what every DT reference calls it and matching on that finds nothing. + Check(ChipLabel == "f100000.pinctrl", "TLMM advertises its DT unit address"); + Check(ChipLabel != "tlmm", "the binding name is not the chardev label"); + Check(!ChipLabel.empty(), "never an index: /dev/gpiochipN is not stable"); + + // The line-count check has to actually exclude the other pinctrl on this + // SoC, which has 23 lines, while admitting TLMM's 168. + Check(MinChipLines > IrqLine, "a valid chip can contain every line we open"); + Check(MinChipLines > 23, "excludes the 23-line 3440000.pinctrl"); + Check(MinChipLines <= 168, "admits TLMM"); + + // ---- Sequencing + Check(PowerSettle > ResetSettle, "the rail gets the longer settle"); + Check(PowerSettle.count() == 250 && ResetSettle.count() == 50, "the proven delays"); + Check(AssertResetBeforePower, "a warm restart starts from the cold state"); + + // ---- The reason the daemon owns the rail at all + Check(RcDeviceNotFound == -205, + "a second init in one power cycle answers -205, so recovery is a power cycle"); + + if (Failures == 0) std::println("Sensor: all tests passed"); + return Failures; +}