Quiesce TX before libusb teardown (#281); fix Kestrel CCK + wide-bandwidth RX (#343) - #344
Merged
Merged
Conversation
txdemo segfaults inside libusb on teardown when it is killed during max-duty TX on a Jaguar1 adapter, and exits cleanly at DEVOURER_TX_GAP_US 2000. Two independent defects hid behind that one signature. The crash is a teardown-order bug. ~UsbTransport pumped libusb_handle_events whenever transfers were still in flight, but the demos destroyed their IRtlDevice at end of main — after libusb_release_interface, libusb_close and libusb_exit — so the pump ran on a context libusb had already freed. It is only reachable with URBs outstanding at exit, which is exactly gap 0; at gap 2000 the queue drains between frames and nothing is left to reap. EepromManager appears in the backtrace only because it holds an RtlAdapter by value and is declared first, so its reference to the transport is released last. The second defect: RtlJaguarDevice::send_packet and send_packets built the URB in a local std::vector and handed .data() to a fire-and-forget libusb_submit_transfer. libusb requires that buffer to live until the transfer completes. Linux usbfs happens to copy the payload at submit, so this is invisible there, but it is a real bug on the backends that keep the caller's pointer. Both are fixed at the transport seam: - IRtlTransport::quiesce_tx() cancels every outstanding transfer, reaps the completions and refuses further sends, bounded by a deadline that reports tx.quiesce_timeout rather than hanging teardown. It runs from IRtlDevice::Stop() and from every device destructor, i.e. while all owners are still alive. Jaguar1 gains a Stop() override for it — TX quiesce only, deliberately not a card-disable power sequence, which this family has never run. - tx_async copies the caller's bytes into a transport-owned buffer freed in the completion callback. The free is symmetric with the allocation rather than LIBUSB_TRANSFER_FREE_BUFFER, which would cross CRT heaps on Windows. - examples/common/DeviceSession.h owns device, interface, handle and context, and unwinds them in that order. Every demo adopts it, which also fixes the resources their early returns used to leak. Jaguar1 over USB is the only asynchronous submitter in the tree, so the other generations are unaffected — their quiesce finds nothing in flight. Validated on an RTL8821AU under ASan: 4 of 6 cells crash before, 9 of 9 clean after, including SIGINT, the aggregation URB and a real VBUS cut mid-flood (NO_DEVICE completions, prompt quiesce, adapter re-enumerates). On-air throughput is unchanged, 49.9/39.2 vs 49.8/39.2 Mbps at 2.4/5 GHz, and regress.py is green on ch6 and ch36. DEVOURER_SANITIZE builds the tree instrumented, with the vendored halbb/halrf exempted from UBSan and MSVC restricted to its ASan; a CI job runs ctest under ASan+UBSan. The hardware harnesses are tests/tx_teardown_asan.sh and tests/teardown_gen_sanity.sh — the bugs this class produces need a real device, so they cannot live in ctest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Kestrel receiver was dead in two whole regimes: no CCK at all on 2.4 GHz, and nothing at any bandwidth above 20 MHz. 20 MHz OFDM was unaffected on both bands, and TX was unaffected at every width — which is why it went unnoticed. The 40/80 MHz validation rested on SDR transmit duty, and a duty measurement cannot see a broken receiver. HalKestrel::set_channel passed halbb_ctrl_bw_ch a primary sub-band index where the vendor wants the primary channel number. Vendor phl hands it rtw_chan_def::chan unchanged, and inside, pri_ch is used three ways, all of them receive-side: it indexes the 2.4 GHz CCK SCO threshold tables at pri_ch - 1, it is compared against central_ch to derive the 0x4978[11:8] sub-band index, and it places the NBI spur notch. An index therefore selects the wrong CCK thresholds and yields a nonsense sub-band — pri_ch 1 against central_ch 42 gives 21, into a field whose range is 0..8. At 20 MHz the index is 0, so the CCK path reads the threshold table at -1. That out-of-bounds read is what surfaced this: under DEVOURER_SANITIZE=address every 2.4 GHz bring-up reports a stack-buffer-underflow in halbb_ctrl_sco_cck_8852c. Measured on an RTL8832CU against an RTL8821AU emitter, hits over 12 s, each figure reproduced twice. CCK 1M on ch6 goes 0 -> 57; receiving at 40 MHz 0 -> 57; at 80 MHz 0 -> 57. The two 20 MHz OFDM cells are the controls and do not move (56 and 57 either way), and the transmit direction is 38 hits at 20, 40 and 80 MHz before and after. The 8852B backend has the identical function, so that die is affected the same way; it is untested here for want of a plugged one. tests/kestrel_prich_onair.sh is the harness. Its REF_ONLY mode takes the Kestrel out of the path entirely and is worth running first whenever a wide-bandwidth cell reads zero: the emitter's own channel width comes from DEVOURER_HOP_BW, not from the /40 in DEVOURER_TX_RATE, which only fills the descriptor field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Two bugs that a sanitizer build found in the same session. They are independent
— separate commits — but they share the reason nobody had seen them: neither is
reachable from
ctest, and one of them is invisible to the measurement wenormally trust.
1. TX teardown — fixes #281
txdemosegfaults inside libusb on teardown when killed during max-duty TX ona Jaguar1 adapter, and exits cleanly at
DEVOURER_TX_GAP_US=2000. Two defectshid behind that one signature.
The crash is a teardown-order bug.
~UsbTransportpumpedlibusb_handle_eventswhenever transfers were in flight, but the demosdestroyed their
IRtlDeviceat end ofmain— afterlibusb_release_interface/
close/exit— so the pump ran on a context libusb had already freed. Onlyreachable with URBs outstanding at exit, i.e. exactly gap 0.
EepromManagerappears in the backtrace only because it holds an
RtlAdapterby value and isdeclared first, so it drops the last transport reference; reordering members
would move the frame, not fix anything.
The second defect:
RtlJaguarDevice::send_packet/send_packetsbuilt theURB in a local
std::vectorand handed.data()to a fire-and-forgetlibusb_submit_transfer. libusb requires that buffer to outlive the transfer.Linux usbfs copies the payload at submit, so it is invisible there — but it is a
real bug on the backends that keep the caller's pointer.
Fixed at the transport seam:
IRtlTransport::quiesce_tx()— cancels every outstanding transfer, reapsthe completions, refuses further sends; bounded by a deadline that reports
tx.quiesce_timeoutrather than hanging teardown. Called fromIRtlDevice::Stop()and from every device destructor, i.e. while all ownersare alive. Jaguar1 gains a
Stop()override for it — TX quiesce only,deliberately not a card-disable power sequence, which this family has
never run.
tx_asynccopies the payload into a transport-owned buffer freed in thecallback. Symmetric alloc/free rather than
LIBUSB_TRANSFER_FREE_BUFFER,which would cross CRT heaps on Windows.
examples/common/DeviceSession.howns device → interface → handle →context and unwinds in that order; every demo adopts it, which also fixes the
resources their early returns leaked.
Jaguar1-over-USB is the only asynchronous submitter in the tree, so the other
generations are unaffected — their quiesce finds nothing in flight.
Validation (RTL8821AU, ASan build): 4/6 cells crash before, 9/9 clean
after — including SIGINT, the aggregation URB, and a real VBUS cut mid-flood
(NO_DEVICE completions, prompt quiesce, adapter re-enumerates). On-air
throughput unchanged (49.9/39.2 → 49.8/39.2 Mbps at 2.4/5 GHz);
regress.pygreen on ch6 and ch36; all generations clean under ASan.
2. Kestrel RX — fixes #343
The Kestrel receiver was dead in two whole regimes: no CCK at all on 2.4 GHz,
and nothing above 20 MHz. 20 MHz OFDM was fine on both bands and TX was fine
at every width, which is why it survived — the 40/80 MHz validation rested on
SDR transmit duty, and a duty measurement cannot see a broken receiver.
HalKestrel::set_channelpassedhalbb_ctrl_bw_cha primary sub-band indexwhere the vendor wants the primary channel number (phl hands it
rtw_chan_def::chanunchanged). Inside,pri_chis used three ways, allreceive-side: it indexes the 2.4 GHz CCK SCO threshold tables at
pri_ch - 1,it is compared against
central_chto derive the0x4978[11:8]sub-band index,and it places the NBI spur notch. At 20 MHz the index is 0, so the CCK path
reads that table at
-1— the stack-buffer-underflow that surfaced all of this.RTL8832CU against an RTL8821AU emitter, hits over 12 s, each figure reproduced
twice. The 8852B backend has the identical function, so that die is affected the
same way — untested here for want of a plugged one.
Tooling
DEVOURER_SANITIZE=address|address+undefined|threadbuilds the library, demosand selftests instrumented (vendored halbb/halrf exempted from UBSan; MSVC
restricted to its ASan and made to error rather than silently degrade), plus a
build-sanitizersCI job runningctestunder ASan+UBSan.The bugs this class produces need a real device, so the hardware harnesses carry
the weight:
tests/tx_teardown_asan.sh,tests/teardown_gen_sanity.sh,tests/kestrel_prich_onair.sh. All three takeBUILD=<dir>, which is how eachbefore/after table above was produced. One new headless test,
tx_quiesce_seam, guards the wiring;ctestis 47/47 under ASan+UBSan.🤖 Generated with Claude Code