fix(tci): use-after-free of TciProtocol in queued lambdas (#2814)#2946
Conversation
…sdr#2814) TciProtocol's command handlers post Qt::QueuedConnection lambdas that captured the raw `TciProtocol* this` and dereferenced `m_model` at delivery time. TciServer destroys the TciProtocol synchronously when the WebSocket client disconnects (TciServer::onClientDisconnected) and in stop(). If a client disconnects while a command lambda is still in the event queue, the lambda reads m_model out of the freed TciProtocol — a use-after-free: the bogus RadioModel* yields a bogus TransmitModel, and stopTune() emits commandReady() on a garbage sender, crashing in QMetaObject::activate (0xc0000005 on Windows). Capture m_model by value ([model = m_model, ...]) at all 19 invokeMethod sites. RadioModel is owned by MainWindow and outlives every TciProtocol, so the lambdas no longer depend on the protocol's lifetime. Confirmed with an AddressSanitizer build: before, the tune on/off repro faults with heap-use-after-free in the cmdTune lambda (freed by TciServer::onClientDisconnected); after, the same repro runs clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Further corroboration: a build carrying this fix ran a full TX-drive calibration sweep — repeated |
|
@jensenpat — this is ready for review. Fixes the #2814 TCI use-after-free; CI is green and it's ASan-verified clean before/after. Low-risk (capture |
There was a problem hiding this comment.
Thanks @nigelfenton — diagnosis and fix both look correct.
Verified:
RadioModel m_radioModel;is a value-typed member ofMainWindow(MainWindow.h:367), so it outlives every per-clientTciProtocol. Capturingmodel = m_modelby value is sound.- The synchronous
delete m_clients[i].protocol;atTciServer.cpp:379is the exact lifetime hazard the queued lambdas were tripping over. - All
m_model-targetedinvokeMethodsites inTciProtocol.cppnow capturemodel = m_modelby value — no remaining[this, …]captures dereferencem_model. - Nice touch in
cmdTrx(TciProtocol.cpp:427) resolvingsliceForTrx(trx)beforeinvokeMethod. That call would have implicitly recapturedthisinside the lambda and reintroduced the bug; pulling the slice pointer out up front is the right move and worth the inline comment you added. - ASan trace in the PR body matches the suspect dereference path (
m_model->transmitModel().stopTune()→commandReady()emitted from freed sender → crash inQMetaObject::activate).
One out-of-scope observation, not blocking:
The slice-targeted lambdas ([s, …] at lines 377, 401, 453, 552, 575, etc.) still capture raw SliceModel* pointers. If a slice were removed between queueing and delivery (RadioModel::removeSlice or a Flex-side slice teardown racing a TCI command), those would face the same class of UAF. That's pre-existing and outside this PR's scope — m_model was the documented #2814 signature — but worth a follow-up issue if slice churn during TCI traffic is plausible in practice.
LGTM as-is.
🤖 aethersdr-agent · cost: $6.2189 · model: claude-opus-4-7
|
Claude here — merged. Thanks @nigelfenton, this is exemplary ASan-evidenced both directions (before AND after) is the right way Scope is exactly right too — touched `TciProtocol.cpp` only, Filed #2990 for the broader queued-lambda+\`[this]\` audit. The Ships in v26.5.3. 73, |
RigctlServer::onClientDisconnected() synchronously deletes RigctlProtocol (same pattern as TciServer/TciProtocol fixed in aethersdr#2946). All 16 queued Qt::QueuedConnection lambdas that captured `this` (RigctlProtocol*) are converted to capture `[model = m_model, ...]` by value. RadioModel is owned by MainWindow and outlives every RigctlProtocol, so the queued lambdas no longer depend on the protocol's lifetime. cmdSetPtt also inlines currentSlice() to avoid capturing `this` for the slice lookup, adding the isConnected() guard that the helper had. Closes aethersdr#2990. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary - Fixes #2990 — `RigctlProtocol` has the same queued-lambda use-after-free pattern fixed for `TciProtocol` in #2946 - `RigctlServer::onClientDisconnected()` synchronously `delete`s `RigctlProtocol` while `Qt::QueuedConnection` lambdas capturing `this` may still be queued - All 16 vulnerable `invokeMethod` sites converted from `[this, ...]` to `[model = m_model, ...]` - `cmdSetPtt` inlines `currentSlice()` to avoid capturing `this`, preserving the `isConnected()` guard - `RadioModel` is owned by `MainWindow` and outlives every `RigctlProtocol` — captured `model` pointer is always valid when the lambda fires ## Test plan - [ ] Build passes (CI) - [ ] Rapid rigctl client connect/disconnect cycle does not crash under ASan - [ ] WSJT-X / fldigi rigctld connection still functional (freq/mode/PTT/split) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… + @G6PWY-Chris (#3067) Two attribution errors in the v26.5.3 release CHANGELOG: 1. @M7HNF-Ian (Ian, separate GitHub account) → @nigelfenton (Nigel Fenton, G0JKN) — Nigel authored all 5 of the commits I bucketed under that line: TCI Network dialog tab (#2879), tx_gain + ALC (#2950), use-after-free fix (#2946), vfo emit on band-change (#2828), mic-capture suppress during TCI feed (#2982). 2. @chrisb1964 → @G6PWY-Chris (Chris G6PWY) — single commit author in the cycle was the latter; chrisb1964 is unrelated. Reported by Nigel directly. All other handles verified against gh api repos/.../commits/<sha>. GitHub release notes already updated; this lands the same fix in the in-repo CHANGELOG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2814) (aethersdr#2946) ## Summary Fixes aethersdr#2814 — the deterministic `0xc0000005` crash when a TCI client cycles `tune:` on/off. `TciProtocol`'s command handlers post `Qt::QueuedConnection` lambdas that captured the raw `TciProtocol* this` and dereferenced `m_model` at delivery time. `TciServer` destroys the `TciProtocol` **synchronously** on client disconnect (`TciServer::onClientDisconnected`, and in `stop()`). If a client disconnects while a command lambda is still queued, the lambda reads `m_model` out of the freed `TciProtocol` — a use-after-free: the bogus `RadioModel*` yields a bogus `TransmitModel`, and `stopTune()` emits `commandReady()` on a garbage sender → crash in `QMetaObject::activate`. ## Fix Capture `m_model` by value (`[model = m_model, …]`) at all 19 `invokeMethod` sites in `TciProtocol.cpp`. `RadioModel` is owned by `MainWindow` and outlives every `TciProtocol`, so the queued lambdas no longer depend on the protocol's lifetime. `TciServer` is unchanged — the synchronous `delete` is safe once nothing queued references the protocol. This is the fix proposed by `@aethersdr-agent` in the issue thread. ## Verification — AddressSanitizer, before & after ASan build (`-fsanitize=address`), run against the `tune:` on/off repro: **Before** — faults on the 3rd cycle: ``` ERROR: AddressSanitizer: heap-use-after-free #0 operator() src/core/TciProtocol.cpp:483 freed by: TciServer::onClientDisconnected() src/core/TciServer.cpp:350 allocated: TciServer::onNewConnection() src/core/TciServer.cpp:292 ``` **After** — same repro, 3 cycles, AetherSDR stays up, ASan reports nothing. ## Test plan - [x] ASan build + `tune:` on/off repro — clean (was heap-use-after-free) - [ ] Build check on CI (Qt 6.5) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary - Fixes aethersdr#2990 — `RigctlProtocol` has the same queued-lambda use-after-free pattern fixed for `TciProtocol` in aethersdr#2946 - `RigctlServer::onClientDisconnected()` synchronously `delete`s `RigctlProtocol` while `Qt::QueuedConnection` lambdas capturing `this` may still be queued - All 16 vulnerable `invokeMethod` sites converted from `[this, ...]` to `[model = m_model, ...]` - `cmdSetPtt` inlines `currentSlice()` to avoid capturing `this`, preserving the `isConnected()` guard - `RadioModel` is owned by `MainWindow` and outlives every `RigctlProtocol` — captured `model` pointer is always valid when the lambda fires ## Test plan - [ ] Build passes (CI) - [ ] Rapid rigctl client connect/disconnect cycle does not crash under ASan - [ ] WSJT-X / fldigi rigctld connection still functional (freq/mode/PTT/split) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… + @G6PWY-Chris (aethersdr#3067) Two attribution errors in the v26.5.3 release CHANGELOG: 1. @M7HNF-Ian (Ian, separate GitHub account) → @nigelfenton (Nigel Fenton, G0JKN) — Nigel authored all 5 of the commits I bucketed under that line: TCI Network dialog tab (aethersdr#2879), tx_gain + ALC (aethersdr#2950), use-after-free fix (aethersdr#2946), vfo emit on band-change (aethersdr#2828), mic-capture suppress during TCI feed (aethersdr#2982). 2. @chrisb1964 → @G6PWY-Chris (Chris G6PWY) — single commit author in the cycle was the latter; chrisb1964 is unrelated. Reported by Nigel directly. All other handles verified against gh api repos/.../commits/<sha>. GitHub release notes already updated; this lands the same fix in the in-repo CHANGELOG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Fixes #2814 — the deterministic
0xc0000005crash when a TCI client cyclestune:on/off.TciProtocol's command handlers postQt::QueuedConnectionlambdas that captured the rawTciProtocol* thisand dereferencedm_modelat delivery time.TciServerdestroys theTciProtocolsynchronously on client disconnect (TciServer::onClientDisconnected, and instop()). If a client disconnects while a command lambda is still queued, the lambda readsm_modelout of the freedTciProtocol— a use-after-free: the bogusRadioModel*yields a bogusTransmitModel, andstopTune()emitscommandReady()on a garbage sender → crash inQMetaObject::activate.Fix
Capture
m_modelby value ([model = m_model, …]) at all 19invokeMethodsites inTciProtocol.cpp.RadioModelis owned byMainWindowand outlives everyTciProtocol, so the queued lambdas no longer depend on the protocol's lifetime.TciServeris unchanged — the synchronousdeleteis safe once nothing queued references the protocol. This is the fix proposed by@aethersdr-agentin the issue thread.Verification — AddressSanitizer, before & after
ASan build (
-fsanitize=address), run against thetune:on/off repro:Before — faults on the 3rd cycle:
After — same repro, 3 cycles, AetherSDR stays up, ASan reports nothing.
Test plan
tune:on/off repro — clean (was heap-use-after-free)