Own the sensor rail, and run the init chain against it

The daemon now powers the sensor and initialises the trustlet against it. On
the phone, every step of the chain returning rc=0:

    gpiochip 'f100000.pinctrl' is /dev/gpiochip5 (168 lines)
    sensor powered, reset released, irq=1
      CMD 0x1006 INIT_SPI          rc=0
      CMD 0x100a PROBE_DEVICE      rc=0
      CMD 0x100b INIT_DEVICE       rc=0
      CMD 0x1004 TA_INIT           rc=0
      CMD 0x1020 WORK_MODE         rc=0
      CMD 0x100e SYNC_STATISTICS   rc=0

GPIO v2 chardev ioctls directly rather than libgpiod, which is on neither the
phone nor the sysroot and would be a dependency for three lines.

The chip is found by label, and the label is not what the device tree calls it:
the node is pinctrl@f100000 so the chardev advertises "f100000.pinctrl", while
every DT reference says "tlmm". Matching on "tlmm" finds nothing, which is how
the first run failed. There is a second check on the line count, because this
SoC has another pinctrl with 23 lines and driving line 75 of the wrong
controller is not something you recover from over ssh.

The XPU guard is enforced where the line is actually opened, not only asserted
in the core. gpio8-11 are the fingerprint SPI pads and touching one is an
immediate SError with the phone rebooting where it stands, so a refusal has to
sit in front of the ioctl.

Owning the rail is what makes the session recoverable at all: one reset buys
exactly one trustlet init and a second answers -205, so a failed session needs
the rail cycled rather than the chain retried. The harness split these across
two processes and every run began by restarting the one holding the rail.

CAPTURE_IMAGE answers -201 here and that is correct, not a regression: it needs
a shared memory region whose address QTEE patches into the payload, and none is
supplied yet. That is the next piece.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 18:24:12 +02:00
commit 1fb57cd1be
5 changed files with 357 additions and 3 deletions

82
tests/Sensor/main.cpp Normal file
View file

@ -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;
}