fix(rigctld): resolve SmartSDR rigctld behavior gaps and fix on-demand split VFO#3724
Conversation
Two behavior corrections to the rigctld split path, independent of the slice-lifecycle cleanup (which is tracked separately): - Implicit split-enable was not recorded. WSJT-X "Rig" split enables split via set_freq/set_mode VFOB (→ ensureSplitTxSlice) without ever sending set_split_vfo, so m_lastSplitEnable stayed unset; a later set_split_vfo 0 then saw no 1→0 edge and never reclaimed TX onto the RX slice. ensureSplitTxSlice now records m_lastSplitEnable=1 whenever split actually engages, and cmdSetSplitVfo no longer blanket-sets the flag (it sets 0 on disable and lets the engage path set 1). A set_split_vfo 1 that can't engage therefore can't leave a false "enabled" state that a subsequent disable would act on. - ensureSplitTxSlice now guards the radio's slice-receiver limit before addSlice(): on a full radio the create would never land, leaving split stuck "pending" forever (get_split_freq → -1). It now bails cleanly instead. No change to the slice create/track/remove machinery — created-slice cleanup (orphan/race) is deferred to the slice-management consolidation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rsdr#3703) 5.12 — implicit split enable via set_freq VFOB must record the enable so a later set_split_vfo 0 reclaims TX (regression for aethersdr#3703 bug a). 5.13 — conditional guard test: on a single-slice-capacity port, set_split_vfo 1 must fail cleanly (get_split_freq RPRT -1) rather than arm a phantom create (aethersdr#3703 bug b); skips when the radio has slice capacity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emand) Four cohesive fixes to the on-demand split path, all in RigctlProtocol, found via WSJT-X "Rig" split integration testing: 1. Create the TX slice at the RX frequency (addSliceOnPan) instead of addSlice()'s default geometry (pan-center + 20% BW), so the split slice is born on-frequency — WSJT-X sends set_split_freq AFTER the slice is created. 2. Guard ensureSplitTxSlice() against an already-in-flight create (m_pendingSplitEnable) so back-to-back set_freq/set_mode VFOB don't create duplicate TX slices, and the stashed split freq/mode isn't dropped by the promote-existing path. 3. Don't seize TX onto the bound (RX) slice on PTT while split is active, so the radio transmits on the split slice (VFOB), not VFOA. 4. set_split_freq / set_split_mode establish the TX slice on demand (like set_freq/set_mode VFOB), so a direct or race-timed call resolves instead of returning RPRT -1 and aborting the transmit. Manually verified on-air with WSJT-X: TX lands on VFOB at the WSJT-X-set frequency, following VFOB retunes. These extend the create-on-demand path the aethersdr#3703 triage flagged (aethersdr#1/aethersdr#2); the broader consolidation remains RFC aethersdr#3715. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inter guard) Follow-on to the create-on-demand split fixes, found via WSJT-X integration testing: - Best-effort removal of the TX slice we created on demand: track its id in tryPromoteTxSlice (only reached after our own create, so never an operator slice), and remove it on a genuine split→non-split transition (set_split_vfo 0) and in a new ~RigctlProtocol() destructor (hard disconnect, e.g. WSJT-X quit). Re-resolved by id at removal time and skipped if already gone; m_model is the invoke context so a destroyed model cancels the queued call. Operator-promoted slices are never removed. - QPointer-guard m_pendingTxSlice: if the user closes the split TX slice out-of-band, the raw pointer dangled and findTxSlice() handed it back, crashing on deref (UAF). QPointer auto-nulls so findTxSlice() returns nullptr and split re-establishes on demand. rigctld runs on the GUI thread (same as SliceModel), so the guard is safe; it was the only long-lived stored slice pointer in the class. Robust create-ack-id ownership (eliminating the small mis-target window when a slice is opened during the create) is deferred to RFC aethersdr#3715. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rdowns 5.14 — set_split_freq / set_split_mode from a split-off state must establish split on demand (RPRT 0, not -1) and engage split, so a direct or race-timed call (WSJT-X "Rig" split via Hamlib set_split_freq_mode) doesn't abort the transmit. disableSplitSettled() helper (kDaxSettleMs=700) — settle before every on-demand split teardown in Section 5. Closing a just-created TX slice can fire a deferred DAX-channel-assign timer on the freed slice and crash ASDR; only this test churns create/remove that fast, so the settle lets the timer fire on the live slice. The underlying app-side DAX-assign UAF is tracked separately (out of scope here). Run: rigctld_test --port 4532 → Section 5 incl. 5.14 passes (5.13 skips when the radio has slice capacity). aethersdr#3703. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sdr#1-aethersdr#3) From the high-effort review of the split work: aethersdr#1 In-flight create guard is now time-bounded. The hard `if (m_pendingSplitEnable) return;` could wedge split "pending" forever if the create never landed for a reason other than local capacity (radio NAK, or Multi-Flex where local slices().size() < maxSlices() but the radio is really full — RadioModel has no sliceCreateFailed wiring here). After kPendingCreateTimeoutMs the guard clears the stale pending and lets the next call retry; the window still dedupes the rapid WSJT-X set_freq/set_mode VFOB burst. aethersdr#2 PTT no longer seizes TX during the create window. splitActive now also covers m_pendingSplitEnable, so a key that arrives before the on-demand slice has appeared doesn't fall through to the seize and transmit on the RX VFO (VFOA). aethersdr#3 Passive set_split_freq/set_split_mode no longer arm the reclaim edge. These now call ensureSplitTxSlice(recordExistingAsEnabled=false); when split is already engaged on a distinct slice they no longer set m_lastSplitEnable=1, so a client that never sent set_split_vfo 1 can't trigger a phantom 1->0 reclaim that steals TX off a slice it didn't put into split. Enable-intent callers (set_split_vfo 1, set_freq/set_mode VFOB) keep the default true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rologue (review aethersdr#8-aethersdr#10) Test cleanup (aethersdr#9/aethersdr#10) — Section 5 had grown a hand-rolled "poll get_split_vfo until Split==0/1 with timeout" loop copied ~8 times. Add two local helpers: - waitForSplit(want, timeoutMs) → returns the final Split value - resetSplitOff() → settle-disable + confirm off (the "known-off baseline") and replace every duplicated poll/baseline loop (5.3, 5.9b, 5.11, 5.12, 5.13, 5.14) with them. No behavior change; rigctld_test still 183/184 (5.10b is the known single-slice create-echo timing race, slice-count dependent). The kDaxSettleMs=700 guard stays — it's deliberately masking the separate app-side DAX-assign-timer UAF during the test's fast create/remove churn. Production cleanup (aethersdr#8) — cmdSetSplitFreq and cmdSetSplitMode were copy-paste twins of the create-on-demand contract (ensureSplitTxSlice(false) → stash if pending → resolve TX slice → apply or RPRT -1). Factor it into applySplitParam() so the two can't diverge (a divergence would reintroduce the aethersdr#3703 -1 symptom). Left m_lastSplitEnable as per-branch writes: the records are intentional and the recordExistingAsEnabled gate makes a single assignment awkward; the deeper restructure belongs to RFC aethersdr#3715. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for this, @K5PTB — this is an unusually careful piece of work. I traced the changes against the surrounding code and they hold up well. A few notes below, none blocking.
Verified
- Threading /
QPointeris correct. Them_pendingTxSlice→QPointer<SliceModel>change is sound, and the comment justifying it ("rigctld runs on the GUI thread, same thread asSliceModel") matches reality:CatPortis constructed withnew CatPort(&m_radioModel, nullptr)inMainWindow_Session.cppand is nevermoveToThread'd, soRigctlProtocoland its socket callbacks run on the GUI thread. The auto-null on out-of-band slice close genuinely closes the use-after-free, and the queued connections elsewhere remain harmless. (Worth noting the oldertryPromoteTxSlicecomments still describe a "CAT socket thread" — those read as over-cautious given the actual model, but they're pre-existing and not introduced here.) - API surface checks out.
addSliceOnPan(panId, freqMhz),maxSlices(),slice(int),panId(), andfrequency()all exist with the signatures used, and themaxSlices()capacity guard mirrors the existing GUI create paths (MainWindow_Wiring.cpp,MainWindow_Controllers.cpp). - PTT split gate is right.
model->setTransmit(tx, …)stays outside the!splitActiveguard, so keying still transmits — only the badge-seize back to the RX slice is suppressed. That's exactly the WSJT-X "Rig" split symptom fix, and covering them_pendingSplitEnablewindow closes the key-arrives-before-slice-lands race. m_lastSplitEnablerefactor is consistent. Moving the=1intoensureSplitTxSlice()only when split actually engages (and leaving it untouched at themaxSlicesceiling) correctly avoids the spurious 1→0 reclaim, and the implicit-enable path is covered by 5.12. TherecordExistingAsEnabled=falsedistinction for the passive setters is a nice touch.
Minor / non-blocking
-
Destructor cleanup depends on a live event loop.
~RigctlProtocol()→removeCreatedTxSlice()posts aQt::QueuedConnection. On a normal client disconnect this fires fine, but if the protocol is torn down during app shutdown after the event loop has stopped, the queuedslice removenever runs and the slice is left behind. Best-effort by design, so I'd just suggest a one-line note in the comment that the queued path is no-op'd at shutdown — the radio session is going away then anyway. -
m_createdTxSliceIdid-reuse window — you already call this out and defer the create-ack-id fix to #3715. Agreed that's the right home for it; flagging only so it's visible in the review thread. The "only slices WE created are tracked, operator slices never touched" invariant holds in the current code (m_createdTxSliceIdis set solely on the create-on-demand promote path).
The test refactor (waitForSplit/resetSplitOff/applySplitParam, ~70 fewer lines) is a clear readability win, the new 5.12–5.14 coverage maps directly onto the behavior changes, and CI is green across build/macos/windows/CodeQL. The documented 5.10b timing race and the deferred fragilities are honestly scoped to #3715 rather than papered over, which I appreciate.
Nice work — no changes requested from me.
🤖 aethersdr-agent · cost: $2.6135 · model: claude-opus-4-8
|
Reopened — this was closed by accident: the #3728 squash-merge commit message contained the phrase "Closes #3724-class disconnect crashes", and GitHub parsed "Closes #3724" as a close directive. That was sloppy wording on my part, not an intentional action. This PR is unaffected and remains held for after the current release cycle per your intent (@K5PTB), to be marked ready when the cycle closes. Apologies for the noise. |
ten9876
left a comment
There was a problem hiding this comment.
Approving. Code verdict stands from the prior review: the QPointer use-after-free fix (m_pendingTxSlice), the split-active PTT gate so keying transmits on VFOB (not seizing TX back to VFOA), the time-bounded in-flight guard against a wedged 'pending', on-demand TX slice created at the RX frequency, best-effort cleanup of only the slice we created, and the maxSlices capacity guard are all correct and well-tested (183/184; the one failure a documented pre-existing timing race). FlexLib/WSJT-X 'Rig' split aligned, hardware-verified on a FLEX-8400M, CI green. The remaining create-on-demand fragilities are honestly scoped to the slice-lifecycle RFC #3715. Landing now per maintainer decision (the author's hold-for-next-cycle is being lifted by the operator). Thanks @K5PTB.
Summary
This started as the rigctld-vs-SmartSDR behavior-difference fixes from the #3703 triage. While
implementing and testing them, integration testing with WSJT-X "Rig" split surfaced several real split-VFO bugs, which are fixed here too. Finally, the regression test — which had accreted a lot of one-off tweaks — is cleaned up after adding coverage for the new behavior.
1. rigctld behavior alignment (#3703 triage)
set_freq/set_mode VFOB(WSJT-X "Rig" split) is nowrecorded, so a later
set_split_vfo 0reclaims TX to the RX slice.maxSlicesguard: at capacity,set_split_vfo 1fails cleanly instead of arming a create that cannever land (which left split stuck "pending").
2. Split-VFO fixes found during integration testing (WSJT-X "Rig" split)
addSlice()'s default geometry (it was landing several kHz off).set_freq/set_mode VFOBfrom creating two TX slices.set_split_freq/set_split_modenow create/promote the TX slice rather than returningRPRT -1and aborting the transmit on a race.m_pendingTxSliceis nowQPointer-guarded).3. Code-review hardening
A high-effort review of the above produced three further fixes: a time-bounded in-flight guard so a
NAK'd/lost create (incl. Multi-Flex where the local slice count understates the radio's) can't wedge split
"pending" forever; PTT no longer seizes TX during the create window; and a passive
set_split_freq/set_split_modeno longer arms a phantom reclaim edge that could steal TX off a slice the client never put into split.4. Tests
5.12implicit-enable reclaim,5.13maxSlicesguard(conditional),
5.14set_split_freq/set_split_modeestablish-on-demand.get_split_vfountil 0/1" loops (≈8 copies) arefactored into
waitForSplit()/resetSplitOff()helpers, and theset_split_freq/set_split_modecreate-on-demand prologue is shared via
applySplitParam(). Net ≈70 fewer lines.rigctld_test: 183/184. The one failure (5.10b) is a documented single-slice create-status-echotiming race that is slice-count dependent (passes on the promote path); it is not a regression from
this PR.
Scope / deferred
This grew beyond the original #3703 triage (two narrow bugs) into making WSJT-X "Rig" split work end-to-end. The remaining create-on-demand fragilities the review confirmed — operator-slice mis-target (no create-ack id), id reuse, fast-disable orphan, shutdown-time deref — are deferred to the slice-lifecycle RFC #3715, whose create-ack id / handle is the proper fix. A separate, app-side DAX-assign-timer UAF (only reachable by the test's rapid create/remove churn, not normal client use) is tracked as a to-do; the test settles around it.
Addresses #3703 (triage bugs fixed; the create-on-demand-vs-
NOT_ENABLEDpolicy alignment is deferred to #3715). Refs #3715.Built and regression tested on MacOS (M2), Linux (RPi 5) and Windows 11. Integration testing (WSJT-X 3.0.0, fldigi-4.2) on MacOS (M2).
🤖 Generated with Claude Code