From 14b910ea5d20ad6d7ddcd950088aa714fc40e9bc Mon Sep 17 00:00:00 2001 From: Scott Lerch Date: Thu, 6 Aug 2026 20:49:56 -0700 Subject: [PATCH] Guard finalizer against partial construction (#446) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- UnitTests/Tests.cs | 29 +++++++++++++++++++++++++++++ src/RecyclableMemoryStream.cs | 5 +++++ 2 files changed, 34 insertions(+) diff --git a/UnitTests/Tests.cs b/UnitTests/Tests.cs index c5380b53..5b07a0d3 100644 --- a/UnitTests/Tests.cs +++ b/UnitTests/Tests.cs @@ -27,6 +27,8 @@ namespace Microsoft.IO.UnitTests using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.Reflection; + using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -2481,6 +2483,33 @@ public void AdvanceOverReplacedTempBufferDoesNotMakeWritesVisible() #endregion #region Dispose and Pooling Tests + [TestCase(false, false)] + [TestCase(true, false)] + [TestCase(false, true)] + public void Dispose_PartiallyConstructedStream_DoesNotThrow(bool assignMemoryManager, bool assignBlocks) + { + var stream = (RecyclableMemoryStream)RuntimeHelpers.GetUninitializedObject(typeof(RecyclableMemoryStream)); + try + { + if (assignMemoryManager) + { + typeof(RecyclableMemoryStream).GetField("memoryManager", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(stream, this.GetMemoryManager()); + } + + if (assignBlocks) + { + typeof(RecyclableMemoryStream).GetField("blocks", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(stream, new List()); + } + + var dispose = typeof(RecyclableMemoryStream).GetMethod("Dispose", BindingFlags.Instance | BindingFlags.NonPublic, null, [typeof(bool)], null)!; + Assert.DoesNotThrow(() => dispose.Invoke(stream, [false])); + } + finally + { + GC.SuppressFinalize(stream); + } + } + [Test] public void Pooling_NewMemoryManagerHasZeroFreeAndInUseBytes() { diff --git a/src/RecyclableMemoryStream.cs b/src/RecyclableMemoryStream.cs index 27a2064f..32dda9ac 100644 --- a/src/RecyclableMemoryStream.cs +++ b/src/RecyclableMemoryStream.cs @@ -277,6 +277,11 @@ internal RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Gui /// Whether we're disposing (true), or being called by the finalizer (false). protected override void Dispose(bool disposing) { + if (this.memoryManager == null || this.blocks == null) + { + return; + } + if (this.disposed) { string? doubleDisposeStack = null;