Summary
A deferred per-slice DAX-channel restore (#1221) captures a raw SliceModel* in a 300 ms singleShot. If the slice is removed within that window, the timer fires on a freed object → use-after-free / EXC_BAD_ACCESS.
Crash — EXC_BAD_ACCESS (SIGSEGV)
Crash report excerpt (Testing/ASDR-crash.txt)
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x000000000000b430
Exception Codes: 0x0000000000000001, 0x000000000000b430
Termination Reason: Namespace SIGNAL, Code 11, Segmentation fault: 11
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 QtCore void doActivate<false>(QObject*, int, void**) + 52
1 AetherSDR AetherSDR::SliceModel::commandReady(QString const&) + 52
2 AetherSDR AetherSDR::SliceModel::setDaxChannel(int) + 156
3 QtCore void doActivate<false>(QObject*, int, void**) + 1344
4 QtCore QSingleShotTimer::timerEvent(QTimerEvent*) + 56
5 QtCore QObject::event(QEvent*) + 668
6 QtWidgets QApplicationPrivate::notify_helper(QObject*, QEvent*) + 336
7 QtWidgets QApplication::notify(QObject*, QEvent*) + 468
8 QtCore QCoreApplication::sendEvent(QObject*, QEvent*) + 168
9 QtCore QTimerInfoList::activateTimers() + 1392
A deferred timer fires setDaxChannel() on a SliceModel that has already been destroyed, then emits commandReady from the freed QObject — crashing in doActivate.
Root cause — src/gui/MainWindow_Wiring.cpp:261
QTimer::singleShot(300, this, [this, s, savedDax]() {
if (s && !profileLoadRadioStateWritesHeld()) {
s->setDaxChannel(savedDax); // s may be freed
}
});
s is a raw SliceModel* captured by value. The if (s && …) check does not protect against a dangling pointer — s stays non-null after the slice's SliceModel is destroyed, so s->setDaxChannel() dereferences freed memory.
Same class as the singleShot/SliceModel UAF fixed in 67d3d232, and the QPointer guard recently added to RigctlProtocol::m_pendingTxSlice.
How it was hit
Found while testing rigctld create-on-demand split (#3724): the rigctld regression test rapidly creates and removes split TX slices; with a DAX/TCI audio client active (after WSJT-X had been used), a created slice was removed inside the 300 ms restore window → crash. A normal user is unlikely to churn create/remove this fast, so real-world incidence is low — but it's a genuine UAF, and on-demand split makes the create→remove path more reachable.
Fix
Capture a QPointer<SliceModel> so the guard actually detects the destroyed slice (the timer then no-ops):
QPointer<SliceModel> guard(s);
QTimer::singleShot(300, this, [this, guard, savedDax]() {
if (guard && !profileLoadRadioStateWritesHeld())
guard->setDaxChannel(savedDax);
});
(Same pattern already used at TciServer.cpp:1446.) The structural fix — id/handle-based slice references so deferred operations can't dangle — is the slice-lifecycle RFC #3715.
Notes
Refs #3715 (structural fix), #3724 (where found / current test workaround).
Summary
A deferred per-slice DAX-channel restore (#1221) captures a raw
SliceModel*in a 300 mssingleShot. If the slice is removed within that window, the timer fires on a freed object → use-after-free /EXC_BAD_ACCESS.Crash —
EXC_BAD_ACCESS (SIGSEGV)Crash report excerpt (Testing/ASDR-crash.txt)
A deferred timer fires
setDaxChannel()on aSliceModelthat has already been destroyed, then emitscommandReadyfrom the freedQObject— crashing indoActivate.Root cause —
src/gui/MainWindow_Wiring.cpp:261sis a rawSliceModel*captured by value. Theif (s && …)check does not protect against a dangling pointer —sstays non-null after the slice'sSliceModelis destroyed, sos->setDaxChannel()dereferences freed memory.Same class as the singleShot/
SliceModelUAF fixed in67d3d232, and theQPointerguard recently added toRigctlProtocol::m_pendingTxSlice.How it was hit
Found while testing rigctld create-on-demand split (#3724): the rigctld regression test rapidly creates and removes split TX slices; with a DAX/TCI audio client active (after WSJT-X had been used), a created slice was removed inside the 300 ms restore window → crash. A normal user is unlikely to churn create/remove this fast, so real-world incidence is low — but it's a genuine UAF, and on-demand split makes the create→remove path more reachable.
Fix
Capture a
QPointer<SliceModel>so the guard actually detects the destroyed slice (the timer then no-ops):(Same pattern already used at
TciServer.cpp:1446.) The structural fix — id/handle-based slice references so deferred operations can't dangle — is the slice-lifecycle RFC #3715.Notes
SliceModel*(a scan flagsMainWindow_Wiring.cpp:261as the raw-pointer one; most siblings capture slice ids).Refs #3715 (structural fix), #3724 (where found / current test workaround).