You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is an RFC on slice lifecycle management. AetherSDR has no single authority for slice create / track /
remove: the same lifecycle is hand-rolled independently by many consumers, with divergent behavior and
recurring create/close race conditions. This has been a long-standing source of fragility (several
candidate issues are listed at the end). The goal here is to agree on a unified approach to slice
lifecycle before anyone implements it — feedback on scope and design is what's wanted.
We surfaced the full extent of this while implementing and testing CAT split VFO (#3702, #3703), and a unified
solution would also resolve the split-VFO problems we found there. But the problem is general — it spans the
GUI, DAX, TCI, and the CAT protocols alike.
The problem — no single authority for slice lifecycle
A survey of slice create / remove / track shows the same do-it-yourself pattern repeated everywhere, with no
component that owns "a slice exists / is mine / is gone":
Creation — 8+ sites, two classes. Some go through RadioModel::addSlice()/addSliceOnPan() (which discards the radio's create-ack id, RadioModel.cpp:1595); several others bypass it entirely with raw slice create command strings (MainWindow_Wiring.cpp:2860, MainWindow_Controllers.cpp:445 & :968, MainWindow_Shortcuts.cpp:780, plus RadioModel.cpp:5485) — so they get neither the id nor uniform maxSlices enforcement.
Removal — raw slice remove N strings scattered across MainWindow.cpp:6125, MainWindow_Wiring.cpp:2359 & :2729, with no single teardown path.
Lifecycle reaction — 15+ independent sliceAdded/sliceRemoved subscribers (TciServer, DaxApplet, RxApplet, TciApplet, MainWindow_Session ×3, MainWindow_DigitalModes, MainWindow_DspApplets, RadioSetupDialog, pan-follow at MainWindow.cpp:8320, …), each re-deriving which slices matter to it
and separately handling out-of-band create/remove (Multi-Flex / front-panel / rigctld — see the explicit
"removed out-of-band" handling at MainWindow_Wiring.cpp:733).
Identity held by raw pointer — holding a SliceModel* across async time is hand-guarded with QPointer<SliceModel> independently in 8+ sites (TciServer.cpp, FlexControlDialog, KiwiSdrApplet, Ax25HfPacketDecodeDialog, AgcTCalibrator, AgcCalibrationDialog, FreeDvReporterDialog, SpectrumOverlayMenu). One such raw-pointer-across-time bug was the singleShot-timer UAF fixed in 67d3d23.
That absence of an owner is the shared root of the create/close races, the DAX/TCI ownership gap (#3305), and
the out-of-band-change handling sprinkled through the GUI.
Fragmented solutions — the same lifecycle, three different ways
Split VFO is the sharpest example: the "create a transient slice → figure out which one I just made → track
it → remove it on teardown" pattern is implemented three times, each with its own identity heuristic:
rigctld — src/core/RigctlProtocol.cpp: a deferred-promotion state machine. set_split_vfo/set_freq VFOB arms m_pendingSplitEnable (RigctlProtocol.h:120) and tryPromoteTxSlice()
(RigctlProtocol.cpp:932) reconciles once the new slice appears asynchronously.
GUI split — src/gui/MainWindow.cpp: m_splitActive / m_splitTxSliceId / m_splitRxSliceId, where
the created slice is identified by a heuristic in the slice-added handler (MainWindow.cpp:5988:
"first new non-RX slice while split active and no TX slice yet"), cleared on slice-removed, and torn down
with slice remove (MainWindow.cpp:6125).
(For reference, TCI is not a fourth creator — it never calls create/remove; it is a lifecycle consumer
plus a QPointer holder, both counted above. Its split_enable SET is a no-op, TciProtocol.cpp:746: "not fully controllable from TCI (would need to create a second slice)" — i.e. a feature blocked by the
missing shared create path, #1686.)
Root cause
RadioModel::addSlice() is fire-and-forget — it emits slice create and does not return the created
slice's id (the radio acks it asynchronously). So every caller reconstructs "which slice is mine" by
snapshot/diff or a positional heuristic, which is inherently racy when create/remove overlap. The raw-string
create/remove sites don't even go through addSlice, so there is no chokepoint at which to fix this once.
Concrete failures we observed
These surfaced while testing split VFO (the failures landed on the rigctld / SmartCAT paths), but they are
symptoms of the lifecycle gap, not of split specifically:
The deferred-promotion machine could hang when the radio was at slice capacity — set_split_vfo 1
armed a create that would never land (fixed narrowly in rigctld CAT behavior diverges from SmartSDR for Mac #3703 bug b with a maxSlices guard).
set_split_freq could write to the wrong slice when m_pendingTxSlice wasn't set synchronously (the
MacLoggerDX race, guarded in rigctld_test 5.5b).
The integration tests had to add settle-delays / polling loops around create (poll up to 5 s for the
slice to appear) and removal (poll for teardown) to avoid races rather than fixing them — i.e. the tests
work around the lifecycle fragility instead of the fragility being eliminated.
Direction for a unified approach (not yet implemented)
No fix is implemented yet — only a work-in-progress prototype came out of the split-VFO work. The points
below are direction for discussion, not a committed design.
Enabling insight: the radio already returns the new slice's id — slice create replies with the slice
index, and RadioModel::addSlice() receives it in its sendCmd callback (RadioModel.cpp:1595,
"new slice created, index = ") then discards it. Every "which slice is mine?" heuristic exists only
because that id is thrown away; surfacing it needs no protocol change.
A unified approach would likely provide:
One create/remove chokepoint in RadioModel that surfaces the create-ack id and enforces maxSlices
once — so every creator stops reinventing it and gets the id directly instead of guessing.
A stable identity/handle (id + generation) to retire the scattered raw SliceModel* + QPointer
guards and close the id-reuse/ABA hole.
A normalized lifecycle (created/removed carrying a Local-vs-OutOfBand origin) so the 15+ subscribers
stop re-deriving who caused a change and the out-of-band cases (Multi-Flex / front panel / external
controllers) are explicit.
Per-slice ownership + one-place teardown so transient slices are removed on disconnect/shutdown, and an
owner reconciles a slice that vanished out-of-band instead of hanging or double-removing.
It could land incrementally and behavior-neutral until the last step (introduce the chokepoint/handle behind
today's signals, route the bypass sites, then port the per-consumer owners). Beyond removing the duplication,
a unified lifecycle would also fix the split-VFO problems we found — making the polled rigctld_test 5.x
split cases deterministic, unblocking TCI split_enable (#1686), and giving the deferred SmartCAT
create-on-demand (#3702#7) a safe home.
WIP design sketch (illustrative, not implemented)
// 1. One create/remove chokepoint that surfaces the id (id is already on the wire, today discarded)voidRadioModel::createSlice(const SliceSpec& spec, // pan/freq/antenna/mode
std::function<void(int sliceId)> onCreated,
std::function<void()> onFailed = {});
voidRadioModel::removeSlice(int sliceId);
// existing no-arg addSlice() stays as a thin delegate; a CI guard bans raw "slice create"/"slice remove".// 2. A stable handle — retires raw SliceModel* + QPointer; generation closes ABA/id-reuseclassSliceHandle { // value type: { id, generation }public:
SliceModel* get(const RadioModel&) const; // nullptr if gone (or ABA-stale)boolvalid(const RadioModel&) const;
intid() const;
};
// 3. A registry in RadioModel owning id -> (SliceModel, generation, owner, origin), emitting:
signals:
voidsliceCreated(SliceHandle, SliceOrigin); // Local | OutOfBandvoidsliceRemoved(SliceHandle, RemoveReason); // Local | OutOfBand | Shutdown// ownership tokens: GuiUser | RigctldSplit | SmartCatSplit | Dax | Tci | ...// 4. Transient owners (split TX, future FX.25/VARA/Winlink/KISS) = a thin state machine over the registry:// Idle -> Creating -> Active -> Removing -> Idle// - release() during Creating flags remove-on-arrival (vs today's machine that can hang, #3703 bug b)// - pre-check maxSlices; treat onFailed/sliceCreateFailed as a clean failure, not an armed create
Concurrency: creators run on protocol/GUI threads; the registry would be the single marshalling point
(mutations queued to the model thread, replacing the ad-hoc QMetaObject::invokeMethod(... QueuedConnection)
at each create site, e.g. RigctlProtocol.cpp:1082).
Open questions: SliceHandle generation source (client counter vs radio token); whether the registry should
also own DAX/TCI stream lifecycle (#3305) or just slices; capacity backpressure across competing owners.
Candidate variants to triage
These open issues may share this slice-lifecycle / slice-identity root cause; each needs triage to confirm
vs. an unrelated slice feature.
Summary
This is an RFC on slice lifecycle management. AetherSDR has no single authority for slice create / track /
remove: the same lifecycle is hand-rolled independently by many consumers, with divergent behavior and
recurring create/close race conditions. This has been a long-standing source of fragility (several
candidate issues are listed at the end). The goal here is to agree on a unified approach to slice
lifecycle before anyone implements it — feedback on scope and design is what's wanted.
We surfaced the full extent of this while implementing and testing CAT split VFO (#3702, #3703), and a unified
solution would also resolve the split-VFO problems we found there. But the problem is general — it spans the
GUI, DAX, TCI, and the CAT protocols alike.
The problem — no single authority for slice lifecycle
A survey of slice create / remove / track shows the same do-it-yourself pattern repeated everywhere, with no
component that owns "a slice exists / is mine / is gone":
RadioModel::addSlice()/addSliceOnPan()(whichdiscards the radio's create-ack id,
RadioModel.cpp:1595); several others bypass it entirely with rawslice createcommand strings (MainWindow_Wiring.cpp:2860,MainWindow_Controllers.cpp:445&:968,MainWindow_Shortcuts.cpp:780, plusRadioModel.cpp:5485) — so they get neither the id nor uniformmaxSlicesenforcement.slice remove Nstrings scattered acrossMainWindow.cpp:6125,MainWindow_Wiring.cpp:2359&:2729, with no single teardown path.sliceAdded/sliceRemovedsubscribers (TciServer,DaxApplet,RxApplet,TciApplet,MainWindow_Session×3,MainWindow_DigitalModes,MainWindow_DspApplets,RadioSetupDialog, pan-follow atMainWindow.cpp:8320, …), each re-deriving which slices matter to itand separately handling out-of-band create/remove (Multi-Flex / front-panel / rigctld — see the explicit
"removed out-of-band" handling at
MainWindow_Wiring.cpp:733).SliceModel*across async time is hand-guarded withQPointer<SliceModel>independently in 8+ sites (TciServer.cpp,FlexControlDialog,KiwiSdrApplet,Ax25HfPacketDecodeDialog,AgcTCalibrator,AgcCalibrationDialog,FreeDvReporterDialog,SpectrumOverlayMenu). One such raw-pointer-across-time bug was the singleShot-timer UAF fixed in 67d3d23.That absence of an owner is the shared root of the create/close races, the DAX/TCI ownership gap (#3305), and
the out-of-band-change handling sprinkled through the GUI.
Fragmented solutions — the same lifecycle, three different ways
Split VFO is the sharpest example: the "create a transient slice → figure out which one I just made → track
it → remove it on teardown" pattern is implemented three times, each with its own identity heuristic:
src/core/RigctlProtocol.cpp: a deferred-promotion state machine.set_split_vfo/set_freq VFOBarmsm_pendingSplitEnable(RigctlProtocol.h:120) andtryPromoteTxSlice()(
RigctlProtocol.cpp:932) reconciles once the new slice appears asynchronously.src/core/SmartCatProtocol.cpp: a flag (m_splitEnabled,SmartCatProtocol.h:167) plus reuse of the operator-configured VFO B slice. (Create-on-demand wasdeliberately not implemented here precisely because of this fragility — see Flex/TS-2000 CAT (SmartCAT) behavior diverges from SmartSDR for Mac & Windows #3702 DAX audio channels and Hamlib/rigctld interface for digital mode apps #7.)
src/gui/MainWindow.cpp:m_splitActive/m_splitTxSliceId/m_splitRxSliceId, wherethe created slice is identified by a heuristic in the slice-added handler (
MainWindow.cpp:5988:"first new non-RX slice while split active and no TX slice yet"), cleared on slice-removed, and torn down
with
slice remove(MainWindow.cpp:6125).(For reference, TCI is not a fourth creator — it never calls create/remove; it is a lifecycle consumer
plus a
QPointerholder, both counted above. Itssplit_enableSET is a no-op,TciProtocol.cpp:746:"not fully controllable from TCI (would need to create a second slice)" — i.e. a feature blocked by the
missing shared create path, #1686.)
Root cause
RadioModel::addSlice()is fire-and-forget — it emitsslice createand does not return the createdslice's id (the radio acks it asynchronously). So every caller reconstructs "which slice is mine" by
snapshot/diff or a positional heuristic, which is inherently racy when create/remove overlap. The raw-string
create/remove sites don't even go through
addSlice, so there is no chokepoint at which to fix this once.Concrete failures we observed
These surfaced while testing split VFO (the failures landed on the rigctld / SmartCAT paths), but they are
symptoms of the lifecycle gap, not of split specifically:
set_split_vfo 1armed a create that would never land (fixed narrowly in rigctld CAT behavior diverges from SmartSDR for Mac #3703 bug b with a
maxSlicesguard).set_split_freqcould write to the wrong slice whenm_pendingTxSlicewasn't set synchronously (theMacLoggerDX race, guarded in
rigctld_test5.5b).slice to appear) and removal (poll for teardown) to avoid races rather than fixing them — i.e. the tests
work around the lifecycle fragility instead of the fragility being eliminated.
Direction for a unified approach (not yet implemented)
No fix is implemented yet — only a work-in-progress prototype came out of the split-VFO work. The points
below are direction for discussion, not a committed design.
Enabling insight: the radio already returns the new slice's id —
slice createreplies with the sliceindex, and
RadioModel::addSlice()receives it in itssendCmdcallback (RadioModel.cpp:1595,"new slice created, index = ") then discards it. Every "which slice is mine?" heuristic exists only
because that id is thrown away; surfacing it needs no protocol change.
A unified approach would likely provide:
RadioModelthat surfaces the create-ack id and enforcesmaxSlicesonce — so every creator stops reinventing it and gets the id directly instead of guessing.
SliceModel*+QPointerguards and close the id-reuse/ABA hole.
stop re-deriving who caused a change and the out-of-band cases (Multi-Flex / front panel / external
controllers) are explicit.
owner reconciles a slice that vanished out-of-band instead of hanging or double-removing.
It could land incrementally and behavior-neutral until the last step (introduce the chokepoint/handle behind
today's signals, route the bypass sites, then port the per-consumer owners). Beyond removing the duplication,
a unified lifecycle would also fix the split-VFO problems we found — making the polled
rigctld_test5.xsplit cases deterministic, unblocking TCI
split_enable(#1686), and giving the deferred SmartCATcreate-on-demand (#3702 #7) a safe home.
WIP design sketch (illustrative, not implemented)
Concurrency: creators run on protocol/GUI threads; the registry would be the single marshalling point
(mutations queued to the model thread, replacing the ad-hoc
QMetaObject::invokeMethod(... QueuedConnection)at each create site, e.g.
RigctlProtocol.cpp:1082).Open questions:
SliceHandlegeneration source (client counter vs radio token); whether the registry shouldalso own DAX/TCI stream lifecycle (#3305) or just slices; capacity backpressure across competing owners.
Candidate variants to triage
These open issues may share this slice-lifecycle / slice-identity root cause; each needs triage to confirm
vs. an unrelated slice feature.
Stronger siblings:
Lower confidence: