Skip to content

Port StreamWriter leaveOpen:true dispose fix (runtime#125189) — Impact Assessment#1

Draft
ViveliDuCh wants to merge 1 commit into
mainfrom
fix-streamwriter-dispose-vmr
Draft

Port StreamWriter leaveOpen:true dispose fix (runtime#125189) — Impact Assessment#1
ViveliDuCh wants to merge 1 commit into
mainfrom
fix-streamwriter-dispose-vmr

Conversation

@ViveliDuCh
Copy link
Copy Markdown
Owner

@ViveliDuCh ViveliDuCh commented Mar 11, 2026

Goal

Assess whether dotnet/runtime#125189 — which fixes StreamWriter.Dispose() not setting _disposed = true when leaveOpen: true — should be merged into dotnet/runtime, by porting the changes into this VMR fork and measuring the blast radius across the entire .NET codebase.

Upstream issue: dotnet/runtime#89646


What this branch contains

All 5 file changes from runtime#125189, ported into src/runtime/:

File Change
StreamWriter.cs Core fix — _closable moved from outer guard to inner if, so _disposed = true executes unconditionally
Console.cs Reactive fix — new NonClosableStreamWriter subclass prevents Console.Out/Console.Error from breaking
StreamWriter.CloseTests.cs Tests expanded to cover leaveOpen: true disposal
StreamWriter.DisposeAsync.cs New DisposeAsync tests for disposed state
StreamReaderTests.cs Parallel test coverage for StreamReader consistency

Investigation Summary

More details in: https://gist.github.com/ViveliDuCh/e6e796a88f14d8a9918b67b06e3fe431

VMR-wide codebase search (25+ repos, 178,000+ files)

  • 46 files use StreamWriter with leaveOpen: true (9 production, 37 test)
  • 0 files contain the breaking pattern (writing to a disposed StreamWriter)
  • 5 StreamWriter subclasses found — none affected
  • Every usage follows the intended pattern: dispose writer → reuse stream

Test results

Core test suites (directly exercise the fix):

Test Suite Total Passed Failed Regressions
System.IO.Tests 1,713 1,712 1 (pre-existing, unrelated) 0
System.Console.Tests 4,266 4,266 0 0

Extended test suites (exercise leaveOpen consumers found in codebase search):

Test Suite Consumer(s) Tested Total Passed Failed Regressions
System.IO.Tests (TestLeaveOpen) StreamWriter/Reader leaveOpen contract 4 4 0 0
System.Formats.Tar.Tests TarReader/TarWriter with leaveOpen streams 6,384 6,363 0 0
System.Xml.Linq.SDMSample.Tests BridgeHelpers (29 StreamWriter leaveOpen usages) 110 110 0 0
NuGet.Packaging.Test KeyPairFileWriter (net10.0 TFM) 2,944 2,905 0 0
Microsoft.NET.Sdk.Razor.Tool.Tests ServerCommand.cs (leaveOpen StreamWriter) 50 37 13 (infra) 0

Previously blocked — now unblocked (after disk resize + repo bootstrapping):

Test Suite Consumer(s) Tested Total Passed Failed Regressions
GetDocumentInsider.Tests GetDocumentCommandWorker.cs (aspnetcore) 14 14 0 0
Microsoft.Build.Tasks.UnitTests ManifestWriter.cs (msbuild) 3,389 3,318 0 0

Combined: 18,874 total tests | 18,729 passed | 0 regressions

The 13 Razor Tool failures are SdkTestContext initialization errors (missing redist build artifact) — the 2 ServerCommand tests that directly exercise StreamWriter + leaveOpen both passed.

Key findings

  1. Zero internal breaking patterns. The only .NET code that depended on this bug was Console.Out/Console.Error (for 12+ years), which the PR addresses with NonClosableStreamWriter.
  2. Correctness alignment. StreamReader has always set _disposed = true regardless of leaveOpen. This fix makes StreamWriter consistent.
  3. Safe failure mode. Any external code that breaks will get a clear ObjectDisposedException at the exact call site — no silent corruption.
  4. Bounded external risk. The affected pattern (use-after-dispose) is documented as erroneous. Shipping in Preview 3 provides early signal before GA.

Reviewer concerns addressed

  • "Is there value in fixing behavior that has existed forever?" This is not a recent regression — the behavior dates back to when leaveOpen was introduced. Our finding: the measured blast radius is zero across the VMR, the correctness value is clear (IDisposable contract consistency with StreamReader), and there is no evidence of problems.
  • "The reactive Console fix hints at considerable noise." The concern is that needing to fix Console.Out/Console.Error suggests widespread dependents. Our finding: the VMR-wide search shows zero other code depending on this bug. The Console dependency was the sole exception, not the rule.

Recommendation

✅ Merge the fix into dotnet/runtime.

The change is correct, the measured impact is zero within .NET's own codebase (18,874 tests across 9 suites, 0 regressions — including all previously blocked suites now unblocked and passing), and the risk to external consumers is bounded by Preview 3 feedback and a trivial workaround. Not fixing it preserves a 12-year inconsistency between StreamWriter and StreamReader that undermines the IDisposable contract.

Suggested follow-up actions

  • Document as a breaking change in dotnet/docs
  • Monitor Preview 3 feedback for external reports of breakage
  • Consider adding an analyzer rule to detect use-after-dispose on StreamWriter
  • If significant external breakage emerges during preview, consider adding an AppContext switch as a temporary compat bridge

Port changes from dotnet/runtime#125189 into the VMR.
Fixes StreamWriter keeping working after Dispose() when leaveOpen:true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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.

1 participant