Summary
Removing a slice within ~300 ms of adding it crashes the app with a
use-after-free (EXC_BAD_ACCESS / SIGSEGV). The dangling access happens in
the per-slice DAX-channel recall timer scheduled by onSliceAdded().
This is a real crash on an ordinary path — fast slice add/remove, profile
recall churn, or band-stack restore that recreates slices quickly can all hit
it. It was found while exercising the slice lifecycle (open → close → reopen)
through the agent automation bridge, but it is not automation-specific.
Root cause
MainWindow::onSliceAdded() arms a 300 ms QTimer::singleShot to restore a
slice's saved DAX channel (#1221):
QTimer::singleShot(300, this, [this, s, savedDax]() { // s = raw SliceModel*
if (s && !profileLoadRadioStateWritesHeld())
s->setDaxChannel(savedDax);
});
Two problems compound:
- The
if (s && …) guard is ineffective. s is a raw SliceModel*
captured by value. A raw pointer does not become null when the pointee
is destroyed, so the guard passes on a dangling pointer.
- The timer is not auto-cancelled. Its context object is
this
(MainWindow, long-lived), not the slice. So when the slice is removed, the
timer survives and fires anyway.
If the slice is destroyed inside the 300 ms window, the timer dereferences
freed memory:
QTimer (300ms) → lambda → SliceModel::setDaxChannel()
→ SliceModel::sendCommand() (SliceModel.cpp:43, inlined)
→ emit SliceModel::commandReady()
→ QtCore doActivate() ← SIGSEGV on the dangling sender
The timer only arms when the slice index maps to a saved
DaxChannel_Slice{A,B,…} > 0 setting, so it reproduces most readily for users
who have previously assigned a DAX channel to slice A/B.
Reproduction
- Have a saved
DaxChannel_SliceA/DaxChannel_SliceB > 0 (assign a DAX
channel once, so the recall timer arms on the next add).
- Add a slice, then remove it < 300 ms later (rapid add/remove, or profile
recall that recreates slices quickly).
- ~300 ms after the add, the app segfaults.
Observed timeline (live): reopen at T=0, removed at T+172 ms, timer fired
at ~T+300 ms → crash.
Crash backtrace (macOS, arm64)
Thread 0 Crashed (main):
0 QtCore doActivate<false>(QObject*, int, void**)
1 AetherSDR SliceModel::commandReady(QString const&)
2 AetherSDR SliceModel::sendCommand(QString const&) SliceModel.cpp:43 [inlined]
3 AetherSDR SliceModel::setDaxChannel(int) SliceModel.cpp:354
4 QtCore doActivate<false>(QObject*, int, void**)
5 QtCore QSingleShotTimer::timerEvent(QTimerEvent*)
...
Exception: EXC_BAD_ACCESS (SIGSEGV), KERN_INVALID_ADDRESS
Fix
Capture a QPointer<SliceModel> instead of a raw pointer, so the existing
if (sp && …) guard actually nulls out when the slice is destroyed. This
matches the existing dangling-object guards already used in this translation
unit (MainWindow_Wiring.cpp, #381).
Regression-tested: 6× add/remove at 120 ms dwell (well inside the window) plus
a full open → tune → close → reopen → restore lifecycle — no crash, process
stays up and responsive.
PR incoming.
Environment
- AetherSDR 26.6.3, macOS 26.5.1 (arm64, M2 Ultra)
- FLEX-8400M, SmartSDR protocol, fw 4.2.18
Summary
Removing a slice within ~300 ms of adding it crashes the app with a
use-after-free (
EXC_BAD_ACCESS/ SIGSEGV). The dangling access happens inthe per-slice DAX-channel recall timer scheduled by
onSliceAdded().This is a real crash on an ordinary path — fast slice add/remove, profile
recall churn, or band-stack restore that recreates slices quickly can all hit
it. It was found while exercising the slice lifecycle (open → close → reopen)
through the agent automation bridge, but it is not automation-specific.
Root cause
MainWindow::onSliceAdded()arms a 300 msQTimer::singleShotto restore aslice's saved DAX channel (#1221):
Two problems compound:
if (s && …)guard is ineffective.sis a rawSliceModel*captured by value. A raw pointer does not become null when the pointee
is destroyed, so the guard passes on a dangling pointer.
this(MainWindow, long-lived), not the slice. So when the slice is removed, the
timer survives and fires anyway.
If the slice is destroyed inside the 300 ms window, the timer dereferences
freed memory:
The timer only arms when the slice index maps to a saved
DaxChannel_Slice{A,B,…} > 0setting, so it reproduces most readily for userswho have previously assigned a DAX channel to slice A/B.
Reproduction
DaxChannel_SliceA/DaxChannel_SliceB> 0 (assign a DAXchannel once, so the recall timer arms on the next add).
recall that recreates slices quickly).
Observed timeline (live): reopen at T=0, removed at T+172 ms, timer fired
at ~T+300 ms → crash.
Crash backtrace (macOS, arm64)
Fix
Capture a
QPointer<SliceModel>instead of a raw pointer, so the existingif (sp && …)guard actually nulls out when the slice is destroyed. Thismatches the existing dangling-object guards already used in this translation
unit (
MainWindow_Wiring.cpp, #381).Regression-tested: 6× add/remove at 120 ms dwell (well inside the window) plus
a full open → tune → close → reopen → restore lifecycle — no crash, process
stays up and responsive.
PR incoming.
Environment