Skip to content

Add 0x969/0xd51 no-template-found handling b[0]=4/5 (Wrong Finger handling) - #5

Open
Karloss1234 wants to merge 18 commits into
SimpleX-T:masterfrom
Karloss1234:969-no-template-retry
Open

Add 0x969/0xd51 no-template-found handling b[0]=4/5 (Wrong Finger handling)#5
Karloss1234 wants to merge 18 commits into
SimpleX-T:masterfrom
Karloss1234:969-no-template-retry

Conversation

@Karloss1234

@Karloss1234 Karloss1234 commented Jul 31, 2026

Copy link
Copy Markdown

Summary

This PR adds improved handling for HP 0xd51 and 0x969 fingerprint sensors. Much of the code was taken with thanks from the @kelexine fork (now closed).

The current SimpleX-T fork seems to successfully treat b[0] = 4 or 5 interrupts as a general b[0] != 3 failure.

Commit 1: Add clean no-matching-template handling

Some sensors return a distinct interrupt when a fingerprint image is captured successfully but no enrolled template matches.

This commit:

  • Adds FingerNotMatchedException
  • Detects the sensor-specific no-template responses:
    • 0xd51: b[0] = 5
    • 0x969: b[0] = 4
  • Allows dbus-service to distinguish a valid scan of a wrong finger from a low quality capture failure

Commit 2: Add retry handling for clean no-match results

This commit builds on Commit 1.

It adds limited retry handling in dbus-service for clean no-match results before returning a final verification failure. Sometimes the correct finger is pressed and a high quality scan is achieved and an incorrect No-Match is reported. For such false negative instances a retry is allowed.

The retry policy remains outside sensor.py so the sensor layer only reports the result and does not control authentication behaviour.

Testing

Tested on:

  • HP ZBook Studio x360 G5
  • Kubuntu 26.04
  • Synaptics 138a:00ab / sensor type 0x969

Verified:

  • Matching fingerprints authenticate correctly
  • Non-matching fingerprints return verify-no-match
  • Capture failures continue to retry

Granular Detail of Chip Interrupts

Known interrupt codes observed for 0x969 / 0xd51 sensors:

glow_start_scan() is called to switch on sensor.

capture() is called to request the following chip interrupt returns:

0000000000 b[0] = 0
Chip Started / Capture initialisation / waiting for finger.

0340010000 b[0] = 3
Object detected on sensor. Finger/object acquisition has started.
capture() remains in its capture-complete loop until the chip returns
an image quality result:
0320070000 - capture rejected
0342040040 - capture accepted

0320070000 b[0] = 3 and b[2] & 4
Capture completed but image was rejected by the sensor.
Possible causes:

  • no finger placed (blank image)
  • insufficient finger contact
  • poor quality fingerprint image

capture() raises an exception which returns control to identify().
identify() calls dbus-service.update_cb(e), allowing another capture
attempt. Subsequent attempts loop every second or so.

0342040040 b[0] = 3 and b[2] & 4
Capture completed successfully.
A valid fingerprint image is available for matching.

At this stage capture().get_prg_status2() seeks a detailed chip response:
For 06cb:00b7 0x969 sensor:

0320070000 (capture rejected) - PRG STATUS2 RESPONSE: 07000c000000000000000000000000002080 bytes=18
0342040040 (capture accepted) - PRG STATUS2 RESPONSE: 00000c000000700070004d01080000000000 bytes=18
capture() then returns to identify().

identify() then calls match_finger() which requests chip to return Image Match result:

03000100db b[0] = 3
Match result: matching fingerprint template found.
dbus-service.run() then determines:

  • 'template' Matches this user: Match
  • 'template' Matches other user (or Windows Hello template): No Match - End fprintd.

04000100db b[0] = 4 (for 0x969 only)
Match result: no matching fingerprint template found. No Match - Retry three times.
Raised as FingerNotMatchedException by kelexine fork so dbus-service.run() can allow retries.

05000100db b[0] = 5 (for 0xd51 only)
Match result: no matching fingerprint template found. No Match - Retry three times.
Raised as FingerNotMatchedException by kelexine so dbus-service.run() can allow retries.

SimpleX-T and others added 18 commits May 19, 2026 08:48
The 0xd51 chip does not emit the b[0]=2 "finger detected" interrupt
during capture, so the wait-finger loop in Sensor.capture() hangs
forever. Accept b[0]=3 as a substitute (gated on the real device
type so existing chips are unaffected) and pass the interrupt
through to the wait-capture-complete loop.

Also wires the two known USB IDs through SupportedDevices, blobs,
firmware_tables, the udev rule, and aliases sensor type 0xd51 to
the 0x199 profile so SensorTypeInfo / SensorCaptureProg lookups
succeed (no native profile exists yet).

Closes uunicorn#181
Closes uunicorn#225
Closes uunicorn#238
Without these entries `validity-sensors-firmware` crashes with a
KeyError for users on 138a:00ab and 06cb:00b7 who follow the README's
standard install flow. The 0xd51-family chips ship with firmware
pre-loaded so the downloader is only needed for factory-reset chips,
but the script should not crash.

Both PIDs point at HP softpaq sp135736.exe (the same blob extracted
to 6_07f_hp_cmit_mis_qm.xpfwext in FIRMWARE_NAMES). sha512 verified
against the canonical ftp.hp.com URL.

Reported by a 06cb:00b7 user on PR uunicorn#256.
The chip's database rejects creating a second finger record with the
same subtype for the same user, which previously caused fprintd-enroll
to fail at the final stage with enroll-failed after all per-stage
captures had passed. Detect and delete any pre-existing record with the
same subtype right before db.new_finger.

Placing the delete here (inside do_create_finger, after all captures
have completed) rather than at EnrollStart matters: pre-deleting before
the enrollment session starts left the chip in a state where every
subsequent capture returned retry-scan indefinitely until the daemon
was restarted. By the time do_create_finger runs the captures are done
and the chip is ready to accept the save, so deletion at this point
doesn't disrupt session state.
Lenovo softpaqs (the previously supported devices) ship as Inno Setup
installers, which is what innoextract handles. HP softpaqs (sp135736.exe
used for the new DEV_AB and DEV_B7 entries) are CAB-wrapped self-
extracting exes — innoextract rejects them with "Not a supported Inno
Setup installer!" and the postinst surfaces a noisy Python traceback.

Try innoextract first; on CalledProcessError or FileNotFoundError, fall
back to cabextract. The standalone tool-availability check accepts
either extractor so installations on either family work out of the box.

Note: dropped the `-F <fwname>` filter from cabextract — its pattern
matches the full path inside the cab (e.g. src/driver/INF/x64/6_07f_...
xpfwext) not just the basename, so -F would silently skip the target.
Extract everything; the existing `find` afterward locates the file
regardless of the subdirectory it landed in.
debian/python3-validity.postinst: run pam-auth-update --package --enable
fprintd so sudo / screen-unlock / GNOME Settings fingerprint flows work
immediately after install, without the user having to know about
pam-auth-update.

debian/control: recommend libpam-fprintd (the PAM module our postinst
enables) and cabextract (the fallback extractor used by the new
validity-sensors-firmware path for HP softpaqs).

Bumped changelog through 0.16~hp1/hp2/hp3, with hp3 documenting the
same-finger re-enroll fix shipped this release.
pam_fprintd re-prints "Place your finger on the reader" for every
verify-retry-scan signal it receives. On the 0xd51 chip the capture
loop is chatty — a single 10-second verify window can fire the signal
20+ times, flooding the terminal during sudo authentication.

Suppress all but the first verify-retry-scan per VerifyStart so the
user sees one initial prompt and one early "place again" hint, then
silence until match or timeout. Enrollment behavior is unchanged
because per-stage retry hints are useful there (each stage is a
discrete user action where lift-and-retry feedback matters).
debian/source/options: tar-ignore .pybuild, build, *.egg-info,
__pycache__, *.pyc.

3.0 (native) source packages bundle the working tree verbatim, so
any leftover pybuild cache from prior local debuild runs gets shipped
to Launchpad. pybuild's cached state embeds the developer's absolute
paths (e.g. /home/<dev>/.../python-validity/.pybuild/...), which the
build chroot can't write to. The first per-series build succeeded
because pybuild created the cache fresh; subsequent series builds
inherited the dirty cache and failed with Permission denied trying
to write into the baked-in path.

This is a packaging-only change; no source code differs.
sensor.py: log resolved capture geometry once at chip open. Useful
when investigating whether the 0x199 spoof produces appropriate
dimensions for whatever chip the daemon is talking to. The log line
includes real_type vs spoofed_type, lines_2d from the capture
program, computed lines_per_frame, bytes_per_line, line_width, and
lines_per_calibration_data — everything needed to recognize a
profile mismatch without re-instrumenting.

dbus-service: in VerifyStart, log every internal chip retry-scan to
the journal independent of the D-Bus signal throttle. Lets users
and support tickets quantify capture quality without manual
instrumentation — `journalctl -u python3-validity.service | grep
'Chip capture retry-scan'` gives the raw count immediately.

Both changes are INFO-level log lines, no functional impact. About
1 log per chip open + 0-3 lines per verify in normal use.
Multiple users reported the 0xd51-family chips (138a:00ab, 06cb:00b7)
getting stuck after cold boot, unclean exit, or suspend/resume — the
chip accepts the bulk-OUT but never responds on bulk-IN, so the first
cleartext cmd (cmd 3e get_flash_info) times out and the daemon
restart-loops every 15 seconds. The workaround users were running
manually is a USB-level reset:

    sudo systemctl stop python3-validity open-fprintd
    sudo udevadm trigger --attr-match=idVendor=138a --attr-match=idProduct=00ab
    sudo systemctl start python3-validity open-fprintd

The reset call here is the in-driver equivalent. The chip's USB
address can shift after reset, so we re-find by vid/pid.

Reported by Killersparrow1 (issue uunicorn#238, Fedora 44, sensor vanishes
on reboot) and a separate Arch / ZBook G5 user (USBTimeoutError on
cmd 3e). The patch matches what the project memory has flagged for
the past two sessions as "kept local and not in PR" — turns out it
was the actually-load-bearing piece.

Locally confirmed: clean daemon restart, sudo matches in 1 retry,
no traceback, no "USB reset failed" warning.
The 138a:00ab USB ID is shared across HP machines but maps to different
sensor silicon. The EliteBook 840 G5 reports sensor type 0xd51; the ZBook
Studio x360 G5 reports 0x969. Both lack native SensorTypeInfo /
SensorCaptureProg entries, and empirically both work when aliased to the
0x199 capture profile -- the on-chip matcher accepts the resulting images.

Mirror the existing 0xd51 handling for 0x969:
- add 0x969 to line_update_type1_devices
- alias 0x969 -> 0x199 in open()
- accept the b[0]==3 finger interrupt when real_device_type is 0x969

Verified on an HP ZBook Studio x360 G5: init + calibration + enroll
(5 stages, enroll-completed) + verify (verify-match).
After suspend/resume, 0x969 chips re-enumerate reporting sensor type
0x199 directly rather than 0x969. Without intervention Sensor.open()
skips the alias-to-0x199 block and capture()'s b[0]==3 interrupt fix
(which keys off real_device_type), so verify hangs indefinitely
post-resume.

Detect via the device *name*, which is stable across boot and resume:
FM-3439-xxx and FM- 154-xxx are the two known 0x969 model families
(HP ZBook 17 G6, ZBook Studio G5, ProBook G6). Genuine 0x199 chips
(FM-3367-xxx, FM-3380-xxx, FM-155-xxx) don't match either pattern.

Diagnosed and tested by @Karloss1234 on Kubuntu 26.04 with an HP
ZBook 17 G6 (06cb:00b7). Reported at Point 4 of the review on
uunicorn#256.

0xd51 users: if verify hangs after resume, please report; we may need
the same treatment for 'FM-154-xxx' (no space).
Both @bcoutts (fresh factory 0xd51 on ProBook 445R G6) and
@ntoyiakhona06-creator (Windows-Hello-paired 0xd51 on EliteBook 840 G5)
hit the same failure: usb.cmd(reset_blob) returns status 0404 —
from opposite chip states (factory-fresh vs. previously paired).

reset_blob is byte-identical across blobs_97 / blobs_9a / blobs_9d
(shared "Prometheus" blob), and was originally extracted from Windows
drivers for 0x199-class chips. The failure is not blob content or
selection — it's that 0xd51 / 0x969 silicon does not accept this
reset primitive at all. Until someone extracts a working reset_blob
for the newer chip family, init_flash on a factory-fresh chip and
factory_reset for unpair-from-Windows are both unsupported on this
hardware.

Catch 0404 at both call sites and raise a specific, actionable error
(pointing at the tracking PR and telling users what to attach when
reporting) instead of the bare "Failed: 0404" that assert_status
produces. Also add a "known issue" section to the README.

References: uunicorn#256 (bcoutts,
ntoyiakhona06-creator comments).
Two related flash-management issues found by @Karloss1234 while
tracing 04b5 errors during enroll on an HP ZBook 17 G6 (Point 6 of
the review on uunicorn#256).

1. del_record was sending 0x48 without the db_write_enable prefix
   that new_record and every write in flash.py use. On 0x969 chips
   this returns error 04b6 (silent) and the record stays on flash —
   deletes never actually free space. Mirror new_record's shape:
   db_write_enable + try/finally: call_cleanups().

2. Even with (1) fixed, the chip does not automatically compact the
   database partition. Windows only triggers compaction as part of
   its own enroll flow after a TPM reset, so a long-running Linux-
   only setup accumulates uncompacted dead records until enroll
   fails at new_finger with the chip's opaque 04b5.

   new_record was already fetching db_info() with a TODO to actually
   check the numbers — actually check them. If the payload plus a
   small header estimate doesn't fit in Info.free, raise a clear
   error that names the recovery path (erase_flash(4) + reboot),
   instead of letting the on-wire 04b5 bubble up.
…ewers

Three additions from the discussion on uunicorn#256:

- Template competition (Windows Hello vs. Linux fprintd sharing the
  on-chip database) with the "enroll different fingers per OS" and
  "erase partition 4" workarounds. From @Karloss1234's Point 2.

- Kubuntu / KDE lock-screen PAM setup — three /etc/pam.d files and a
  pam_permit.so → pam_unix.so sddm-greeter fix. Not needed on GNOME.
  From @Karloss1234's Point 5.

- Known issue: 0404 on reset_blob for 0xd51/0x969 silicon (factory
  init and factory_reset are unsupported on this chip family). Tells
  users what to attach when reporting.

Code changes for the same items are in prior commits; this is docs.
0xd51 / 0x969 chips can enter a "wedged capture-quality gate" state
after long uptime + heavy dev cycles: every captured frame is
rejected by the on-chip quality gate and no capture-complete
interrupt is ever emitted, so sensor.identify() loops in wait_int()
indefinitely. Nothing the daemon can do from userspace clears this —
even sensor.cancel() only unblocks wait_int; the wedge itself
survives systemctl restart. Recovery is a real cold power cycle
(shutdown, unplug charger, hold power ~15s, boot).

Historically the daemon-side identify() thread would keep running
past pam_fprintd's 10s timeout, and diagnosing the state took ~30
minutes of guessing because the journal just showed retry-scan
repetitions with no summary.

Add a retry-count watchdog to VerifyStart's update_cb: after 25
consecutive retries with no capture-complete (typically 12-15s),
log a specific warning naming the wedge condition and the required
recovery path, and call sensor.cancel() to unblock the identify()
thread. The outer except in run() then emits verify-no-match
cleanly.

25 is empirically past the tail of a healthy verify (~2 retries
typical, p95 = 2 in the task uunicorn#17 benchmark) with comfortable
margin. Enrollment path is left alone — its retry-scans are
per-stage user actions, not the same signature.

Diagnosed on the maintainer's own 840 G5 (0xd51) after 4 days of
uptime + a diagnostic init.open() that didn't clean up.
Rolls up:
  - 0x969 support (ggiesen SimpleX-T#2, Karloss1234)
  - post-resume 0x969 recovery (Karloss1234)
  - 0404 friendlier error at reset_blob (bcoutts, ntoyiakhona06)
  - del_record db_write_enable fix + flash-full pre-check (Karloss1234)
  - VerifyStart watchdog for wedged-chip state
  - README caveats for 0xd51/0x969

Full context in uunicorn#256.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants