Skip to content

[06cb:009a] Sensor fails to initialize on Python 3.14 / libusb 1.0.30: busy 0x0104, empty-flash unpack error, and USB timeout — root cause + patch #272

Description

@tuxarch

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:

  1. Exception: Failed: 0401 from assert_status() on the very first 3e / 01 command.
  2. 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.
  3. 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.pyget_flash_info() (lines ~39-57)
  • validitysensor/init.pyopen_common() (lines ~29-37)
  • validitysensor/usb.pysend_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

  1. Make get_flash_info() tolerant of a short/empty response (return an empty FlashInfo).
  2. 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().
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions