Skip to content

Crash (use-after-free): removing a slice within 300 ms of adding it segfaults via the DAX-recall timer #3732

Description

@jensenpat

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:

  1. 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.
  2. 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

  1. Have a saved DaxChannel_SliceA/DaxChannel_SliceB > 0 (assign a DAX
    channel once, so the recall timer arms on the next add).
  2. Add a slice, then remove it < 300 ms later (rapid add/remove, or profile
    recall that recreates slices quickly).
  3. ~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

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfaceaudioAudio engine and streamingbugSomething 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