From 261668fef9cb3c61c9b809b0a715ee873358a9d9 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:23:25 -0800 Subject: [PATCH] Report StackOverflowException on NativeAOT on Linux (#94485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Report stack overflow in linux Use SA_ONSTACK for SIGSEGV handler Call sigaltstack() to set the alternate stack on the main thread only Check if faulting address is near the stack pointer, if so, report stack overflow * Set up signal alternate stack on all new threads * Add test to naot smoke tests * Only allocate sigaltstack in Thread::Construct * Remove other getpagesize calls and guard sigaltstack when using mach exceptions * Use CLR stackoverflow test for NAOT, don't use sigaltstack on tvos * Cast to size_t before comparison * Uncomment code and fix for NAOT * Merge stackoverflow tests into one assembly * Update src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.cs Co-authored-by: Michal Strehovský * Revert msbuild change that's no longer needed * Fix namespace changes * Set DOTNET_DbgEnableMiniDump=0 on subprocesses to avoid dumps on expected failures and remove messages about failing to find createdump * Missed semicolon * Convert generated xunit runner test to work with NAOT * Don't use xunit generator for stackoverflowtester * Update src/coreclr/nativeaot/Runtime/thread.h Co-authored-by: Jan Vorlicek * Use IsSingleFile and update Main signature --------- Co-authored-by: Michal Strehovský Co-authored-by: Jan Vorlicek --- .../nativeaot/Runtime/inc/CommonTypes.h | 1 + src/coreclr/nativeaot/Runtime/thread.cpp | 104 +++++++- src/coreclr/nativeaot/Runtime/thread.h | 12 +- .../Runtime/unix/HardwareExceptions.cpp | 43 ++-- .../nativeaot/Runtime/unix/UnixSignals.cpp | 4 +- .../nativeaot/Runtime/unix/UnixSignals.h | 2 +- .../Common/CoreCLRTestLibrary/Utilities.cs | 2 +- .../exceptions/stackoverflow/stackoverflow.cs | 5 +- .../stackoverflow/stackoverflow.csproj | 14 -- .../stackoverflow/stackoverflow3.cs | 8 +- .../stackoverflow/stackoverflow3.csproj | 15 -- .../stackoverflow/stackoverflowtester.cs | 223 ++++++++++-------- .../stackoverflow/stackoverflowtester.csproj | 8 +- src/tests/issues.targets | 6 +- 14 files changed, 288 insertions(+), 159 deletions(-) delete mode 100644 src/tests/baseservices/exceptions/stackoverflow/stackoverflow.csproj delete mode 100644 src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.csproj diff --git a/src/coreclr/nativeaot/Runtime/inc/CommonTypes.h b/src/coreclr/nativeaot/Runtime/inc/CommonTypes.h index fde2d9247e07b..bd0675971c0a8 100644 --- a/src/coreclr/nativeaot/Runtime/inc/CommonTypes.h +++ b/src/coreclr/nativeaot/Runtime/inc/CommonTypes.h @@ -4,6 +4,7 @@ #ifndef __COMMON_TYPES_H__ #define __COMMON_TYPES_H__ +#include #include #include #include diff --git a/src/coreclr/nativeaot/Runtime/thread.cpp b/src/coreclr/nativeaot/Runtime/thread.cpp index 4c924524fd4d7..8972cd46ac9cb 100644 --- a/src/coreclr/nativeaot/Runtime/thread.cpp +++ b/src/coreclr/nativeaot/Runtime/thread.cpp @@ -5,6 +5,7 @@ #include "CommonTypes.h" #include "CommonMacros.h" #include "daccess.h" +#include "CommonMacros.inl" #include "PalRedhawkCommon.h" #include "PalRedhawk.h" #include "rhassert.h" @@ -27,6 +28,11 @@ #include "RhConfig.h" #include "RhVolatile.h" +#if defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) +#include +#include +#endif // defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + #ifndef DACCESS_COMPILE EXTERN_C NATIVEAOT_API void* REDHAWK_CALLCONV RhpHandleAlloc(void* pObject, int type); @@ -281,6 +287,9 @@ void Thread::Construct() if (StressLog::StressLogOn(~0u, 0)) m_pThreadStressLog = StressLog::CreateThreadStressLog(this); #endif // STRESS_LOG +#if defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + EnsureSignalAlternateStack(); +#endif // defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) // Everything else should be initialized to 0 via the static initialization of tls_CurrentThread. @@ -297,6 +306,89 @@ void Thread::Construct() ASSERT(m_interruptedContext == NULL); } +#if defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) +void Thread::FreeSignalAlternateStack() +{ + void *altstack = m_alternateStack; + m_alternateStack = nullptr; + + if (altstack != nullptr) + { + stack_t ss, oss; + // The man page for sigaltstack says that when the ss.ss_flags is set to SS_DISABLE, + // all other ss fields are ignored. However, MUSL implementation checks that the + // ss_size is >= MINSIGSTKSZ even in this case. + ss.ss_size = MINSIGSTKSZ; + ss.ss_flags = SS_DISABLE; + ss.ss_sp = NULL; + int st = sigaltstack(&ss, &oss); + if ((st == 0) && (oss.ss_flags != SS_DISABLE)) + { + // Make sure this altstack is this PAL's before freeing. + if (oss.ss_sp == altstack) + { + int st = munmap(oss.ss_sp, oss.ss_size); + _ASSERTE(st == 0); + } + } + } +} + +bool Thread::EnsureSignalAlternateStack() +{ + int st = 0; + + stack_t oss; + + // Query the current alternate signal stack + st = sigaltstack(NULL, &oss); + if ((st == 0) && (oss.ss_flags == SS_DISABLE)) + { + // There is no alternate stack for SIGSEGV handling installed yet so allocate one + + // We include the size of the SignalHandlerWorkerReturnPoint in the alternate stack size since the + // context contained in it is large and the SIGSTKSZ was not sufficient on ARM64 during testing. + int altStackSize = SIGSTKSZ; +#ifdef HAS_ADDRESS_SANITIZER + // Asan also uses alternate stack so we increase its size on the SIGSTKSZ * 4 that enough for asan + // (see kAltStackSize in compiler-rt/lib/sanitizer_common/sanitizer_posix_libcdep.cc) + altStackSize += SIGSTKSZ * 4; +#endif + altStackSize = ALIGN_UP(altStackSize, PalOsPageSize()); + int flags = MAP_ANONYMOUS | MAP_PRIVATE; +#ifdef MAP_STACK + flags |= MAP_STACK; +#endif + void* altStack = mmap(NULL, altStackSize, PROT_READ | PROT_WRITE, flags, -1, 0); + if (altStack != MAP_FAILED) + { + // create a guard page for the alternate stack + st = mprotect(altStack, PalOsPageSize(), PROT_NONE); + if (st == 0) + { + stack_t ss; + ss.ss_sp = (char*)altStack; + ss.ss_size = altStackSize; + ss.ss_flags = 0; + st = sigaltstack(&ss, NULL); + } + + if (st == 0) + { + m_alternateStack = altStack; + } + else + { + int st2 = munmap(altStack, altStackSize); + _ASSERTE(st2 == 0); + } + } + } + + return (st == 0); +} +#endif // defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + bool Thread::IsInitialized() { return (m_ThreadStateFlags != TSF_Unknown); @@ -361,6 +453,10 @@ void Thread::Destroy() } #endif //FEATURE_SUSPEND_REDIRECTION +#if defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + FreeSignalAlternateStack(); +#endif // defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + ASSERT(m_pGCFrameRegistrations == NULL); } @@ -866,19 +962,19 @@ void Thread::Unhijack() } // This unhijack routine is called to undo a hijack, that is potentially on a different thread. -// +// // Although there are many code sequences (here and in asm) to // perform an unhijack operation, they will never execute concurrently: -// +// // - A thread may unhijack itself at any time so long as it does that from unmanaged code while in coop mode. // This ensures that coop thread can access its stack synchronously. // Unhijacking from unmanaged code ensures that another thread will not attempt to hijack it, // since we only hijack threads that are executing managed code. -// +// // - A GC thread may access a thread asynchronously, including unhijacking it. // Asynchronously accessed thread must be in preemptive mode and should not // access the managed portion of its stack. -// +// // - A thread that owns the suspension can access another thread as long as the other thread is // in preemptive mode or suspended in managed code. // Either way the other thread cannot be accessing its hijack. diff --git a/src/coreclr/nativeaot/Runtime/thread.h b/src/coreclr/nativeaot/Runtime/thread.h index 7aa473eb6763d..aa3f370a8643d 100644 --- a/src/coreclr/nativeaot/Runtime/thread.h +++ b/src/coreclr/nativeaot/Runtime/thread.h @@ -94,7 +94,7 @@ struct ThreadBuffer HANDLE m_hPalThread; // WARNING: this may legitimately be INVALID_HANDLE_VALUE void ** m_ppvHijackedReturnAddressLocation; void * m_pvHijackedReturnAddress; - uintptr_t m_uHijackedReturnValueFlags; + uintptr_t m_uHijackedReturnValueFlags; PTR_ExInfo m_pExInfoStackHead; Object* m_threadAbortException; // ThreadAbortException instance -set only during thread abort Object* m_pThreadLocalStatics; @@ -112,6 +112,9 @@ struct ThreadBuffer #ifdef FEATURE_GC_STRESS uint32_t m_uRand; // current per-thread random number #endif // FEATURE_GC_STRESS +#if defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + void * m_alternateStack; // ptr to alternate signal stack +#endif // defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) }; struct ReversePInvokeFrame @@ -152,7 +155,7 @@ class Thread : private ThreadBuffer // For suspension APCs it is mostly harmless, but wasteful and in extreme // cases may force the target thread into stack oveflow. // We use this flag to avoid sending another APC when one is still going through. - // + // // On Unix this is an optimization to not queue up more signals when one is // still being processed. }; @@ -312,6 +315,11 @@ class Thread : private ThreadBuffer bool IsActivationPending(); void SetActivationPending(bool isPending); + +#if defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) + bool EnsureSignalAlternateStack(); + void FreeSignalAlternateStack(); +#endif // defined(TARGET_UNIX) && !HAVE_MACH_EXCEPTIONS && !defined(HOST_TVOS) }; #ifndef __GCENV_BASE_INCLUDED__ diff --git a/src/coreclr/nativeaot/Runtime/unix/HardwareExceptions.cpp b/src/coreclr/nativeaot/Runtime/unix/HardwareExceptions.cpp index 03ee5d514c3dc..99682c36362f2 100644 --- a/src/coreclr/nativeaot/Runtime/unix/HardwareExceptions.cpp +++ b/src/coreclr/nativeaot/Runtime/unix/HardwareExceptions.cpp @@ -11,6 +11,9 @@ #include "HardwareExceptions.h" #include "UnixSignals.h" #include "PalCreateDump.h" +#include "thread.h" +#include "threadstore.h" +#include #if defined(HOST_APPLE) #include @@ -545,6 +548,17 @@ bool HardwareExceptionHandler(int code, siginfo_t *siginfo, void *context, void* // Handler for the SIGSEGV signal void SIGSEGVHandler(int code, siginfo_t *siginfo, void *context) { + // First check if we have a stack overflow + size_t sp = ((UNIX_CONTEXT *)context)->GetSp(); + size_t failureAddress = (size_t)siginfo->si_addr; + + // If the failure address is at most one page above or below the stack pointer, + // we have a stack overflow. + if ((failureAddress - (sp - PalOsPageSize())) < (size_t)PalOsPageSize() * 2) + { + PalPrintFatalError("\nProcess is terminating due to StackOverflowException.\n"); + RhFailFast(); + } bool isHandled = HardwareExceptionHandler(code, siginfo, context, siginfo->si_addr); if (isHandled) { @@ -589,7 +603,8 @@ void SIGFPEHandler(int code, siginfo_t *siginfo, void *context) // Initialize hardware exception handling bool InitializeHardwareExceptionHandling() { - if (!AddSignalHandler(SIGSEGV, SIGSEGVHandler, &g_previousSIGSEGV)) + // Run SIGSEGV handler on separate stack so we can handle stack overflow. Otherwise, the current (invalid) stack is used and another segfault is raised. + if (!AddSignalHandler(SIGSEGV, SIGSEGVHandler, &g_previousSIGSEGV, SA_ONSTACK)) { return false; } @@ -601,23 +616,23 @@ bool InitializeHardwareExceptionHandling() #if defined(HOST_APPLE) #ifndef HOST_TVOS // task_set_exception_ports is not supported on tvOS - // LLDB installs task-wide Mach exception handlers. XNU dispatches Mach - // exceptions first to any registered "activation" handler and then to - // any registered task handler before dispatching the exception to a - // host-wide Mach exception handler that does translation to POSIX - // signals. This makes it impossible to use LLDB with implicit null + // LLDB installs task-wide Mach exception handlers. XNU dispatches Mach + // exceptions first to any registered "activation" handler and then to + // any registered task handler before dispatching the exception to a + // host-wide Mach exception handler that does translation to POSIX + // signals. This makes it impossible to use LLDB with implicit null // checks in NativeAOT; continuing execution after LLDB traps an // EXC_BAD_ACCESS will result in LLDB's EXC_BAD_ACCESS handler being // invoked again. This also interferes with the translation of SIGFPEs // to .NET-level ArithmeticExceptions. Work around this here by - // installing a no-op task-wide Mach exception handler for - // EXC_BAD_ACCESS and EXC_ARITHMETIC. - kern_return_t kr = task_set_exception_ports( - mach_task_self(), - EXC_MASK_BAD_ACCESS | EXC_MASK_ARITHMETIC, /* SIGSEGV, SIGFPE */ - MACH_PORT_NULL, - EXCEPTION_STATE_IDENTITY, - MACHINE_THREAD_STATE); + // installing a no-op task-wide Mach exception handler for + // EXC_BAD_ACCESS and EXC_ARITHMETIC. + kern_return_t kr = task_set_exception_ports( + mach_task_self(), + EXC_MASK_BAD_ACCESS | EXC_MASK_ARITHMETIC, /* SIGSEGV, SIGFPE */ + MACH_PORT_NULL, + EXCEPTION_STATE_IDENTITY, + MACHINE_THREAD_STATE); ASSERT(kr == KERN_SUCCESS); #endif #endif diff --git a/src/coreclr/nativeaot/Runtime/unix/UnixSignals.cpp b/src/coreclr/nativeaot/Runtime/unix/UnixSignals.cpp index 33852920653ff..813c6704b6213 100644 --- a/src/coreclr/nativeaot/Runtime/unix/UnixSignals.cpp +++ b/src/coreclr/nativeaot/Runtime/unix/UnixSignals.cpp @@ -9,11 +9,11 @@ #include "UnixSignals.h" // Add handler for hardware exception signal -bool AddSignalHandler(int signal, SignalHandler handler, struct sigaction* previousAction) +bool AddSignalHandler(int signal, SignalHandler handler, struct sigaction* previousAction, int additionalFlags) { struct sigaction newAction; - newAction.sa_flags = SA_RESTART; + newAction.sa_flags = SA_RESTART | additionalFlags; newAction.sa_handler = NULL; newAction.sa_sigaction = handler; newAction.sa_flags |= SA_SIGINFO; diff --git a/src/coreclr/nativeaot/Runtime/unix/UnixSignals.h b/src/coreclr/nativeaot/Runtime/unix/UnixSignals.h index 60e08d461e383..9980ffb5df9e5 100644 --- a/src/coreclr/nativeaot/Runtime/unix/UnixSignals.h +++ b/src/coreclr/nativeaot/Runtime/unix/UnixSignals.h @@ -14,7 +14,7 @@ typedef void (*SignalHandler)(int code, siginfo_t* siginfo, void* context); -bool AddSignalHandler(int signal, SignalHandler handler, struct sigaction* previousAction); +bool AddSignalHandler(int signal, SignalHandler handler, struct sigaction* previousAction, int additionalFlags = 0); void RestoreSignalHandler(int signal_id, struct sigaction* previousAction); #endif // __UNIX_SIGNALS_H__ diff --git a/src/tests/Common/CoreCLRTestLibrary/Utilities.cs b/src/tests/Common/CoreCLRTestLibrary/Utilities.cs index c298a46abca87..20b299b814fce 100644 --- a/src/tests/Common/CoreCLRTestLibrary/Utilities.cs +++ b/src/tests/Common/CoreCLRTestLibrary/Utilities.cs @@ -94,7 +94,7 @@ public static bool IsWindowsIoTCore public static bool IsMonoRuntime => Type.GetType("Mono.RuntimeStructs") != null; public static bool IsNotMonoRuntime => !IsMonoRuntime; - public static bool IsNativeAot => IsNotMonoRuntime && !IsReflectionEmitSupported; + public static bool IsNativeAot => IsSingleFile && IsNotMonoRuntime && !IsReflectionEmitSupported; public static bool HasAssemblyFiles => !string.IsNullOrEmpty(typeof(Utilities).Assembly.Location); public static bool IsSingleFile => !HasAssemblyFiles; diff --git a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.cs b/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.cs index 8249f4bbb44ac..64aafb1238a47 100644 --- a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.cs +++ b/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.cs @@ -65,7 +65,8 @@ struct LargeStruct65536 LargeStruct4096 se; LargeStruct4096 sf; } - class Program + + internal class StackOverflow { [MethodImpl(MethodImplOptions.NoInlining)] static void InfiniteRecursionA() @@ -134,7 +135,7 @@ static void SecondaryThreadsTest(bool smallframe) } } - static void Main(string[] args) + internal static void Run(string[] args) { bool smallframe = (args[0] == "smallframe"); if (args[1] == "secondary") diff --git a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.csproj b/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.csproj deleted file mode 100644 index e75283a0f2d0f..0000000000000 --- a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - true - false - BuildOnly - - false - - - - - - diff --git a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.cs b/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.cs index c833bae8afc15..62f6afd06fd62 100644 --- a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.cs +++ b/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.cs @@ -2,16 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -namespace TestStackOverflow3 +namespace TestStackOverflow { - class Program + internal class StackOverflow3 { private const int MAX_RECURSIVE_CALLS = 1000000; static int ctr = 0; - public static void Main() + public static void Run() { - Program ex = new Program(); + StackOverflow3 ex = new StackOverflow3(); ex.Execute(); } diff --git a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.csproj b/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.csproj deleted file mode 100644 index d805f0b4c7ecb..0000000000000 --- a/src/tests/baseservices/exceptions/stackoverflow/stackoverflow3.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - true - false - - false - true - BuildOnly - - - - - - diff --git a/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.cs b/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.cs index 07f5e03e7cb62..6e97fd7bb5516 100644 --- a/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.cs +++ b/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.IO; +using System.Linq; using System.Text; using Xunit; @@ -11,18 +14,72 @@ namespace TestStackOverflow { public class Program { - static void TestStackOverflow(string testName, string testArgs, out List stderrLines) + const string EnableMiniDumpEnvVar = "DOTNET_DbgEnableMiniDump"; + + const string MainSignature = ".Main(System.String[])"; + + public static int Main(string[] args) + { + if (args.Length > 0) + { + string testName = args[0]; + string[] testArgs = Array.Empty(); + switch (testName) + { + case "stackoverflow": + Assert.True(args.Length >= 2); + testArgs = args[1..].ToArray(); + StackOverflow.Run(testArgs); + break; + case "stackoverflow3": + StackOverflow3.Run(); + break; + default: + throw new InvalidOperationException($"Invalid arguments to 'stackoverflowtester' process. Test '{testName}' does not exist"); + } + throw new InvalidOperationException($"Invalid arguments to 'stackoverflowtester' process. Test '{testName}' with arguments '{testArgs}' should have thrown an exception."); + } + + int exitCode = 101; + try + { + TestStackOverflowSmallFrameMainThread(); + exitCode++; + TestStackOverflowLargeFrameMainThread(); + exitCode++; + TestStackOverflowSmallFrameSecondaryThread(); + exitCode++; + TestStackOverflowLargeFrameSecondaryThread(); + exitCode++; + TestStackOverflow3(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return exitCode; + } + + return 100; + } + + [UnconditionalSuppressMessage("SingleFile", "IL3000", Justification = "We want an empty string for location if the test is running as single file")] + static bool TestStackOverflow(string testName, string testArgs, out string[] stderrLines, out bool checkStackFrame) { - Console.WriteLine($"Running {testName} test({testArgs})"); List lines = new List(); - Process testProcess = new Process(); + string thisAssemblyPath = typeof(Program).Assembly.Location; + bool isSingleFile = TestLibrary.Utilities.IsSingleFile; - testProcess.StartInfo.FileName = Path.Combine(Environment.GetEnvironmentVariable("CORE_ROOT"), "corerun"); - testProcess.StartInfo.Arguments = $"{Path.Combine(Directory.GetCurrentDirectory(), "..", testName, $"{testName}.dll")} {testArgs}"; + Process testProcess = new Process(); + // Always use whatever runner started this test, or the exe if we're running single file / NativeAOT + testProcess.StartInfo.FileName = Environment.ProcessPath; + // We want the path to this assembly in CoreCLR and the empty string for single file / NativeAOT + testProcess.StartInfo.Arguments = $"{typeof(Program).Assembly.Location} {testName} {testArgs}"; + Console.WriteLine($"Running {testName} {testArgs}"); + testProcess.StartInfo.Environment.Add(EnableMiniDumpEnvVar, "0"); testProcess.StartInfo.UseShellExecute = false; testProcess.StartInfo.RedirectStandardError = true; - testProcess.ErrorDataReceived += (sender, line) => + testProcess.ErrorDataReceived += (sender, line) => { Console.WriteLine($"\"{line.Data}\""); if (!string.IsNullOrEmpty(line.Data)) @@ -36,12 +93,15 @@ static void TestStackOverflow(string testName, string testArgs, out List testProcess.WaitForExit(); testProcess.CancelErrorRead(); - stderrLines = lines; + stderrLines = lines.ToArray(); + + // NativeAOT doesn't provide a stack trace on stack overflow + checkStackFrame = !isSingleFile; int[] expectedExitCodes; if ((Environment.OSVersion.Platform == PlatformID.Unix) || (Environment.OSVersion.Platform == PlatformID.MacOSX)) { - expectedExitCodes = new int[] { 128 + 6}; + expectedExitCodes = new int[] { 128 + 6 }; } else { @@ -52,148 +112,121 @@ static void TestStackOverflow(string testName, string testArgs, out List { string separator = string.Empty; StringBuilder expectedListBuilder = new StringBuilder(); - Array.ForEach(expectedExitCodes, code => { + Array.ForEach(expectedExitCodes, code => + { expectedListBuilder.Append($"{separator}0x{code:X8}"); separator = " or "; }); throw new Exception($"Exit code: 0x{testProcess.ExitCode:X8}, expected {expectedListBuilder.ToString()}"); } - if (lines[0] != "Stack overflow.") - { - throw new Exception("Missing \"Stack overflow.\" at the first line"); - } - } - - [Fact] - public static void TestStackOverflowSmallFrameMainThread() - { - TestStackOverflow("stackoverflow", "smallframe main", out List lines); - - if (!lines[lines.Count - 1].EndsWith(".Main(System.String[])")) + string expectedMessage; + if (isSingleFile) { - throw new Exception("Missing \"Main\" method frame at the last line"); + expectedMessage = "Process is terminating due to StackOverflowException."; } - - if (!lines.Exists(elem => elem.EndsWith("TestStackOverflow.Program.Test(Boolean)"))) + else { - throw new Exception("Missing \"Test\" method frame"); + expectedMessage = "Stack overflow."; } - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionA()"))) + if (lines.Count > 0 && lines[0] == expectedMessage) { - throw new Exception("Missing \"InfiniteRecursionA\" method frame"); + return true; } - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionB()"))) - { - throw new Exception("Missing \"InfiniteRecursionB\" method frame"); - } + throw new Exception($"Missing \"{expectedMessage}\" at the first line of stderr"); + } - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionC()"))) + public static void AssertStackFramePresent(string stackFrame, ReadOnlySpan lines) + { + foreach(var line in lines) { - throw new Exception("Missing \"InfiniteRecursionC\" method frame"); + if (line.EndsWith(stackFrame)) + return; } + throw new Exception($"Missing \"{stackFrame}\" from stack trace"); } - [Fact] - public static void TestStackOverflowLargeFrameMainThread() + public static void TestStackOverflowSmallFrameMainThread() { - TestStackOverflow("stackoverflow", "largeframe main", out List lines); + TestStackOverflow("stackoverflow", "smallframe main", out string[] lines, out bool checkStackFrame); - if (!lines[lines.Count - 1].EndsWith("at TestStackOverflow.Program.Main(System.String[])")) + if (!checkStackFrame) { - throw new Exception("Missing \"Main\" method frame at the last line"); + return; } - if (!lines.Exists(elem => elem.EndsWith("TestStackOverflow.Program.Test(Boolean)"))) - { - throw new Exception("Missing \"Test\" method frame"); - } + AssertStackFramePresent(MainSignature, lines[(lines.Length-1)..]); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.Run(System.String[])", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.Test(Boolean)", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionB()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionC()", lines); + } - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionA2()"))) - { - throw new Exception("Missing \"InfiniteRecursionA2\" method frame"); - } + public static void TestStackOverflowLargeFrameMainThread() + { + TestStackOverflow("stackoverflow", "largeframe main", out string[] lines, out bool checkStackFrame); - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionB2()"))) + if (!checkStackFrame) { - throw new Exception("Missing \"InfiniteRecursionB2\" method frame"); + return; } - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionC2()"))) + if (!lines[lines.Length - 1].EndsWith(MainSignature)) { - throw new Exception("Missing \"InfiniteRecursionC2\" method frame"); + throw new Exception("Missing \"Main\" method frame at the last line"); } + AssertStackFramePresent(MainSignature, lines[(lines.Length-1)..]); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.Run(System.String[])", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.Test(Boolean)", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionA2()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionB2()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionC2()", lines); } - [Fact] public static void TestStackOverflowSmallFrameSecondaryThread() { - TestStackOverflow("stackoverflow", "smallframe secondary", out List lines); - - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.Test(Boolean)"))) - { - throw new Exception("Missing \"TestStackOverflow.Program.Test\" method frame"); - } - - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionA()"))) - { - throw new Exception("Missing \"InfiniteRecursionA\" method frame"); - } + TestStackOverflow("stackoverflow", "smallframe secondary", out string[] lines, out bool checkStackFrame); - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionB()"))) + if (!checkStackFrame) { - throw new Exception("Missing \"InfiniteRecursionB\" method frame"); + return; } - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionC()"))) - { - throw new Exception("Missing \"InfiniteRecursionC\" method frame"); - } + AssertStackFramePresent("at TestStackOverflow.StackOverflow.Test(Boolean)", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionA()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionB()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionC()", lines); } - [Fact] public static void TestStackOverflowLargeFrameSecondaryThread() { - TestStackOverflow("stackoverflow", "largeframe secondary", out List lines); - - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.Test(Boolean)"))) - { - throw new Exception("Missing \"TestStackOverflow.Program.Test\" method frame"); - } - - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow.Program.InfiniteRecursionA2()"))) - { - throw new Exception("Missing \"InfiniteRecursionA2\" method frame"); - } + TestStackOverflow("stackoverflow", "largeframe secondary", out string[] lines, out bool checkStackFrame); - if (!lines.Exists(elem => elem.EndsWith("TestStackOverflow.Program.InfiniteRecursionB2()"))) + if (!checkStackFrame) { - throw new Exception("Missing \"InfiniteRecursionB2\" method frame"); + return; } - if (!lines.Exists(elem => elem.EndsWith("TestStackOverflow.Program.InfiniteRecursionC2()"))) - { - throw new Exception("Missing \"InfiniteRecursionC2\" method frame"); - } + AssertStackFramePresent("at TestStackOverflow.StackOverflow.Test(Boolean)", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionA2()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionB2()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow.InfiniteRecursionC2()", lines); } - [Fact] public static void TestStackOverflow3() { - TestStackOverflow("stackoverflow3", "", out List lines); - - if (!lines[lines.Count - 1].EndsWith("at TestStackOverflow3.Program.Main()")) - { - throw new Exception("Missing \"Main\" method frame at the last line"); - } + TestStackOverflow("stackoverflow3", "", out string[] lines, out bool checkStackFrame); - if (!lines.Exists(elem => elem.EndsWith("at TestStackOverflow3.Program.Execute(System.String)"))) + if (!checkStackFrame) { - throw new Exception("Missing \"Execute\" method frame"); + return; } + AssertStackFramePresent(MainSignature, lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow3.Run()", lines); + AssertStackFramePresent("at TestStackOverflow.StackOverflow3.Execute(System.String)", lines); } } } diff --git a/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.csproj b/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.csproj index eb0ae0ea4cd17..402a9c65f3fdd 100644 --- a/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.csproj +++ b/src/tests/baseservices/exceptions/stackoverflow/stackoverflowtester.csproj @@ -5,9 +5,15 @@ false true + true + false + + + + + - diff --git a/src/tests/issues.targets b/src/tests/issues.targets index 45e23266330ba..006d0eba9d95f 100644 --- a/src/tests/issues.targets +++ b/src/tests/issues.targets @@ -89,7 +89,8 @@ - + + https://github.com/dotnet/runtime/issues/46175 @@ -730,9 +731,6 @@ https://github.com/dotnet/runtimelab/issues/155: Reflection.Emit - - Specific to CoreCLR - No crossgen folder under Core_Root