Fix Resampler heap corruption when input block exceeds r8b MaxInLen#2114
Fix Resampler heap corruption when input block exceeds r8b MaxInLen#2114ten9876 merged 1 commit intoten9876:mainfrom
Conversation
There was a problem hiding this comment.
Nice fix, @NF0T. The root cause analysis and reproduction method in the PR description are excellent.
Code review — looks good. A few observations:
-
Correctness: The chunking logic is sound across all four methods. Pointer arithmetic correctly accounts for interleaved stereo (
offset * 2) inprocessStereoToMonoandprocessStereoToStereo, and uses plainoffsetfor the mono paths.std::minhandles the final partial chunk correctly. -
Recursion terminates: Each recursive call passes a chunk size
<= m_maxBlockSamples, so the guard can never trigger again — no risk of unbounded recursion. -
Member init order:
m_maxBlockSamplesis declared betweenm_dstRateandm_resamplerin the header, and the constructor init list matches that order. Good — no-Wreorderwarning. -
Scope: Changes are confined to
Resampler.handResampler.cpp, exactly matching the stated scope. No unrelated changes. -
r8b filter state continuity: As noted in the PR description,
CDSPResampler24::process()maintains internal filter state across calls, so chunking doesn't introduce discontinuities. This is the key property that makes the fix transparent to audio quality.
One minor note (not blocking): The repeated QByteArray::append() in the chunking loop will trigger a reallocation on each iteration. For the typical case of 2–3 chunks this is negligible, but if you wanted to tighten it up, you could result.reserve(estimatedOutputSize) before the loop. Not necessary for this use case — just mentioning it.
Thanks for tracking this down and for the thorough testing with the SIGSTOP/SIGCONT approach. 73.
r8b::CDSPResampler24 pre-allocates internal filter buffers sized to aMaxInLen at construction time and documents that process() must not be called with more samples than that limit. No bounds check is performed — exceeding it silently overflows the internal TmpBufs, corrupting adjacent heap memory. The default maxBlockSamples is 4096, which equals ~170 ms of audio at 24 kHz. AudioEngine::onTxAudioReady() drains the mic source with readAll(), so any event-loop hiccup longer than 170 ms (e.g. a burst of POTA spot messages being parsed on the main thread) produces a super-block that exceeds the limit. The resulting heap corruption is detected on the next malloc call inside processStereoToMono(), producing malloc(): invalid size (unsorted) and SIGABRT. Fix: add an overflow guard at the top of all four process* methods. When the input exceeds m_maxBlockSamples the method chunks itself recursively, each chunk ≤ maxBlockSamples. r8b is stateful between calls on the same instance, so filter continuity is preserved across chunk boundaries at no extra cost. Root-caused from three SIGABRT core dumps (07:59, 08:46, 19:23 CDT 2026-04-26), all crashing in Resampler::processStereoToMono via RADEEngine::feedTxAudio. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
0bbecff to
f926cb1
Compare
Community-driven release. WAVE Phase 2 visualization (#2124), DAX-aware TCI multi-stream routing for FlexRadio firmware 4.2.18 (#2140), TCXO frequency-offset calibration (#2119), VFO marker tri-state UX (#2141), v4.2.18 discovery beacon parsing (#2138). Bug fixes from the community: r8b heap corruption (#2114, NF0T), serial PTT triple-fix (#2125, chibondking), slice-audio mute on band change (#2128, jensenpat), CWX Live toggle (#2122, jensenpat), connect-radio dialog polish (#2121, jensenpat). Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Problem
r8b::CDSPResampler24pre-allocates its internal filter buffers (TmpBufs) toaMaxInLenat construction time. The library does not bounds-check thelparameter on subsequentprocess()calls — ifl > aMaxInLen, it silentlyoverflows those pre-allocated buffers and corrupts adjacent heap memory.
Resamplerwas constructed with the defaultmaxBlockSamples = 4096, whichequals approximately 170 ms of audio at 24 kHz (the RADE TX sample rate). The
overflow guard was never stored, so there was no way to enforce it at call sites.
How the overflow was triggered in practice
The TX audio path reads mic data via
QAudioSource::readAll()insideonTxAudioReady().readAll()returns all data accumulated in the OS audiobuffer since the last call — not just one fixed-size block.
When the Qt event loop is stalled for longer than ~170 ms (for example, while
processing a burst of DX cluster or POTA spot messages, or during heavy UI
redraws), mic data accumulates in the PulseAudio/PipeWire server-side buffer.
On resume, the single
readAll()call returns a block that can be severalhundred milliseconds deep — 2–3× the r8b
MaxInLenlimit.When RADE mode is active on the TX slice,
onTxAudioReady()runs continuouslyregardless of MOX state (by design, for zero-latency key-up). This kept the TX
resampler active and accumulating data even while not transmitting, making it
reachable on any event loop stall.
Observed crash signature
This is the glibc allocator detecting corrupted free-chunk metadata in the
unsorted bin. The corruption itself happened inside r8b's
TmpBufsoverflow;the detection was triggered by the next
QByteArrayallocation inprocessStereoToMono(). Three instances were recorded on 2026-04-26 at07:59, 08:46, and 19:23 CDT on a FLEX-8400 running firmware v4.1.5.39794.
Fix
Store
maxBlockSamplesasm_maxBlockSamplesinResamplerso it isavailable at call time. Add an overflow guard at the top of all four
process*()methods:Each method recurses into itself with chunks ≤
m_maxBlockSamples. Becauser8b::CDSPResampler24maintains its filter state across calls on the sameinstance, chunking is transparent to output quality — the resampled stream is
continuous across chunk boundaries.
Stereo methods use
offset * 2for pointer arithmetic to account forinterleaved L/R samples.
Changes
src/core/Resampler.h— addint m_maxBlockSamplesto private sectionsrc/core/Resampler.cpp— storemaxBlockSamplesin constructor init list;add overflow guard to
process(),processStereoToMono(),processMonoToStereo(), andprocessStereoToStereo()Testing
Tested on Linux (Arch) with a FLEX-8400, firmware v4.1.5.39794, RADE mode
active on the TX slice (no transmission — MOX was not keyed).
To reliably reproduce a block larger than the 170 ms / 4096-frame threshold,
the process was frozen with
SIGSTOPfor 500 ms then resumed withSIGCONT:This forces PulseAudio/PipeWire to accumulate ~500 ms of mic data in the
server-side buffer. On resume,
readAll()returns a single ~12 000-frameblock — approximately 3× the old
MaxInLenlimit. Under the same conditionsthat produced three hard crashes on the unpatched binary, the patched binary
resumed cleanly with no crash and no audio artifacts. The test was repeated
multiple times consecutively with consistent results.