Skip to content

fix(hid): wall-clock timeout and continuation-packet validation#205

Open
AlfioEmanueleFresta wants to merge 2 commits into
masterfrom
fix/hid-receive-hardening
Open

fix(hid): wall-clock timeout and continuation-packet validation#205
AlfioEmanueleFresta wants to merge 2 commits into
masterfrom
fix/hid-receive-hardening

Conversation

@AlfioEmanueleFresta
Copy link
Copy Markdown
Member

@AlfioEmanueleFresta AlfioEmanueleFresta commented May 10, 2026

Two HID protocol-level fixes:

  • Continuation-packet validation. The HID framing parser was accepting continuation packets without checking the CID, the sequence number, or whether the high-bit-set init flag had been re-asserted. A device could splice in foreign-CID continuation frames or repeat init packets and the parser would reassemble a corrupted payload. The parser now enforces the rules from CTAP 2.2 §11.2.4: the first packet must have the init bit set, continuation packets must carry the same CID, SEQ must start at 0 and increment monotonically, and any init-bit packet during continuation is rejected. All-zero packets are also rejected outright (CID 0x00000000 is reserved).

  • Wall-clock timeout. The receive loop used hidapi::read_timeout whose Ok(0) return-on-timeout was silently treated as a successful read of an all-zero report (because the report buffer was zero-initialised and the framing parser discarded all-zero packets). The CTAP2 send/recv paths additionally had no outer wall-clock timeout, so a stalled or unplugged device hung the channel until the caller dropped the future. Each read_timeout is now capped at a 100ms poll interval, the loop tracks the overall deadline and returns TransportError::Timeout once it is exhausted, and CTAPHID_CANCEL is sent on timeout in addition to caller cancellation. A new cbor_send_recv helper wraps every CTAP2 exchange in tokio::time::timeout, symmetric with send_apdu_request_wait_uv in the CTAP1 protocol module.

Notes

Why 100ms for HID_READ_POLL_INTERVAL? This bounds the latency of two things: cancel propagation when the caller's future is dropped, and wake-up after the per-op wall-clock budget expires. It does not affect end-to-end response latency: hidapi::read_timeout returns as soon as the device delivers a report. Per-syscall overhead is negligible and ~10 wakeups/sec per active channel is fine on every platform we care about. 1s would feel sluggish for cancel; 10ms would be wastefully busy. 100ms is a reasonable middle.

About the third commit / cancel-on-drop change. The previous version of this PR also contained a commit that replaced the embedded mpsc cancel channel with an AtomicBool + Notify (CancelState) shared between HidChannel and any HidChannelHandle, fixed a Drop deadlock, and propagated future-drop to in-flight reads via a CancelOnDrop RAII guard. That work is independent of the timeout fixes here (the wall-clock fix works fine on top of the existing mpsc cancel) and has been split into a separate draft PR (#208, branch fix/hid-cancel-on-drop) for cleaner review. It stacks on top of this branch.

Spec refs

Test plan

  • cargo build clean
  • cargo test --lib (140 passed, 2 ignored, including 7 new parser tests)
  • cargo clippy --lib --all-features -- -D warnings clean
  • cargo fmt --check clean
  • Manual smoke test against a real HID authenticator (MakeCredential, GetAssertion, ClientPin)
  • Timeout test: unplug the device mid-transaction; verify TransportError::Timeout after the configured budget and that CTAPHID_CANCEL is logged

@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/hid-receive-hardening branch from 2be1951 to fb6269e Compare May 10, 2026 20:37
@AlfioEmanueleFresta AlfioEmanueleFresta changed the title fix(hid): wall-clock timeout, drop deadlock, continuation packet validation fix(hid): wall-clock timeout and continuation-packet validation May 10, 2026
Per CTAP 2.2 §11.2.4, HidMessageParser must reject any continuation
packet whose CID differs from the initial packet, whose SEQ does not
start at 0 and increment monotonically, or whose 5th byte has the high
bit set (i.e., an init packet for a different transaction).

Also reject all-zero packets at the parser level. The previous code
silently dropped them as a workaround for hidapi `read_timeout`
returning a zeroed buffer on timeout; that workaround now belongs at
the read layer, gated on bytes_read == 0.

Spec refs:
- CTAP 2.2 §11.2.4 USB HID message and packet structure
- FIDO U2F HID Protocol v1.2 (the older base spec)
hidapi `read_timeout` returns `Ok(0)` on timeout (not an error) and the
leftover zero-initialised buffer used to be silently dropped by the
parser, so a stalled or unplugged device could hang `hid_recv_hidapi`
indefinitely. Additionally, the CTAP2 protocol layer did not wrap the
`cbor_send`/`cbor_recv` pair in any wall-clock timeout (unlike CTAP1).

Changes:

- `hid_recv_hidapi` now caps each blocking read to a short poll
  interval, tracks the overall deadline across iterations, treats
  `Ok(0)` byte counts as per-iteration timeouts to retry against the
  remaining budget, and returns `TransportError::Timeout` once the
  budget is exhausted.
- `hid_recv` sends `CTAPHID_CANCEL` on `TransportError::Timeout` in
  addition to `PlatformError::Cancelled`, per CTAP 2.2 §11.2.9.1.5.
- A new `cbor_send_recv` helper wraps the CTAP2 send/recv pair in
  `tokio::time::timeout` and is used by every `ctap2_*` method on the
  `Ctap2` trait, mirroring `send_apdu_request_wait_uv` in the CTAP1
  protocol module.

Spec refs:
- CTAP 2.2 §11.2 USB HID transport
- CTAP 2.2 §11.2.9.1.5 CTAPHID_CANCEL
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/hid-receive-hardening branch from fb6269e to 130d947 Compare May 12, 2026 18:38
@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as ready for review May 12, 2026 18:38
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