Skip to content

Run init_fido() at boot to fix USB hang on CTAPHID_INIT - #273

Open
DmitriyLyalyuev wants to merge 1 commit into
polhenarejos:mainfrom
DmitriyLyalyuev:fix/ctaphid-init-usb-hang
Open

DmitriyLyalyuev wants to merge 1 commit into
polhenarejos:mainfrom
DmitriyLyalyuev:fix/ctaphid-init-usb-hang

Conversation

@DmitriyLyalyuev

Copy link
Copy Markdown

Summary

The CTAPHID_INIT handler in pico-keys-sdk calls init_fido(), which on first invocation after power-up runs scan_files_fido() and init_otp(). init_otp() increments the OTP session counter and calls flash_commit(). The resulting deferred flash sector erase blocks USB IRQs for ~50–150 ms via flash_range_erase + save_and_disable_interrupts inside low_flash_task.

Effect: the first CTAPHID_INIT responds slowly (~215 ms instead of the typical 5–10 ms), and every subsequent CTAPHID_INIT in the same session times out — the IN endpoint stalls and macOS refuses further OUT writes until physical re-plug. Result: Chrome, Safari, and Firefox all sit on "waiting for security key" because their initial CTAPHID handshake never completes.

Fix: override the WEAK picokey_init() hook (called from main() before core0_loop starts USB enumeration) and invoke init_fido() there. The static scanned flag in init_otp() plus the file_has_data() checks in scan_files_fido() make the in-handler call cheap, so flash work no longer runs inside the set_report callback.

Reproduction (before)

Hardware: Pico Key Ultimate RP2040 16 MB, current pico-fido v7.6 + pico-keys-sdk 8.6, macOS 26 (Tahoe).

import hid, os, struct
t = next(d for d in hid.enumerate() if d['vendor_id']==0x2E8A and d.get('usage_page')==0xF1D0)
dev = hid.device(); dev.open_path(t['path'])
for i in range(5):
    nonce = os.urandom(8)
    dev.write(bytes([0]) + struct.pack('>IBBB', 0xFFFFFFFF, 0x86, 0, 8) + nonce + b'\x00'*47)
    r = dev.read(64, timeout_ms=2000)
    print('INIT', i+1, 'OK' if r else 'FAIL')

Before:

INIT 1: write=65 read=64 dt=215ms
INIT 2: write=65 NO RESPONSE dt=3013ms
INIT 3: write=-1 (kIOUSB error) ...

After:

INIT 1: OK dt=32ms
INIT 2: OK dt=32ms
INIT 3: OK dt=32ms
INIT 4: OK dt=32ms
INIT 5: OK dt=32ms

After re-flash with this patch, registering and authenticating with the device on webauthn.io and real services (GitHub, Google) works in Chrome, Safari, and Firefox.

Notes

  • Only modifies src/fido/fido.c; the SDK side (pico-keys-sdk) is untouched. The WEAK picokey_init() hook in pico-keys-sdk/src/main.c was already designed for exactly this kind of override.
  • Subsequent init_fido() calls (still triggered from the SDK's CTAPHID_INIT handler on every channel re-init) are idempotent thanks to the static scanned flag and existing file-presence checks, so no behavior change there.
  • A deeper SDK-side improvement would be to drop the init_fido() call from the CTAPHID_INIT handler entirely, but that is intentionally left out of this PR to keep the change minimal.

Test plan

  • Five back-to-back CTAPHID_INIT calls via python-hidapi: stable 32 ms latency, no stalls.
  • Browser WebAuthn (register + authenticate) on macOS 26 in Chrome, Safari, Firefox.
  • Other platforms (Linux, Windows) — not regressed but not specifically re-verified.

The CTAPHID_INIT handler in pico-keys-sdk calls init_fido(), which on
the first invocation after power-up runs scan_files_fido() (potentially
generating ECDSA keys and X.509 cert on factory state) and init_otp()
(increments OTP session counter and calls flash_commit). The deferred
flash sector erase that follows blocks USB IRQs for ~50-150 ms via
flash_range_erase + save_and_disable_interrupts in low_flash_task.

Symptom on macOS Tahoe (and reproducible with raw hidapi):

  1. Device enumerates correctly: PrimaryUsagePage=0xF1D0, two HID
     interfaces (FIDO + Keyboard).
  2. First CTAPHID_INIT responds, but slowly (~215 ms instead of
     typical 5-10 ms).
  3. Every subsequent CTAPHID_INIT in the same session times out with
     no IN report; macOS then refuses further OUT writes
     (kIOUSB-style errors) until physical re-plug.
  4. Chrome, Safari and Firefox all show "waiting for security key"
     because their initial CTAPHID handshake fails.

Fix: override the WEAK picokey_init() hook (called from main() before
core0_loop starts USB enumeration) and invoke init_fido() there. The
static `scanned` flag in init_otp() and the file_has_data() checks in
scan_files_fido() make subsequent calls cheap, so the first
CTAPHID_INIT no longer triggers flash work inside the set_report
callback. Verified with five back-to-back CTAPHID_INIT calls via
python-hidapi: stable 32 ms latency, no stalls.
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.

1 participant