fix: tolerate post-close debug adapter messages (fixes #340511) by vscodebot-pr · Pull Request #340519 · microsoft/vscode · GitHub
Skip to content

fix: tolerate post-close debug adapter messages (fixes #340511) - #340519

Merged
Dmitriy Vasyura (dmitrivMS) merged 3 commits into
microsoft:mainfrom
vscodebot-pr:fix/invalid-debug-adapter-340511-aw-37800676437
Oct 12, 2026
Merged

Dmitriy Vasyura (dmitrivMS) merged 3 commits into
microsoft:mainfrom
vscodebot-pr:fix/invalid-debug-adapter-340511-aw-37800676437

Conversation

@vscodebot-pr

Copy link
Copy Markdown
Contributor

Summary

$acceptDAMessage on the main-thread side of the debug extension-host bridge dereferences the debug adapter via getDebugAdapter(handle), which throws Error('Invalid debug adapter') when the handle is no longer in _debugAdapters. Debug-adapter messages arrive over IPC from the extension host; when a session ends (or restarts), the main thread removes the adapter from the map in its onDidEndSession handler, so any message already in flight across the process boundary lands after the entry is gone and crashes the unhandled-error pipeline. Impact: a reliably reproducible post-close race that surfaced to 1,542 users in the first 1.141.0 stable build across Windows, Mac, and Linux.

Fixes #340511
Recommended reviewer: @dmitrivMS

Culprit Commit

Field Value
Commit 0d321b80
Author @dmitrivMS
PR #338371
Message debug: dispose inline adapter message subscriptions
Why The only debug-area commit in the regression range 544a8291...73d5322b. It makes DirectDebugAdapter.stopSession() dispose the adapter (and its message listener) eagerly on stop, which tightens session teardown timing on the extension-host side and makes it more likely that a DA message is delivered to the main thread after the main thread has already evicted the adapter in onDidEndSession, reaching the throwing getDebugAdapter() path. The pre-existing throw site had always been reachable in principle, but this change shifted teardown ordering enough to turn it into a stable-release anomaly.

Code Flow

sequenceDiagram
    participant EH as ExtHost DirectDebugAdapter
    participant IPC as rpcProtocol (IPC)
    participant MT as MainThreadDebugService
    participant Map as _debugAdapters map

    Note over MT,Map: onDidEndSession removes adapter:<br/>_debugAdapters.delete(handle)
    EH->>IPC: $acceptDAMessage(handle, message)
    Note over EH: ⚠️ message already in flight<br/>when session ended
    IPC->>MT: $acceptDAMessage(handle, message)
    MT->>Map: getDebugAdapter(handle)
    Note over MT: 💥 adapter absent →<br/>throw Error('Invalid debug adapter')
Loading

Affected Files

File Role Evidence
src/vs/workbench/api/browser/mainThreadDebugService.ts crash site / fix L379 (from stack getDebugAdapter): this.getDebugAdapter(handle).acceptMessage(...) threw when handle missing
src/vs/workbench/api/browser/mainThreadDebugService.ts producer of the stale handle L71-L77: onDidEndSession → for (const [handle, value] of this._debugAdapters) { if (value.session === session) { this._debugAdapters.delete(handle); } } removes the adapter the in-flight message still references
src/vs/workbench/api/common/extHostDebugService.ts cross-process sender L682: mythis._debugServiceProxy.$acceptDAMessage(debugAdapterHandle, message) fires from the extension host without coordination with main-thread map state

Repro Steps

This is a cross-process timing race; it triggers when a debug adapter emits a protocol message at the same moment its session is ending or restarting.

  1. Start a debug session backed by an extension-host debug adapter (e.g. an inline/direct adapter).
  2. Have the adapter emit a protocol message (any event/response) at or just after session stop or restart.
  3. On session end the main thread removes the adapter from _debugAdapters (onDidEndSession), while the already-dispatched $acceptDAMessage IPC call is still being delivered.
  4. $acceptDAMessage calls getDebugAdapter(handle), the handle is gone, and Error('Invalid debug adapter') is thrown to the unhandled-error telemetry pipeline. Rapid stop/restart cycles increase the likelihood.

How the Fix Works

Chosen approach — src/vs/workbench/api/browser/mainThreadDebugService.ts → $acceptDAMessage(): deliver the message with the same non-throwing lookup its sibling handlers already use. $acceptDAError and $acceptDAExit both read this._debugAdapters.get(handle)?.... with the standing comment "don't use getDebugAdapter since an error can be expected on a post-close" — i.e. the codebase already acknowledges that cross-process messages legitimately arrive after the adapter is gone. $acceptDAMessage was the one handler still using the throwing getDebugAdapter(). The change makes it this._debugAdapters.get(handle)?.acceptMessage(...), so a post-close message is silently dropped (there is no longer an adapter to deliver it to) instead of crashing. The now-unused private getDebugAdapter() helper (its only caller) is removed.

This follows the principle of handling a cross-process lifecycle race at the receiver that genuinely cannot control delivery ordering, rather than adding a try/catch or suppressing the telemetry: the extension host is an external boundary across an IPC channel, so the main thread cannot guarantee a message is retracted once the session ends. No logService.error call is removed and no error is swallowed — the message is a benign post-close artifact, consistent with how errors and exits in the same state are already treated. A dropped message for an already-removed adapter is correct because the adapter (and its session) no longer exist to consume it.

Alternatives considered:

  • Producer-side reordering in onDidEndSession (delay the _debugAdapters.delete) — rejected: the delete is intentional ("any restarted session will create a new DA, so always throw the old one away") and cannot retract messages already dispatched across the IPC boundary, so the race window cannot be fully closed on the producer side.
  • Wrapping the call in try/catch — rejected: hides the condition instead of handling it, and is inconsistent with the explicit ?. pattern already established by the two sibling handlers.

Recommended Owner

@dmitrivMS — author of the culprit commit/PR #338371 and merger of that PR into microsoft/vscode (write access), active in the repository in the last 90 days; owns the debug adapter teardown change whose timing surfaced this throw.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI 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.

🟡 Changes recommended

The post-close behavior needs regression coverage in the existing service test suite.

1 open finding
What changed in this PR

Prevents crashes from debug adapter messages arriving after session teardown.

Changes:

  • Uses a non-throwing adapter lookup for delayed messages.
  • Removes the obsolete throwing helper.
File Description
src/​vs/​workbench/​api/​browser/​mainThreadDebugService.ts Safely drops post-close adapter messages.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/vs/workbench/api/browser/mainThreadDebugService.ts
@dmitrivMS Dmitriy Vasyura (dmitrivMS) added the freeze-slow-crash-leak VS Code crashing, performance, freeze and memory leak issues label Oct 11, 2026
@dmitrivMS
Dmitriy Vasyura (dmitrivMS) merged commit 4dff635 into microsoft:main Oct 12, 2026
35 checks passed
@vs-code-engineering vs-code-engineering Bot added this to the 1.143.0 milestone Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

*error-fix-driving errors-fix freeze-slow-crash-leak VS Code crashing, performance, freeze and memory leak issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Error] unhandlederror-Invalid debug adapter

6 participants