-
Notifications
You must be signed in to change notification settings - Fork 5.5k
[NativeAOT] Print OOM message before Abort() on Linux #125311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
faf77aa
bc41dce
3ef2a6d
ac7e07e
99a6529
57092c7
cf2ca95
2eafa92
cc43588
15e81d4
f373a39
aa3ce5e
821b61d
fb34a0d
c12f292
88e7eae
5984be7
027d91f
a2b7dec
643046c
1e2888e
e3b0bd4
ae0981e
9559af2
258f4ed
d370881
a4111bb
80b6dce
08aa758
c943e14
5a8e077
2701338
faf6e09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,8 +242,20 @@ 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); | ||
| if (!minimalFailFast) | ||
| bool minimalFailFast = exception == PreallocatedOutOfMemoryException.Instance; | ||
| 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 | ||
| { | ||
| // Try to print the same short message CoreCLR prints. | ||
| Internal.Console.Error.Write("Out of memory."); | ||
| Internal.Console.Error.WriteLine(); | ||
|
eduardo-vp marked this conversation as resolved.
|
||
| } | ||
|
eduardo-vp marked this conversation as resolved.
eduardo-vp marked this conversation as resolved.
|
||
| catch { } | ||
|
Comment on lines
+248
to
+256
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to address this by changing I suspect that the test may be flaky otherwise given that it tries to allocate every last bit of managed memory.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test was flaky indeed but I think the problem was using a list that tried to resize from 2048 to 4096 and failed leaving a remaining size of still ~32 KB in memory, too big to start trying to do very tiny allocations. I think the GC was just thrashing so updated the test. I also added this change so I'm expecting the test doesn't fail, will observe the CI.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test consistently times out only on osx x64. At this point I'm wondering if there's a problem with the test or it's actually the runtime the one that is thrashing when it should OOM.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not be surprised if there are runtime bugs that lead to hang on OOM like the one this test is exercising. |
||
| } | ||
| else | ||
| { | ||
| Internal.Console.Error.Write(((exception == null) || (reason is RhFailFastReason.EnvironmentFailFast or RhFailFastReason.AssertionFailure)) ? | ||
|
jkotas marked this conversation as resolved.
|
||
| "Process terminated. " : "Unhandled exception. "); | ||
|
|
@@ -266,8 +285,21 @@ 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(exception.GetType().FullName); | ||
|
eduardo-vp marked this conversation as resolved.
|
||
| Internal.Console.Error.WriteLine(); | ||
| } | ||
|
eduardo-vp marked this conversation as resolved.
|
||
| catch { } | ||
| } | ||
| } | ||
|
|
||
| #if TARGET_WINDOWS | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
|
eduardo-vp marked this conversation as resolved.
|
||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| // 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. | ||
| // 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 OutOfMemoryExceptionTest | ||
| { | ||
| const int Pass = 100; | ||
| const int Fail = -1; | ||
| const int TimeoutMilliseconds = 60 * 1000; | ||
|
|
||
| const string AllocateSmallArg = "--allocate-small"; | ||
| const string AllocateLargeArg = "--allocate-large"; | ||
| // 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."; | ||
|
|
||
| static int Main(string[] args) | ||
| { | ||
| if (args.Length > 0 && args[0] == AllocateSmallArg) | ||
| { | ||
| // 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++] = GC.AllocateArray<byte>(16 * 1024, pinned: true); } catch (OutOfMemoryException) { } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is OOM caused by
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Locally I'm seeing it sometimes hits the minimalFailFast, sometimes it outputs the full stack trace. But in any case the test still doesn't work on osx x64. Should we exclude the test from that config and file an issue?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sounds good |
||
| try { while (idx < storage.Length) storage[idx++] = GC.AllocateArray<byte>(256, pinned: true); } catch (OutOfMemoryException) { } | ||
| // < 280 bytes free. | ||
| // Use the smallest possible allocation to exhaust the last scraps. | ||
| while (idx < storage.Length) storage[idx++] = GC.AllocateArray<byte>(1, pinned: true); | ||
| return Fail; | ||
| } | ||
|
eduardo-vp marked this conversation as resolved.
|
||
|
|
||
| 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<byte[]>(); | ||
| while (true) list.Add(new byte[128 * 1024]); | ||
| } | ||
|
|
||
| // Controller mode: launch subprocesses with a GC heap limit and verify their output. | ||
| int result = RunSubprocess(AllocateSmallArg, "small allocations"); | ||
| if (result != Pass) | ||
| return result; | ||
|
|
||
| return RunSubprocess(AllocateLargeArg, "large allocations"); | ||
| } | ||
|
|
||
| static int RunSubprocess(string allocateArg, string description) | ||
| { | ||
| Console.WriteLine($"Testing OOM with {description}..."); | ||
|
|
||
| string fileName = Environment.ProcessPath; | ||
|
eduardo-vp marked this conversation as resolved.
eduardo-vp marked this conversation as resolved.
|
||
| string[] arguments = TestLibrary.Utilities.IsNativeAot | ||
| ? [allocateArg] | ||
| : [typeof(OutOfMemoryExceptionTest).Assembly.Location, allocateArg]; | ||
|
|
||
|
eduardo-vp marked this conversation as resolved.
|
||
| var psi = new ProcessStartInfo(fileName, arguments) | ||
| { | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, | ||
| }; | ||
| // 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; | ||
| try | ||
| { | ||
| output = Process.RunAndCaptureText(psi, TimeSpan.FromMilliseconds(TimeoutMilliseconds)); | ||
| } | ||
| catch (TimeoutException) | ||
| { | ||
| Console.WriteLine($"Subprocess timed out after {TimeoutMilliseconds / 1000} seconds."); | ||
| return Fail; | ||
| } | ||
|
|
||
| if (output.ExitStatus.ExitCode == 0 || output.ExitStatus.ExitCode == Pass) | ||
| { | ||
| Console.WriteLine($"Subprocess exit code: {output.ExitStatus.ExitCode}"); | ||
| Console.WriteLine($"Subprocess stderr: {output.StandardError}"); | ||
| Console.WriteLine("Expected a non-success exit code from the OOM subprocess."); | ||
| return Fail; | ||
| } | ||
|
|
||
| string stderr = output.StandardError; | ||
|
|
||
| // 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))) | ||
|
eduardo-vp marked this conversation as resolved.
|
||
| { | ||
|
Comment on lines
+102
to
+106
|
||
| Console.WriteLine($"Subprocess exit code: {output.ExitStatus.ExitCode}"); | ||
| Console.WriteLine($"Subprocess stderr: {stderr}"); | ||
| Console.WriteLine("Expected OOM diagnostic token not found in subprocess stderr."); | ||
| return Fail; | ||
| } | ||
|
|
||
| return Pass; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
eduardo-vp marked this conversation as resolved.
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <CLRTestPriority>0</CLRTestPriority> | ||
| <!-- This test spawns a subprocess; not supported on mobile, browser, or WASI platforms --> | ||
| <CLRTestTargetUnsupported Condition="'$(TargetsAppleMobile)' == 'true' or '$(TargetsAndroid)' == 'true' or '$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true'">true</CLRTestTargetUnsupported> | ||
| <RequiresProcessIsolation>true</RequiresProcessIsolation> | ||
| <ReferenceXUnitWrapperGenerator>false</ReferenceXUnitWrapperGenerator> | ||
| <!-- Mono doesn't enforce DOTNET_GCHeapHardLimit as a GC heap limit --> | ||
| <DisableProjectBuild Condition="'$(RuntimeFlavor)' == 'mono'">true</DisableProjectBuild> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="OutOfMemoryException.cs" /> | ||
| <ProjectReference Include="$(TestLibraryProjectPath)" /> | ||
| </ItemGroup> | ||
| </Project> | ||
Uh oh!
There was an error while loading. Please reload this page.