Skip to content

fix(devourer): prevent deadlock when StopRxLoop is called during Init - #503

Open
floppyhammer wants to merge 2 commits into
OpenIPC:masterfrom
floppyhammer:fix-deadlock
Open

floppyhammer wants to merge 2 commits into
OpenIPC:masterfrom
floppyhammer:fix-deadlock

Conversation

@floppyhammer

Copy link
Copy Markdown
Contributor

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.

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.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Prevent RX initialization from discarding stop requests

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Preserve stop requests made during device initialization across four radio families.
• Skip RX-loop startup when a device or global stop is pending, preventing an indefinite worker
 wait.
Diagram

graph TD
  Init["Init"] --> BringUp["Device bring-up"] --> Start["StartRxLoop"] --> Check{"Stop requested?"}
  Stop["StopRxLoop"] --> Flag["Stop flag"] --> Check
  Init -->|clear| Flag
  Check -->|yes| Abort["Return early"]
  Check -->|no| Read["Async bulk read"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Serialize RX lifecycle transitions
  • ➕ Could define reliable behavior for concurrent Init, StartRxLoop, and StopRxLoop calls, including a stop arriving between the check and flag reset.
  • ➖ Requires broader locking or state-machine changes across four device implementations and their blocking worker loops.

Recommendation: The targeted change fits the reported stop-during-bring-up window and preserves the existing restart behavior. If concurrent StartRxLoop and StopRxLoop calls must also be guaranteed, use a synchronized lifecycle design: the current check followed by a reset does not make that transition atomic.

Files changed (4) +12 / -0

Bug fix (4) +12 / -0
RtlJaguarDevice.cppPreserve Jaguar1 stop requests through initialization +3/-0

Preserve Jaguar1 stop requests through initialization

• Clears should_stop before monitor-mode setup. StartRxLoop returns before RX setup when should_stop or the global stop flag is set.

src/jaguar1/RtlJaguarDevice.cpp

RtlJaguar2Device.cppHonor Jaguar2 stops before RX worker startup +3/-0

Honor Jaguar2 stops before RX worker startup

• Clears _rx_stop before bring-up and checks device and global stop flags before starting RX-related work.

src/jaguar2/RtlJaguar2Device.cpp

RtlJaguar3Device.cppHonor Jaguar3 stops before RX handoff +3/-0

Honor Jaguar3 stops before RX handoff

• Clears _rx_stop before channel setup and calibration. StartRxLoop returns on a pending device or global stop before taking over bulk-IN.

src/jaguar3/RtlJaguar3Device.cpp

RtlKestrelDevice.cppPreserve Kestrel stops during monitor bring-up +3/-0

Preserve Kestrel stops during monitor bring-up

• Clears _rx_stop at Init entry and checks device and global stop flags before starting the RX loop, after stopping the TX WP-release drain.

src/kestrel/RtlKestrelDevice.cpp

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stopped receivers cannot restart ✓ Resolved
Description
In all four backends, StartRxLoop() now returns on the stop flag before reaching the existing code
that clears a request left by StopRxLoop(). After a caller stops and joins a receive loop, another
StartRxLoop() without Init() immediately returns instead of receiving; on Kestrel it first stops
the USB transmit-release drain, leaving neither reader running.
Code

src/kestrel/RtlKestrelDevice.cpp[R444-445]

+  if (_rx_stop || g_devourer_should_stop)
+    return;
Evidence
The interface expressly promises restart after the blocking loop returns. Each backend's StopRxLoop
only sets its flag, while the added guard now tests that flag before the former restart reset.
Kestrel additionally calls stop_wp_drain before the guard, and that drain consumes transmit-release
reports.

src/IRadio.h[59-67]
src/jaguar1/RtlJaguarDevice.h[147-147]
src/jaguar1/RtlJaguarDevice.cpp[1806-1812]
src/jaguar2/RtlJaguar2Device.h[60-60]
src/jaguar2/RtlJaguar2Device.cpp[597-603]
src/jaguar3/RtlJaguar3Device.h[57-57]
src/jaguar3/RtlJaguar3Device.cpp[212-218]
src/kestrel/RtlKestrelDevice.h[66-66]
src/kestrel/RtlKestrelDevice.cpp[123-137]
src/kestrel/RtlKestrelDevice.cpp[440-446]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A stop requested during Init must prevent its receive loop from starting, but a stop left by a completed receive loop must not prevent a later standalone StartRxLoop. The new guards treat both cases alike; Kestrel also stops its transmit-release drain before returning.
## Fix Focus Areas
- src/jaguar1/RtlJaguarDevice.cpp[1806-1812]
- src/jaguar2/RtlJaguar2Device.cpp[597-603]
- src/jaguar3/RtlJaguar3Device.cpp[212-218]
- src/kestrel/RtlKestrelDevice.cpp[440-446]
## Recommended Fix
Track whether a stop request belongs to an initialization currently in progress, using synchronization appropriate for calls from another thread. Abort that initialization's receive start when requested, but clear a completed loop's stale stop request for a standalone restart. In Kestrel, do not relinquish the transmit-release drain when the receive start is aborted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/kestrel/RtlKestrelDevice.cpp Outdated

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Coverage. Rtl8733bDevice::StartRxLoop and Mt7612uRadio::StartRxLoop have the same unconditional _rx_stop = false and both Inits call StartRxLoop the same way, so the identical deadlock remains on those two backends. The title says the library; it currently fixes four of six.
  2. The InitWrite + StartRxLoop-on-a-thread shape (DEVOURER_TX_WITH_RX=thread) keeps the same race: a stop issued before that thread reaches StartRxLoop takes the "stale, clear it" branch. Fine to leave out of scope, but say so in the IRadio.h StopRxLoop comment so callers know which window is covered.
  3. Simpler mechanism (inline comments): a private start_rx_loop(proc, bool clear_stale) that Init calls with false and the public override with true needs no atomic, no scope guard, and has no window at all.
  4. The clear in Init should be its first statement (inline comment).
  5. Indentation: 4-space blocks in 2-space files, and the Jaguar3 StartRxLoop prologue re-indented, which churns untouched comment lines.
  6. The red qodo-gate is only because resolving the thread doesn't re-trigger it — a reply inside that thread re-runs the check.

_init_in_progress.store(false, std::memory_order_release);
};
struct ScopeGuard {
std::function<void()> fn;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a class definition, and it's pasted into three files with a std::function it doesn't need. The whole flag + guard pair goes away if Init calls a private start_rx_loop(std::move(packetProcessor), /*clear_stale=*/false) and the public StartRxLoop override calls it with true: the only thing the atomic encodes is "who called me", which the call site already knows. No shared state, no release/acquire, nothing to roll back on an exception.

_station_ready = false;
_station.retire(_device, _logger, "Jaguar2");
}
_rx_stop = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A StopRxLoop() that lands between Init being entered and this line is dropped. The window is microseconds, but it's avoidable: make the clear the first statement of Init (same on Jaguar1, where it sits after the beacon/sounder rollback registration, and on Jaguar3).

* - If this is a standalone restart after initialization has completed, clear
* the stale stop request.
*/
if (_init_in_progress.load(std::memory_order_acquire)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _init_in_progress name over-promises: it stays true for the whole blocking RX loop that Init runs, not just the bring-up. If the flag stays, call it what it is (_rx_loop_from_init or similar) — or drop it for the clear_stale parameter above.

* - If this is a standalone restart after initialization has completed, clear
* the stale stop request.
*/
if (_init_in_progress.load(std::memory_order_acquire)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4-space indentation in a 2-space file, and the existing "Take over bulk-IN" comment below got re-indented with it. Please match the surrounding style so the diff is only the behaviour change.

* rpkt_type=7 releases) so the two never read the endpoint concurrently. */
stop_wp_drain();
_rx_stop = false;
if (g_devourer_should_stop)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The g_devourer_should_stop early return is new behaviour on every backend and unrelated to the fix: the loop's own stop predicate already checks the same flag on its first pass. Harmless, but drop it (or justify it in the commit) so the change stays about the Init-time stop. Here specifically, stop_wp_drain() has already run by this point, so an early return leaves no bulk-IN reader at all — same as before, but worth not adding a second path to it.

_logger->error("Jaguar1: post-arm Init rollback was not fully verified; "
"see the register-specific error above");
});
should_stop = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as Jaguar2: this clear belongs at the top of Init, before the busy-window reset and the rollback guard, so the covered window starts the moment Init is entered.

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.

2 participants