24 KiB
TASK 44 — Fingerprint (FPC1020) as a capability factor
Status: the TrustZone wall is down (2026-08-01 evening) and the
command protocol is recovered (2026-08-09). The kernel loads and
authenticates a signed TA, fpctzappfingerprint runs, and its SPI traffic
clears the interrupt latch that killed piece 1 — the sensor now sits armed
with the line idle-low. The open question — does a finger raise an edge — now
has a direct instrument instead of a correlational one: target 10 command 1
is fpc_ta_check_finger_lost. Raised 2026-07-28 (Casey: "a huge win
possible"). Size: two separable pieces — one was a session, the other is a
research frontier. Do not scope them as one thing. Repos: Pixel3Arch
(kernel + DT), souveraine (the factor, if the second piece lands).
The protocol is in
Pixel3Arch/docs/fpc-ta-protocol.md. The application is not stripped, so its dispatch tables resolve to real function names — eight targets, and the full bio lifecycle (begin_enrol,enrol,end_enrol,identify,get_template_ids,delete_template) at target 11. Recovered statically from the vendor image on disk: no device, no Android, no Frida. Two names below are wrong and the doc corrects them — sensor command 3 iswakeup_setupand command 5 isdeep_sleep, not "idle".
Read the 18:5x section below before the 16:0x one. The earlier measurement concluded piece 1 "does not exist on this hardware"; that conclusion was correct about the AP acting alone and is superseded by the TA path.
The substrate was extracted — but from the wrong driver
Pixel3Arch/staging/fingerprint/ held this from 2026-07-07 and was never
referenced by a build. Two corrections came out of actually wiring it
(2026-08-01):
1. The staged fpc1020_platform_tee.c is the variant blueline does not
build. LOS carries two copies, and staging/'s is byte-identical to
drivers/input/misc/fpc_fingerprint/. b1c1_defconfig sets
CONFIG_FPR_FPC=y — the other one,
drivers/input/misc/fpr_FingerprintCard/. The difference is load-bearing:
the shipping driver's vreg_conf[] is empty, so ARRAY_SIZE is 0 and
device_prepare() manages no regulators at all. The rail is always-on and
the vendor deliberately gutted the table. Anything scoped around this driver's
power management was scoped around code that does not run on this phone.
2. The pin list is confirmed, and the FPC1075 caution below is resolved.
This unit's own DTBO (~/pixel3-work/dtbo_9.dts, fragment@40, fp_fpc1020)
carries compatible = "fpc,fpc1020", fpc,gpio_rst = <… 0x07 …>,
fpc,gpio_irq = <… 0x79 …> (121), vdd_io-supply = <&pm8998_s4>. Identical to
LOS's own sdm845-b1c1-fingerprint.dtsi. There is no FPC1075 on this board and
nothing reserves GPIO 81–84.
Note pm8998_s4 is a downstream label that does not exist in our tree —
the same rail is vreg_s4a_1p8 (a regulator-fixed, always-on, boot-on 1.8 V).
The staged .dtsi also uses the retired mux {}/config {} pinctrl style and
fpc,gpio_* properties that gpiod cannot resolve, so it could never have been
included as written.
Read the driver before planning around it
fpc1020_platform_tee.c is not a fingerprint driver. It is a power,
reset and interrupt shim with a sysfs face: clk_enable, pinctl_set,
regulator_enable, hw_reset, device_prepare, wakeup_enable, irq.
There is no image capture in it and no matching. On Android the sensor's SPI
data path belongs to a TrustZone application under QSEECOM — the AP never
sees a ridge image; it sees "a finger arrived" and later "the TA says yes."
That single fact splits this task, and it is why the two halves must not share a schedule.
Piece 1 — the sensor as an input and a wake source (BUILT 2026-08-01)
Written as drivers/input/misc/fpc1020.c on the fingerprint-fpc1020 branch
of linux-blueline (6f637c4c2), not as a copy of the staged vendor file. The
2015 original cannot compile here: of_gpio.h is gone, wakeup_source_init
/_trash are gone, platform_driver.remove returns void. The port uses
descriptor GPIOs, devm_pm_set_wake_irq, and an implicit default pinctrl
state in place of the three named reset/irq states.
The vendor sysfs surface is not shipped — no device_prepare, hw_reset,
regulator_enable, clk_enable, irq. That is a control interface for a TA
which is not loaded here, and a poll()-driven sysfs node is not how input is
routed on this device. The driver registers an input device reporting
KEY_WAKEUP instead, which puts the reader on the same evdev path as
everything else — so it becomes a row in the existing gesture table rather than
a new hardcoded branch (INTERFACE-ARCHITECTURE §4). TASK-13 fought exactly this
and the rule came out of it.
DT is in sdm845-google-blueline.dts: interrupts-extended = <&tlmm 121 IRQ_TYPE_EDGE_RISING>, reset-gpios = <&tlmm 7 GPIO_ACTIVE_LOW> (active-low —
the polarity trap that cost a day on FTS, PAF/touch.md), vdd_io-supply = <&vreg_s4a_1p8>, plus an fp-default-state pinctrl group.
CONFIG_INPUT_FPC1020=m is in kernel/config-blueline.aarch64 and survives
olddefconfig in place, so the PKGBUILD drift guard stays quiet.
Measured on glass 2026-08-01 — the AP half is complete, and it is not enough
Everything the AP owns is correct and verified on the running kernel
(7.1.1-sdm845-g8eebcaff65ef): module loaded, driver bound at
/sys/bus/platform/drivers/fpc1020, FPC1020 Fingerprint Sensor registered as
input4/event4 advertising KEY_WAKEUP, line 7: output active-low consumer="reset", and IRQ 168 wired as msmgpio 121 Edge fpc1020.
And a touch produces nothing. evtest on event4 for 60 s across repeated
touches: zero events, and the interrupt count never moved.
The reason is measurable, not inferred:
gpioget --chip gpiochip2 121readsactive— the IRQ line is stuck high, so anIRQ_TYPE_EDGE_RISINGrequest can only ever see one edge.- Unbind the driver and the line stays high, against the DT's
bias-pull-down. The sensor is driving it, not a pull or a stale config. - Rebind and the count goes 1 → 2, one edge per reset, then latched again.
So the FPC1020 asserts its interrupt at power-up and holds it until the host clears it over SPI. That is the "half a driver" this task already named, now with a number on it: without the SPI side the line latches once and the pin is dead thereafter.
There is no SPI path to it from the AP. The device's own DTBO gives
fp_fpc1020 exactly interrupts, fpc,gpio_rst, fpc,gpio_irq and
vdd_io-supply — no SPI node, no bus. The one nearby SPI, qupv3_se0_spi
(fragment@36, TLMM gpio0–3), hosts intel,mnh-spi — the Pixel Visual Core.
The fingerprint's SPI is not in the AP's device tree anywhere.
Therefore piece 1, as scoped above, does not exist on this hardware. "Wire the DT and the phone gains a real button under the reader" was the premise, and it is wrong: a finger cannot raise an edge the AP can see, because the first edge is already spent and only a TrustZone SPI transaction rearms it. The driver is correct and worth keeping — it is the AP half of whatever piece 2 becomes — but it is not an input, not a wake source, and must not be described as either.
SUPERSEDED the same evening — and by its own prediction. "Only a TrustZone SPI transaction rearms it" was the exit condition, and the TA path below satisfies it: the latch clears and the line returns to idle-low. What stays true is the narrower claim that the AP alone can never rearm the sensor. Piece 1 does not exist without TZ; with the TA loaded it is no longer blocked on the hardware.
What this does to Mis012's GPIO-mode lead. Driving the sensor in GPIO mode means bit-banging its SPI from TLMM pins, which requires knowing which pins carry it. On blueline they are absent from the DTBO, so the lead is not bounded from here — it needs a schematic, a probe, or a downstream source that names the pins. Worth exactly that much investigation and no more before it is believed.
This piece does not authenticate anything. Do not let it get described as fingerprint unlock, in a commit or a settings string. An input that looks like an auth factor and is not is worse than no input.
Measured 2026-08-01 18:5x — the latch clears, and the sensor does not free-run
First cold boot on 7.1.1-sdm845-g08e9db54cfce. The TA loads from a cold
start with nothing resident: app_id=2, and init (0), idle (5) and
arm (3) each return 0x00000000.
The interrupt latch clears. This is the measurement that moves the task.
At 16:0x the line read active and stayed high with the driver unbound — the
sensor driving it against the DT's bias-pull-down. After the TA sequence,
/sys/kernel/debug/gpio reads:
gpio121 : in low func0 2mA pull down
Idle-low, pull-down effective, and stable across seconds. An
IRQ_TYPE_EDGE_RISING request can fire again, which it could not before.
Read the level through debugfs, not gpioget — while the driver holds the
line, a request returns EBUSY.
Edges are TA-driven SPI traffic, and every one is accounted for. Counted
from /proc/interrupts around each command:
| command | IRQs | line after |
|---|---|---|
init (0) |
+68 | low |
idle (5) |
+1 | low |
arm (3) |
+1 | low |
68 edges on init is a real register conversation with the sensor, not noise.
The boot count went 1 → 91 over the first sequence and 91 → 161 over a second.
Free-running is refuted. Armed once, never re-armed, 15 s with nothing on
the reader: 0 hardware IRQs, 0 poll notifications, and the count held flat
per-second for the whole window. The 1444-event run that raised this doubt
re-armed on every interrupt, so one assert became a loop — the loop was the
signal, not the sensor. Any future instrument must arm exactly once per window;
fp-window.py on the phone does, and prints per-second deltas so a real
detection can be seen to cluster rather than spread.
The chain reaches userspace. With the sensor armed and idle-low, an assert was caught end to end:
EDGE at t+94.73s +1 total=169 line=high
EVDEV at t+94.73s code=143 value=1 <-- KEY_WAKEUP
EVDEV at t+94.73s code=143 value=0 <-- KEY_WAKEUP
Sensor → IRQ 168 → driver → KEY_WAKEUP on /dev/input/event4, same
timestamp. That is the evdev path hyprland.lua's existing XF86WakeUp bind
already listens on, so the routing question is answered. Across ~120 s of
armed-but-untouched windows the count never moved; every assert has come in a
window where a touch was being attempted. Attribution to a finger still rests
on the human confirming the touch — the rig cannot tell a finger from any
other cause, so say "an assert" until someone says "I touched it then".
wakeup_enable and device_prepare take the vendor's enable/disable
strings, not 0/1 — a 1 returns EINVAL (wakeup_enable_store). The
driver gates the key on it, so KEY_WAKEUP reaches evdev only after
echo enable > wakeup_enable. Anything productionising this must set it.
Why the 1444-event run happened — solved, and it constrains the design
arm (cmd 3) generates its own edge, +1, measured. So a handler that
re-arms on every interrupt feeds itself: arm → edge → handler → arm → edge,
without end. The 1444 events were the instrument, not the sensor and not a
finger.
That is a design constraint, not a footnote. The sensor asserts once per arm and then latches high until the TA clears it, so a usable input must re-arm after every event — and a naive re-arm is exactly the storm. Whatever lands has to suppress the arm's own edge (ignore the edge that follows an arm within a few ms, or ask the TA for finger state instead of re-arming blind). Do not ship a re-arm loop that has not solved this.
Piece 2 — biometrics, which is a TrustZone question
Matching needs either QSEECOM and Google's TA, or an open SPI image path with our own matcher. Neither exists here today.
Correction 2026-08-01 — this section used to point at
PAF/evidence/secure_load_tz_frontier.md. That is the wrong doc. It is
marked SUPERSEDED at the top and it is about qcom_scm_assign_mem returning
−22 and the modem's XPU faulting — memory ownership transfer, a different SCM
surface from loading and calling a TA. It answers nothing here. The standing
record for this question is PAF/evidence/tz_listener_archaeology.md plus
the four qseecom: commits on the blueline branch.
Read together, they say the question is half answered, and better than expected:
google,bluelineis inqcom_scm_qseecom_allowlist; the QSEECOM version query already succeeds on this device (2e172dcb3). TZ answers us.- Listener registration works against this device's real signed TZ image —
id
0x5000(ssd) returnsQSEECOM_RESULT_SUCCESS, live-tested 2026-07-02. The archaeology doc had listed "whether this exact signed TZ image accepts registration from a differently-built AP driver" as unknown and untestable without trying it. It was tried. It works. /dev/qseecomexists withGET_APP_ID+SEND_CMDwrappingqcom_scm_qseecom_app_get_id/_app_send(5fa36934b) — but that commit says plainly: compiles clean, never run on hardware.
So what is actually missing is narrower than "can we talk to TZ":
There is no app-loading path.BUILT 2026-08-01.qcom_scm_qseecom_app_load()— app-mgr command 1 underQSEE_OS, three VAL params (mdt_len,img_len,phys), downstream'sTZ_OS_APP_START_ID(qseecomi.h:456) — plus aLOAD_APPioctl that reassembles the QSEE image the way TZ expects: the.mdtELF header blob plus one.bNNper program header, laid out contiguously atp_paddr-relative offsets, staged withdma_alloc_coherentunder a 32-bit mask because the request carries a 32-bit physical address. Commitsd7d2e1222,cb150d71d,4d52867fe.Nothing has ever exercisedDONE. Three commands round-tripped to a real TA on hardware, allapp_sendlive.0x00000000.- The FPC TA blob stays vendor-opaque, but its command protocol is no longer
entirely so:
0init,3arm,5idle, sent through a 0x58-byte shared aux buffer (fpc_tac_alloc_shared) whose first two words are0x0Aand the command. Payload-in-a-second-buffer isac9a4dd2a.
The prediction below was tested and resolved. Every GET_APP_ID returned
-ENOENT — for all names, not just uefisecapp — because TZ only resolves
apps something already started, and on Android that something is qseecomd.
So "apps must be loaded before they can be called" was the right branch, and
loading is now built rather than a frontier.
Cheapest next probe, and it is small: run the existing /dev/qseecom
GET_APP_ID against candidate names on the device. uefisecapp is already
confirmed ENOENT; downstream loads keymaster as 'keymaste'. If any name
resolves, resident-app calling is real and only FP-specific work remains. If
none does, apps must be loaded before they can be called, and that is the
frontier — not the SCM channel.
If the answer stays no, the honest outcome is Piece 1 plus a note, and face auth (TASK-12) is the cheaper capability factor. Say so rather than leaving this open forever.
Where it plugs in — and it is a daemon, not a script
The capability tier model already has the shape — StepUpAuth.qml,
LockContext.qml, and the souveraine-stepup PAM service named in
sessiond/protocol.rs:36. TASK-12 scopes face auth against exactly those
primitives and explicitly refuses to wrap gazed: "we own the source —
anything touching the permission/capability gating is in-tree, not vendored."
A fingerprint factor is symmetric with it and inherits that decision.
So the deliverable is souveraine-fpd: a system daemon owning
/dev/qseecom and input4, driving the TA through the protocol in
Pixel3Arch/docs/fpc-ta-protocol.md, exposing enrol/verify over D-Bus the way
gazed does, and minting through souveraine-stepup. The fp-*.py tools are
instruments, not the factor.
Temporary post-login Polkit factor — built 2026-08-10
The smaller blueline-fingerprintd producer now has two bounded outputs from
the same real FPC assertion:
/run/blueline-fingerprintd/preview-pulseis root-owned and world-readable. The Souveraine lock surface may use it only for the optional wiring exercise./run/blueline-fingerprintd/polkit-approvalis root-only, expires after eight seconds, and is atomically consumed exactly once bypam_souveraine_fpc.so. It exists only long enough to satisfy a blankpolkit-1PAM conversation after the fullscreen Polkit surface has shown its three-second confirmation interval.
This is an intentional bridge while match-on-chip verification lands. It is a real PAM result, not a shell-side boolean: the fullscreen Polkit surface continues to own the prompt, PAM decides success, and the ordinary PIN/password conversation remains its fallback. It covers user-facing Polkit only. It cannot satisfy first login, first storage unlock after boot, screen-lock PAM, sudo, SSH, remote admission, or household/device enrollment. A reboot erases the record and returns the device to PIN-required.
The bridge does not claim continuous finger presence or a verified
fingerprint. The current hardware proof is one re-arm-safe FPC IRQ, not a
template match; the visible three seconds make the temporary interaction
deliberate rather than pretending to add biometric evidence. The package
inserts a narrowly marked pam_souveraine_fpc.so line ahead of the existing
polkit-1 auth stack and saves the preimage. If a Polkit upgrade produces a
.pacnew, merge the marked three-line block deliberately; do not let a package
upgrade silently erase the factor.
The reader's KEY_WAKEUP stays mapped to panel wake only: the FTS touch driver
emits the same generic key, so binding that key inside Hyprland would falsely
call a touchscreen double-tap an FPC event. The FPC daemon record, not the
generic key, is the only source the Polkit UI observes.
The intended enduring flow remains PIN after boot, verified FPC after that.
The PIN performs the first unlock that makes Personal-class key material and
the enrolled FPC template database available. souveraine-fpd will replace
this raw-IRQ bridge with a verified match result without changing the Polkit
surface or its PAM ownership. A verified match may then unlock a later screen
lock or mint a narrow read/step-up grant, according to the requested operation;
it never replaces first-boot storage unlock, household membership, or
remote-device admission.
Prior art, checked 2026-08-09
sailfishos-open/sailfish-fpd-community |
Live (Nov 2025). Implements Jolla's org.sailfishos.fingerprint1, but reaches hardware through an Android library (libbiometry_fp_api, built in HADK, descended from Ubuntu Touch's biometryd). Unusable here — Souveraine has no hybris. Its D-Bus surface and state machine are worth copying, and its README doubles as a reverse-engineering method note. |
wrobelda/libfprint branch goodix-qsee |
The only working example of a libfprint driver doing match-on-chip over QSEE. Different TA, same architecture. The template if we ever want fprintd. |
| libfprint match-on-chip | Supported upstream — device features cover on-device storage and identify, so match-on-chip is not fighting the framework. |
| FPC over QSEE, natively on Linux | Nobody has done it. No prior art found. |
The primitives all agree, which settles the API
Cloned to Pixel3Arch/references/biometrics/: fprintd, libfprint (with
wrobelda/goodix-qsee fetched as a remote branch), sailfish-fpd-community,
biometryd.
Reading their interfaces side by side, they are the same API three times over —
and TASK-12's gazed is a fourth. fprintd has Claim/Release,
VerifyStart/VerifyStop, EnrollStart/EnrollStop, ListEnrolledFingers,
DeleteEnrolledFinger, signals VerifyStatus/VerifyFingerMatched/
EnrollStatus, properties finger-present/finger-needed/
num-enroll-stages. gazed has claim/release, verify_start/stop,
enroll_start/stop with a status signal carrying capture quality. Sailfish
adds the enum worth stealing wholesale: FPACQUIRED_GOOD/PARTIAL/
INSUFFICIENT/IMAGER_DIRTY/TOO_SLOW/TOO_FAST.
So there is no API to design. Take that shape, and the face factor and the finger factor become the same surface with two backends — which is what symmetry with TASK-12 was supposed to mean.
It maps onto the recovered protocol without a gap:
| daemon call | TA commands |
|---|---|
EnrollStart |
11/6 load_empty_db (first run), 11/9 set_active_fingerprint_set, 11/0 begin_enrol |
| enroll stage | 10/4 qualify_capture → 11/1 enrol; remaining count drives num-enroll-stages |
EnrollStop |
11/2 end_enrol |
VerifyStart |
10/3 wakeup_setup, then on IRQ 10/1 check_finger_lost → 10/4 → 11/3 identify |
ListEnrolledFingers |
11/7 get_template_ids |
DeleteEnrolledFinger |
11/8 delete_template |
finger-present |
10/1 check_finger_lost |
| acquired quality | capture/qualify return codes → the Sailfish enum |
| persistence | target 2, via the supplicant — see below |
wrobelda/goodix-qsee is the structural template for the backend: 1,418 lines
split into -transport.c (the TEE channel), -sensor.c (the misc device),
-token.c (the auth token), -proto.h (the command table) and the driver
proper. Those four files are exactly our four layers — /dev/qseecom, the
fpc1020 input device, target 3, and fpc-ta-protocol.md.
The listener is a separate owner, and that is a constraint
Templates only persist through target 2, which goes out to a QSEE listener. The
kernel allows one receiver per device, not per application, so whoever
registers the file-service listener locks out every other QSEE client. That
argues the listener does not belong inside souveraine-fpd — it belongs in
one machine-wide supplicant, with the fingerprint daemon as a client of it.
wrobelda/qsee-supplicant is that component, is TA-agnostic, and made this
argument first.
Order that falls out: sensor attribution (fp-finger.py) → RAM-only enrol and
identify → supplicant + target 2 for persistence → daemon → PAM factor. Only
the last two are the "app".
Do not
- Do not build Piece 2 first. The cheap win is real and independent.
- Do not ship the shim's sysfs to userspace as an API.
The port drops that surface entirely— AMENDED 2026-08-01 (329737a23): the surface is back, because the premise it rested on ("a vendor interface for a TA that does not exist here") stopped being true the moment the TA loaded. The TZ stack needsdevice_prepare/hw_reset/wakeup_enableto sequence the sensor. The prohibition still holds where it was aimed: these are root-only and write-only on the device (--w-------; onlyirqis readable), and none of them may become a settings switch or an agent verb. A control that reports success while authenticating nothing is exactly the failure this bullet was written against — that argument survives, only its factual premise changed. Do not confuse FPC1020 with the FPC1075— RESOLVED 2026-08-01. This unit's own DTBO carries FPC1020 on gpio7/gpio121 and nothing reserves GPIO 81–84. TASK-45's pin-conflict problem (GPIO 85/86, SE5) is real and separate; it is a neighbouring range, not this one.- Do not take
staging/fingerprint/as the reference. It is thefpc_fingerprintvariant, which blueline does not build, and its.dtsiuses downstream labels and retired pinctrl syntax. The authority isreferences/los-kernel-blueline-4.9/drivers/input/misc/fpr_FingerprintCard/plus this unit's DTBO.
Connects to
TASK-12 (face auth — the other capability factor, and the fallback if TZ says
no), TASK-40 (what a biometric stream permits inferring), SECURITY-AUDIT.md,
SESSION-AUTHORITY-DOCTRINE.md §2 (reveal-on-lock-surface — "a fingerprint
touch reveals personal content in place, without unlocking the session" — is
gated on this sensor; piece 1 does not deliver it, piece 2 does),
PAF/evidence/tz_listener_archaeology.md (the TZ record that actually applies).