diff --git a/src/coreclr/nativeaot/Runtime/threadstore.cpp b/src/coreclr/nativeaot/Runtime/threadstore.cpp index 9beb0448377ab..12cdea592ce49 100644 --- a/src/coreclr/nativeaot/Runtime/threadstore.cpp +++ b/src/coreclr/nativeaot/Runtime/threadstore.cpp @@ -505,60 +505,4 @@ void ThreadStore::SaveCurrentThreadOffsetForDAC() { } -#endif // _WIN32 - - -#ifndef DACCESS_COMPILE - -// internal static extern unsafe bool RhGetExceptionsForCurrentThread(Exception[] outputArray, out int writtenCountOut); -COOP_PINVOKE_HELPER(FC_BOOL_RET, RhGetExceptionsForCurrentThread, (Array* pOutputArray, int32_t* pWrittenCountOut)) -{ - FC_RETURN_BOOL(GetThreadStore()->GetExceptionsForCurrentThread(pOutputArray, pWrittenCountOut)); -} - -bool ThreadStore::GetExceptionsForCurrentThread(Array* pOutputArray, int32_t* pWrittenCountOut) -{ - int32_t countWritten = 0; - Object** pArrayElements; - Thread * pThread = GetCurrentThread(); - - for (PTR_ExInfo pInfo = pThread->m_pExInfoStackHead; pInfo != NULL; pInfo = pInfo->m_pPrevExInfo) - { - if (pInfo->m_exception == NULL) - continue; - - countWritten++; - } - - // No input array provided, or it was of the wrong kind. We'll fill out the count and return false. - if ((pOutputArray == NULL) || (pOutputArray->get_EEType()->RawGetComponentSize() != POINTER_SIZE)) - goto Error; - - // Input array was not big enough. We don't even partially fill it. - if (pOutputArray->GetArrayLength() < (uint32_t)countWritten) - goto Error; - - *pWrittenCountOut = countWritten; - - // Success, but nothing to report. - if (countWritten == 0) - return true; - - pArrayElements = (Object**)pOutputArray->GetArrayData(); - for (PTR_ExInfo pInfo = pThread->m_pExInfoStackHead; pInfo != NULL; pInfo = pInfo->m_pPrevExInfo) - { - if (pInfo->m_exception == NULL) - continue; - - *pArrayElements = pInfo->m_exception; - pArrayElements++; - } - - RhpBulkWriteBarrier(pArrayElements, countWritten * POINTER_SIZE); - return true; - -Error: - *pWrittenCountOut = countWritten; - return false; -} -#endif // DACCESS_COMPILE +#endif // _WIN32 \ No newline at end of file diff --git a/src/coreclr/nativeaot/Runtime/threadstore.h b/src/coreclr/nativeaot/Runtime/threadstore.h index 2e2aba2d86bc8..c9208cecf3b8d 100644 --- a/src/coreclr/nativeaot/Runtime/threadstore.h +++ b/src/coreclr/nativeaot/Runtime/threadstore.h @@ -59,7 +59,6 @@ class ThreadStore #else static PTR_Thread GetThreadFromTEB(TADDR pvTEB); #endif - bool GetExceptionsForCurrentThread(Array* pOutputArray, int32_t* pWrittenCountOut); void Destroy(); void SuspendAllThreads(bool waitForGCEvent); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs index a0b70c8f45a4a..b2a5819faa2d9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs @@ -599,10 +599,6 @@ internal static IntPtr RhGetModuleSection(TypeManagerHandle module, ReadyToRunSe internal static extern unsafe int RhGetModuleFileName(IntPtr moduleHandle, out char* moduleName); #endif - [MethodImplAttribute(MethodImplOptions.InternalCall)] - [RuntimeImport(RuntimeLibrary, "RhGetExceptionsForCurrentThread")] - internal static extern unsafe bool RhGetExceptionsForCurrentThread(Exception[] outputArray, out int writtenCountOut); - // returns the previous value. [MethodImplAttribute(MethodImplOptions.InternalCall)] [RuntimeImport(RuntimeLibrary, "RhSetErrorInfoBuffer")] 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 465dbe83356a0..97ced6c5e361c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime; using System.Runtime.CompilerServices; - +using System.Text; +using System.Text.Unicode; using Internal.DeveloperExperience; using Internal.Runtime.Augments; @@ -167,21 +169,7 @@ public static unsafe void FailFast(string message, Exception? exception) // exception that escapes from a ThreadPool workitem, or from a void-returning async method. public static void ReportUnhandledException(Exception exception) { -#if FEATURE_DUMP_DEBUGGING - // ReportUnhandledError will also call this in APPX scenarios, - // but WinRT can failfast before we get another chance - // (in APPX scenarios, this one will get overwritten by the one with the CCW pointer) - GenerateExceptionInformationForDump(exception, IntPtr.Zero); -#endif - -#if ENABLE_WINRT - // If possible report the exception to GEH, if not fail fast. - WinRTInteropCallbacks callbacks = WinRTInterop.UnsafeCallbacks; - if (callbacks == null || !callbacks.ReportUnhandledError(exception)) - FailFast(GetStringForFailFastReason(RhFailFastReason.UnhandledException), exception); -#else FailFast(GetStringForFailFastReason(RhFailFastReason.UnhandledException), exception); -#endif } // This is the classlib-provided fail-fast function that will be invoked whenever the runtime @@ -248,14 +236,37 @@ internal static void FailFast(string message, Exception? exception, RhFailFastRe outputMessage = message; } + Internal.Console.Error.Write(prefix); if (outputMessage != null) Internal.Console.Error.Write(outputMessage); Internal.Console.Error.Write(Environment.NewLine); -#if FEATURE_DUMP_DEBUGGING - GenerateExceptionInformationForDump(exception, IntPtr.Zero); -#endif + if (outputMessage != null) + { + // Try to save the exception stack trace in a buffer on the stack. If the exception is too large, we'll truncate it. + const int MaxStack = 2048; + Span exceptionStack = stackalloc byte[MaxStack]; + + // Ignore output, as this is best-effort + _ = Utf8.FromUtf16(outputMessage, exceptionStack, out _, out int length); + // Fill the rest of the buffer with nulls + if (length < MaxStack) + exceptionStack.Slice(length).Clear(); + + unsafe + { + byte* stackExceptionRecord = stackalloc byte[sizeof(CrashDumpRecord)]; + CrashDumpRecord* pExceptionRecord = (CrashDumpRecord*)stackExceptionRecord; + var cookieSpan = new Span(pExceptionRecord->Cookie, CrashDumpRecord.CookieSize); + // Random 10 bytes to identify the record + ((ReadOnlySpan)new byte[] { 0x1c, 0x73, 0xd0, 0x2d, 0xda, 0x6b, 0x4c, 0xef, 0xbf, 0xa1 }).CopyTo(cookieSpan); + "NETRUNTIME"u8.CopyTo(cookieSpan.Slice(10)); + pExceptionRecord->Type = 1; + pExceptionRecord->Data = (void*)exceptionStack.GetPinnableReference(); + pExceptionRecord->Length = length; + } + } } #if TARGET_WINDOWS @@ -282,6 +293,15 @@ internal static void FailFast(string message, Exception? exception, RhFailFastRe #endif } + private unsafe struct CrashDumpRecord + { + public const int CookieSize = 20; + public fixed byte Cookie[CookieSize]; + public int Type; + public void* Data; + public int Length; + } + // Use a nested class to avoid running the class constructor of the outer class when // accessing this flag. private static class InFailFast @@ -304,374 +324,5 @@ public static bool SafeToPerformRichExceptionSupport return true; } } - -#if FEATURE_DUMP_DEBUGGING - -#pragma warning disable 414 // field is assigned, but never used -- This is because C# doesn't realize that we - // copy the field into a buffer. - /// - /// This is the header that describes our 'error report' buffer to the minidump auxiliary provider. - /// Its format is know to that system-wide DLL, so do not change it. The remainder of the buffer is - /// opaque to the minidump auxiliary provider, so it'll have its own format that is more easily - /// changed. - /// - [StructLayout(LayoutKind.Sequential)] - private struct ERROR_REPORT_BUFFER_HEADER - { - private int _headerSignature; - private int _bufferByteCount; - - public void WriteHeader(int cbBuffer) - { - _headerSignature = 0x31304244; // 'DB01' - _bufferByteCount = cbBuffer; - } - } - - /// - /// This header describes the contents of the serialized error report to DAC, which can deserialize it - /// from a dump file or live debugging session. This format is easier to change than the - /// ERROR_REPORT_BUFFER_HEADER, but it is still well-known to DAC, so any changes must update the - /// version number and also have corresponding changes made to DAC. - /// - [StructLayout(LayoutKind.Sequential)] - private struct SERIALIZED_ERROR_REPORT_HEADER - { - private int _errorReportSignature; // This is the version of the 'container format'. - private int _exceptionSerializationVersion; // This is the version of the Exception format. It is - // separate from the 'container format' version since the - // implementation of the Exception serialization is owned by - // the Exception class. - private int _exceptionCount; // We just contain a logical array of exceptions. - private int _loadedModuleCount; // Number of loaded modules. present when signature >= ER02. - // {ExceptionCount} serialized Exceptions follow. - // {LoadedModuleCount} module handles follow. present when signature >= ER02. - - public void WriteHeader(int nExceptions, int nLoadedModules) - { - _errorReportSignature = 0x32305245; // 'ER02' - _exceptionSerializationVersion = Exception.CurrentSerializationSignature; - _exceptionCount = nExceptions; - _loadedModuleCount = nLoadedModules; - } - } - - /// - /// Holds metadata about an exception in flight. Class because ConditionalWeakTable only accepts reference types - /// - private class ExceptionData - { - public ExceptionData() - { - // Set this to a non-zero value so that logic mapping entries to threads - // doesn't think an uninitialized ExceptionData is on thread 0 - ExceptionMetadata.ThreadId = 0xFFFFFFFF; - } - - public struct ExceptionMetadataStruct - { - public uint ExceptionId { get; set; } // Id assigned to the exception. May not be contiguous or start at 0. - public uint InnerExceptionId { get; set; } // ID of the inner exception or 0xFFFFFFFF for 'no inner exception' - public uint ThreadId { get; set; } // Managed thread ID the eception was thrown on - public int NestingLevel { get; set; } // If multiple exceptions are currently active on a thread, this gives the ordering for them. - // The highest number is the most recent exception. -1 means the exception is not currently in flight - // (but it may still be an InnerException). - public IntPtr ExceptionCCWPtr { get; set; } // If the exception was thrown in an interop scenario, this contains the CCW pointer, otherwise, IntPtr.Zero - } - - public ExceptionMetadataStruct ExceptionMetadata; - - /// - /// Data created by Exception.SerializeForDump() - /// - public byte[] SerializedExceptionData { get; set; } - - /// - /// Serializes the exception metadata and SerializedExceptionData - /// - public unsafe byte[] Serialize() - { - checked - { - byte[] serializedData = new byte[sizeof(ExceptionMetadataStruct) + SerializedExceptionData.Length]; - fixed (byte* pSerializedData = &serializedData[0]) - { - ExceptionMetadataStruct* pMetadata = (ExceptionMetadataStruct*)pSerializedData; - pMetadata->ExceptionId = ExceptionMetadata.ExceptionId; - pMetadata->InnerExceptionId = ExceptionMetadata.InnerExceptionId; - pMetadata->ThreadId = ExceptionMetadata.ThreadId; - pMetadata->NestingLevel = ExceptionMetadata.NestingLevel; - pMetadata->ExceptionCCWPtr = ExceptionMetadata.ExceptionCCWPtr; - - SerializedExceptionData.AsSpan().CopyTo(new Span(pSerializedData + sizeof(ExceptionMetadataStruct), SerializedExceptionData.Length)); - } - return serializedData; - } - } - } - - /// - /// Table of exceptions that were on stacks triggering GenerateExceptionInformationForDump - /// - private static readonly ConditionalWeakTable s_exceptionDataTable = new ConditionalWeakTable(); - - /// - /// Counter for exception ID assignment - /// - private static int s_currentExceptionId; - - /// - /// This method will call the runtime to gather the Exception objects from every exception dispatch in - /// progress on the current thread. It will then serialize them into a new buffer and pass that - /// buffer back to the runtime, which will publish it to a place where a global "minidump auxiliary - /// provider" will be able to save the buffer's contents into triage dumps. - /// - /// Thread safety information: The guarantee of this method is that the buffer it produces will have - /// complete and correct information for all live exceptions on the current thread (as long as the same exception object - /// is not thrown simultaneously on multiple threads). It will do a best-effort attempt to serialize information about exceptions - /// already recorded on other threads, but that data can be lost or corrupted. The restrictions are: - /// 1. Only exceptions active or recorded on the current thread have their table data modified. - /// 2. After updating data in the table, we serialize a snapshot of the table (provided by ConditionalWeakTable.Values), - /// regardless of what other threads might do to the table before or after. However, because of #1, this thread's - /// exception data should stay stable - /// 3. There is a dependency on the fact that ConditionalWeakTable's members are all threadsafe and that .Values returns a snapshot - /// - public static void GenerateExceptionInformationForDump(Exception currentException, IntPtr exceptionCCWPtr) - { - LowLevelList serializedExceptions = new LowLevelList(); - - // If currentException is null, there's a state corrupting exception in flight and we can't serialize it - if (currentException != null) - { - SerializeExceptionsForDump(currentException, exceptionCCWPtr, serializedExceptions); - } - - GenerateErrorReportForDump(serializedExceptions); - } - - private static void SerializeExceptionsForDump(Exception currentException, IntPtr exceptionCCWPtr, LowLevelList serializedExceptions) - { - const uint NoInnerExceptionValue = 0xFFFFFFFF; - - // Approximate upper size limit for the serialized exceptions (but we'll always serialize currentException) - // If we hit the limit, because we serialize in arbitrary order, there may be missing InnerExceptions or nested exceptions. - const int MaxBufferSize = 20000; - - int nExceptions; - RuntimeImports.RhGetExceptionsForCurrentThread(null, out nExceptions); - Exception[] curThreadExceptions = new Exception[nExceptions]; - RuntimeImports.RhGetExceptionsForCurrentThread(curThreadExceptions, out nExceptions); - LowLevelList exceptions = new LowLevelList(curThreadExceptions); - LowLevelList nonThrownInnerExceptions = new LowLevelList(); - - uint currentThreadId = (uint)Environment.CurrentManagedThreadId; - - // Reset nesting levels for exceptions on this thread that might not be currently in flight - foreach (KeyValuePair item in s_exceptionDataTable) - { - ExceptionData exceptionData = item.Value; - if (exceptionData.ExceptionMetadata.ThreadId == currentThreadId) - { - exceptionData.ExceptionMetadata.NestingLevel = -1; - } - } - - // Find all inner exceptions, even if they're not currently being handled - for (int i = 0; i < exceptions.Count; i++) - { - if (exceptions[i].InnerException != null && !exceptions.Contains(exceptions[i].InnerException)) - { - exceptions.Add(exceptions[i].InnerException); - nonThrownInnerExceptions.Add(exceptions[i].InnerException); - } - } - - int currentNestingLevel = curThreadExceptions.Length - 1; - - // Make sure we serialize currentException - if (!exceptions.Contains(currentException)) - { - // When this happens, currentException is probably passed to this function through System.Environment.FailFast(), we - // would want to treat as if this exception is last thrown in the current thread. - exceptions.Insert(0, currentException); - currentNestingLevel++; - } - - // Populate exception data for all exceptions interesting to this thread. - // Whether or not there was previously data for that object, it might have changed. - for (int i = 0; i < exceptions.Count; i++) - { - ExceptionData exceptionData = s_exceptionDataTable.GetOrCreateValue(exceptions[i]); - - exceptionData.ExceptionMetadata.ExceptionId = (uint)System.Threading.Interlocked.Increment(ref s_currentExceptionId); - if (exceptionData.ExceptionMetadata.ExceptionId == NoInnerExceptionValue) - { - exceptionData.ExceptionMetadata.ExceptionId = (uint)System.Threading.Interlocked.Increment(ref s_currentExceptionId); - } - - exceptionData.ExceptionMetadata.ThreadId = currentThreadId; - - // Only include nesting information for exceptions that were thrown on this thread - if (!nonThrownInnerExceptions.Contains(exceptions[i])) - { - exceptionData.ExceptionMetadata.NestingLevel = currentNestingLevel; - currentNestingLevel--; - } - else - { - exceptionData.ExceptionMetadata.NestingLevel = -1; - } - - // Only match the CCW pointer up to the current exception - if (object.ReferenceEquals(exceptions[i], currentException)) - { - exceptionData.ExceptionMetadata.ExceptionCCWPtr = exceptionCCWPtr; - } - - byte[] serializedEx = exceptions[i].SerializeForDump(); - exceptionData.SerializedExceptionData = serializedEx; - } - - // Populate inner exception ids now that we have all of them in the table - for (int i = 0; i < exceptions.Count; i++) - { - ExceptionData exceptionData; - if (!s_exceptionDataTable.TryGetValue(exceptions[i], out exceptionData)) - { - // This shouldn't happen, but we can't meaningfully throw here - continue; - } - - if (exceptions[i].InnerException != null) - { - ExceptionData innerExceptionData; - if (s_exceptionDataTable.TryGetValue(exceptions[i].InnerException, out innerExceptionData)) - { - exceptionData.ExceptionMetadata.InnerExceptionId = innerExceptionData.ExceptionMetadata.ExceptionId; - } - } - else - { - exceptionData.ExceptionMetadata.InnerExceptionId = NoInnerExceptionValue; - } - } - - int totalSerializedExceptionSize = 0; - // Make sure we include the current exception, regardless of buffer size - ExceptionData currentExceptionData = null; - if (s_exceptionDataTable.TryGetValue(currentException, out currentExceptionData)) - { - byte[] serializedExceptionData = currentExceptionData.Serialize(); - serializedExceptions.Add(serializedExceptionData); - totalSerializedExceptionSize = serializedExceptionData.Length; - } - - checked - { - foreach (KeyValuePair item in s_exceptionDataTable) - { - ExceptionData exceptionData = item.Value; - - // Already serialized currentException - if (currentExceptionData != null && exceptionData.ExceptionMetadata.ExceptionId == currentExceptionData.ExceptionMetadata.ExceptionId) - { - continue; - } - - byte[] serializedExceptionData = exceptionData.Serialize(); - if (totalSerializedExceptionSize + serializedExceptionData.Length >= MaxBufferSize) - { - break; - } - - serializedExceptions.Add(serializedExceptionData); - totalSerializedExceptionSize += serializedExceptionData.Length; - } - } - } - - private static unsafe void GenerateErrorReportForDump(LowLevelList serializedExceptions) - { - checked - { - int loadedModuleCount = (int)RuntimeImports.RhGetLoadedOSModules(null); - int cbModuleHandles = sizeof(System.IntPtr) * loadedModuleCount; - int cbFinalBuffer = sizeof(ERROR_REPORT_BUFFER_HEADER) + sizeof(SERIALIZED_ERROR_REPORT_HEADER) + cbModuleHandles; - for (int i = 0; i < serializedExceptions.Count; i++) - { - cbFinalBuffer += serializedExceptions[i].Length; - } - - byte[] finalBuffer = new byte[cbFinalBuffer]; - fixed (byte* pBuffer = &finalBuffer[0]) - { - byte* pCursor = pBuffer; - int cbRemaining = cbFinalBuffer; - - ERROR_REPORT_BUFFER_HEADER* pDacHeader = (ERROR_REPORT_BUFFER_HEADER*)pCursor; - pDacHeader->WriteHeader(cbFinalBuffer); - pCursor += sizeof(ERROR_REPORT_BUFFER_HEADER); - cbRemaining -= sizeof(ERROR_REPORT_BUFFER_HEADER); - - SERIALIZED_ERROR_REPORT_HEADER* pPayloadHeader = (SERIALIZED_ERROR_REPORT_HEADER*)pCursor; - pPayloadHeader->WriteHeader(serializedExceptions.Count, loadedModuleCount); - pCursor += sizeof(SERIALIZED_ERROR_REPORT_HEADER); - cbRemaining -= sizeof(SERIALIZED_ERROR_REPORT_HEADER); - - // copy the serialized exceptions to report buffer - for (int i = 0; i < serializedExceptions.Count; i++) - { - int cbChunk = serializedExceptions[i].Length; - serializedExceptions[i].AsSpan().CopyTo(new Span(pCursor, cbChunk)); - cbRemaining -= cbChunk; - pCursor += cbChunk; - } - - // copy the module-handle array to report buffer - IntPtr[] loadedModuleHandles = new IntPtr[loadedModuleCount]; - RuntimeImports.RhGetLoadedOSModules(loadedModuleHandles); - loadedModuleHandles.AsSpan().CopyTo(new Span(pCursor, loadedModuleHandles.Length)); - cbRemaining -= cbModuleHandles; - pCursor += cbModuleHandles; - - Debug.Assert(cbRemaining == 0); - } - UpdateErrorReportBuffer(finalBuffer); - } - } - - private static GCHandle s_ExceptionInfoBufferPinningHandle; - private static Lock s_ExceptionInfoBufferLock = new Lock(); - - private static unsafe void UpdateErrorReportBuffer(byte[] finalBuffer) - { - Debug.Assert(finalBuffer?.Length > 0); - - using (LockHolder.Hold(s_ExceptionInfoBufferLock)) - { - fixed (byte* pBuffer = &finalBuffer[0]) - { - byte* pPrevBuffer = (byte*)RuntimeImports.RhSetErrorInfoBuffer(pBuffer); - Debug.Assert(s_ExceptionInfoBufferPinningHandle.IsAllocated == (pPrevBuffer != null)); - if (pPrevBuffer != null) - { - byte[] currentExceptionInfoBuffer = (byte[])s_ExceptionInfoBufferPinningHandle.Target; - Debug.Assert(currentExceptionInfoBuffer?.Length > 0); - fixed (byte* pPrev = ¤tExceptionInfoBuffer[0]) - Debug.Assert(pPrev == pPrevBuffer); - } - if (!s_ExceptionInfoBufferPinningHandle.IsAllocated) - { - // We allocate a pinning GC handle because we are logically giving the runtime 'unmanaged memory'. - s_ExceptionInfoBufferPinningHandle = GCHandle.Alloc(finalBuffer, GCHandleType.Pinned); - } - else - { - s_ExceptionInfoBufferPinningHandle.Target = finalBuffer; - } - } - } - } -#endif // FEATURE_DUMP_DEBUGGING } }