Skip to content

Use-after-free: deferred per-slice DAX-channel restore fires on a freed slice #3725

Description

@K5PTB

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfacebugSomething isn't workingmaintainer-reviewRequires maintainer review before any action is takenpriority: highHigh priority

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions