From faf77aab7b78180fad80df850c00cef2f9424628 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:55:27 +0000 Subject: [PATCH 01/25] Initial plan From bc41dce6fc6f4f32e6cd0683cc44b7d3d74d8652 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:22:26 +0000 Subject: [PATCH 02/25] Fix NativeAOT OOM message not printed on Linux before Abort() Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- .../src/System/RuntimeExceptionHelpers.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index a139757b4b69e6..72299c4bb9fc3c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -236,7 +236,18 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio if (previousThreadId == 0) { bool minimalFailFast = (exception == PreallocatedOutOfMemoryException.Instance); - if (!minimalFailFast) + if (minimalFailFast) + { + // Minimal OOM fail-fast path: avoid heap allocations as much as possible, but still + // report that OOM is the reason for the crash. + try + { + Internal.Console.Error.Write("Process is terminating due to OutOfMemoryException."); + Internal.Console.Error.WriteLine(); + } + catch { } + } + else { Internal.Console.Error.Write(((exception == null) || (reason is RhFailFastReason.EnvironmentFailFast or RhFailFastReason.AssertionFailure)) ? "Process terminated. " : "Unhandled exception. "); From 3ef2a6d8351e970f98cb615b51c9e267c4591352 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:55:44 +0000 Subject: [PATCH 03/25] Add OomHandling smoke test for NativeAOT OOM message reporting Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- .../SmokeTests/OomHandling/OomHandling.cs | 82 +++++++++++++++++++ .../SmokeTests/OomHandling/OomHandling.csproj | 13 +++ 2 files changed, 95 insertions(+) create mode 100644 src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs create mode 100644 src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs new file mode 100644 index 00000000000000..b1a2547c7bfa21 --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This test verifies that an out-of-memory condition in a NativeAOT process +// produces a diagnostic message on stderr before the process terminates. +// +// The test spawns itself as a subprocess with a small GC heap limit set via +// DOTNET_GCHeapHardLimit so that the subprocess reliably runs out of memory. +// The outer process then validates that the subprocess wrote the expected +// OOM message to its standard error stream. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +class OomHandlingTest +{ + const int Pass = 100; + const int Fail = -1; + + const string AllocateArg = "--allocate"; + // Both the minimal OOM fail-fast path ("Process is terminating due to OutOfMemoryException.") + // and the standard unhandled-exception path ("Unhandled exception. System.OutOfMemoryException...") + // contain this token. The test validates that some OOM diagnostic is printed rather than + // just "Aborted" with no context. + const string ExpectedToken = "OutOfMemoryException"; + + static int Main(string[] args) + { + if (args.Length > 0 && args[0] == AllocateArg) + { + // Subprocess mode: allocate until OOM is triggered. + List list = new(); + while (true) + list.Add(new byte[128 * 1024]); + } + + // Controller mode: launch a subprocess with a GC heap limit and verify its output. + string? processPath = Environment.ProcessPath; + if (processPath == null) + { + Console.WriteLine("ProcessPath is null, skipping test."); + return Pass; + } + + var psi = new ProcessStartInfo(processPath, AllocateArg) + { + RedirectStandardError = true, + UseShellExecute = false, + }; + // A 20 MB GC heap limit is small enough to exhaust quickly but large enough for startup. + psi.Environment["DOTNET_GCHeapHardLimit"] = "20000000"; + + using Process? p = Process.Start(psi); + if (p == null) + { + Console.WriteLine("Failed to start subprocess."); + return Fail; + } + + // Read stderr before waiting to avoid deadlock. + string stderr = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + Console.WriteLine($"Subprocess exit code: {p.ExitCode}"); + Console.WriteLine($"Subprocess stderr: {stderr}"); + + if (p.ExitCode == 0 || p.ExitCode == Pass) + { + Console.WriteLine("Expected a non-success exit code from the OOM subprocess."); + return Fail; + } + + if (!stderr.Contains(ExpectedToken)) + { + Console.WriteLine($"Expected stderr to contain: {ExpectedToken}"); + return Fail; + } + + return Pass; + } +} diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj new file mode 100644 index 00000000000000..704c5d2ab17dd5 --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj @@ -0,0 +1,13 @@ + + + Exe + 0 + + true + true + false + + + + + From 99a6529e072990c930d65b6e74a43913b9b30a90 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 14 Apr 2026 18:22:34 -0700 Subject: [PATCH 04/25] Update minimalFailFast condition and test --- .../src/System/RuntimeExceptionHelpers.cs | 2 +- .../nativeaot/SmokeTests/OomHandling/OomHandling.cs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 72299c4bb9fc3c..687f3211c2f1b8 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -235,7 +235,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio ulong previousThreadId = Interlocked.CompareExchange(ref s_crashingThreadId, currentThreadId, 0); if (previousThreadId == 0) { - bool minimalFailFast = (exception == PreallocatedOutOfMemoryException.Instance); + bool minimalFailFast = (exception is OutOfMemoryException); if (minimalFailFast) { // Minimal OOM fail-fast path: avoid heap allocations as much as possible, but still diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs index b1a2547c7bfa21..c64ec27a73977a 100644 --- a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs +++ b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs @@ -30,9 +30,12 @@ static int Main(string[] args) if (args.Length > 0 && args[0] == AllocateArg) { // Subprocess mode: allocate until OOM is triggered. - List list = new(); - while (true) - list.Add(new byte[128 * 1024]); + // Phase 1: fill quickly with large blocks to use most of the heap. + // Phase 2: exhaust remaining scraps with small allocations so that + // virtually no memory is left when OOM is finally thrown. + var list = new List(); + try { while (true) list.Add(new byte[16 * 1024]); } catch (OutOfMemoryException) { } + while (true) list.Add(new object()); } // Controller mode: launch a subprocess with a GC heap limit and verify its output. @@ -48,8 +51,8 @@ static int Main(string[] args) RedirectStandardError = true, UseShellExecute = false, }; - // A 20 MB GC heap limit is small enough to exhaust quickly but large enough for startup. - psi.Environment["DOTNET_GCHeapHardLimit"] = "20000000"; + // A 32 MB GC heap limit is small enough to exhaust quickly but large enough for startup. + psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; using Process? p = Process.Start(psi); if (p == null) From 57092c7815e74f64c50108a1d40b120e72b4826c Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 14 Apr 2026 18:41:58 -0700 Subject: [PATCH 05/25] Add test timeout --- src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs index c64ec27a73977a..fc921dcdacf581 100644 --- a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs +++ b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs @@ -17,6 +17,7 @@ class OomHandlingTest { const int Pass = 100; const int Fail = -1; + const int TimeoutMilliseconds = 30 * 1000; const string AllocateArg = "--allocate"; // Both the minimal OOM fail-fast path ("Process is terminating due to OutOfMemoryException.") @@ -63,7 +64,12 @@ static int Main(string[] args) // Read stderr before waiting to avoid deadlock. string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); + if (!p.WaitForExit(TimeoutMilliseconds)) + { + p.Kill(true); + Console.WriteLine($"Subprocess timed out after {TimeoutMilliseconds / 1000} seconds."); + return Fail; + } Console.WriteLine($"Subprocess exit code: {p.ExitCode}"); Console.WriteLine($"Subprocess stderr: {stderr}"); From cf2ca9541daeff7a9ffed13d1d361972746138c6 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 14 Apr 2026 18:48:26 -0700 Subject: [PATCH 06/25] Revert changes to minimalFailFast --- .../src/System/RuntimeExceptionHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 687f3211c2f1b8..72299c4bb9fc3c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -235,7 +235,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio ulong previousThreadId = Interlocked.CompareExchange(ref s_crashingThreadId, currentThreadId, 0); if (previousThreadId == 0) { - bool minimalFailFast = (exception is OutOfMemoryException); + bool minimalFailFast = (exception == PreallocatedOutOfMemoryException.Instance); if (minimalFailFast) { // Minimal OOM fail-fast path: avoid heap allocations as much as possible, but still From 2eafa92c7bc48be4779c8ac7f462d09e9b611c6f Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 15 Apr 2026 23:55:14 -0700 Subject: [PATCH 07/25] Fix test --- .../src/System/RuntimeExceptionHelpers.cs | 42 ++++++++++++++----- .../SmokeTests/OomHandling/OomHandling.cs | 41 ++++++++++++++---- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 72299c4bb9fc3c..1ed1f75e7b9aac 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -201,16 +201,23 @@ internal static void SerializeCrashInfo(RhFailFastReason reason, string? message int previousState = Interlocked.CompareExchange(ref s_crashInfoPresent, -1, 0); if (previousState == 0) { - CrashInfo crashInfo = new(); + try + { + CrashInfo crashInfo = new(); - crashInfo.Open(reason, Thread.CurrentOSThreadId, message ?? GetStringForFailFastReason(reason)); - if (exception != null) + crashInfo.Open(reason, Thread.CurrentOSThreadId, message ?? GetStringForFailFastReason(reason)); + if (exception != null) + { + crashInfo.WriteException(exception); + } + crashInfo.Close(); + s_triageBufferAddress = crashInfo.TriageBufferAddress; + s_triageBufferSize = crashInfo.TriageBufferSize; + } + catch { - crashInfo.WriteException(exception); + // If crash info serialization fails (for example, due to OOM), proceed without it. } - crashInfo.Close(); - s_triageBufferAddress = crashInfo.TriageBufferAddress; - s_triageBufferSize = crashInfo.TriageBufferSize; s_crashInfoPresent = 1; } @@ -235,7 +242,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio ulong previousThreadId = Interlocked.CompareExchange(ref s_crashingThreadId, currentThreadId, 0); if (previousThreadId == 0) { - bool minimalFailFast = (exception == PreallocatedOutOfMemoryException.Instance); + bool minimalFailFast = exception == PreallocatedOutOfMemoryException.Instance; if (minimalFailFast) { // Minimal OOM fail-fast path: avoid heap allocations as much as possible, but still @@ -277,8 +284,23 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio if ((exception != null) && (reason is not RhFailFastReason.AssertionFailure)) { - Internal.Console.Error.Write(exception.ToString()); - Internal.Console.Error.WriteLine(); + try + { + Internal.Console.Error.Write(exception.ToString()); + Internal.Console.Error.WriteLine(); + } + catch + { + // If ToString() fails (for example, due to OOM), fall back to printing just the type name. + try + { + Internal.Console.Error.Write("Process is terminating due to "); + Internal.Console.Error.Write(exception.GetType().FullName); + Internal.Console.Error.Write("."); + Internal.Console.Error.WriteLine(); + } + catch { } + } } #if TARGET_WINDOWS diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs index fc921dcdacf581..dc9dd3d8ef2462 100644 --- a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs +++ b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading.Tasks; class OomHandlingTest { @@ -19,7 +20,8 @@ class OomHandlingTest const int Fail = -1; const int TimeoutMilliseconds = 30 * 1000; - const string AllocateArg = "--allocate"; + const string AllocateSmallArg = "--allocate-small"; + const string AllocateLargeArg = "--allocate-large"; // Both the minimal OOM fail-fast path ("Process is terminating due to OutOfMemoryException.") // and the standard unhandled-exception path ("Unhandled exception. System.OutOfMemoryException...") // contain this token. The test validates that some OOM diagnostic is printed rather than @@ -28,7 +30,7 @@ class OomHandlingTest static int Main(string[] args) { - if (args.Length > 0 && args[0] == AllocateArg) + if (args.Length > 0 && args[0] == AllocateSmallArg) { // Subprocess mode: allocate until OOM is triggered. // Phase 1: fill quickly with large blocks to use most of the heap. @@ -39,7 +41,16 @@ static int Main(string[] args) while (true) list.Add(new object()); } - // Controller mode: launch a subprocess with a GC heap limit and verify its output. + if (args.Length > 0 && args[0] == AllocateLargeArg) + { + // Subprocess mode: allocate 128 KB chunks until OOM is triggered. + // This leaves some free memory when OOM fires, exercising the code + // path where GetRuntimeException may allocate a new OutOfMemoryException. + var list = new List(); + while (true) list.Add(new byte[128 * 1024]); + } + + // Controller mode: launch subprocesses with a GC heap limit and verify their output. string? processPath = Environment.ProcessPath; if (processPath == null) { @@ -47,29 +58,43 @@ static int Main(string[] args) return Pass; } - var psi = new ProcessStartInfo(processPath, AllocateArg) + int result = RunSubprocess(processPath, AllocateSmallArg, "small allocations"); + if (result != Pass) + return result; + + result = RunSubprocess(processPath, AllocateLargeArg, "large allocations"); + return result; + } + + static int RunSubprocess(string processPath, string allocateArg, string description) + { + Console.WriteLine($"Testing OOM with {description}..."); + + var psi = new ProcessStartInfo(processPath, allocateArg) { RedirectStandardError = true, UseShellExecute = false, }; - // A 32 MB GC heap limit is small enough to exhaust quickly but large enough for startup. + // 0x2000000 = 32 MB GC heap limit: small enough to exhaust quickly but large enough for startup. psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; using Process? p = Process.Start(psi); - if (p == null) + if (p is null) { Console.WriteLine("Failed to start subprocess."); return Fail; } - // Read stderr before waiting to avoid deadlock. - string stderr = p.StandardError.ReadToEnd(); + // Read stderr asynchronously so that WaitForExit can enforce the timeout. + // A synchronous ReadToEnd() would block until the child exits, defeating the timeout. + Task stderrTask = p.StandardError.ReadToEndAsync(); if (!p.WaitForExit(TimeoutMilliseconds)) { p.Kill(true); Console.WriteLine($"Subprocess timed out after {TimeoutMilliseconds / 1000} seconds."); return Fail; } + string stderr = stderrTask.GetAwaiter().GetResult(); Console.WriteLine($"Subprocess exit code: {p.ExitCode}"); Console.WriteLine($"Subprocess stderr: {stderr}"); From cc4358889122a6d9658560f31c8e0306ef14b27a Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 16 Apr 2026 18:51:14 -0700 Subject: [PATCH 08/25] Show consistent error messages --- .../src/System/RuntimeExceptionHelpers.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 1ed1f75e7b9aac..8b6c0608580299 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -249,7 +249,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio // report that OOM is the reason for the crash. try { - Internal.Console.Error.Write("Process is terminating due to OutOfMemoryException."); + Internal.Console.Error.Write("Process terminated. System.OutOfMemoryException."); Internal.Console.Error.WriteLine(); } catch { } @@ -294,7 +294,6 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio // If ToString() fails (for example, due to OOM), fall back to printing just the type name. try { - Internal.Console.Error.Write("Process is terminating due to "); Internal.Console.Error.Write(exception.GetType().FullName); Internal.Console.Error.Write("."); Internal.Console.Error.WriteLine(); From 15e81d4da873f6614ee7beda3ad590069e764f19 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Thu, 16 Apr 2026 19:04:13 -0700 Subject: [PATCH 09/25] Apply suggestion from @jkotas --- .../System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 8b6c0608580299..c0726e357c416c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -295,7 +295,6 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio try { Internal.Console.Error.Write(exception.GetType().FullName); - Internal.Console.Error.Write("."); Internal.Console.Error.WriteLine(); } catch { } From f373a39131ebf18a89c1288e497becfc73ac253b Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 20 Apr 2026 16:38:28 -0700 Subject: [PATCH 10/25] Code review feedback --- .../src/System/RuntimeExceptionHelpers.cs | 2 +- src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index c0726e357c416c..08d3b7b621f9d6 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -249,7 +249,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio // report that OOM is the reason for the crash. try { - Internal.Console.Error.Write("Process terminated. System.OutOfMemoryException."); + Internal.Console.Error.Write("Process terminated. System.OutOfMemoryException"); Internal.Console.Error.WriteLine(); } catch { } diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs index dc9dd3d8ef2462..031c367d23a54f 100644 --- a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs +++ b/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs @@ -22,7 +22,7 @@ class OomHandlingTest const string AllocateSmallArg = "--allocate-small"; const string AllocateLargeArg = "--allocate-large"; - // Both the minimal OOM fail-fast path ("Process is terminating due to OutOfMemoryException.") + // Both the minimal OOM fail-fast path ("Process terminated. System.OutOfMemoryException") // and the standard unhandled-exception path ("Unhandled exception. System.OutOfMemoryException...") // contain this token. The test validates that some OOM diagnostic is printed rather than // just "Aborted" with no context. @@ -91,6 +91,8 @@ static int RunSubprocess(string processPath, string allocateArg, string descript if (!p.WaitForExit(TimeoutMilliseconds)) { p.Kill(true); + p.WaitForExit(); + _ = stderrTask.GetAwaiter().GetResult(); Console.WriteLine($"Subprocess timed out after {TimeoutMilliseconds / 1000} seconds."); return Fail; } From aa3ce5e27f490df7b95db3178fec5b47dbdf5abf Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 13 May 2026 15:58:26 -0700 Subject: [PATCH 11/25] Move test to src/tests/baseservices/exceptions --- .../exceptions/OutOfMemoryException/OutOfMemoryException.cs} | 4 ++-- .../OutOfMemoryException/OutOfMemoryException.csproj} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/tests/{nativeaot/SmokeTests/OomHandling/OomHandling.cs => baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs} (96%) rename src/tests/{nativeaot/SmokeTests/OomHandling/OomHandling.csproj => baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj} (92%) diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs similarity index 96% rename from src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs rename to src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 031c367d23a54f..0a21760f187f78 100644 --- a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// This test verifies that an out-of-memory condition in a NativeAOT process -// produces a diagnostic message on stderr before the process terminates. +// This test verifies that an out-of-memory condition produces a diagnostic +// message on stderr before the process terminates. // // The test spawns itself as a subprocess with a small GC heap limit set via // DOTNET_GCHeapHardLimit so that the subprocess reliably runs out of memory. diff --git a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj similarity index 92% rename from src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj rename to src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj index 704c5d2ab17dd5..1ed2cfe43451f3 100644 --- a/src/tests/nativeaot/SmokeTests/OomHandling/OomHandling.csproj +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj @@ -8,6 +8,6 @@ false - + From 821b61d20175f2ce533f31ec578039ee9de3eeb0 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 13 May 2026 17:23:06 -0700 Subject: [PATCH 12/25] Move test to src/tests/baseservices/exceptions, make it work correctly for CoreCLR/NativeAOT --- .../OutOfMemoryException.cs | 24 +++++++++---------- .../OutOfMemoryException.csproj | 1 + 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 0a21760f187f78..336724b0f6b22a 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -14,7 +14,7 @@ using System.Diagnostics; using System.Threading.Tasks; -class OomHandlingTest +class OutOfMemoryExceptionTest { const int Pass = 100; const int Fail = -1; @@ -51,32 +51,30 @@ static int Main(string[] args) } // Controller mode: launch subprocesses with a GC heap limit and verify their output. - string? processPath = Environment.ProcessPath; - if (processPath == null) - { - Console.WriteLine("ProcessPath is null, skipping test."); - return Pass; - } - - int result = RunSubprocess(processPath, AllocateSmallArg, "small allocations"); + int result = RunSubprocess(AllocateSmallArg, "small allocations"); if (result != Pass) return result; - result = RunSubprocess(processPath, AllocateLargeArg, "large allocations"); - return result; + return RunSubprocess(AllocateLargeArg, "large allocations"); } - static int RunSubprocess(string processPath, string allocateArg, string description) + static int RunSubprocess(string allocateArg, string description) { Console.WriteLine($"Testing OOM with {description}..."); - var psi = new ProcessStartInfo(processPath, allocateArg) + string fileName = Process.GetCurrentProcess().MainModule.FileName; + string arguments = TestLibrary.Utilities.IsNativeAot + ? allocateArg + : $"{typeof(OutOfMemoryExceptionTest).Assembly.Location} {allocateArg}"; + + var psi = new ProcessStartInfo(fileName, arguments) { RedirectStandardError = true, UseShellExecute = false, }; // 0x2000000 = 32 MB GC heap limit: small enough to exhaust quickly but large enough for startup. psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; + psi.Environment["DOTNET_DbgEnableMiniDump"] = "0"; using Process? p = Process.Start(psi); if (p is null) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj index 1ed2cfe43451f3..75dc5e880d1dc6 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj @@ -9,5 +9,6 @@ + From fb34a0dfceaf12654b7a0c111758b69eee57d2b7 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 13 May 2026 17:44:37 -0700 Subject: [PATCH 13/25] Nit --- .../exceptions/OutOfMemoryException/OutOfMemoryException.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 336724b0f6b22a..8145c54feefcdb 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -72,8 +72,8 @@ static int RunSubprocess(string allocateArg, string description) RedirectStandardError = true, UseShellExecute = false, }; - // 0x2000000 = 32 MB GC heap limit: small enough to exhaust quickly but large enough for startup. - psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; + // 32 MB GC heap limit (hex): small enough to exhaust quickly but large enough for startup. + psi.Environment["DOTNET_GCHeapHardLimit"] = "0x2000000"; psi.Environment["DOTNET_DbgEnableMiniDump"] = "0"; using Process? p = Process.Start(psi); From c12f2924d9d450b3f59855e6f19a9a19238a6b10 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 13 May 2026 21:55:20 -0700 Subject: [PATCH 14/25] Adjust timeout and disable on Mono --- .../exceptions/OutOfMemoryException/OutOfMemoryException.cs | 2 +- .../exceptions/OutOfMemoryException/OutOfMemoryException.csproj | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 8145c54feefcdb..ec029a0ec69432 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -18,7 +18,7 @@ class OutOfMemoryExceptionTest { const int Pass = 100; const int Fail = -1; - const int TimeoutMilliseconds = 30 * 1000; + const int TimeoutMilliseconds = 60 * 1000; const string AllocateSmallArg = "--allocate-small"; const string AllocateLargeArg = "--allocate-large"; diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj index 75dc5e880d1dc6..f1b173fcd5e948 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj @@ -6,6 +6,8 @@ true true false + + true From 88e7eae8b6449dfc63ac83a7c5e1ba01cc9eaa50 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 13 May 2026 21:57:39 -0700 Subject: [PATCH 15/25] Nit --- .../exceptions/OutOfMemoryException/OutOfMemoryException.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj index f1b173fcd5e948..5337f033469a3f 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj @@ -6,7 +6,7 @@ true true false - + true From 5984be757d61f09901973d19ff3c166ba90fb489 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Thu, 28 May 2026 11:57:35 -0700 Subject: [PATCH 16/25] Update src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michal Strehovský --- .../src/System/RuntimeExceptionHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 08d3b7b621f9d6..6322f0996a5bc3 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -249,7 +249,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio // report that OOM is the reason for the crash. try { - Internal.Console.Error.Write("Process terminated. System.OutOfMemoryException"); + Internal.Console.Error.Write("Process is terminating due to OutOfMemoryException."); Internal.Console.Error.WriteLine(); } catch { } From 027d91fd494ddf93be6f6d5f2b1e1e7ed9487f37 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 28 May 2026 16:20:48 -0700 Subject: [PATCH 17/25] Make OOM minimal message consistent with CoreCLR --- .../src/System/RuntimeExceptionHelpers.cs | 3 ++- .../OutOfMemoryException.cs | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 6322f0996a5bc3..119b590d5c2140 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -249,7 +249,8 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio // report that OOM is the reason for the crash. try { - Internal.Console.Error.Write("Process is terminating due to OutOfMemoryException."); + // Try to print a short message at least. + Internal.Console.Error.Write("Out of memory."); Internal.Console.Error.WriteLine(); } catch { } diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index ec029a0ec69432..19cabd361383e1 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -26,7 +26,8 @@ class OutOfMemoryExceptionTest // and the standard unhandled-exception path ("Unhandled exception. System.OutOfMemoryException...") // contain this token. The test validates that some OOM diagnostic is printed rather than // just "Aborted" with no context. - const string ExpectedToken = "OutOfMemoryException"; + const string ExpectedOomToken = "OutOfMemoryException"; + const string ExpectedMinimalOomToken = "Out of memory."; static int Main(string[] args) { @@ -38,7 +39,12 @@ static int Main(string[] args) // virtually no memory is left when OOM is finally thrown. var list = new List(); try { while (true) list.Add(new byte[16 * 1024]); } catch (OutOfMemoryException) { } - while (true) list.Add(new object()); + // If we keep adding elements to the list, it's possible that the list's + // internal array fails when trying a big allocation to grow. + // Instead, we create a long chain of objects that will fail with OOM when + // trying to allocate the next one. + object a = null; + for (;;) a = new object[] { a }; } if (args.Length > 0 && args[0] == AllocateLargeArg) @@ -72,8 +78,8 @@ static int RunSubprocess(string allocateArg, string description) RedirectStandardError = true, UseShellExecute = false, }; - // 32 MB GC heap limit (hex): small enough to exhaust quickly but large enough for startup. - psi.Environment["DOTNET_GCHeapHardLimit"] = "0x2000000"; + // 32 MB GC heap limit: small enough to exhaust quickly but large enough for startup. + psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; psi.Environment["DOTNET_DbgEnableMiniDump"] = "0"; using Process? p = Process.Start(psi); @@ -105,9 +111,17 @@ static int RunSubprocess(string allocateArg, string description) return Fail; } - if (!stderr.Contains(ExpectedToken)) + if (allocateArg == AllocateSmallArg && !stderr.Contains(ExpectedMinimalOomToken)) { - Console.WriteLine($"Expected stderr to contain: {ExpectedToken}"); + // This test should exercise the minimal OOM fail-fast path. + Console.WriteLine($"Expected minimal OOM diagnostic token not found in subprocess stderr."); + return Fail; + } + + // In the general case, we expect either a message containing "OutOfMemoryException" or the minimal OOM message. + if (!(stderr.Contains(ExpectedOomToken) || stderr.Contains(ExpectedMinimalOomToken))) + { + Console.WriteLine($"Expected OOM diagnostic token not found in subprocess stderr."); return Fail; } From a2b7dec5a9c1a330cd9c6d97e37e27d98cbfdc4b Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 28 May 2026 16:24:14 -0700 Subject: [PATCH 18/25] Update comment --- .../src/System/RuntimeExceptionHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs index 119b590d5c2140..52463e91b87e04 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -249,7 +249,7 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio // report that OOM is the reason for the crash. try { - // Try to print a short message at least. + // Try to print the same short message CoreCLR prints. Internal.Console.Error.Write("Out of memory."); Internal.Console.Error.WriteLine(); } From 1e2888e0bb2a54affcac2e27c84efe18ff5b55f0 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 2 Jun 2026 11:21:13 -0700 Subject: [PATCH 19/25] Use Process.RunAndCaptureText --- .../OutOfMemoryException.cs | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 19cabd361383e1..ae1c12a800a294 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -12,7 +12,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; class OutOfMemoryExceptionTest { @@ -68,49 +67,42 @@ static int RunSubprocess(string allocateArg, string description) { Console.WriteLine($"Testing OOM with {description}..."); - string fileName = Process.GetCurrentProcess().MainModule.FileName; - string arguments = TestLibrary.Utilities.IsNativeAot - ? allocateArg - : $"{typeof(OutOfMemoryExceptionTest).Assembly.Location} {allocateArg}"; + string fileName = Environment.ProcessPath; + string[] arguments = TestLibrary.Utilities.IsNativeAot + ? [allocateArg] + : [typeof(OutOfMemoryExceptionTest).Assembly.Location, allocateArg]; var psi = new ProcessStartInfo(fileName, arguments) { + RedirectStandardOutput = true, RedirectStandardError = true, - UseShellExecute = false, }; // 32 MB GC heap limit: small enough to exhaust quickly but large enough for startup. psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; psi.Environment["DOTNET_DbgEnableMiniDump"] = "0"; - using Process? p = Process.Start(psi); - if (p is null) + ProcessTextOutput output; + try { - Console.WriteLine("Failed to start subprocess."); - return Fail; + output = Process.RunAndCaptureText(psi, TimeSpan.FromMilliseconds(TimeoutMilliseconds)); } - - // Read stderr asynchronously so that WaitForExit can enforce the timeout. - // A synchronous ReadToEnd() would block until the child exits, defeating the timeout. - Task stderrTask = p.StandardError.ReadToEndAsync(); - if (!p.WaitForExit(TimeoutMilliseconds)) + catch (TimeoutException) { - p.Kill(true); - p.WaitForExit(); - _ = stderrTask.GetAwaiter().GetResult(); Console.WriteLine($"Subprocess timed out after {TimeoutMilliseconds / 1000} seconds."); return Fail; } - string stderr = stderrTask.GetAwaiter().GetResult(); - Console.WriteLine($"Subprocess exit code: {p.ExitCode}"); - Console.WriteLine($"Subprocess stderr: {stderr}"); + Console.WriteLine($"Subprocess exit code: {output.ExitStatus.ExitCode}"); + Console.WriteLine($"Subprocess stderr: {output.StandardError}"); - if (p.ExitCode == 0 || p.ExitCode == Pass) + if (output.ExitStatus.ExitCode == 0 || output.ExitStatus.ExitCode == Pass) { Console.WriteLine("Expected a non-success exit code from the OOM subprocess."); return Fail; } + string stderr = output.StandardError; + if (allocateArg == AllocateSmallArg && !stderr.Contains(ExpectedMinimalOomToken)) { // This test should exercise the minimal OOM fail-fast path. From e3b0bd467afe02fc2e0dc8595105082b2a467adc Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:36:53 -0700 Subject: [PATCH 20/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../OutOfMemoryException/OutOfMemoryException.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index ae1c12a800a294..4c8133965403de 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -21,10 +21,9 @@ class OutOfMemoryExceptionTest const string AllocateSmallArg = "--allocate-small"; const string AllocateLargeArg = "--allocate-large"; - // Both the minimal OOM fail-fast path ("Process terminated. System.OutOfMemoryException") - // and the standard unhandled-exception path ("Unhandled exception. System.OutOfMemoryException...") - // contain this token. The test validates that some OOM diagnostic is printed rather than - // just "Aborted" with no context. + // The standard unhandled-exception path ("Unhandled exception. System.OutOfMemoryException...") + // contains this token. The minimal OOM fail-fast path may only print a short "Out of memory." message. + // The test validates that some OOM diagnostic is printed rather than just "Aborted" with no context. const string ExpectedOomToken = "OutOfMemoryException"; const string ExpectedMinimalOomToken = "Out of memory."; From ae0981ec8aac79839d264ac0c7e35bade395d7ec Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:44:18 -0700 Subject: [PATCH 21/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../exceptions/OutOfMemoryException/OutOfMemoryException.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 4c8133965403de..4c59acb02813d5 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -76,8 +76,8 @@ static int RunSubprocess(string allocateArg, string description) RedirectStandardOutput = true, RedirectStandardError = true, }; - // 32 MB GC heap limit: small enough to exhaust quickly but large enough for startup. - psi.Environment["DOTNET_GCHeapHardLimit"] = "2000000"; + // 32 MB GC heap limit (0x2000000): small enough to exhaust quickly but large enough for startup. + psi.Environment["DOTNET_GCHeapHardLimit"] = "0x2000000"; psi.Environment["DOTNET_DbgEnableMiniDump"] = "0"; ProcessTextOutput output; From 9559af20dd8b6baab06aa42937b8290b20cb03ba Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 2 Jun 2026 15:37:36 -0700 Subject: [PATCH 22/25] Accept both minimal and standard OOM message --- .../OutOfMemoryException/OutOfMemoryException.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 4c59acb02813d5..3b36eefe1b4f56 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -102,14 +102,9 @@ static int RunSubprocess(string allocateArg, string description) string stderr = output.StandardError; - if (allocateArg == AllocateSmallArg && !stderr.Contains(ExpectedMinimalOomToken)) - { - // This test should exercise the minimal OOM fail-fast path. - Console.WriteLine($"Expected minimal OOM diagnostic token not found in subprocess stderr."); - return Fail; - } - - // In the general case, we expect either a message containing "OutOfMemoryException" or the minimal OOM message. + // Even in the small allocations case, the runtime might still have enough memory to construct + // an OutOfMemoryException and print the full diagnostic. + // Either token is acceptable, but at least one should be present to confirm that OOM was the reason for termination. if (!(stderr.Contains(ExpectedOomToken) || stderr.Contains(ExpectedMinimalOomToken))) { Console.WriteLine($"Expected OOM diagnostic token not found in subprocess stderr."); From 258f4edebd5ceb271f8c0243712d5f5b6cd826a7 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 2 Jun 2026 23:31:32 -0700 Subject: [PATCH 23/25] Update test --- .../OutOfMemoryException/OutOfMemoryException.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 3b36eefe1b4f56..01ba7262a6a128 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -33,16 +33,15 @@ static int Main(string[] args) { // Subprocess mode: allocate until OOM is triggered. // Phase 1: fill quickly with large blocks to use most of the heap. - // Phase 2: exhaust remaining scraps with small allocations so that - // virtually no memory is left when OOM is finally thrown. var list = new List(); try { while (true) list.Add(new byte[16 * 1024]); } catch (OutOfMemoryException) { } - // If we keep adding elements to the list, it's possible that the list's - // internal array fails when trying a big allocation to grow. - // Instead, we create a long chain of objects that will fail with OOM when - // trying to allocate the next one. + // Phase 2: fill the remaining gaps with 256 bytes allocations. object a = null; - for (;;) a = new object[] { a }; + try { for (;;) a = new object[] { a, new byte[256] }; } catch (OutOfMemoryException) { } + // Phase 3: exhaust the last scraps with tiny allocations so that + // virtually no memory is left when OOM is finally thrown. + object b = null; + for (;;) b = new object[] { b }; } if (args.Length > 0 && args[0] == AllocateLargeArg) From d37088131dbb074a0d0397e71dd6d887b6c95991 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:03:01 -0700 Subject: [PATCH 24/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../OutOfMemoryException/OutOfMemoryException.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj index 5337f033469a3f..a8f5c20af4f7ff 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.csproj @@ -2,8 +2,8 @@ Exe 0 - - true + + true true false From a4111bbf3b5642a19ccf3e215f48353a43df5514 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 3 Jun 2026 12:22:11 -0700 Subject: [PATCH 25/25] Stop using lists and chain of objects --- .../OutOfMemoryException.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs index 01ba7262a6a128..f320dcc4687911 100644 --- a/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs +++ b/src/tests/baseservices/exceptions/OutOfMemoryException/OutOfMemoryException.cs @@ -31,17 +31,15 @@ static int Main(string[] args) { if (args.Length > 0 && args[0] == AllocateSmallArg) { - // Subprocess mode: allocate until OOM is triggered. - // Phase 1: fill quickly with large blocks to use most of the heap. - var list = new List(); - try { while (true) list.Add(new byte[16 * 1024]); } catch (OutOfMemoryException) { } - // Phase 2: fill the remaining gaps with 256 bytes allocations. - object a = null; - try { for (;;) a = new object[] { a, new byte[256] }; } catch (OutOfMemoryException) { } - // Phase 3: exhaust the last scraps with tiny allocations so that - // virtually no memory is left when OOM is finally thrown. - object b = null; - for (;;) b = new object[] { b }; + // Pre-allocate a flat array for storage. + object[] storage = new object[8192]; + int idx = 0; + // We expect ~2048 iterations in the first loop and ~64 iterations in the second. + try { while (idx < storage.Length) storage[idx++] = new byte[16 * 1024]; } catch (OutOfMemoryException) { } + try { while (idx < storage.Length) storage[idx++] = new byte[256]; } catch (OutOfMemoryException) { } + // < 280 bytes free. + // Use the smallest possible allocation to exhaust the last scraps. + while (idx < storage.Length) storage[idx++] = new object(); } if (args.Length > 0 && args[0] == AllocateLargeArg)