diff --git a/src/SOS/Strike/exts.cpp b/src/SOS/Strike/exts.cpp index f1c3c9fdaf..d40c1290aa 100644 --- a/src/SOS/Strike/exts.cpp +++ b/src/SOS/Strike/exts.cpp @@ -298,6 +298,11 @@ void DACMessage(HRESULT Status) { ExtOut("Failed to load data access module, 0x%08x\n", Status); + HRESULT fallbackFailure = GetDacFallbackFailure(); + if (FAILED(fallbackFailure)) + { + ExtOut("The dbgeng data access fallback also failed to provide ISOSDacInterface, 0x%08x\n", fallbackFailure); + } if (g_pRuntime->GetRuntimeConfiguration() >= IRuntime::ConfigurationEnd) { ExtOut("Unknown runtime type. Command not supported.\n"); diff --git a/src/SOS/Strike/strike.cpp b/src/SOS/Strike/strike.cpp index 93f26c72ce..44470b0b66 100644 --- a/src/SOS/Strike/strike.cpp +++ b/src/SOS/Strike/strike.cpp @@ -5877,6 +5877,40 @@ BOOL CheckCLRNotificationEvent(DEBUG_LAST_EVENT_INFO_EXCEPTION* pdle) #endif } +static bool ShouldReportCLRNotificationFailure() +{ +#ifndef FEATURE_PAL + static ULONG s_processId = 0; + static ULONG64 s_processHandle = 0; + static bool s_reportedFailure = false; + + ULONG processId = 0; + ULONG64 processHandle = 0; + if (g_ExtSystem->GetCurrentProcessSystemId(&processId) != S_OK || + g_ExtSystem->GetCurrentProcessHandle(&processHandle) != S_OK) + { + return true; + } + if (processId != s_processId || processHandle != s_processHandle) + { + s_processId = processId; + s_processHandle = processHandle; + s_reportedFailure = false; + } + if (s_reportedFailure) + { + return false; + } + s_reportedFailure = true; +#endif + return true; +} + +static void ReportCLRNotificationFailure(HRESULT status) +{ + ExtErr("SOS CLR notification handler failed, 0x%08x; attempting to continue the target\n", status); +} + HRESULT HandleCLRNotificationEvent() { /* @@ -5892,7 +5926,11 @@ HRESULT HandleCLRNotificationEvent() if (!CheckCLRNotificationEvent(&dle)) { #ifndef FEATURE_PAL - ExtOut("Expecting first chance CLRN exception\n"); + if (ShouldReportCLRNotificationFailure()) + { + ReportCLRNotificationFailure(E_FAIL); + ExtOut("Expecting first chance CLRN exception\n"); + } return E_FAIL; #else g_ExtControl->Execute(DEBUG_OUTCTL_NOT_LOGGED, "process continue", 0); @@ -5905,7 +5943,11 @@ HRESULT HandleCLRNotificationEvent() HRESULT Status = g_clrData->TranslateExceptionRecordToNotification(&dle.ExceptionRecord, &Notification); if (Status != S_OK) { - ExtErr("Error processing exception notification\n"); + if (ShouldReportCLRNotificationFailure()) + { + ReportCLRNotificationFailure(Status); + ExtErr("Error processing exception notification\n"); + } return Status; } else @@ -5916,11 +5958,10 @@ HRESULT HandleCLRNotificationEvent() case DEBUG_STATUS_GO_HANDLED: case DEBUG_STATUS_GO_NOT_HANDLED: #ifndef FEATURE_PAL - g_ExtControl->Execute(DEBUG_OUTCTL_NOT_LOGGED, "g", 0); + return g_ExtControl->Execute(DEBUG_OUTCTL_NOT_LOGGED, "g", 0); #else - g_ExtControl->Execute(DEBUG_OUTCTL_NOT_LOGGED, "process continue", 0); + return g_ExtControl->Execute(DEBUG_OUTCTL_NOT_LOGGED, "process continue", 0); #endif - break; default: break; } @@ -5941,11 +5982,89 @@ void EnableModuleLoadUnloadCallbacks() #ifndef FEATURE_PAL +class CLRNotificationResumeGuard +{ + ToRelease m_control; + bool m_resume; + +public: + CLRNotificationResumeGuard(IDebugClient* client) + : m_resume(false) + { + if (FAILED(client->QueryInterface(__uuidof(IDebugControl2), (void**)&m_control))) + { + return; + } + + ULONG debugClass = DEBUG_CLASS_UNINITIALIZED; + ULONG qualifier = 0; + if (m_control->GetDebuggeeType(&debugClass, &qualifier) == S_OK && + debugClass == DEBUG_CLASS_USER_WINDOWS && + qualifier < DEBUG_DUMP_SMALL) + { + m_resume = true; + } + } + + ~CLRNotificationResumeGuard() + { + if (m_resume) + { + // Execute can invoke this callback reentrantly while g waits for the next event. + m_resume = false; + m_control->Execute(DEBUG_OUTCTL_NOT_LOGGED, "g", 0); + } + } + + void SuppressResume() + { + m_resume = false; + } +}; + DECLARE_API(SOSHandleCLRN) { - INIT_API(); + INIT_API_EXT(); + CLRNotificationResumeGuard resumeGuard(client); + if ((Status = ArchQuery()) != S_OK) + { + if (ShouldReportCLRNotificationFailure()) + { + ReportCLRNotificationFailure(Status); + } + return Status; + } + if ((Status = GetRuntime(&g_pRuntime)) != S_OK) + { + if (ShouldReportCLRNotificationFailure()) + { + ReportCLRNotificationFailure(Status); + EENotLoadedMessage(Status); + } + return Status; + } + if ((Status = LoadClrDebugDll()) != S_OK) + { + if (ShouldReportCLRNotificationFailure()) + { + ReportCLRNotificationFailure(Status); + DACMessage(Status); + } + return Status; + } + g_bDacBroken = FALSE; + ToRelease spIDP(g_clrData); + ToRelease spISD(g_sos); + ToRelease spISD15(g_sos15); + ToRelease spISD16(g_sos16); + ResetGlobals(); MINIDUMP_NOT_SUPPORTED(); - return HandleCLRNotificationEvent(); + Status = HandleCLRNotificationEvent(); + if (SUCCEEDED(Status)) + { + resumeGuard.SuppressResume(); + } + return Status; } HRESULT HandleRuntimeLoadedNotification(IDebugClient* client) diff --git a/src/SOS/Strike/util.cpp b/src/SOS/Strike/util.cpp index 36f5aca735..e98d1beacb 100644 --- a/src/SOS/Strike/util.cpp +++ b/src/SOS/Strike/util.cpp @@ -3807,10 +3807,20 @@ class SOSDacInterface15Simulator : public ISOSDacInterface15 // Return Value: // HRESULT indicating success or failure // +static HRESULT s_dacFallbackFailure = S_OK; + +HRESULT GetDacFallbackFailure(void) +{ + return s_dacFallbackFailure; +} + HRESULT LoadClrDebugDll(void) { _ASSERTE(g_pRuntime != nullptr); + s_dacFallbackFailure = S_OK; HRESULT hr = g_pRuntime->GetClrDataProcess(IRuntime::ClrDataProcessFlags::UseCDac, &g_clrData); + HRESULT primaryFailure = hr; + bool usedDbgEngFallback = false; if (FAILED(hr)) { if (Runtime::GetCDacLoadPolicy() == CDacLoadPolicy::UseCDac) @@ -3822,6 +3832,7 @@ HRESULT LoadClrDebugDll(void) { return hr; } + usedDbgEngFallback = true; } else { @@ -3832,6 +3843,13 @@ HRESULT LoadClrDebugDll(void) if (FAILED(hr)) { g_sos = NULL; + g_clrData->Release(); + g_clrData = NULL; + if (usedDbgEngFallback) + { + s_dacFallbackFailure = hr; + return primaryFailure; + } return hr; } // Always have an instance of the MethodTable enumerator diff --git a/src/SOS/Strike/util.h b/src/SOS/Strike/util.h index f7aecc1929..f69a55c263 100644 --- a/src/SOS/Strike/util.h +++ b/src/SOS/Strike/util.h @@ -1683,6 +1683,7 @@ inline BOOL IsKernelDebugger () void ResetGlobals(void); HRESULT LoadClrDebugDll(void); +HRESULT GetDacFallbackFailure(void); extern IMetaDataImport* MDImportForModule (DacpModuleData *pModule); extern IMetaDataImport* MDImportForModule (DWORD_PTR pModule); diff --git a/src/tests/SOS.UnitTests/Debuggees/CLRNotificationHandlerFailure/CLRNotificationHandlerFailure.csproj b/src/tests/SOS.UnitTests/Debuggees/CLRNotificationHandlerFailure/CLRNotificationHandlerFailure.csproj new file mode 100644 index 0000000000..1eaa73f512 --- /dev/null +++ b/src/tests/SOS.UnitTests/Debuggees/CLRNotificationHandlerFailure/CLRNotificationHandlerFailure.csproj @@ -0,0 +1,9 @@ + + + + Exe + $(BuildProjectFramework) + $(SupportedSubProcessTargetFrameworks) + + + diff --git a/src/tests/SOS.UnitTests/Debuggees/CLRNotificationHandlerFailure/Program.cs b/src/tests/SOS.UnitTests/Debuggees/CLRNotificationHandlerFailure/Program.cs new file mode 100644 index 0000000000..311d872c70 --- /dev/null +++ b/src/tests/SOS.UnitTests/Debuggees/CLRNotificationHandlerFailure/Program.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace CLRNotificationHandlerFailure +{ + internal static class Program + { + private static void Main() + { + TriggerNotification(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void TriggerNotification() + { + for (int index = 0; index < 4; index++) + { + AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName($"CLRNotificationHandlerFailure.Dynamic{index}"), + AssemblyBuilderAccess.Run); + ModuleBuilder module = assembly.DefineDynamicModule($"Dynamic{index}"); + TypeBuilder type = module.DefineType($"DynamicType{index}"); + type.CreateType(); + } + Debugger.Break(); + } + } +} diff --git a/src/tests/SOS.UnitTests/SOS.cs b/src/tests/SOS.UnitTests/SOS.cs index 443bcf995f..8985936fc7 100644 --- a/src/tests/SOS.UnitTests/SOS.cs +++ b/src/tests/SOS.UnitTests/SOS.cs @@ -60,6 +60,31 @@ public static IEnumerable GetNetCoreConfigurations() .Select(c => new[] { c }); } + public static IEnumerable GetCLRNotificationFailureConfigurations() + { + if (OS.Kind != OSKind.Windows) + { + return new[] { new object[] { TestConfiguration.Empty } }; + } + + List configurations = Configurations + .Select(args => (TestConfiguration)args[0]) + .Where(c => c != TestConfiguration.Empty && c.IsNETCore && !c.PublishSingleFile && c.RuntimeFrameworkVersionMajor < 11) + .OrderByDescending(c => c.RuntimeFrameworkVersion) + .Take(1) + .Select(c => + { + Dictionary settings = new(c.AllSettings) + { + ["HostArgs"] = "exec " + c.HostArgs + }; + return new TestConfiguration(settings); + }) + .Select(c => new object[] { c }) + .ToList(); + return configurations.Count == 0 ? new[] { new object[] { TestConfiguration.Empty } } : configurations; + } + public static IEnumerable GetGCConfigurations() { IEnumerable inputConfigurations = TestRunConfiguration.Instance.Configurations @@ -670,6 +695,46 @@ await SOSTestHelpers.RunTest( }, Output); } + + [SkippableTheory, MemberData(nameof(SOSTestHelpers.GetCLRNotificationFailureConfigurations), MemberType = typeof(SOSTestHelpers))] + public async Task CLRNotificationHandlerFailure(TestConfiguration config) + { + if (config == TestConfiguration.Empty) + { + throw new SkipTestException("CLR notification failure coverage requires Windows and a pre-.NET 11 runtime"); + } + + string sourceSosPath = config.SOSPath(); + string isolatedSosDirectory = Path.Combine(config.LogDirPath, "CLRNotificationHandlerFailure.SOS"); + // Omitting the bundled cDAC makes the forced cDAC failure independent of the build layout. + Directory.CreateDirectory(isolatedSosDirectory); + foreach (string sourcePath in Directory.EnumerateFiles(Path.GetDirectoryName(sourceSosPath))) + { + if (!Path.GetFileName(sourcePath).Equals("mscordaccore_universal.dll", StringComparison.OrdinalIgnoreCase)) + { + File.Copy(sourcePath, Path.Combine(isolatedSosDirectory, Path.GetFileName(sourcePath)), overwrite: true); + } + } + Dictionary settings = new(config.AllSettings) + { + ["SOSPath"] = Path.Combine(isolatedSosDirectory, Path.GetFileName(sourceSosPath)) + }; + config = new TestConfiguration(settings); + + await SOSTestHelpers.RunTest( + scriptName: "CLRNotificationHandlerFailure.script", + new SOSRunner.TestInformation + { + TestConfiguration = config, + TestName = "SOS.CLRNotificationHandlerFailure", + DebuggeeName = "CLRNotificationHandlerFailure", + TestDump = false, + ApplyDacModeOnDemand = true, + DacModeOverride = DacMode.CDac, + FailOnCLRNotificationStop = true, + }, + Output); + } } public class SOSMethodTests diff --git a/src/tests/SOS.UnitTests/SOSRunner.cs b/src/tests/SOS.UnitTests/SOSRunner.cs index d45026f038..3ecdcf9c82 100644 --- a/src/tests/SOS.UnitTests/SOSRunner.cs +++ b/src/tests/SOS.UnitTests/SOSRunner.cs @@ -149,6 +149,12 @@ public DumpType DumpType public bool EnableStressLog { get; set; } + public bool ApplyDacModeOnDemand { get; set; } + + public DacMode? DacModeOverride { get; set; } + + public bool FailOnCLRNotificationStop { get; set; } + public bool TestCrashReport { get { return _testCrashReport && DumpGenerator == DumpGenerator.CreateDump && OS.Kind != OSKind.Windows; } @@ -189,11 +195,14 @@ public string DebuggerToString private readonly ScriptLogger _scriptLogger; private readonly ProcessRunner _processRunner; private readonly DumpType? _dumpType; + private readonly bool _applyDacModeOnDemand; + private readonly DacMode _dacMode; + private readonly bool _failOnCLRNotificationStop; private string _lastCommandOutput; private string _previousCommandCapture; private SOSRunner(NativeDebugger debugger, TestConfiguration config, TestRunner.OutputHelper outputHelper, Dictionary variables, - ScriptLogger scriptLogger, ProcessRunner processRunner, DumpType? dumpType) + ScriptLogger scriptLogger, ProcessRunner processRunner, DumpType? dumpType, bool applyDacModeOnDemand, DacMode dacMode, bool failOnCLRNotificationStop) { Debugger = debugger; _config = config; @@ -202,6 +211,9 @@ private SOSRunner(NativeDebugger debugger, TestConfiguration config, TestRunner. _scriptLogger = scriptLogger; _processRunner = processRunner; _dumpType = dumpType; + _applyDacModeOnDemand = applyDacModeOnDemand; + _dacMode = dacMode; + _failOnCLRNotificationStop = failOnCLRNotificationStop; } /// @@ -480,6 +492,7 @@ public static async Task StartDebugger(TestInformation information, D throw new ArgumentException("Invalid TestInformation"); } TestConfiguration config = information.TestConfiguration; + DacMode dacMode = information.DacModeOverride ?? config.DacMode; TestRunner.OutputHelper outputHelper = null; SOSRunner sosRunner = null; @@ -727,7 +740,7 @@ public static async Task StartDebugger(TestInformation information, D // * SOS's own cDAC load policy ("runtimes --usecdac"), applied in LoadSosExtension. That // command is an SOS extension command and is not available until SOS has been loaded, so // it cannot be issued as a pre-SOS initial debugger command. - switch (config.DacMode) + switch (dacMode) { case DacMode.CDacFallback: // cDAC hosted by the in-box DAC, with per-API fallback to the legacy DAC. @@ -801,7 +814,17 @@ public static async Task StartDebugger(TestInformation information, D } // Create the sos runner instance - sosRunner = new SOSRunner(debugger, config, outputHelper, variables, scriptLogger, processRunner, dumpType); + sosRunner = new SOSRunner( + debugger, + config, + outputHelper, + variables, + scriptLogger, + processRunner, + dumpType, + information.ApplyDacModeOnDemand, + dacMode, + information.FailOnCLRNotificationStop); // Start the native debugger Stopwatch launchSw = Stopwatch.StartNew(); @@ -896,6 +919,10 @@ public async Task RunScript(string scriptRelativePath) { await ContinueExecution(); } + else if (line.StartsWith("APPLYDACMODE")) + { + await ApplyDacMode(); + } // Adds the "!" prefix under dbgeng, nothing under lldb. Meant for SOS (native) commands. else if (line.StartsWith("SOSCOMMAND:")) { @@ -1156,13 +1183,8 @@ public async Task LoadSosExtension() // uses the requested DAC/cDAC the first time it resolves the runtime. CDacFallback/CDacVerify // instead rely on the in-box DAC via env vars set in StartDebugger and keep SOS's default // policy (which does not load the standalone cDAC when DOTNET_ENABLE_CDAC is set). - string cdacPolicyCommand = _config.DacMode switch - { - DacMode.CDac => "runtimes --usecdac true", // Force the standalone cDAC next to sos.dll. - DacMode.Dac => "runtimes --usecdac false", // Force the legacy in-box DAC. - _ => null, - }; - if (cdacPolicyCommand is not null && Debugger != NativeDebugger.Gdb) + string cdacPolicyCommand = GetDacPolicyCommand(); + if (!_applyDacModeOnDemand && cdacPolicyCommand is not null && Debugger != NativeDebugger.Gdb) { commands.Add((Debugger == NativeDebugger.Cdb ? "!" : "") + cdacPolicyCommand); } @@ -1217,10 +1239,49 @@ public async Task ContinueExecution() { throw new Exception($"'{command}' FAILED"); } + if (Debugger == NativeDebugger.Cdb && _lastCommandOutput is not null) + { + string continuationOutput = _lastCommandOutput; + if (!await RunCommand(".lastevent")) + { + throw new Exception("'.lastevent' FAILED"); + } + string lastEventOutput = _lastCommandOutput; + _lastCommandOutput = continuationOutput; + bool stoppedOnCLRNotification = Regex.IsMatch(lastEventOutput ?? string.Empty, @"\be0444143\b", RegexOptions.IgnoreCase); + if (stoppedOnCLRNotification && + (_failOnCLRNotificationStop || continuationOutput.Contains("SOS CLR notification handler failed", StringComparison.Ordinal))) + { + throw new Exception("Continuation returned while the target was parked on an unhandled CLR notification"); + } + } } } } + public async Task ApplyDacMode() + { + string command = GetDacPolicyCommand(); + if (command is null) + { + throw new InvalidOperationException($"DacMode {_dacMode} does not have an SOS load policy command"); + } + if (!await RunSosCommand(command)) + { + throw new Exception($"SOS command FAILED: {command}"); + } + } + + private string GetDacPolicyCommand() + { + return _dacMode switch + { + DacMode.CDac => "runtimes --usecdac true", + DacMode.Dac => "runtimes --usecdac false", + _ => null, + }; + } + public async Task SwitchThread(string threadId) { string command = null; diff --git a/src/tests/SOS.UnitTests/Scripts/CLRNotificationHandlerFailure.script b/src/tests/SOS.UnitTests/Scripts/CLRNotificationHandlerFailure.script new file mode 100644 index 0000000000..392dcaea57 --- /dev/null +++ b/src/tests/SOS.UnitTests/Scripts/CLRNotificationHandlerFailure.script @@ -0,0 +1,13 @@ +# +# Forces the standalone cDAC to fail after bpmd has armed CLR notifications. +# + +LOADSOS +SOSCOMMAND:bpmd CLRNotificationHandlerFailure.dll CLRNotificationHandlerFailure.Program.TriggerNotification +CONTINUE + +APPLYDACMODE +SOSCOMMAND_FAIL:clrstack +VERIFY:Failed to load data access module, 0x80131c(4f|64) +CONTINUE +VERIFY:Break instruction exception - code 80000003