Summary
On a Lenovo ThinkPad T480s with the Synaptics Validity "Metallica" fingerprint reader (USB 06cb:009a), python-validity 0.15 fails to bring the sensor up on a modern stack (Python 3.14, libusb 1.0.30, pyusb 1.3.1). The daemon (python3-validity.service) crashes during open_common() with one of three symptoms depending on the exact boot state of the sensor:
Exception: Failed: 0401 from assert_status() on the very first 3e / 01 command.
struct.error: unpack requires a buffer of 14 bytes inside get_flash_info() because the sensor replies with only the 2-byte status word 0000 (no header/partition data) when the flash is uninitialized.
usb.core.USBTimeoutError: [Errno 110] Operation timed out while formatting the flash / loading fwext.
The reader is confirmed working once the three problems below are fixed. This report includes a minimal, self-contained patch.
Environment
- Device: Lenovo ThinkPad T480s (machine types 20L7 / 20L8)
- Sensor:
Bus 001 Device 008: ID 06cb:009a Synaptics, Inc. Metallica MIS Touch Fingerprint Reader (serial 1972b886d473)
- OS: Arch Linux, kernel
7.1.3-arch1-1 (x86_64)
python-validity 0.15-1
python 3.14.6
python-pyusb 1.3.1-2
libusb 1.0.30-1
python-cryptography 49.0.0-1
- Co-installed (AUR
python-validity deps): open-fprintd 0.7-2, fprintd-clients (git)
Root cause
The sensor, after the USB device is opened, replies with a transient busy status 0x0104 to the first command(s) (01 = RomInfo.get, 3e = get_flash_info) and only returns valid data on the second attempt. Additionally, when the flash is not yet formatted, the sensor answers 3e with just the status word 0000 and no 14-byte header + partition table — but get_flash_info() unconditionally does hdr = rsp[:0xe]; unpack('<HHHHHHH', hdr), which crashes on the short reply.
Finally, open_common() calls init_flash() (which formats the flash and itself opens a TLS session) before usb.send_init() (which loads fwext). On this hardware the ordering makes the protected-flash commands time out.
Relevant code paths (python-validity 0.15):
validitysensor/flash.py — get_flash_info() (lines ~39-57)
validitysensor/init.py — open_common() (lines ~29-37)
validitysensor/usb.py — send_init() (lines ~79-96)
Reproduction
sudo systemctl stop python3-validity
sudo validity-sensors-firmware # needed once: downloads 6_07f_lenovo_mis_qm.xpfwext
sudo systemctl start python3-validity
# journalctl -u python3-validity shows one of the three tracebacks above
Raw probe confirming the busy + short-flash behaviour:
# first command after open -> busy
cmd 01 -> 0104
# second command -> valid RomInfo
cmd 01 -> 0000f0b05e54a4000000060701300001000091278b684d370023000000000100000003d10007
# get_flash_info on uninitialized flash -> only status, no header
RAW rsp to 3e: 0000 (len 2)
Important: the packaged python3-validity-suspend-hotfix.service restarts the daemon after suspend/resume — and in practice the sensor only initializes cleanly after a suspend/resume cycle or a factory-reset.py run. The fixes below remove that dependency.
Fix (minimal patch)
1. validitysensor/flash.py — handle the empty/uninitialized flash reply
def get_flash_info():
rsp = tls.cmd(unhex('3e'))
assert_status(rsp)
rsp = rsp[2:]
# Some sensors (e.g. 06cb:009a with an uninitialized/empty flash) reply
# with only the status word (0000) and no header/partition data.
# Treat a short response as an uninitialized flash so init_flash() can
# format it instead of crashing on unpack().
if len(rsp) < 0xe:
import logging as _l
_l.info('Flash info response too short (%d bytes); assuming uninitialized flash' % len(rsp))
return FlashInfo(None, 0, 0, 0, 0, [])
hdr = rsp[:0xe]
rsp = rsp[0xe:]
jid0, jid1, blocks, unknown0, blocksize, unknown1, pcnt = unpack('<HHHHHHH', hdr)
...
2. validitysensor/init.py — run send_init() before init_flash()
def open_common():
init_data_dir()
usb.send_init() # load fwext FIRST
init_flash() # then format/initialize flash
tls.parse_tls_flash(read_tls_flash())
tls.open()
upload_fwext()
sensor.open()
init_db()
...
3. validitysensor/usb.py — retry the transient 0x0104 (busy) status in send_init()
def send_init(self):
# self.dev.set_configuration()
# TODO analyse responses, detect hardware type
# The sensor may reply with a transient "busy" (0x0104) status to the
# first command(s) after it wakes up. Retry a few times to let it settle.
import time as _t
for _ in range(20):
rsp_init = self.cmd(unhexlify('01')) # RomInfo.get()
s_stat, = unpack('<H', rsp_init[:2])
if s_stat == 0x0104:
_t.sleep(0.5)
continue
break
assert_status(rsp_init)
assert_status(self.cmd(unhexlify('19')))
# 43 -- get partition header(?) (02 -- fwext partition)
for _ in range(20):
rsp = self.cmd(unhexlify('4302')) # get_fw_info()
s_stat, = unpack('<H', rsp[:2])
if s_stat == 0x0104:
_t.sleep(0.5)
continue
break
assert_status(self.cmd(init_hardcoded))
...
Verification after the patch
sudo systemctl stop python3-validity
sudo python3 /usr/share/python-validity/playground/factory-reset.py # optional, clears paired state
sudo systemctl start python3-validity
sudo python3 -c "import dbus; b=dbus.SystemBus(); m=b.get_object('net.reactivated.Fprint','/net/reactivated/Fprint/Manager'); print(m.GetDevices())"
# -> [dbus.ObjectPath('/net/reactivated/Fprint/Device/0')]
fprintd-enroll -f right-index-finger $USER
# Enroll result: enroll-completed
# PAM (sudo + login + graphical DM via system-auth):
# auth sufficient pam_fprintd.so
# auth [success=1 default=bad] pam_unix.so try_first_pass nullok
The sensor now enumerates, enrolls, and verifies; fingerprint auth works for sudo, TTY login, and the display manager without requiring a suspend/resume first.
Suggested upstream changes
- Make
get_flash_info() tolerant of a short/empty response (return an empty FlashInfo).
- Reorder
open_common() so firmware (send_init/upload_fwext) is loaded before flash formatting, OR have init_flash() open the TLS session explicitly before partition_flash().
- Add a small retry loop for the
0x0104 busy status in send_init() (and possibly get_flash_info()), since it is a normal transient state on wake-up for 06cb:009a.
Related: #3 (general 06cb:009a thread), #233 (factory reset failed), #271 (GetFlashInfo 057e on Ubuntu 26.04 — possibly the same family with a different status code).
Summary
On a Lenovo ThinkPad T480s with the Synaptics Validity "Metallica" fingerprint reader (USB
06cb:009a),python-validity0.15 fails to bring the sensor up on a modern stack (Python 3.14, libusb 1.0.30, pyusb 1.3.1). The daemon (python3-validity.service) crashes duringopen_common()with one of three symptoms depending on the exact boot state of the sensor:Exception: Failed: 0401fromassert_status()on the very first3e/01command.struct.error: unpack requires a buffer of 14 bytesinsideget_flash_info()because the sensor replies with only the 2-byte status word0000(no header/partition data) when the flash is uninitialized.usb.core.USBTimeoutError: [Errno 110] Operation timed outwhile formatting the flash / loadingfwext.The reader is confirmed working once the three problems below are fixed. This report includes a minimal, self-contained patch.
Environment
Bus 001 Device 008: ID 06cb:009a Synaptics, Inc. Metallica MIS Touch Fingerprint Reader(serial1972b886d473)7.1.3-arch1-1(x86_64)python-validity0.15-1python3.14.6python-pyusb1.3.1-2libusb1.0.30-1python-cryptography49.0.0-1python-validitydeps):open-fprintd0.7-2,fprintd-clients(git)Root cause
The sensor, after the USB device is opened, replies with a transient busy status
0x0104to the first command(s) (01= RomInfo.get,3e= get_flash_info) and only returns valid data on the second attempt. Additionally, when the flash is not yet formatted, the sensor answers3ewith just the status word0000and no 14-byte header + partition table — butget_flash_info()unconditionally doeshdr = rsp[:0xe]; unpack('<HHHHHHH', hdr), which crashes on the short reply.Finally,
open_common()callsinit_flash()(which formats the flash and itself opens a TLS session) beforeusb.send_init()(which loadsfwext). On this hardware the ordering makes the protected-flash commands time out.Relevant code paths (python-validity 0.15):
validitysensor/flash.py—get_flash_info()(lines ~39-57)validitysensor/init.py—open_common()(lines ~29-37)validitysensor/usb.py—send_init()(lines ~79-96)Reproduction
Raw probe confirming the busy + short-flash behaviour:
Important: the packaged
python3-validity-suspend-hotfix.servicerestarts the daemon after suspend/resume — and in practice the sensor only initializes cleanly after a suspend/resume cycle or afactory-reset.pyrun. The fixes below remove that dependency.Fix (minimal patch)
1.
validitysensor/flash.py— handle the empty/uninitialized flash reply2.
validitysensor/init.py— runsend_init()beforeinit_flash()3.
validitysensor/usb.py— retry the transient0x0104(busy) status insend_init()Verification after the patch
The sensor now enumerates, enrolls, and verifies; fingerprint auth works for
sudo, TTYlogin, and the display manager without requiring a suspend/resume first.Suggested upstream changes
get_flash_info()tolerant of a short/empty response (return an emptyFlashInfo).open_common()so firmware (send_init/upload_fwext) is loaded before flash formatting, OR haveinit_flash()open the TLS session explicitly beforepartition_flash().0x0104busy status insend_init()(and possiblyget_flash_info()), since it is a normal transient state on wake-up for06cb:009a.Related: #3 (general 06cb:009a thread), #233 (factory reset failed), #271 (GetFlashInfo 057e on Ubuntu 26.04 — possibly the same family with a different status code).