fix(gui): guard slice DAX-recall singleShot against dangling slice (UAF crash)#3733
Conversation
…AF crash) onSliceAdded() schedules a 300 ms QTimer::singleShot to restore a slice's saved DAX channel (aethersdr#1221). The lambda captured the SliceModel by raw pointer and guarded with `if (s && ...)` — but a raw pointer does not become null when the QObject is destroyed, so the guard was ineffective. The timer's context object is MainWindow (long-lived), so the timer is not auto-cancelled when the slice goes away. Removing a slice within 300 ms of adding it (rapid add/remove, e.g. profile churn or fast lifecycle automation) therefore fired the timer on a freed SliceModel: setDaxChannel() -> sendCommand() -> emit commandReady() segfaulted in doActivate() on the dangling sender. Confirmed live via the automation bridge — a reopen at T=0 removed at T+172ms crashed when the timer fired at ~T+300ms (EXC_BAD_ACCESS). Capture a QPointer<SliceModel> instead, matching the existing dangling-widget guards in this TU (aethersdr#381). Regression-tested: 6x add/remove at 120 ms dwell plus a full open/tune/handoff/close/reopen lifecycle now run with no crash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed against main. The diagnosis and fix are both correct.
Verified:
- The pre-change code matches the described bug exactly (
MainWindow_Wiring.cpp:261-265): thesingleShotcapturessas a rawSliceModel*, the timer's context object is the long-livedMainWindow, and theif (s && …)guard cannot detect a freed pointee. Removing the slice inside the 300 ms window does firesetDaxChannel()on dangling memory. sis aSliceModel*QObject, soQPointer<SliceModel>is valid; capturing theQPointerby value and re-checkingif (sp && …)makes the existing guard actually do its job. Behavior is unchanged when the slice survives.- The
QPointer-guard-against-dangling-object pattern is already established in this same TU (SpectrumWidgetat lines 1000/1605,QTimerat 1679), so this is consistent with house style. - The
profileLoadRadioStateWritesHeld()skip and surrounding logic are preserved.
Scope is tight (+9/−3, one file), no convention concerns, and the nearby singleShot at line 2444 captures only a panId value so it's unaffected.
Nice catch — this is exactly the right minimal fix, and the agent-driven sub-300 ms reproduction is a good example of surfacing a timing-dependent UAF that manual testing rarely hits. Thanks @jensenpat. (Non-blocking: the same DaxChannel_Slice%1 deferred-restore shape is the kind of thing worth a grep for other long-lived-context singleShots capturing slice/model raw pointers, but nothing else jumped out in this file.)
🤖 aethersdr-agent · cost: $2.7748 · model: claude-opus-4-8
|
@jensenpat this also fixes #3725. I opened it yesterday when I encountered the bug under similar circumstances, but hadn't yet circled back to fix it. |
Thanks Paul for the eyes on this, I'll add to the PR and it should auto close. |
ten9876
left a comment
There was a problem hiding this comment.
Approving — textbook UAF fix, verified. The raw SliceModel* capture in the 300 ms DAX-recall singleShot (long-lived MainWindow context, so not auto-cancelled) is correctly switched to QPointer<SliceModel> so the existing if (sp && …) guard actually nulls on destruction. Confirmed: SliceModel is a QObject (QPointer valid); the pattern is already house style in this TU (lines 1012/1621/1695); behavior unchanged when the slice survives. Real crash on ordinary paths (rapid add/remove, profile recall, band-stack restore), reproduced + fixed on hardware, CI green. Also fixes #3725 (per @K5PTB). Thanks @jensenpat.
What
Fixes a use-after-free crash (
EXC_BAD_ACCESS/ SIGSEGV) when a slice isremoved within ~300 ms of being added. Closes #3732.
MainWindow::onSliceAdded()arms a 300 msQTimer::singleShotto restore aslice's saved DAX channel (#1221), capturing the slice by raw pointer:
The
if (s && …)guard is ineffective — a raw pointer does not go null whenthe pointee is destroyed — and the timer's context object is the long-lived
MainWindow, so it is not auto-cancelled when the slice goes away. Removingthe slice inside the 300 ms window fires the timer on freed memory:
Why it matters
This is reachable on ordinary paths, not just automation: rapid slice
add/remove, profile recall that recreates slices, and band-stack restore can
all destroy a slice within 300 ms of creating it. The timer arms whenever the
slice index maps to a saved
DaxChannel_Slice{A,B,…} > 0, so users who haveever assigned a DAX channel are the most exposed.
The fix
Capture a
QPointer<SliceModel>so the existing guard actually nulls out ondestruction — the same dangling-object pattern already used elsewhere in this
TU (#381). One file, +9/-3.
Testing
Reproduced live on a FLEX-8400M, then verified the fix:
(
EXC_BAD_ACCESS, stack as in Crash (use-after-free): removing a slice within 300 ms of adding it segfaults via the DAX-recall timer #3732).full open → tune → handoff → close → reopen → restore lifecycle — no crash,
process stays up and the control channel stays responsive.
RelWithDebInfobuild offorigin/main(Qt 6.11, macOS arm64).Discovery
Found while exercising the slice open/close/reopen lifecycle through the agent
automation bridge — a concrete example of agent-driven testing surfacing a real
pre-existing crash that manual clicking rarely hits because it requires
sub-300 ms timing.
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat