Repository navigation
fix(devourer): prevent deadlock when StopRxLoop is called during Init - #503
Conversation
Fix a deadlock where calling StopRxLoop() during device initialization (inside Init()) was ignored and overwritten. Root cause: StartRxLoop() unconditionally cleared the stop flag (should_stop / _rx_stop) upon entry. If StopRxLoop() was called while Init() was executing (e.g. during IQK calibration or channel setup), StartRxLoop() wiped out the pending stop request and launched bulk_read_async_loop indefinitely, deadlocking the caller waiting on the worker thread. Changes: - Clear stop flag at entry of Init() to mark the start of initialization. - In StartRxLoop(), check whether stop was requested (should_stop / _rx_stop) before resetting the flag. Abort StartRxLoop() immediately if true.
PR Summary by QodoPrevent RX initialization from discarding stop requests
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
josephnef
left a comment
There was a problem hiding this comment.
Reviewed with a hardware reproducer (Init on a thread, StopRxLoop() at N ms, join with a 20 s timeout), both libraries built from clean worktrees — master f46c28a and this PR's head — run as root with no kernel driver bound.
| Build | Adapter | Stop at 100 / 400 / 1000 / 2500 ms | Stop+join, then standalone StartRxLoop |
StopRxLoop before Init |
|---|---|---|---|---|
| master | RTL8822BU (Jaguar2, ch6) | DEADLOCK / DEADLOCK / DEADLOCK / ok | ok | ok |
| master | RTL8812EU (Jaguar3, ch36) | DEADLOCK / DEADLOCK / ok / ok | ok | ok |
| PR | RTL8822BU | ok / ok / ok / ok | ok, frames flow | ok, frames flow |
| PR | RTL8812EU | ok / ok / ok / ok | ok, frames flow | ok, frames flow |
DEADLOCK = the Init thread never returned (frames kept arriving the whole 20 s). "ok" on the PR = Init returned 0.5–1.4 s after the stop, i.e. bring-up completed and StartRxLoop refused to start. The last two columns are the scenarios the Qodo thread raised; both behave correctly. Jaguar1 and Kestrel were not on the bench, so those two are compile-verified only.
The fix is real and the mechanism works. Before merge I'd like these addressed, ranked:
- Coverage.
Rtl8733bDevice::StartRxLoopandMt7612uRadio::StartRxLoophave the same unconditional_rx_stop = falseand bothInits callStartRxLoopthe same way, so the identical deadlock remains on those two backends. The title says the library; it currently fixes four of six. - The
InitWrite+StartRxLoop-on-a-thread shape (DEVOURER_TX_WITH_RX=thread) keeps the same race: a stop issued before that thread reachesStartRxLooptakes the "stale, clear it" branch. Fine to leave out of scope, but say so in theIRadio.hStopRxLoopcomment so callers know which window is covered. - Simpler mechanism (inline comments): a private
start_rx_loop(proc, bool clear_stale)thatInitcalls withfalseand the public override withtrueneeds no atomic, no scope guard, and has no window at all. - The clear in
Initshould be its first statement (inline comment). - Indentation: 4-space blocks in 2-space files, and the Jaguar3
StartRxLoopprologue re-indented, which churns untouched comment lines. - The red
qodo-gateis only because resolving the thread doesn't re-trigger it — a reply inside that thread re-runs the check.
…backends - Replace _init_in_progress atomic and ScopeGuard with a clean private helper start_rx_loop(packetProcessor, clear_stale) across all 6 radio backends - Ensure the stop flag is cleared as the very first statement of Init() - Extend the init-time vs stale stop guard to Rtl8733bDevice and Mt7612uRadio - Add an early g_devourer_should_stop check at the entry of start_rx_loop - Document the InitWrite + StartRxLoop thread race window in IRadio.h
josephnef
left a comment
There was a problem hiding this comment.
Re-review of 6e36580. Every point from the previous round is addressed: the _init_in_progress atomic and the pasted ScopeGuard are gone in favour of the private start_rx_loop(proc, clear_stale), the clear is now the first statement of every Init, the Kestrel early return moved ahead of stop_wp_drain(), the indentation matches, and the RTL8733B and MT7612U backends get the same guard. Two nits inline, neither blocking.
Hardware re-run of yesterday's reproducer at this head (Init on a thread, StopRxLoop at N ms, 20 s join timeout), three backends plugged today, no kernel driver bound:
| backend | stop at 100 / 400 / 1000 / 2500 ms | restart after stop+join | stale stop before Init |
|---|---|---|---|
| 8822BU (Jaguar2, ch6) | OK, Init returns 1420 / 1097 / 473 / 10 ms later | OK, 314 frames | OK, 268 frames |
| 8812EU (Jaguar3, ch36) | OK, 845 / 563 / 100 / 60 ms | OK, 3778 frames | OK, 5244 frames |
| 8832CU (Kestrel, ch36) | OK, 3376 / 3058 / 2385 / 1012 ms | OK, 2497 frames | OK, 1183 frames |
Kestrel was unmeasured in the previous round; its bring-up is the longest (~3.4 s), so every stop up to 2500 ms lands inside it and all four are honoured. Jaguar1, RTL8733B and MT7612U are not on the bench today and are reviewed by reading only.
🤖 Generated with Claude Code
…t probe to tests/ (#505) Follow-ups from #504 (left open by #503). - `src/IRadio.h`: the `StopRxLoop` note stated a race "window" and what was out of scope; it now states the contract. A stop issued while `Init()` is still bringing the chip up ends Init without a loop. A stop issued before `Init()` or before a standalone `StartRxLoop()` is stale and cleared by them, which includes a stop that lands between `InitWrite()` and a `StartRxLoop()` on another thread. - The signal-flag check at the entry of every `start_rx_loop` gets its reason: it skips the bring-up-side setup for a loop whose own predicate would exit on its first pass. - The reproducer that validated #503 moves into the tree: `tests/stop_during_init_probe.cpp` (target `StopDuringInitProbe`, `--no-reset` for Kestrel) and `tests/stop_during_init.sh` (VID/PID/CHANNEL/NO_RESET, one verdict line, exit code), plus a `tests/README.md` entry. Validation through the promoted script at this commit, no kernel driver bound: | backend | Init returns after a stop at 100 / 400 / 1000 / 2500 ms | restart after stop | stale stop before Init | |---|---|---|---| | 8822BU (Jaguar2, ch6) | 1409 / 1097 / 474 / 100 ms | OK, 216 frames | OK, 318 frames | | 8812EU (Jaguar3, ch36) | 866 / 543 / 80 / 40 ms | OK, 4570 frames | OK, 4510 frames | | 8832CU (Kestrel, ch36) | 3346 / 3114 / 2425 / 999 ms | OK, 3373 frames | OK, 1745 frames | The 8812AU, RTL8733B and MT7612U carry the same guard and were not on the bench; that item stays open on #504. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Fix a deadlock where calling StopRxLoop() during device initialization (inside Init()) was ignored and overwritten.
Root cause:
StartRxLoop() unconditionally cleared the stop flag (should_stop / _rx_stop) upon entry. If StopRxLoop() was called while Init() was executing (e.g. during IQK calibration or channel setup), StartRxLoop() wiped out the pending stop request and launched bulk_read_async_loop indefinitely, deadlocking the caller waiting on the worker thread.
Changes: