fix: guard multi-diff item clearBinding against re-entrant disposal (fixes #339312) - #339332
Open
VS Code PR Bot (vscodebot-pr) wants to merge 1 commit into
Conversation
…ixes microsoft#339312) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
VS Code PR Bot (vscodebot-pr)
October 2, 2026 17:13
View session
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The timing-sensitive disposal fix needs a focused regression test.
Review effort: Balanced
Findings: 1
What changed in this PR
Prevents re-entrant disposal from unbinding the same multi-diff editor template twice.
Changes:
- Adds a guarded binding-clear lifecycle with
try/finally. - Preserves existing template invariant checks and cleanup.
| File | Description |
|---|---|
virtualizedItemManager.ts |
Guards _clearBinding() against synchronous re-entry. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+229
to
+232
| if (!templateReference || !binding || this._isClearingBinding) { | ||
| return; | ||
| } | ||
| transaction(tx => { | ||
| this._delegate.onWillUnbind?.(binding, tx); | ||
| }); | ||
| binding.hide(); | ||
| binding.dispose(); | ||
| if (templateReference.object.currentBinding.get()) { | ||
| throw new BugIndicatingError('Virtualized binding did not release its template when disposed'); | ||
| this._isClearingBinding = true; |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
The multi-diff editor throws
BugIndicatingError: Cannot unbind a diff editor template from a different itemwhen aManagedVirtualizedItemdisposes a binding. Clearing a binding synchronously disposes the diff editor, which fires editor model-change events that drive the item's ownautorun; that autorun re-enters_clearBinding()and disposes the same binding a second time, sounbind()finds_viewModelalready cleared (undefined !== item) and throws. Impact: unhandled error surfaced to telemetry when closing/switching multi-diff editors (e.g. the Agent Sessions diff view) on all platforms.Fixes #339312
Recommended reviewer:
@hedietCulprit Commit
10de1b93436@hedietManagedVirtualizedItemlifecycle where_clearBinding()disposes a binding whosedispose()synchronously re-enters the same item'sautorunvia editor events; the clear path has no re-entrancy guard, so the nested dispose runs against already-cleared state. Subsequent commits (98e228b47bc,519f5fa46f0) refined the same lifecycle but kept the re-entrant clear path.Code Flow
sequenceDiagram participant Autorun as ManagedVirtualizedItem.autorun participant Clear as _clearBinding() participant Binding as DiffEditorItemBinding.dispose() participant Unbind as DiffEditorItemTemplate.unbind()/setItem() participant Editor as DiffEditor events Autorun->>Clear: item hidden, not kept alive Note over Clear: ⚠️ Root cause:<br/>no re-entrancy guard Clear->>Binding: binding.dispose() Binding->>Unbind: _template.unbind(item) (before super.dispose) Note over Unbind: setItem(undefined)<br/>_viewModel = undefined<br/>editor.setDiffModel(null) Unbind->>Editor: synchronous model-change event Editor->>Autorun: re-enters autorun Autorun->>Clear: _clearBinding() again Clear->>Binding: binding.dispose() again Binding->>Unbind: unbind(item) Note over Unbind: 💥 _viewModel (undefined) !== item<br/>Cannot unbind a diff editor template from a different itemAffected Files
src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.tsthrow new BugIndicatingError('Cannot unbind a diff editor template from a different item')src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.tsdispose()callsthis._template.unbind(this.item)beforesuper.dispose(), socurrentBinding/_storeare still live;unbind→setItem(undefined)fires editor events synchronouslysrc/vs/editor/browser/widget/multiDiffEditor/virtualizedItemManager.ts_clearBinding()disposes the binding with no guard; the autorun at L149-L155 re-enters it during that synchronous disposeRepro Steps
This is a timing-dependent re-entrancy, reproduced by closing/hiding a multi-diff item whose diff editor fires a synchronous model-change event during disposal.
_clearBinding(),binding.dispose()→unbind()→setItem(undefined)→editor.setDiffModel(null)fires an editor event that re-enters the item'sautorun, calling_clearBinding()a second time on the same binding.unbind()sees_viewModel === undefined,undefined !== item, and throwsBugIndicatingError.How the Fix Works
Chosen approach —
src/vs/editor/browser/widget/multiDiffEditor/virtualizedItemManager.ts,ManagedVirtualizedItem._clearBinding(): add an_isClearingBindingguard flag. On entry, if a clear is already in progress the method returns immediately; otherwise it sets the flag, performs the unbind/dispose/reset sequence inside atry, and resets the flag infinally. This fixes the bug at the data producer — the clear path that initiates the re-entrant disposal — rather than at the crash site. The outer_clearBinding()invocation still completes the full clear atomically (it sets_templateReferencetoundefinedand disposes the reference after the binding dispose returns), so the nested call being a no-op does not leave the item half-cleared. The throw insideunbind()is preserved as a genuine invariant check for other callers; it is simply no longer reached on this re-entrant path because the duplicate dispose never starts.Why this is correct: the guard makes the unsupported "dispose during dispose" ordering unrepresentable at the producer, which is the lifecycle-fix priority (reorder/serialize the producer's state mutations), instead of teaching the consumer
unbind()to tolerate inconsistent state. NologService.erroror telemetry path is removed, and notry/catchswallows the error.Alternatives considered:
unbind()/DiffEditorItemBinding.dispose()by comparing_viewModeland returning early — rejected: that is a crash-site guard that silently tolerates double-dispose and would mask the same re-entrancy for every other binding type, hiding the producer bug.editor.setDiffModel(null)to a microtask so events do not fire synchronously — rejected: broader behavioral change to editor teardown timing with higher regression risk than serializing the clear.Recommended Owner
@hediet— author of the multi-diff virtualization refactor (10de1b93436) and the follow-up lifecycle fixes; activemicrosoft/vscodecore team member (write access, multiple commits in the last 90 days).