fix(hid): cancel propagation via Notify and CancelOnDrop#208
Draft
AlfioEmanueleFresta wants to merge 3 commits into
Draft
fix(hid): cancel propagation via Notify and CancelOnDrop#208AlfioEmanueleFresta wants to merge 3 commits into
AlfioEmanueleFresta wants to merge 3 commits into
Conversation
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
…reads
The previous Drop implementation called
`futures::executor::block_on(self.hid_cancel())`, which routes through
`hid_send` and tries to lock the same device mutex that the in-flight
`hid_recv` is holding in `spawn_blocking`. That deadlocks; on
multi-threaded tokio runtimes the embedded `block_on` would also
panic. Future cancellation (caller's task being dropped) was likewise
not wired to abort the read.
Changes:
- The cancel-rx mpsc channel is replaced by a lock-free
`CancelState { AtomicBool, tokio::sync::Notify }` shared between the
channel and any `HidChannelHandle`. The atomic flag is checked by
the blocking reader between hidapi poll iterations; the notify
wakes the async caller without waiting out the poll interval. The
device mutex now contains only the hidapi handle.
- `hid_recv` now races the spawn_blocking read against
`cancel.notify.notified()` in a `tokio::select!` so signals
propagate immediately. A `CancelOnDrop` RAII guard fires if the
`hid_recv` future is dropped before completing, so dropping the
caller's task aborts the in-flight read within one poll interval.
- `Drop` no longer takes any lock or `block_on`s. It signals the
cancel flag (lock-free, never deadlocks) and best-effort emits
CTAPHID_CANCEL via `try_lock` only.
6 tasks
fb6269e to
130d947
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on top of #205. Replaces the per-channel mpsc cancel channel with a lock-free
CancelState { AtomicBool, tokio::sync::Notify }shared betweenHidChanneland anyHidChannelHandle, fixes aDrop-time deadlock, and wires future-drop to abort in-flight blocking reads.Why
The previous
Drop for HidChannelcalledfutures::executor::block_on(self.hid_cancel()).hid_cancelroutes throughhid_send, which tries to lock the same device mutex an in-flighthid_recvmay be holding viaspawn_blocking. That deadlocks; on multi-threaded tokio runtimes the embeddedblock_onwould also panic. Caller-future cancellation was likewise not wired to abort the blocking read — dropping the receive future left the spawned task running until the wall-clock budget eventually expired.Changes
CancelState. Lock-freeAtomicBoolplustokio::sync::Notify, shared viaArcbetweenHidChannelandHidChannelHandle. The atomic flag is checked by the blocking reader between hidapi poll iterations (bounded byHID_READ_POLL_INTERVAL); the notify wakes the async caller without waiting out the poll interval. The device mutex now contains only the hidapi handle.hid_recvraces read against cancel.tokio::select!betweenspawn_blocking(hid_recv_hidapi)andcancel.notify.notified().spawn_blockingcannot be aborted, but storing the flag on a notified branch lets the reader self-terminate on its next poll.CancelOnDropRAII guard. Set up at the start ofhid_recv, disarmed on the normal-return path. If the surrounding future is dropped before completing, the guard signals the cancel flag so the spawned blocking task stops within one poll interval.Drop for HidChannelno longer blocks. It signals the cancel flag (lock-free, never deadlocks) and then best-effort emitsCTAPHID_CANCELviatry_lock. If the device mutex is contended (an active reader has it), the cancel packet is skipped — the reader is about to release the mutex and the device's own transaction timeout will reclaim the channel.Spec refs
Test plan
cargo buildcleancargo test --lib(140 passed, 2 ignored)cargo clippy --lib --all-features -- -D warningscleancargo fmt --checkcleanHidChannelwhile a blocking read is in flight; verify no deadlock andCTAPHID_CANCELis emitted (or skipped if contended) without panicking