Skip to content

fix(tci): use-after-free of TciProtocol in queued lambdas (#2814)#2946

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
nigelfenton:fix/tci-protocol-uaf-2814
May 23, 2026
Merged

fix(tci): use-after-free of TciProtocol in queued lambdas (#2814)#2946
ten9876 merged 1 commit into
aethersdr:mainfrom
nigelfenton:fix/tci-protocol-uaf-2814

Conversation

@nigelfenton

Copy link
Copy Markdown
Contributor

Summary

Fixes #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

  • ASan build + tune: on/off repro — clean (was heap-use-after-free)
  • Build check on CI (Qt 6.5)

…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>
@nigelfenton

Copy link
Copy Markdown
Contributor Author

Further corroboration: a build carrying this fix ran a full TX-drive calibration sweep — repeated trx: keying + client disconnect — with no crash. The same workload on an un-fixed build segfaulted in QMetaObject::activate (the original #2814 signature). — Nigel G0JKN & Claude

@nigelfenton nigelfenton marked this pull request as ready for review May 22, 2026 20:54
@nigelfenton

Copy link
Copy Markdown
Contributor Author

@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 m_model by value + deleteLater). Would appreciate a look when you have a moment. — Nigel G0JKN

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @nigelfenton — diagnosis and fix both look correct.

Verified:

  • RadioModel m_radioModel; is a value-typed member of MainWindow (MainWindow.h:367), so it outlives every per-client TciProtocol. Capturing model = m_model by value is sound.
  • The synchronous delete m_clients[i].protocol; at TciServer.cpp:379 is the exact lifetime hazard the queued lambdas were tripping over.
  • All m_model-targeted invokeMethod sites in TciProtocol.cpp now capture model = m_model by value — no remaining [this, …] captures dereference m_model.
  • Nice touch in cmdTrx (TciProtocol.cpp:427) resolving sliceForTrx(trx) before invokeMethod. That call would have implicitly recaptured this inside 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 in QMetaObject::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

@ten9876 ten9876 merged commit 59ab508 into aethersdr:main May 23, 2026
5 checks passed
@ten9876

ten9876 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Claude here — merged. Thanks @nigelfenton, this is exemplary
bug-fix discipline.

ASan-evidenced both directions (before AND after) is the right way
to fix UAFs. Without the sanitizer trace pinning the use-after-free
to `TciProtocol.cpp:483` with the allocation/free lines from
`TciServer.cpp:292/350`, the diagnosis would have been a guess.
With it, the fix is mechanical.

Scope is exactly right too — touched `TciProtocol.cpp` only,
left `TciServer` alone because its `[this]` captures refer to
TciServer (long-lived), not TciProtocol (per-connection). And the
`cmdTrx` slice-lookup hoist to the sender thread is a nice
side-effect: makes the queued lambda deterministic about which slice
it touches, rather than re-resolving on the receiver side.

Filed #2990 for the broader queued-lambda+\`[this]\` audit. The
highest-risk match the grep turned up is `RigctlProtocol.cpp` —
five sites that look like an exact structural mirror of TciProtocol's
pattern. Worth confirming whether RigctlProtocol is synchronously
deleted by RigctlServer on disconnect; if so, the same UAF window
exists and the same fix template applies.

Ships in v26.5.3.

73,
Jeremy KK7GWY & Claude (AI dev partner)

G6PWY-Chris added a commit to G6PWY-Chris/AetherSDR that referenced this pull request May 23, 2026
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>
ten9876 pushed a commit that referenced this pull request May 23, 2026
## 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>
@nigelfenton nigelfenton deleted the fix/tci-protocol-uaf-2814 branch May 23, 2026 16:06
ten9876 added a commit that referenced this pull request May 24, 2026
… + @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>
@aethersdr-agent aethersdr-agent Bot mentioned this pull request May 28, 2026
2 tasks
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
G6PWY-Chris added a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
## 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 pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
… + @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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deterministic 0xc0000005 crash on TCI tune on/off (TransmitModel::stopTune -> commandReady)

2 participants