From 4db58debd663cf7f77d71722c2e1476b32c25182 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 17:09:07 -0500 Subject: [PATCH 01/14] [cDAC][wasm] Resolve ReadyToRun virtual IP ranges Expose WebAssembly ReadyToRun virtual IP ranges through the runtime data descriptor and resolve them before the RangeSectionMap. Mask funclet flags and keep virtual code identity separate from loaded-image RVA reads. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 36 +- .../data-descriptor-meanings.json | 7 +- src/coreclr/vm/codeman.h | 8 + .../vm/datadescriptor/datadescriptor.inc | 7 + .../Constants.cs | 1 + ...ecutionManagerCore.ReadyToRunJitManager.cs | 114 ++++-- .../ExecutionManager/ExecutionManagerCore.cs | 121 +++++- .../Helpers/RuntimeFunctionLookup.cs | 28 +- .../Helpers/UnwindDataSize.cs | 19 + .../StackWalk/Context/Wasm/WasmR2RInfo.cs | 9 +- .../Data/VirtualIPRangeSection.cs | 11 + .../DataType.cs | 1 + .../ExecutionManager/ExecutionManagerTests.cs | 354 ++++++++++++++++-- .../MockDescriptors.ExecutionManager.cs | 128 ++++++- 14 files changed, 736 insertions(+), 108 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VirtualIPRangeSection.cs diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 25de45a140dc1c..c525a25769990b 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -157,6 +157,12 @@ The [range section map](#rangesectionmap) is used to partition the address space Within a range section fragment, a [nibble map](#nibblemap) structure is used to map arbitrary IP addresses back to the start of the method (and to the code header which immediately preceeeds the entrypoint to the code). +WebAssembly ReadyToRun code uses encoded virtual IPs rather than linear-memory code addresses. +These ranges are registered in `VirtualIPRangeList`, not the range section map. A virtual-IP +lookup walks that bounded intrusive list before consulting the range section map. Invalid, +cyclic, excessive, or ambiguous lists fail closed, and an encoded virtual IP that is absent from +the list does not fall through to the real-address map. + ### Data descriptors used @@ -214,6 +220,7 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to | `RangeSection` | `NextForDelete` | `pointer` | Pointer to next range section for deletion | | `RangeSection` | `R2RModule` | `pointer` | ReadyToRun module | | `RangeSection` | `RangeBegin` | `pointer` | Begin address of the range section | +| `RangeSection` | `RangeEndOpen` | `pointer` | Exclusive end address of the range section | | `RangeSection` | `RangeList` | `pointer` | Pointer to the `CodeRangeMapRangeList` associated with this range section | | `RangeSectionFragment` | `Next` | `pointer` | Tagged pointer to the next fragment (bit 0 is the collectible flag; must be stripped to obtain the address) | | `RangeSectionFragment` | `RangeBegin` | `pointer` | Begin address of the fragment | @@ -231,6 +238,7 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to | `ReadyToRunInfo` | `EntryPointToMethodDescMap` | `HashMap` | `HashMap` of entry point addresses to `MethodDesc` pointers | | `ReadyToRunInfo` | `HotColdMap` | `pointer` | Pointer to an array of 32-bit integers - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80) | | `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image | +| `ReadyToRunInfo` | `MinVirtualIP` | `pointer` | Base virtual IP assigned to the ReadyToRun module on WebAssembly | | `ReadyToRunInfo` | `NumHotColdMap` | `uint32` | Number of entries in the `HotColdMap` | | `ReadyToRunInfo` | `NumRuntimeFunctions` | `uint32` | Number of `RuntimeFunctions` | | `ReadyToRunInfo` | `ReadyToRunHeader` | `pointer` | Pointer to the ReadyToRunHeader | @@ -245,10 +253,12 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to | `RealCodeHeader` | `NumUnwindInfos` | `uint32` | Number of Unwind Infos | | `RealCodeHeader` | `UnwindInfos` | `pointer` | Start address of Unwind Infos | | `RuntimeFunction` | *(type size)* | `uint32` | Size of a runtime function entry in bytes | -| `RuntimeFunction` | `BeginAddress` | `uint32` | Begin address of the function. On ARM32, bit 0 (the Thumb bit) is set. | +| `RuntimeFunction` | `BeginAddress` | `uint32` | Begin address of the function. On ARM32, bit 0 is the Thumb bit; on WebAssembly, bit 31 marks a funclet and is excluded from address arithmetic. | | `RuntimeFunction` | `EndAddress` | `uint32` | End address of the function. Only exists on some platforms | | `RuntimeFunction` | `UnwindData` | `uint32` | Pointer to the unwind info for the function | | `UnwindInfo` | `FunctionLength` | `uint32` | Length of the associated function in bytes. Only exists on some platforms | +| `VirtualIPRangeSection` | `Next` | `pointer` | Pointer to the next registered WebAssembly ReadyToRun virtual-IP range | +| `VirtualIPRangeSection` | `RangeSection` | `pointer` | Address of the embedded synthetic `RangeSection` describing the virtual-IP range | ### Global variables used @@ -262,6 +272,7 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to | `ObjectMethodTable` | `pointer` | Address of the global variable holding the System.Object MethodTable pointer | | `StubCodeBlockLast` | `uint8` | Maximum sentinel code header value indentifying a stub code block | | `ThePreStub` | `pointer` | Address of the global containing the prestub entrypoint | +| `VirtualIPRangeList` | `pointer` | Address of the global pointer to the WebAssembly ReadyToRun virtual-IP range list | ### Contracts used @@ -351,8 +362,9 @@ bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddre // Find the relative address that we are looking for TargetCodePointer addr = /* code pointer from jittedCodeAddress using PlatformMetadata.GetCodePointerFlags */ - TargetPointer imageBase = Target.ReadPointer(/* range section address + RangeSection::RangeBegin offset */); - TargetPointer relativeAddr = addr - imageBase; + TargetPointer codeBase = /* RangeSection.RangeBegin, or ReadyToRunInfo.MinVirtualIP on WebAssembly */; + TargetPointer loadedImageBase = /* ReadyToRunInfo.LoadedImageBase on WebAssembly, otherwise codeBase */; + TargetPointer relativeAddr = addr - codeBase; TargetPointer runtimeFunctions = Target.ReadPointer(r2rInfo + /* ReadyToRunInfo::RuntimeFunctions offset */); int index = // Iterate through runtimeFunctions and find index of function with relativeAddress @@ -367,7 +379,8 @@ bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddre TargetPointer function = runtimeFunctions + (ulong)(index * /* size of RuntimeFunction */); - TargetPointer startAddress = imageBase + Target.Read(function + /* RuntimeFunction::BeginAddress offset */); + uint beginAddress = /* RuntimeFunction.BeginAddress with platform flags removed */; + TargetPointer startAddress = codeBase + beginAddress; TargetPointer entryPoint = /* code pointer from startAddress using PlatformMetadata.GetCodePointerFlags */ TargetPointer mapAddress = r2rInfo + /* ReadyToRunInfo::EntryPointToMethodDescMap offset */; @@ -383,7 +396,7 @@ bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddre { uint coldIndex = // look up cold part in hot/cold map TargetPointer coldFunction = runtimeFunctions + (ulong)(coldIndex * /* size of RuntimeFunction */); - TargetPointer coldStart = imageBase + Target.Read(function + /* RuntimeFunction::BeginAddress offset */); + TargetPointer coldStart = codeBase + /* masked cold RuntimeFunction.BeginAddress */; relativeOffset = /* function length of hot part */ + addr - coldStart; } @@ -425,7 +438,7 @@ public override void GetMethodRegionInfo(RangeSection rangeSection, TargetCodePo if (/* found in hot/cold map */) { // Compute cold region bounds from cold runtime function start/end indices - coldStart = imageBase + coldStartFunc.BeginAddress; + coldStart = codeBase + /* masked cold RuntimeFunction.BeginAddress */; coldSize = coldEndOffset - coldBeginOffset; hotSize -= coldSize; } @@ -508,7 +521,7 @@ The `GetMethodDesc`, `GetStartAddress`, and `GetRelativeOffset` APIs extract fie * For interpreted code (`InterpreterJitManager`), there is no native unwind info. `GetUnwindInfo` returns null. -Unwind info (`RUNTIME_FUNCTION`) use relative addressing. For managed code, these values are relative to the start of the code's containing range in the RangeSectionMap (described below). This could be the beginning of a `CodeHeap` for jitted code or the base address of the loaded image for ReadyToRun code. +Unwind info (`RUNTIME_FUNCTION`) uses relative addressing. For managed code, these values are relative to the beginning of a `CodeHeap` for jitted code or the loaded-image base for ReadyToRun code. On WebAssembly, ReadyToRun entrypoint identity remains relative to `MinVirtualIP`, while unwind, debug, GC, and exception data RVAs are resolved from `LoadedImageBase`. `GetUnwindInfoBaseAddress` finds this base address for a given `CodeBlockHandle`. `IExecutionManager.GetDebugInfo` gets a pointer to the relevant DebugInfo for a `CodeBlockHandle`. The ExecutionManager delegates to the JitManager implementations as the DebugInfo is stored in different ways on jitted and R2R code. @@ -554,15 +567,14 @@ After obtaining the clause array bounds, the common iteration logic classifies e `GetCodeKind` classifies a code address by finding its owning range section and determining the code kind. It distinguishes between jitted code, stub code blocks (jump stubs, precode stubs, VSD stubs, etc.), ReadyToRun code, interpreter code, and the global prestub entrypoint. If no range section owns the address, it compares the address against the exposed prestub entrypoint. Returns `Unknown` if the address cannot be classified. We depend on the values of the StubCodeBlockKind enum defined in codeman.h; for non-R2R code, we compare either the RangeList type or the code header against the values of this enum. ### FindReadyToRunModule -`FindReadyToRunModule` locates the ReadyToRun module whose PE image contains the given address. Unlike `GetCodeBlockHandle` (which only matches code regions), this API matches against the full PE image range - including data sections such as import tables. This is used in GCRefMap resolution as it requires finding the module that owns an import section indirection address, which is in the data section rather than the code section. +`FindReadyToRunModule` locates the ReadyToRun module that owns an address. Real addresses use the range section map, whose ReadyToRun ranges cover the full PE image including import tables. Encoded WebAssembly virtual IPs use `VirtualIPRangeList` and never fall through to the real-address map. ```csharp TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) { - // Use the RangeSectionMap to find the RangeSection containing the address. - // ReadyToRun range sections cover the entire PE image (code + data), - // so this works for import section addresses used by GCRefMap lookup. - RangeSection range = RangeSection.Find(target, topRangeSectionMap, address); + // Encoded WebAssembly virtual IPs use VirtualIPRangeList first. + // All other addresses use the RangeSectionMap. + RangeSection range = RangeSection.Find(target, topRangeSectionMap, virtualIPRangeList, address); if (range.Data is null) return TargetPointer.Null; diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 221478b81fe25c..f04a2059e1a898 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -488,6 +488,7 @@ "RangeSection.NextForDelete": "Pointer to next range section for deletion", "RangeSection.R2RModule": "ReadyToRun module", "RangeSection.RangeBegin": "Begin address of the range section", + "RangeSection.RangeEndOpen": "Exclusive end address of the range section", "RangeSection.RangeList": "Pointer to the `CodeRangeMapRangeList` associated with this range section", "RangeSectionFragment.Next": "Tagged pointer to the next fragment (bit 0 is the collectible flag; must be stripped to obtain the address)", "RangeSectionFragment.RangeBegin": "Begin address of the fragment", @@ -519,6 +520,7 @@ "ReadyToRunInfo.HotColdMap": "Pointer to an array of 32-bit integers - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80)", "ReadyToRunInfo.ImportSections": "Pointer to the array of ReadyToRun import sections", "ReadyToRunInfo.LoadedImageBase": "Base address of the loaded R2R image", + "ReadyToRunInfo.MinVirtualIP": "Base virtual IP assigned to the ReadyToRun module on WebAssembly", "ReadyToRunInfo.NumHotColdMap": "Number of entries in the `HotColdMap`", "ReadyToRunInfo.NumImportSections": "Number of ReadyToRun import sections", "ReadyToRunInfo.NumRuntimeFunctions": "Number of `RuntimeFunctions`", @@ -536,7 +538,7 @@ "RegionFreeList.HeadFreeRegion": "Head of the free region segment list", "RegionFreeList.Size": "Size in bytes of each region free-list structure", "ResumableFrame.TargetContextPtr": "Pointer to the Frame's Target Context", - "RuntimeFunction.BeginAddress": "Begin address of the function. On ARM32, bit 0 (the Thumb bit) is set.", + "RuntimeFunction.BeginAddress": "Begin address of the function. On ARM32, bit 0 is the Thumb bit; on WebAssembly, bit 31 marks a funclet and is excluded from address arithmetic.", "RuntimeFunction.EndAddress": "End address of the function. Only exists on some platforms", "RuntimeFunction.Size": "Size of a runtime function entry in bytes", "RuntimeFunction.UnwindData": "Pointer to the unwind info for the function", @@ -695,6 +697,8 @@ "UnorderedArrayBase.Count": "Number of valid entries currently stored in the array.", "UnorderedArrayBase.Table": "Pointer to the backing storage holding the array's entries.", "UnwindInfo.FunctionLength": "Length of the associated function in bytes. Only exists on some platforms", + "VirtualIPRangeSection.Next": "Pointer to the next registered WebAssembly ReadyToRun virtual-IP range", + "VirtualIPRangeSection.RangeSection": "Address of the embedded synthetic `RangeSection` describing the virtual-IP range", "VASigCookie.Signature": "The raw vararg signature (see `Signature`).", "VASigCookie.SignatureLength": "Length in bytes of the raw vararg signature blob.", "VASigCookie.SignaturePointer": "Target address of the raw vararg signature blob.", @@ -856,6 +860,7 @@ "TearOffAddRefSimple": "Address of Unknown_AddRefSpecial; identifies SimpleComCallWrapper interface pointers", "TearOffAddRefSimpleInner": "Address of Unknown_AddRefInner; identifies inner SimpleComCallWrapper interface pointers", "ThePreStub": "Address of the global containing the prestub entrypoint", + "VirtualIPRangeList": "Address of the global pointer to the WebAssembly ReadyToRun virtual-IP range list", "ThinlockThreadIdDispenser": "Pointer to the dispenser of thin-lock thread IDs", "ThreadStore": "Pointer to the runtime thread store", "TotalCpuCount": "Number of available processors", diff --git a/src/coreclr/vm/codeman.h b/src/coreclr/vm/codeman.h index 6e67a78c584d8b..ef16a7d897c424 100644 --- a/src/coreclr/vm/codeman.h +++ b/src/coreclr/vm/codeman.h @@ -2803,11 +2803,19 @@ struct cdac_data static constexpr void* const CodeRangeMapAddress = (void*)&ExecutionManager::g_codeRangeMap.Data[0]; static constexpr PTR_EEJitManager* EEJitManagerAddress = &ExecutionManager::m_pEEJitManager; #ifdef TARGET_WASM + static constexpr VirtualIPRangeSection** VirtualIPRangeListAddress = &ExecutionManager::s_pVirtualIPRangeList; static constexpr FunctionTableIndexRangeSection** FunctionTableIndexRangeListAddress = &ExecutionManager::s_pFunctionTableIndexRangeList; #endif // TARGET_WASM }; #ifdef TARGET_WASM +template<> +struct cdac_data +{ + static constexpr size_t RangeSection = offsetof(VirtualIPRangeSection, rangeSection); + static constexpr size_t Next = offsetof(VirtualIPRangeSection, pNext); +}; + template<> struct cdac_data { diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 25c22dc3a928fc..1a3f8c17921666 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -973,6 +973,12 @@ CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, MinVirtualIP, cdac_data::RangeSection) +CDAC_TYPE_FIELD(VirtualIPRangeSection, T_POINTER, Next, cdac_data::Next) +CDAC_TYPE_END(VirtualIPRangeSection) + CDAC_TYPE_BEGIN(FunctionTableIndexRangeSection) CDAC_TYPE_INDETERMINATE(FunctionTableIndexRangeSection) CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_UINT32, MinFunctionTableIndex, cdac_data::MinFunctionTableIndex) @@ -1828,6 +1834,7 @@ CDAC_GLOBAL(StressLogEnabled, T_UINT8, 0) CDAC_GLOBAL_POINTER(ExecutionManagerCodeRangeMapAddress, cdac_data::CodeRangeMapAddress) CDAC_GLOBAL_POINTER(EEJitManagerAddress, cdac_data::EEJitManagerAddress) #ifdef TARGET_WASM +CDAC_GLOBAL_POINTER(VirtualIPRangeList, cdac_data::VirtualIPRangeListAddress) CDAC_GLOBAL_POINTER(FunctionTableIndexRangeList, cdac_data::FunctionTableIndexRangeListAddress) #endif // TARGET_WASM CDAC_GLOBAL_POINTER(PlatformMetadata, &::g_cdacPlatformMetadata) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 8c257267fa5072..0cdf1d40014021 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -12,6 +12,7 @@ public static class Globals public const string SystemDomain = nameof(SystemDomain); public const string ThreadStore = nameof(ThreadStore); public const string FinalizerThread = nameof(FinalizerThread); + public const string VirtualIPRangeList = nameof(VirtualIPRangeList); public const string FunctionTableIndexRangeList = nameof(FunctionTableIndexRangeList); public const string GCThread = nameof(GCThread); public const string Debugger = nameof(Debugger); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 8168c1c0929666..d51e67d6e868f3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -21,7 +21,9 @@ public ReadyToRunJitManager(Target target) : base(target) { _hashMap = PtrHashMapLookup.Create(target); _hotCold = HotColdLookup.Create(target); - _runtimeFunctions = RuntimeFunctionLookup.Create(target); + _runtimeFunctions = RuntimeFunctionLookup.Create( + target, + target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.Wasm); } private enum ReadyToRunSectionType @@ -35,20 +37,22 @@ public override bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer info = default; Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(rangeSection); - if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer imageBase, out uint index)) + if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer codeBase, out uint index)) return false; index = AdjustRuntimeFunctionIndexForHotCold(r2rInfo, index); - index = AdjustRuntimeFunctionToMethodStart(r2rInfo, imageBase, index, out TargetPointer methodDesc); + if (!TryAdjustRuntimeFunctionToMethodStart(r2rInfo, codeBase, index, out index, out TargetPointer methodDesc)) + return false; Data.RuntimeFunction function = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, index); + uint functionBeginAddress = _runtimeFunctions.GetBeginAddress(function); TargetPointer addr = CodePointerUtils.AddressFromCodePointer(jittedCodeAddress, Target); // The R2R RUNTIME_FUNCTION.BeginAddress encodes thumb code with the thumb bit set on // ARM32. Native RUNTIME_FUNCTION__BeginAddress uses ThumbCodeToDataPointer to strip it // (clrnt.h, ARM32 branch). Mirror that so we store a raw TADDR-style start address. TargetPointer startAddress = CodePointerUtils.AddressFromCodePointer( - new TargetCodePointer(imageBase.Value + function.BeginAddress), Target); + new TargetCodePointer(codeBase.Value + functionBeginAddress), Target); TargetNUInt relativeOffset = new TargetNUInt(addr - startAddress); // Take hot/cold splitting into account for the relative offset @@ -57,12 +61,12 @@ public override bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer Debug.Assert(coldFunctionIndex < r2rInfo.NumRuntimeFunctions); Data.RuntimeFunction coldFunction = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldFunctionIndex); TargetPointer coldStart = CodePointerUtils.AddressFromCodePointer( - new TargetCodePointer(imageBase.Value + coldFunction.BeginAddress), Target); + new TargetCodePointer(codeBase.Value + _runtimeFunctions.GetBeginAddress(coldFunction)), Target); if (addr >= coldStart) { // If the address is in the cold part, the relative offset is the size of the // hot part plus the offset from the address to the start of the cold part - uint hotSize = _runtimeFunctions.GetFunctionLength(imageBase, function); + uint hotSize = _runtimeFunctions.GetFunctionLength(GetLoadedImageBase(rangeSection, r2rInfo), function); relativeOffset = new TargetNUInt(hotSize + addr - coldStart); } } @@ -87,17 +91,18 @@ public override void GetMethodRegionInfo( hotSize = gcInfo.GetCodeLength(gcInfoHandle); Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(rangeSection); - if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer imageBase, out uint index)) + if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer codeBase, out uint index)) return; if (_hotCold.TryGetColdFunctionIndex(r2rInfo.NumHotColdMap, r2rInfo.HotColdMap, index, r2rInfo.NumRuntimeFunctions, out uint coldStartIdx, out uint coldEndIdx)) { Data.RuntimeFunction coldStartFunc = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldStartIdx); Data.RuntimeFunction coldEndFunc = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldEndIdx); - uint coldBeginOffset = coldStartFunc.BeginAddress; - uint coldEndOffset = coldEndFunc.BeginAddress + _runtimeFunctions.GetFunctionLength(imageBase, coldEndFunc); + uint coldBeginOffset = _runtimeFunctions.GetBeginAddress(coldStartFunc); + uint coldEndOffset = _runtimeFunctions.GetBeginAddress(coldEndFunc) + + _runtimeFunctions.GetFunctionLength(GetLoadedImageBase(rangeSection, r2rInfo), coldEndFunc); coldSize = coldEndOffset - coldBeginOffset; - coldStart = imageBase + coldBeginOffset; + coldStart = codeBase + coldBeginOffset; hotSize -= coldSize; } @@ -120,16 +125,18 @@ public override TargetPointer GetDebugInfo(RangeSection rangeSection, TargetCode // ReadyToRunJitManager::GetDebugInfo Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(rangeSection); - if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer imageBase, out uint index)) + if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer codeBase, out uint index)) return TargetPointer.Null; index = AdjustRuntimeFunctionIndexForHotCold(r2rInfo, index); - index = AdjustRuntimeFunctionToMethodStart(r2rInfo, imageBase, index, out _); + if (!TryAdjustRuntimeFunctionToMethodStart(r2rInfo, codeBase, index, out index, out _)) + return TargetPointer.Null; Data.ImageDataDirectory debugInfoData = Target.ProcessedData.GetOrAdd(r2rInfo.DebugInfoSection); + TargetPointer loadedImageBase = GetLoadedImageBase(rangeSection, r2rInfo); ILCompiler.Reflection.ReadyToRun.NativeReader imageReader = new( - new TargetStream(Target, imageBase, debugInfoData.VirtualAddress + debugInfoData.Size), + new TargetStream(Target, loadedImageBase, debugInfoData.VirtualAddress + debugInfoData.Size), Target.IsLittleEndian ); ILCompiler.Reflection.ReadyToRun.NativeArray debugInfoArray = new(imageReader, debugInfoData.VirtualAddress); @@ -144,14 +151,14 @@ public override TargetPointer GetDebugInfo(RangeSection rangeSection, TargetCode if (lookBack != 0) debugInfoOffset = (uint)offset - lookBack; - return imageBase + debugInfoOffset; + return loadedImageBase + debugInfoOffset; } public override CodeKind GetCodeKind(RangeSection rangeSection, TargetCodePointer codeAddress) { if (rangeSection.Data == null) return CodeKind.Unknown; - return IsStubCodeBlockThunk(rangeSection.Data, GetReadyToRunInfo(rangeSection), codeAddress) ? CodeKind.MethodCallThunk : CodeKind.ReadyToRun; + return IsStubCodeBlockThunk(rangeSection, GetReadyToRunInfo(rangeSection), codeAddress) ? CodeKind.MethodCallThunk : CodeKind.ReadyToRun; } public override void GetGCInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, out TargetPointer gcInfo, out uint gcVersion) @@ -161,15 +168,16 @@ public override void GetGCInfo(RangeSection rangeSection, TargetCodePointer jitt // ReadyToRunJitManager::GetGCInfoToken Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(rangeSection); - if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer imageBase, out uint index)) + if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer codeBase, out uint index)) return; index = AdjustRuntimeFunctionIndexForHotCold(r2rInfo, index); - index = AdjustRuntimeFunctionToMethodStart(r2rInfo, imageBase, index, out _); + if (!TryAdjustRuntimeFunctionToMethodStart(r2rInfo, codeBase, index, out index, out _)) + return; Data.RuntimeFunction runtimeFunction = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, index); - TargetPointer unwindInfo = runtimeFunction.UnwindData + imageBase; + TargetPointer unwindInfo = runtimeFunction.UnwindData + GetLoadedImageBase(rangeSection, r2rInfo); uint unwindDataSize = UnwindDataSize.GetUnwindDataSize(Target, unwindInfo, Target.Contracts.RuntimeInfo.GetTargetArchitecture()); gcInfo = unwindInfo + unwindDataSize; gcVersion = GetR2RGCInfoVersion(r2rInfo); @@ -210,23 +218,26 @@ private bool GetRuntimeFunction( RangeSection rangeSection, Data.ReadyToRunInfo r2rInfo, TargetCodePointer jittedCodeAddress, - out TargetPointer imageBase, + out TargetPointer codeBase, out uint runtimeFunctionIndex) { - imageBase = TargetPointer.Null; + codeBase = TargetPointer.Null; runtimeFunctionIndex = 0; if (rangeSection.Data == null) throw new ArgumentException(nameof(rangeSection)); // Check if address is in a thunk - if (IsStubCodeBlockThunk(rangeSection.Data, r2rInfo, jittedCodeAddress)) + if (IsStubCodeBlockThunk(rangeSection, r2rInfo, jittedCodeAddress)) return false; // Find the relative address that we are looking for TargetPointer addr = CodePointerUtils.AddressFromCodePointer(jittedCodeAddress, Target); - imageBase = rangeSection.Data.RangeBegin; - TargetPointer relativeAddr = addr - imageBase; + codeBase = GetCodeBase(rangeSection, r2rInfo); + if (codeBase == TargetPointer.Null) + return false; + + TargetPointer relativeAddr = addr - codeBase; return _runtimeFunctions.TryGetRuntimeFunctionIndexForAddress(r2rInfo.RuntimeFunctions, r2rInfo.NumRuntimeFunctions, relativeAddr, out runtimeFunctionIndex); } @@ -239,39 +250,51 @@ private uint AdjustRuntimeFunctionIndexForHotCold(Data.ReadyToRunInfo r2rInfo, u return index; } - private uint AdjustRuntimeFunctionToMethodStart(Data.ReadyToRunInfo r2rInfo, TargetPointer imageBase, uint index, out TargetPointer methodDesc) + private bool TryAdjustRuntimeFunctionToMethodStart( + Data.ReadyToRunInfo r2rInfo, + TargetPointer codeBase, + uint index, + out uint methodIndex, + out TargetPointer methodDesc) { - methodDesc = GetMethodDescForRuntimeFunction(r2rInfo, imageBase, index); + methodIndex = index; + methodDesc = GetMethodDescForRuntimeFunction(r2rInfo, codeBase, methodIndex); while (methodDesc == TargetPointer.Null) { + if (methodIndex == 0) + return false; + // Funclets won't have a direct entry in the map of runtime function entry point to method desc. // The funclet's address (and index) will be greater than that of the corresponding function, so // we decrement the index to find the actual function / method desc for the funclet. - index--; - methodDesc = GetMethodDescForRuntimeFunction(r2rInfo, imageBase, index); + methodIndex--; + methodDesc = GetMethodDescForRuntimeFunction(r2rInfo, codeBase, methodIndex); } Debug.Assert(methodDesc != TargetPointer.Null); - return index; + return true; } - private bool IsStubCodeBlockThunk(Data.RangeSection rangeSection, Data.ReadyToRunInfo r2rInfo, TargetCodePointer jittedCodeAddress) + private bool IsStubCodeBlockThunk(RangeSection rangeSection, Data.ReadyToRunInfo r2rInfo, TargetCodePointer jittedCodeAddress) { + if (rangeSection.IsVirtualIP) + return false; + if (r2rInfo.DelayLoadMethodCallThunks == TargetPointer.Null) return false; // Check if the address is in the region containing thunks for READYTORUN_HELPER_DelayLoad_MethodCall Data.ImageDataDirectory thunksData = Target.ProcessedData.GetOrAdd(r2rInfo.DelayLoadMethodCallThunks); - ulong rva = jittedCodeAddress - rangeSection.RangeBegin; + ulong rva = jittedCodeAddress - GetLoadedImageBase(rangeSection, r2rInfo); return thunksData.VirtualAddress <= rva && rva < thunksData.VirtualAddress + thunksData.Size; } - private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInfo, TargetPointer imageBase, uint runtimeFunctionIndex) + private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInfo, TargetPointer codeBase, uint runtimeFunctionIndex) { Data.RuntimeFunction function = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, runtimeFunctionIndex); // ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage - TargetCodePointer startAddress = imageBase + function.BeginAddress; + TargetCodePointer startAddress = codeBase + _runtimeFunctions.GetBeginAddress(function); TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); TargetPointer methodDesc = _hashMap.GetValue(r2rInfo.EntryPointToMethodDescMap, entryPoint); @@ -280,6 +303,12 @@ private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInf return methodDesc; } + + private static TargetPointer GetCodeBase(RangeSection rangeSection, Data.ReadyToRunInfo r2rInfo) + => rangeSection.IsVirtualIP ? r2rInfo.MinVirtualIP ?? TargetPointer.Null : rangeSection.Data!.RangeBegin; + + private static TargetPointer GetLoadedImageBase(RangeSection rangeSection, Data.ReadyToRunInfo r2rInfo) + => rangeSection.IsVirtualIP ? r2rInfo.LoadedImageBase : rangeSection.Data!.RangeBegin; #endregion private void GetExceptionClauses(TargetPointer exceptionLookupTableAddr, uint count, TargetPointer rangeStart, uint methodRVA, out TargetPointer startExInfoRVA, out TargetPointer endExInfoRVA) @@ -320,12 +349,12 @@ public override void GetExceptionClauses(RangeSection range, CodeBlockHandle cbh } uint count = section.Size / Data.ExceptionLookupTableEntry.GetSize(Target); - ulong exceptionLookupTableAddr = section.VirtualAddress + r2rInfo.LoadedImageBase; + TargetPointer loadedImageBase = GetLoadedImageBase(range, r2rInfo); + ulong exceptionLookupTableAddr = section.VirtualAddress + loadedImageBase; - GetMethodRVAAndRangeStart(cbh, out TargetPointer methodStart, out TargetPointer rangeStart); - uint methodRVA = (uint)(methodStart - rangeStart); + GetMethodRVAAndImageBase(range, r2rInfo, cbh, out uint methodRVA, out TargetPointer imageBase); - GetExceptionClauses(exceptionLookupTableAddr, count, rangeStart, methodRVA, out startAddr, out endAddr); + GetExceptionClauses(exceptionLookupTableAddr, count, imageBase, methodRVA, out startAddr, out endAddr); } private ImageDataDirectory? FindSection(Data.ReadyToRunInfo r2rInfo, uint sectionType) @@ -339,11 +368,18 @@ public override void GetExceptionClauses(RangeSection range, CodeBlockHandle cbh return null; } - private void GetMethodRVAAndRangeStart(CodeBlockHandle cbh, out TargetPointer methodStart, out TargetPointer rangeStart) + private void GetMethodRVAAndImageBase( + RangeSection range, + Data.ReadyToRunInfo r2rInfo, + CodeBlockHandle cbh, + out uint methodRVA, + out TargetPointer imageBase) { IExecutionManager executionManager = Target.Contracts.ExecutionManager; - methodStart = executionManager.GetStartAddress(cbh); - rangeStart = executionManager.GetUnwindInfoBaseAddress(cbh); + TargetPointer methodStart = executionManager.GetStartAddress(cbh); + TargetPointer codeBase = GetCodeBase(range, r2rInfo); + methodRVA = checked((uint)(methodStart - codeBase)); + imageBase = GetLoadedImageBase(range, r2rInfo); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 1e7fad5357992c..f78a9cc7d967d5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -23,9 +23,7 @@ internal sealed partial class ExecutionManagerCore : IExecutionManager private readonly ReadyToRunJitManager _r2rJitManager; private readonly InterpreterJitManager _interpreterJitManager; private readonly TargetPointer _thePreStub; - - private Data.RangeSectionMap _topRangeSectionMap - => _target.ProcessedData.GetOrAdd(_topRangeSectionMapAddress); + private readonly TargetPointer _virtualIPRangeListAddress; public ExecutionManagerCore(Target target, TargetPointer topRangeSectionMapAddress) { @@ -39,6 +37,9 @@ public ExecutionManagerCore(Target target, TargetPointer topRangeSectionMapAddre _thePreStub = _target.TryReadGlobalPointer(Constants.Globals.ThePreStub, out TargetPointer? thePreStubPtr) ? _target.ReadPointer(thePreStubPtr.Value) : TargetPointer.Null; + _virtualIPRangeListAddress = _target.TryReadGlobalPointer(Constants.Globals.VirtualIPRangeList, out TargetPointer? virtualIPRangeListAddress) + ? virtualIPRangeListAddress.Value + : TargetPointer.Null; } public void Flush(FlushScope scope) @@ -70,6 +71,7 @@ private enum RangeSectionFlags : int CodeHeap = 0x02, RangeList = 0x04, Interpreter = 0x08, + VirtualIP = 0x10, } // Mirrors the native CodeHeap::CodeHeapType enum in codeman.h. @@ -146,6 +148,7 @@ public RangeSection(Data.RangeSection rangeSection) internal bool IsRangeList => HasFlags(RangeSectionFlags.RangeList); internal bool IsCodeHeap => HasFlags(RangeSectionFlags.CodeHeap); internal bool IsInterpreter => HasFlags(RangeSectionFlags.Interpreter); + internal bool IsVirtualIP => HasFlags(RangeSectionFlags.VirtualIP); internal bool HasR2RModule => Data!.R2RModule != TargetPointer.Null; @@ -155,8 +158,17 @@ internal static bool IsStubCodeBlock(Target target, TargetPointer codeHeaderIndi return codeHeaderIndirect.Value <= stubCodeBlockLast; } - internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSectionMap, ExecutionManagerHelpers.RangeSectionMap rangeSectionLookup, TargetCodePointer jittedCodeAddress) + internal static RangeSection Find( + Target target, + TargetPointer topRangeSectionMapAddress, + ExecutionManagerHelpers.RangeSectionMap rangeSectionLookup, + TargetPointer virtualIPRangeListAddress, + TargetCodePointer jittedCodeAddress) { + if (IsVirtualIPAddress(target, jittedCodeAddress)) + return FindVirtualIPRangeSection(target, virtualIPRangeListAddress, jittedCodeAddress); + + Data.RangeSectionMap topRangeSectionMap = target.ProcessedData.GetOrAdd(topRangeSectionMapAddress); TargetPointer rangeSectionFragmentPtr = rangeSectionLookup.FindFragment(target, topRangeSectionMap, jittedCodeAddress); // The lowest level of the range section map covers a large address space which may contain multiple small fragments. // Iterate over them to find the one that contains the jitted code address. @@ -181,6 +193,85 @@ internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSe } return new RangeSection(rangeSection); } + + private static RangeSection FindVirtualIPRangeSection( + Target target, + TargetPointer virtualIPRangeListAddress, + TargetCodePointer virtualIP) + { + const int MaxVirtualIPRangeNodes = 1024; + + if (virtualIPRangeListAddress == TargetPointer.Null) + return new RangeSection(); + + try + { + TargetPointer current = target.ReadPointer(virtualIPRangeListAddress); + HashSet visited = new(); + Data.RangeSection? matchingRange = null; + int count = 0; + + while (current != TargetPointer.Null && count < MaxVirtualIPRangeNodes) + { + if (!visited.Add(current)) + return new RangeSection(); + + Data.VirtualIPRangeSection node = target.ProcessedData.GetOrAdd(current); + Data.RangeSection range = target.ProcessedData.GetOrAdd(node.RangeSection); + if (range.RangeBegin >= range.RangeEndOpen + || range.R2RModule == TargetPointer.Null + || range.JitManager == TargetPointer.Null + || range.NextForDelete != TargetPointer.Null + || (range.Flags & (int)RangeSectionFlags.VirtualIP) == 0) + { + return new RangeSection(); + } + + Data.Module module = target.ProcessedData.GetOrAdd(range.R2RModule); + if (module.ReadyToRunInfo == TargetPointer.Null) + return new RangeSection(); + + Data.ReadyToRunInfo r2rInfo = target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + if (r2rInfo.MinVirtualIP is not TargetPointer minVirtualIP + || minVirtualIP != range.RangeBegin + || r2rInfo.LoadedImageBase == TargetPointer.Null) + { + return new RangeSection(); + } + + if (range.RangeBegin <= virtualIP.Value && virtualIP.Value < range.RangeEndOpen) + { + if (matchingRange is not null) + return new RangeSection(); + + matchingRange = range; + } + + current = node.Next; + count++; + } + + if (current != TargetPointer.Null) + return new RangeSection(); + + return matchingRange is null ? new RangeSection() : new RangeSection(matchingRange); + } + catch (VirtualReadException) + { + return new RangeSection(); + } + } + + private static bool IsVirtualIPAddress(Target target, TargetCodePointer codeAddress) + { + if (target.Contracts.RuntimeInfo.GetTargetArchitecture() != RuntimeInfoArchitecture.Wasm) + return false; + + ulong mask = target.PointerSize == sizeof(uint) + ? 0x80000001u + : 0x8000000000000001ul; + return (codeAddress.Value & mask) == mask; + } } private JitManager? GetJitManager(RangeSection rangeSection) @@ -205,7 +296,7 @@ internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSe private CodeBlock? GetCodeBlock(TargetCodePointer jittedCodeAddress) { - RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, jittedCodeAddress); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMapAddress, _rangeSectionMapLookup, _virtualIPRangeListAddress, jittedCodeAddress); if (range.Data == null) { return null; @@ -269,8 +360,11 @@ TargetPointer IExecutionManager.GetFuncletStartAddress(CodeBlockHandle codeInfoH // TODO(cdac): EXCEPTION_DATA_SUPPORTS_FUNCTION_FRAGMENTS, implement iterating over fragments until finding // non-fragment RuntimeFunction + uint beginAddress = range.IsVirtualIP + ? runtimeFunction.BeginAddress & 0x7fff_ffff + : runtimeFunction.BeginAddress; return CodePointerUtils.AddressFromCodePointer( - new TargetCodePointer(range.Data.RangeBegin + runtimeFunction.BeginAddress), _target); + new TargetCodePointer(range.Data.RangeBegin + beginAddress), _target); } void IExecutionManager.GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) @@ -295,7 +389,7 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent Data.PortableEntryPoint portableEntryPoint = _target.ProcessedData.GetOrAdd(entrypoint.AsTargetPointer); return portableEntryPoint.MethodDesc; } - RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, entrypoint); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMapAddress, _rangeSectionMapLookup, _virtualIPRangeListAddress, entrypoint); if (range.Data == null) return TargetPointer.Null; if (range.IsRangeList) @@ -376,7 +470,12 @@ TargetPointer IExecutionManager.GetUnwindInfoBaseAddress(CodeBlockHandle codeInf if (range.Data == null) throw new InvalidOperationException($"{nameof(RangeSection)} not found for {codeInfoHandle.Address}"); - return range.Data.RangeBegin; + if (!range.IsVirtualIP) + return range.Data.RangeBegin; + + Data.Module module = _target.ProcessedData.GetOrAdd(range.Data.R2RModule); + Data.ReadyToRunInfo r2rInfo = _target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + return r2rInfo.LoadedImageBase; } TargetPointer IExecutionManager.GetDebugInfo(CodeBlockHandle codeInfoHandle, out bool hasFlagByte) @@ -450,7 +549,7 @@ TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) // The R2R range section covers the entire PE image (code + data), so this // works for import section addresses used by FindGCRefMap. TargetCodePointer codeAddr = CodePointerUtils.CodePointerFromAddress(address, _target); - RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeAddr); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMapAddress, _rangeSectionMapLookup, _virtualIPRangeListAddress, codeAddr); if (range.Data is null) return TargetPointer.Null; @@ -542,7 +641,7 @@ private RangeSection RangeSectionFromCodeBlockHandle(CodeBlockHandle codeInfoHan if (!_codeInfos.TryGetValue(codeInfoHandle.Address, out CodeBlock? info)) throw new InvalidOperationException($"{nameof(CodeBlock)} not found for {codeInfoHandle.Address}"); - RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeInfoHandle.Address.Value); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMapAddress, _rangeSectionMapLookup, _virtualIPRangeListAddress, codeInfoHandle.Address.Value); return range; } @@ -669,7 +768,7 @@ private static CodeKind GetStubKind(StubKind stubKind) public CodeKind GetCodeKind(TargetCodePointer codeAddress) { - RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeAddress); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMapAddress, _rangeSectionMapLookup, _virtualIPRangeListAddress, codeAddress); if (range.Data == null) { TargetPointer address = new(codeAddress.Value); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs index 8b2d2a00747880..6ef30f830c6877 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs @@ -7,22 +7,32 @@ namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; internal sealed class RuntimeFunctionLookup { - public static RuntimeFunctionLookup Create(Target target) - => new RuntimeFunctionLookup(target); + private const uint WasmFuncletFlag = 0x80000000; + public static RuntimeFunctionLookup Create(Target target, bool isWasm = false) + => new RuntimeFunctionLookup(target, isWasm); + + private readonly bool _isWasm; private readonly uint _runtimeFunctionSize; private readonly Target _target; - private RuntimeFunctionLookup(Target target) + private RuntimeFunctionLookup(Target target, bool isWasm) { _target = target; + _isWasm = isWasm; _runtimeFunctionSize = Data.RuntimeFunction.GetSize(target); } + public uint GetBeginAddress(Data.RuntimeFunction function) + => _isWasm ? function.BeginAddress & ~WasmFuncletFlag : function.BeginAddress; + + public bool IsFunclet(Data.RuntimeFunction function) + => _isWasm && (function.BeginAddress & WasmFuncletFlag) != 0; + public uint GetFunctionLength(TargetPointer imageBase, Data.RuntimeFunction function) { if (function.EndAddress.HasValue) - return function.EndAddress.Value - function.BeginAddress; + return function.EndAddress.Value - GetBeginAddress(function); Data.UnwindInfo unwindInfo = _target.ProcessedData.GetOrAdd(imageBase + function.UnwindData); if (unwindInfo.FunctionLength.HasValue) @@ -38,6 +48,10 @@ public uint GetFunctionLength(TargetPointer imageBase, Data.RuntimeFunction func public bool TryGetRuntimeFunctionIndexForAddress(TargetPointer runtimeFunctions, uint numRuntimeFunctions, TargetPointer relativeAddress, out uint index) { + index = 0; + if (numRuntimeFunctions == 0) + return false; + // NativeUnwindInfoLookupTable::LookupUnwindInfoForMethod uint start = 0; uint end = numRuntimeFunctions - 1; @@ -49,7 +63,7 @@ public bool TryGetRuntimeFunctionIndexForAddress(TargetPointer runtimeFunctions, bool Compare(uint index) { Data.RuntimeFunction func = GetRuntimeFunction(runtimeFunctions, index); - return relativeAddress < func.BeginAddress; + return relativeAddress < GetBeginAddress(func); }; bool Match(uint index) @@ -58,12 +72,12 @@ bool Match(uint index) if (index < numRuntimeFunctions - 1) { Data.RuntimeFunction nextFunc = GetRuntimeFunction(runtimeFunctions, index + 1); - if (relativeAddress >= nextFunc.BeginAddress) + if (relativeAddress >= GetBeginAddress(nextFunc)) return false; } Data.RuntimeFunction func = GetRuntimeFunction(runtimeFunctions, index); - return relativeAddress >= func.BeginAddress; + return relativeAddress >= GetBeginAddress(func); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/UnwindDataSize.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/UnwindDataSize.cs index 952087ab9be95d..426f72377f7cd5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/UnwindDataSize.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/UnwindDataSize.cs @@ -98,11 +98,30 @@ public static uint GetUnwindDataSize(Target target, TargetPointer unwindInfo, Ru size += 4; // exception handler RVA return size; } + case RuntimeInfoArchitecture.Wasm: + { + uint size = GetULEB128Size(target, unwindInfo); + size += GetULEB128Size(target, unwindInfo + size); + return size; + } default: throw new NotSupportedException($"GetUnwindDataSize not supported for architecture: {arch}"); } } + private static uint GetULEB128Size(Target target, TargetPointer address) + { + const int MaxBytes = 5; + + for (uint offset = 0; offset < MaxBytes; offset++) + { + if ((target.Read(address + offset) & 0x80) == 0) + return offset + 1; + } + + throw new InvalidOperationException("Malformed ULEB128 value in WASM unwind data."); + } + private static uint AlignUp(int offset, int align) => (uint)((offset + align - 1) & ~(align - 1)); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs index 0fc20af0914447..f18d6c4bdf7fb6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs @@ -15,16 +15,13 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wa /// internal sealed class WasmR2RInfo : IWasmR2RInfo { - // RUNTIME_FUNCTION__IsFunclet: the funclet flag is the high bit of BeginAddress (clrnt.h). - private const uint FuncletFlag = 0x80000000; - private readonly Target _target; private readonly RuntimeFunctionLookup _runtimeFunctions; public WasmR2RInfo(Target target) { _target = target; - _runtimeFunctions = RuntimeFunctionLookup.Create(target); + _runtimeFunctions = RuntimeFunctionLookup.Create(target, isWasm: true); } // Mirrors ExecutionManager::FindFunctionTableIndexRangeSection. @@ -76,7 +73,7 @@ public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP while (true) { Data.RuntimeFunction runtimeFunction = GetRuntimeFunction(r2rInfo, localIndex); - if ((runtimeFunction.BeginAddress & FuncletFlag) != 0) + if (_runtimeFunctions.IsFunclet(runtimeFunction)) { if (localIndex == 0) return false; @@ -84,7 +81,7 @@ public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP continue; } - baseVirtualIP = minVirtualIP.Value + runtimeFunction.BeginAddress; + baseVirtualIP = minVirtualIP.Value + _runtimeFunctions.GetBeginAddress(runtimeFunction); return true; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VirtualIPRangeSection.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VirtualIPRangeSection.cs new file mode 100644 index 00000000000000..5d8fe80b9f3e83 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/VirtualIPRangeSection.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.VirtualIPRangeSection))] +internal sealed partial class VirtualIPRangeSection : IData +{ + [FieldAddress] public partial TargetPointer RangeSection { get; } + [Field] public partial TargetPointer Next { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index ab30d4293917d9..07742cd8fafba5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -109,6 +109,7 @@ public enum DataType InterpByteCodeStart, InterpMethod, InterpMethodContextFrame, + VirtualIPRangeSection, FunctionTableIndexRangeSection, Array, Delegate, diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index 1c5b92404aa828..bc3a99ea474b89 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -21,6 +21,7 @@ public class ExecutionManagerTests [DataType.RangeSectionMap] = TargetTestHelpers.CreateTypeInfo(emBuilder.RangeSectionMapLayout), [DataType.RangeSectionFragment] = TargetTestHelpers.CreateTypeInfo(emBuilder.RangeSectionFragmentLayout), [DataType.RangeSection] = TargetTestHelpers.CreateTypeInfo(emBuilder.RangeSectionLayout), + [DataType.VirtualIPRangeSection] = TargetTestHelpers.CreateTypeInfo(emBuilder.VirtualIPRangeSectionLayout), [DataType.CodeHeapListNode] = TargetTestHelpers.CreateTypeInfo(emBuilder.CodeHeapListNodeLayout), [DataType.CodeHeap] = TargetTestHelpers.CreateTypeInfo(emBuilder.CodeHeapLayout), [DataType.LoaderCodeHeap] = TargetTestHelpers.CreateTypeInfo(emBuilder.LoaderCodeHeapLayout), @@ -28,6 +29,7 @@ public class ExecutionManagerTests [DataType.RealCodeHeader] = TargetTestHelpers.CreateTypeInfo(emBuilder.RealCodeHeaderLayout), [DataType.InterpreterRealCodeHeader] = TargetTestHelpers.CreateTypeInfo(emBuilder.InterpreterRealCodeHeaderLayout), [DataType.ReadyToRunInfo] = TargetTestHelpers.CreateTypeInfo(emBuilder.ReadyToRunInfoLayout), + [DataType.ReadyToRunHeader] = TargetTestHelpers.CreateTypeInfo(emBuilder.ReadyToRunHeaderLayout), [DataType.EEJitManager] = TargetTestHelpers.CreateTypeInfo(emBuilder.EEJitManagerLayout), [DataType.DynamicFunctionTable] = TargetTestHelpers.CreateTypeInfo(emBuilder.DynamicFunctionTableLayout), [DataType.Module] = TargetTestHelpers.CreateTypeInfo(emBuilder.ModuleLayout), @@ -91,11 +93,12 @@ private static IExecutionManager CreateExecutionManagerContract( string version, MockTarget.Architecture arch, Action? configure = null, - ulong allCodeHeaps = 0) + ulong allCodeHeaps = 0, + RuntimeInfoArchitecture? targetArchitecture = null) { MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange, allCodeHeaps); configure?.Invoke(emBuilder); - Target target = CreateTarget(emBuilder); + Target target = CreateTarget(emBuilder, RuntimeInfoOperatingSystem.Windows, targetArchitecture); return target.Contracts.ExecutionManager; } @@ -324,53 +327,344 @@ public void GetMethodDesc_R2R_OneRuntimeFunction(string version, MockTarget.Arch } } - // On WASM there are no native code pointers: a "code address" is a synthetic virtual IP - // (ExecutionManager::GetWasmVirtualIPFromStackPointer, base + function-local offset). R2R - // modules are registered in the RangeSectionMap by their virtual-IP range, so resolving a - // virtual IP to its MethodDesc uses the same generic RangeSection.Find -> - // ReadyToRunJitManager path as any other architecture -- there is no WASM-specific IP->MethodDesc - // code path (MinVirtualIP / FunctionTableIndexRangeSection are only consumed by the unwinder's - // function-table-index -> base-virtual-IP mapping). This verifies that resolution on a wasm32 - // (32-bit little-endian) target, treating the code address as a virtual IP, and confirms the - // R2R classification. - [Theory] - [InlineData("c1")] - public void GetMethodDesc_R2R_WasmVirtualIP(string version) + [Fact] + public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() { MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; - const ulong virtualIPBase = 0x0050_0000u; // R2R module base virtual IP - const uint virtualIPRangeSize = 0xc000u; - const ulong jitManagerAddress = 0x000b_ff00; - const ulong expectedMethodDescAddress = 0x0101_aaa0; - - uint functionLocalVirtualIP = 0x100; // offset of the R2R function within the module + const ulong VirtualIPRangeStart = 0x8001_0001; + const uint VirtualIPRangeSize = 0x400; + const uint FunctionBeginAddress = 0x108; + const ulong CapturedVirtualIP = 0x8001_0109; + const ulong LoadedImageBase = 0x0090_0000; + const ulong JitManagerAddress = 0x000b_ff00; + const ulong ExpectedMethodDescAddress = 0x0101_aaa0; + ulong moduleAddress = 0; + ulong runtimeFunctionsAddress = 0; + int runtimeFunctionSize = 0; IExecutionManager em = CreateExecutionManagerContract( - version, + "c1", wasmArch, emBuilder => { - var jittedCode = emBuilder.AllocateJittedCodeRange(virtualIPBase, virtualIPRangeSize); - MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([functionLocalVirtualIP], []); + MockExecutionManagerBuilder.JittedCodeRange virtualIPRange = + emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, VirtualIPRangeSize); + MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([0x80, FunctionBeginAddress, 0x200], []); + r2rInfo.MinVirtualIP = VirtualIPRangeStart; + r2rInfo.LoadedImageBase = LoadedImageBase; + runtimeFunctionsAddress = r2rInfo.RuntimeFunctions; + runtimeFunctionSize = emBuilder.RuntimeFunctionLayout.Size; + MockHashMapBuilder hashMapBuilder = new(emBuilder.Builder); hashMapBuilder.PopulatePtrMap( r2rInfo.EntryPointToMethodDescMapAddress, - [(jittedCode.RangeStart + functionLocalVirtualIP, expectedMethodDescAddress)]); + [(CapturedVirtualIP, ExpectedMethodDescAddress)]); MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); - MockRangeSection rangeSection = emBuilder.AddReadyToRunRangeSection(jittedCode, jitManagerAddress, r2rModule.Address); - _ = emBuilder.AddRangeSectionFragment(jittedCode, rangeSection.Address); - }); + moduleAddress = r2rModule.Address; + _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address); + }, + targetArchitecture: RuntimeInfoArchitecture.Wasm); - TargetCodePointer virtualIP = new(virtualIPBase + functionLocalVirtualIP); + TargetCodePointer virtualIP = new(CapturedVirtualIP); + CodeBlockHandle? handle = em.GetCodeBlockHandle(virtualIP); - var handle = em.GetCodeBlockHandle(virtualIP); Assert.NotNull(handle); - Assert.Equal(new TargetPointer(expectedMethodDescAddress), em.GetMethodDesc(handle.Value)); + Assert.Equal(new TargetPointer(ExpectedMethodDescAddress), em.GetMethodDesc(handle.Value)); + Assert.Equal(new TargetPointer(CapturedVirtualIP), em.GetStartAddress(handle.Value)); + Assert.Equal(new TargetPointer(LoadedImageBase), em.GetUnwindInfoBaseAddress(handle.Value)); + Assert.Equal(new TargetPointer(runtimeFunctionsAddress + (ulong)runtimeFunctionSize), em.GetUnwindInfo(handle.Value)); + Assert.Equal(new TargetPointer(moduleAddress), em.FindReadyToRunModule(new TargetPointer(CapturedVirtualIP))); Assert.Equal(CodeKind.ReadyToRun, em.GetCodeKind(virtualIP)); } + [Fact] + public void GetGCInfo_R2R_WasmVirtualIPUsesLoadedImageBase() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong VirtualIPRangeStart = 0x8001_0001; + const uint FunctionBeginAddress = 0x108; + const ulong LoadedImageBase = 0x0090_0000; + const uint UnwindDataRva = 0x80; + const ulong MethodDescAddress = 0x0101_aaa0; + const ulong JitManagerAddress = 0x000b_ff00; + const byte ExpectedGCInfoByte = 0xa5; + + MockExecutionManagerBuilder emBuilder = new( + "c1", + wasmArch, + MockExecutionManagerBuilder.DefaultAllocationRange); + MockExecutionManagerBuilder.JittedCodeRange virtualIPRange = + emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, 0x400); + MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([FunctionBeginAddress], []); + r2rInfo.MinVirtualIP = VirtualIPRangeStart; + r2rInfo.LoadedImageBase = LoadedImageBase; + r2rInfo.ReadyToRunHeader = emBuilder.AddReadyToRunHeader(majorVersion: 21).Address; + emBuilder.SetRuntimeFunctionUnwindData(r2rInfo, index: 0, UnwindDataRva); + emBuilder.Builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = LoadedImageBase + UnwindDataRva, + // Frame size 128, function length 32, followed by the GC info. + Data = [0x80, 0x01, 0x20, ExpectedGCInfoByte], + Name = "WASM unwind and GC info", + }); + + MockHashMapBuilder hashMapBuilder = new(emBuilder.Builder); + hashMapBuilder.PopulatePtrMap( + r2rInfo.EntryPointToMethodDescMapAddress, + [(VirtualIPRangeStart + FunctionBeginAddress, MethodDescAddress)]); + MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); + _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address); + + Target target = CreateTarget( + emBuilder, + RuntimeInfoOperatingSystem.Windows, + RuntimeInfoArchitecture.Wasm); + IExecutionManager em = target.Contracts.ExecutionManager; + CodeBlockHandle? handle = + em.GetCodeBlockHandle(new TargetCodePointer(VirtualIPRangeStart + FunctionBeginAddress)); + + Assert.NotNull(handle); + em.GetGCInfo(handle.Value, out TargetPointer gcInfo, out uint gcVersion); + Assert.Equal(new TargetPointer(LoadedImageBase + UnwindDataRva + 3), gcInfo); + Assert.Equal(5u, gcVersion); + Assert.Equal(ExpectedGCInfoByte, target.Read(gcInfo)); + } + + [Fact] + public void GetCodeKind_R2R_WasmVirtualIPRangeList_RejectsBoundariesAndKeepsAdjacentRangesSeparate() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong FirstRangeStart = 0x8001_0001; + const ulong SecondRangeStart = 0x8001_0201; + const uint RangeSize = 0x200; + const ulong JitManagerAddress = 0x000b_ff00; + ulong firstModuleAddress = 0; + ulong secondModuleAddress = 0; + + IExecutionManager em = CreateExecutionManagerContract( + "c1", + wasmArch, + emBuilder => + { + MockExecutionManagerBuilder.JittedCodeRange firstRange = + emBuilder.AllocateJittedCodeRange(FirstRangeStart, RangeSize); + MockReadyToRunInfo firstInfo = emBuilder.AddReadyToRunInfo([0], []); + firstInfo.MinVirtualIP = FirstRangeStart; + firstInfo.LoadedImageBase = 0x0090_0000; + MockLoaderModule firstModule = emBuilder.AddReadyToRunModule(firstInfo.Address); + firstModuleAddress = firstModule.Address; + MockVirtualIPRangeSection firstNode = + emBuilder.AddVirtualIPRangeSection(firstRange, JitManagerAddress, firstModule.Address, registerAsHead: false); + + MockExecutionManagerBuilder.JittedCodeRange secondRange = + emBuilder.AllocateJittedCodeRange(SecondRangeStart, RangeSize); + MockReadyToRunInfo secondInfo = emBuilder.AddReadyToRunInfo([0], []); + secondInfo.MinVirtualIP = SecondRangeStart; + secondInfo.LoadedImageBase = 0x00a0_0000; + MockLoaderModule secondModule = emBuilder.AddReadyToRunModule(secondInfo.Address); + secondModuleAddress = secondModule.Address; + _ = emBuilder.AddVirtualIPRangeSection(secondRange, JitManagerAddress, secondModule.Address, firstNode.Address); + }, + targetArchitecture: RuntimeInfoArchitecture.Wasm); + + Assert.Equal(CodeKind.Unknown, em.GetCodeKind(new TargetCodePointer(FirstRangeStart - 2))); + Assert.Equal(new TargetPointer(firstModuleAddress), em.FindReadyToRunModule(new TargetPointer(FirstRangeStart))); + Assert.Equal(new TargetPointer(secondModuleAddress), em.FindReadyToRunModule(new TargetPointer(SecondRangeStart))); + Assert.Equal(CodeKind.Unknown, em.GetCodeKind(new TargetCodePointer(SecondRangeStart + RangeSize))); + } + + [Fact] + public void GetCodeKind_R2R_WasmVirtualIPMissingFromList_DoesNotUseRangeSectionMap() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong VirtualIPRangeStart = 0x8001_0001; + const uint VirtualIPRangeSize = 0x400; + const ulong JitManagerAddress = 0x000b_ff00; + + IExecutionManager em = CreateExecutionManagerContract( + "c1", + wasmArch, + emBuilder => + { + MockExecutionManagerBuilder.JittedCodeRange falseMappedRange = + emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, VirtualIPRangeSize); + MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([0x100], []); + r2rInfo.MinVirtualIP = VirtualIPRangeStart; + r2rInfo.LoadedImageBase = 0x0090_0000; + MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); + MockRangeSection rangeSection = + emBuilder.AddReadyToRunRangeSection(falseMappedRange, JitManagerAddress, r2rModule.Address); + _ = emBuilder.AddRangeSectionFragment(falseMappedRange, rangeSection.Address); + }, + targetArchitecture: RuntimeInfoArchitecture.Wasm); + + TargetCodePointer virtualIP = new(VirtualIPRangeStart + 0x100); + Assert.Null(em.GetCodeBlockHandle(virtualIP)); + Assert.Equal(CodeKind.Unknown, em.GetCodeKind(virtualIP)); + Assert.Equal(TargetPointer.Null, em.FindReadyToRunModule(virtualIP.AsTargetPointer)); + } + + [Theory] + [InlineData("self-cycle")] + [InlineData("two-node-cycle")] + [InlineData("inverted-range")] + [InlineData("null-module")] + [InlineData("overlap")] + [InlineData("excessive-nodes")] + public void GetCodeKind_R2R_WasmVirtualIPCorruptList_FailsClosed(string corruption) + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong VirtualIPRangeStart = 0x8001_0001; + const uint VirtualIPRangeSize = 0x200; + const ulong ProbeVirtualIP = VirtualIPRangeStart + 0x10; + const ulong JitManagerAddress = 0x000b_ff00; + + IExecutionManager em = CreateExecutionManagerContract( + "c1", + wasmArch, + emBuilder => + { + MockExecutionManagerBuilder.JittedCodeRange virtualIPRange = + emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, VirtualIPRangeSize); + MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([0], []); + r2rInfo.MinVirtualIP = VirtualIPRangeStart; + r2rInfo.LoadedImageBase = 0x0090_0000; + MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); + + switch (corruption) + { + case "self-cycle": + { + MockVirtualIPRangeSection node = + emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address); + node.Next = node.Address; + break; + } + case "two-node-cycle": + { + MockVirtualIPRangeSection first = + emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address, registerAsHead: false); + MockVirtualIPRangeSection second = + emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address, first.Address, registerAsHead: false); + first.Next = second.Address; + emBuilder.SetVirtualIPRangeListHead(first.Address); + break; + } + case "inverted-range": + { + MockVirtualIPRangeSection node = + emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address); + MockRangeSection range = emBuilder.GetRangeSection(node); + range.RangeBegin = range.RangeEndOpen; + break; + } + case "null-module": + _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModuleAddress: 0); + break; + case "overlap": + { + MockVirtualIPRangeSection first = + emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address, registerAsHead: false); + _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address, first.Address); + break; + } + case "excessive-nodes": + { + MockVirtualIPRangeSection matchingTail = + emBuilder.AddVirtualIPRangeSection( + virtualIPRange, + JitManagerAddress, + r2rModule.Address, + registerAsHead: false); + ulong head = matchingTail.Address; + for (int i = 0; i < 1024; i++) + { + ulong rangeStart = 0x8003_0001ul + ((ulong)i * VirtualIPRangeSize); + MockExecutionManagerBuilder.JittedCodeRange excessiveRange = + emBuilder.AllocateJittedCodeRange(rangeStart, VirtualIPRangeSize); + MockReadyToRunInfo excessiveInfo = emBuilder.AddReadyToRunInfo([0], []); + excessiveInfo.MinVirtualIP = rangeStart; + excessiveInfo.LoadedImageBase = 0x00a0_0000ul + ((ulong)i * VirtualIPRangeSize); + MockLoaderModule excessiveModule = emBuilder.AddReadyToRunModule(excessiveInfo.Address); + MockVirtualIPRangeSection node = + emBuilder.AddVirtualIPRangeSection( + excessiveRange, + JitManagerAddress, + excessiveModule.Address, + head, + registerAsHead: false); + head = node.Address; + } + emBuilder.SetVirtualIPRangeListHead(head); + break; + } + default: + throw new InvalidOperationException(corruption); + } + }, + targetArchitecture: RuntimeInfoArchitecture.Wasm); + + Assert.Null(em.GetCodeBlockHandle(new TargetCodePointer(ProbeVirtualIP))); + Assert.Equal(CodeKind.Unknown, em.GetCodeKind(new TargetCodePointer(ProbeVirtualIP))); + } + + [Fact] + public void GetMethodDesc_R2R_WasmFuncletUsesMaskedBeginAddressAndControllingMethodDesc() + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + const ulong VirtualIPRangeStart = 0x8001_0001; + const uint RootBeginAddress = 0x100; + const uint FuncletBeginAddress = 0x120; + const uint FlaggedFuncletBeginAddress = 0x8000_0120; + const ulong MethodDescAddress = 0x0101_aaa0; + const ulong JitManagerAddress = 0x000b_ff00; + ulong runtimeFunctionsAddress = 0; + int runtimeFunctionSize = 0; + + IExecutionManager em = CreateExecutionManagerContract( + "c1", + wasmArch, + emBuilder => + { + MockExecutionManagerBuilder.JittedCodeRange virtualIPRange = + emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, 0x400); + MockReadyToRunInfo r2rInfo = + emBuilder.AddReadyToRunInfo([RootBeginAddress, FlaggedFuncletBeginAddress, 0x180], []); + r2rInfo.MinVirtualIP = VirtualIPRangeStart; + r2rInfo.LoadedImageBase = 0x0090_0000; + runtimeFunctionsAddress = r2rInfo.RuntimeFunctions; + runtimeFunctionSize = emBuilder.RuntimeFunctionLayout.Size; + MockHashMapBuilder hashMapBuilder = new(emBuilder.Builder); + hashMapBuilder.PopulatePtrMap( + r2rInfo.EntryPointToMethodDescMapAddress, + [(VirtualIPRangeStart + RootBeginAddress, MethodDescAddress)]); + + MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); + _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address); + }, + targetArchitecture: RuntimeInfoArchitecture.Wasm); + + CodeBlockHandle? root = em.GetCodeBlockHandle(new TargetCodePointer(VirtualIPRangeStart + RootBeginAddress)); + CodeBlockHandle? funclet = em.GetCodeBlockHandle(new TargetCodePointer(VirtualIPRangeStart + FuncletBeginAddress + 2)); + + Assert.NotNull(root); + Assert.NotNull(funclet); + Assert.Equal(new TargetPointer(MethodDescAddress), em.GetMethodDesc(root.Value)); + Assert.Equal(new TargetPointer(MethodDescAddress), em.GetMethodDesc(funclet.Value)); + Assert.False(em.IsFunclet(root.Value)); + Assert.True(em.IsFunclet(funclet.Value)); + Assert.Equal(em.GetStartAddress(root.Value), em.GetStartAddress(funclet.Value)); + Assert.Equal(new TargetPointer(VirtualIPRangeStart + FuncletBeginAddress), em.GetFuncletStartAddress(funclet.Value)); + Assert.Equal(new TargetPointer(runtimeFunctionsAddress + (ulong)runtimeFunctionSize), em.GetUnwindInfo(funclet.Value)); + } + [Theory] [MemberData(nameof(StdArchAllVersions))] public void GetMethodDesc_R2R_MultipleRuntimeFunctions(string version, MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 7f4a8ea5d389ad..d453aa660bbdc5 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -157,6 +157,28 @@ public ulong RangeList } } +internal sealed class MockVirtualIPRangeSection : TypedView +{ + private const string RangeSectionFieldName = "RangeSection"; + private const string NextFieldName = "Next"; + + public static Layout CreateLayout( + MockTarget.Architecture architecture, + int rangeSectionSize) + => new SequentialLayoutBuilder("VirtualIPRangeSection", architecture) + .AddField(RangeSectionFieldName, rangeSectionSize) + .AddPointerField(NextFieldName) + .Build(); + + public ulong RangeSectionAddress => GetFieldAddress(RangeSectionFieldName); + + public ulong Next + { + get => ReadPointerField(NextFieldName); + set => WritePointerField(NextFieldName, value); + } +} + internal sealed class MockCodeRangeMapRangeList : TypedView { private const string RangeListTypeFieldName = "RangeListType"; @@ -445,6 +467,12 @@ public ulong CompositeInfo set => WritePointerField(CompositeInfoFieldName, value); } + public ulong ReadyToRunHeader + { + get => ReadPointerField(ReadyToRunHeaderFieldName); + set => WritePointerField(ReadyToRunHeaderFieldName, value); + } + public uint NumRuntimeFunctions { get => ReadUInt32Field(NumRuntimeFunctionsFieldName); @@ -496,6 +524,30 @@ public ulong MinVirtualIP } } +internal sealed class MockReadyToRunHeader : TypedView +{ + private const string MajorVersionFieldName = "MajorVersion"; + private const string MinorVersionFieldName = "MinorVersion"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + => new SequentialLayoutBuilder("ReadyToRunHeader", architecture) + .AddUInt16Field(MajorVersionFieldName) + .AddUInt16Field(MinorVersionFieldName) + .Build(); + + public ushort MajorVersion + { + get => ReadUInt16Field(MajorVersionFieldName); + set => WriteUInt16Field(MajorVersionFieldName, value); + } + + public ushort MinorVersion + { + get => ReadUInt16Field(MinorVersionFieldName); + set => WriteUInt16Field(MinorVersionFieldName, value); + } +} + internal sealed class MockImageDataDirectory : TypedView { private const string VirtualAddressFieldName = "VirtualAddress"; @@ -602,6 +654,7 @@ internal sealed class MockExecutionManagerBuilder private const uint CodeHeapRangeSectionFlag = 0x02; private const uint RangeListRangeSectionFlag = 0x04; private const uint InterpreterRangeSectionFlag = 0x0A; // CodeHeap | Interpreter + private const uint VirtualIPRangeSectionFlag = 0x10; private const string EEJitManagerGlobalName = "EEJitManagerGlobalPointer"; private const int RangeSectionMapBitsPerLevel = 8; @@ -622,7 +675,7 @@ public readonly struct AllocationRange NibbleMapStart = 0x00ee_0000, NibbleMapEnd = 0x00ef_0000, ExecutionManagerStart = 0x0033_4000, - ExecutionManagerEnd = 0x0033_5000, + ExecutionManagerEnd = 0x0037_4000, }; private readonly struct RangeSectionMapCursor @@ -653,6 +706,7 @@ internal readonly struct JittedCodeRange internal Layout RangeSectionMapLayout { get; } internal Layout RangeSectionFragmentLayout { get; } internal Layout RangeSectionLayout { get; } + internal Layout VirtualIPRangeSectionLayout { get; } internal Layout CodeHeapListNodeLayout { get; } internal Layout CodeHeapLayout { get; } internal Layout LoaderCodeHeapLayout { get; } @@ -660,6 +714,7 @@ internal readonly struct JittedCodeRange internal Layout RealCodeHeaderLayout { get; } internal Layout InterpreterRealCodeHeaderLayout { get; } internal Layout ReadyToRunInfoLayout { get; } + internal Layout ReadyToRunHeaderLayout { get; } internal Layout EEJitManagerLayout { get; } internal Layout DynamicFunctionTableLayout { get; } internal Layout ModuleLayout { get; } @@ -667,7 +722,7 @@ internal readonly struct JittedCodeRange internal Layout ImageDataDirectoryLayout { get; } internal Layout RuntimeFunctionLayout => _runtimeFunctions.RuntimeFunctionLayout; internal Layout UnwindInfoLayout => _runtimeFunctions.UnwindInfoLayout; - internal (string Name, ulong Value)[] Globals { get; } + internal (string Name, ulong Value)[] Globals { get; private set; } internal ulong EEJitManagerAddress { get; } internal ulong RangeSectionMapTopLevelAddress => _rangeSectionMapTopLevelAddress; @@ -681,6 +736,7 @@ internal readonly struct JittedCodeRange private readonly ulong _rangeSectionMapTopLevelAddress; private readonly int _rangeSectionMapLevelsCount; private readonly int _rangeSectionMapMaxSetBit; + private ulong _virtualIPRangeListGlobalAddress; internal MockExecutionManagerBuilder(string version, MockTarget.Architecture arch, AllocationRange allocationRange, ulong allCodeHeaps = 0) : this(version, new MockMemorySpace.Builder(new TargetTestHelpers(arch)), allocationRange, allCodeHeaps) @@ -711,6 +767,7 @@ internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder bui RangeSectionMapLayout = MockRangeSectionMap.CreateLayout(architecture); RangeSectionFragmentLayout = MockRangeSectionFragment.CreateLayout(architecture); RangeSectionLayout = MockRangeSection.CreateLayout(architecture); + VirtualIPRangeSectionLayout = MockVirtualIPRangeSection.CreateLayout(architecture, RangeSectionLayout.Size); CodeHeapListNodeLayout = MockCodeHeapListNode.CreateLayout(architecture); CodeHeapLayout = MockCodeHeap.CreateLayout(architecture); LoaderCodeHeapLayout = MockLoaderCodeHeap.CreateLayout(architecture); @@ -718,6 +775,7 @@ internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder bui RealCodeHeaderLayout = MockRealCodeHeader.CreateLayout(architecture); InterpreterRealCodeHeaderLayout = MockInterpreterRealCodeHeader.CreateLayout(architecture); ReadyToRunInfoLayout = MockReadyToRunInfo.CreateLayout(architecture, hashMapStride); + ReadyToRunHeaderLayout = MockReadyToRunHeader.CreateLayout(architecture); EEJitManagerLayout = MockEEJitManager.CreateLayout(architecture); DynamicFunctionTableLayout = MockDynamicFunctionTable.CreateLayout(architecture); ModuleLayout = MockLoaderModule.CreateLayout(architecture); @@ -781,6 +839,54 @@ public MockRangeSection AddReadyToRunRangeSection(JittedCodeRange jittedCodeRang return rangeSection; } + public MockVirtualIPRangeSection AddVirtualIPRangeSection( + JittedCodeRange virtualIPRange, + ulong jitManagerAddress, + ulong r2rModuleAddress, + ulong next = 0, + bool registerAsHead = true) + { + MockMemorySpace.HeapFragment fragment = _allocator.Allocate((ulong)VirtualIPRangeSectionLayout.Size, "VirtualIPRangeSection"); + MockVirtualIPRangeSection node = VirtualIPRangeSectionLayout.Create(fragment); + MockRangeSection rangeSection = RangeSectionLayout.Create( + fragment.Data.AsMemory(0, RangeSectionLayout.Size), + node.RangeSectionAddress); + rangeSection.RangeBegin = virtualIPRange.RangeStart; + rangeSection.RangeEndOpen = virtualIPRange.RangeEnd; + rangeSection.Flags = VirtualIPRangeSectionFlag; + rangeSection.R2RModule = r2rModuleAddress; + rangeSection.JitManager = jitManagerAddress; + node.Next = next; + + if (registerAsHead) + SetVirtualIPRangeListHead(node.Address); + + return node; + } + + public void SetVirtualIPRangeListHead(ulong head) + { + if (_virtualIPRangeListGlobalAddress == 0) + { + _virtualIPRangeListGlobalAddress = AddPointerGlobal(head, "VirtualIPRangeListGlobalPointer"); + Globals = + [ + .. Globals, + (nameof(Constants.Globals.VirtualIPRangeList), _virtualIPRangeListGlobalAddress), + ]; + return; + } + + Builder.TargetTestHelpers.WritePointer( + Builder.BorrowAddressRange(_virtualIPRangeListGlobalAddress, Builder.TargetTestHelpers.PointerSize), + head); + } + + public MockRangeSection GetRangeSection(MockVirtualIPRangeSection node) + => RangeSectionLayout.Create( + node.Memory.Slice(0, RangeSectionLayout.Size), + node.RangeSectionAddress); + public MockRangeSection AddRangeListRangeSection(JittedCodeRange jittedCodeRange, ulong jitManagerAddress, int rangeListType) { MockCodeRangeMapRangeList rangeList = AllocateAndCreate(CodeRangeMapRangeListLayout, "CodeRangeMapRangeList"); @@ -986,6 +1092,24 @@ public MockReadyToRunInfo AddReadyToRunInfo(uint[] runtimeFunctions, uint[] hotC return readyToRunInfo; } + public MockReadyToRunHeader AddReadyToRunHeader(ushort majorVersion, ushort minorVersion = 0) + { + MockReadyToRunHeader header = AllocateAndCreate(ReadyToRunHeaderLayout, "ReadyToRunHeader"); + header.MajorVersion = majorVersion; + header.MinorVersion = minorVersion; + return header; + } + + public void SetRuntimeFunctionUnwindData(MockReadyToRunInfo readyToRunInfo, uint index, uint unwindData) + { + ulong address = readyToRunInfo.RuntimeFunctions + (index * (uint)RuntimeFunctionLayout.Size); + Span runtimeFunction = Builder.BorrowAddressRange(address, RuntimeFunctionLayout.Size); + LayoutField unwindDataField = RuntimeFunctionLayout.GetField("UnwindData"); + Builder.TargetTestHelpers.Write( + runtimeFunction.Slice(unwindDataField.Offset, sizeof(uint)), + unwindData); + } + public void SetDelayLoadMethodCallThunks(MockReadyToRunInfo readyToRunInfo, uint thunkRva, uint thunkSize) { MockImageDataDirectory imageDataDir = AllocateAndCreate(ImageDataDirectoryLayout, "DelayLoadMethodCallThunks"); From d77af00845eb51ead3b93f5bdd0963c7ff9ed134 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 19:08:32 -0500 Subject: [PATCH 02/14] Fix cDAC WASM descriptor and virtual-IP traversal handling Handle feature-gated hot/cold metadata, isolate candidate module validation during registration, and remove the unsupported virtual-IP list length limit. Match WASM descriptor layouts and cover long lists, partial registration, and root/funclet image metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 21 +- .../data-descriptor-meanings.json | 6 +- ...ecutionManagerCore.ReadyToRunJitManager.cs | 6 +- .../ExecutionManager/ExecutionManagerCore.cs | 36 +-- .../Data/ReadyToRunInfo.cs | 8 +- .../ExecutionManager/ExecutionManagerTests.cs | 266 +++++++++++++++--- .../MockDescriptors.ExecutionManager.cs | 71 +++-- .../cdac/tests/UnitTests/WasmR2RInfoTests.cs | 4 +- 8 files changed, 323 insertions(+), 95 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index c525a25769990b..629316c1d307af 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -159,9 +159,18 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to WebAssembly ReadyToRun code uses encoded virtual IPs rather than linear-memory code addresses. These ranges are registered in `VirtualIPRangeList`, not the range section map. A virtual-IP -lookup walks that bounded intrusive list before consulting the range section map. Invalid, -cyclic, excessive, or ambiguous lists fail closed, and an encoded virtual IP that is absent from -the list does not fall through to the real-address map. +lookup walks the intrusive list with cycle detection and no fixed node-count limit. Invalid +range structures, cycles, or multiple ranges containing the requested IP fail closed, and an +encoded virtual IP that is absent from the list does not fall through to the real-address map. +Module and ReadyToRun metadata are validated only for a range containing the requested IP. +In particular, registration publishes a range before assigning its module's `MinVirtualIP`; +an unrelated range in that state must not prevent lookup of already initialized modules. + +ReadyToRun hot/cold mapping is optional: targets built without `FEATURE_COLD_R2R_CODE`, +including WebAssembly, omit `NumHotColdMap` and `HotColdMap`. An absent count is treated as +zero for method-index adjustment, relative-offset calculation, and method-region queries, +without reading the absent map. WebAssembly also omits `DelayLoadMethodCallThunks`; +virtual-IP ranges bypass native thunk classification. ### Data descriptors used @@ -234,12 +243,12 @@ the list does not fall through to the real-address map. | `ReadyToRunInfo` | `Composite` | `pointer` | Pointer to the `ReadyToRunCoreInfo` used for section lookup | | `ReadyToRunInfo` | `CompositeInfo` | `pointer` | Pointer to composite R2R info - or itself for non-composite | | `ReadyToRunInfo` | `DebugInfoSection` | `pointer` | Pointer to an `ImageDataDirectory` for the debug info | -| `ReadyToRunInfo` | `DelayLoadMethodCallThunks` | `pointer` | Pointer to an `ImageDataDirectory` for the delay load method call thunks | +| `ReadyToRunInfo` | `DelayLoadMethodCallThunks` | `pointer` | Pointer to an `ImageDataDirectory` for the delay load method call thunks; absent on WebAssembly | | `ReadyToRunInfo` | `EntryPointToMethodDescMap` | `HashMap` | `HashMap` of entry point addresses to `MethodDesc` pointers | -| `ReadyToRunInfo` | `HotColdMap` | `pointer` | Pointer to an array of 32-bit integers - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80) | +| `ReadyToRunInfo` | `HotColdMap` | `pointer` | Pointer to an array of 32-bit integers; present only with `FEATURE_COLD_R2R_CODE` - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80) | | `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image | | `ReadyToRunInfo` | `MinVirtualIP` | `pointer` | Base virtual IP assigned to the ReadyToRun module on WebAssembly | -| `ReadyToRunInfo` | `NumHotColdMap` | `uint32` | Number of entries in the `HotColdMap` | +| `ReadyToRunInfo` | `NumHotColdMap` | `uint32` | Number of entries in the `HotColdMap`; absent without `FEATURE_COLD_R2R_CODE`, meaning no hot/cold mapping | | `ReadyToRunInfo` | `NumRuntimeFunctions` | `uint32` | Number of `RuntimeFunctions` | | `ReadyToRunInfo` | `ReadyToRunHeader` | `pointer` | Pointer to the ReadyToRunHeader | | `ReadyToRunInfo` | `RuntimeFunctions` | `pointer` | Pointer to an array of `RuntimeFunctions` - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontyperuntimefunctions) | diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index f04a2059e1a898..65d3c9fc03a6a7 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -515,13 +515,13 @@ "ReadyToRunInfo.Composite": "Pointer to the `ReadyToRunCoreInfo` used for section lookup", "ReadyToRunInfo.CompositeInfo": "Pointer to composite R2R info - or itself for non-composite", "ReadyToRunInfo.DebugInfoSection": "Pointer to an `ImageDataDirectory` for the debug info", - "ReadyToRunInfo.DelayLoadMethodCallThunks": "Pointer to an `ImageDataDirectory` for the delay load method call thunks", + "ReadyToRunInfo.DelayLoadMethodCallThunks": "Pointer to an `ImageDataDirectory` for the delay load method call thunks; absent on WebAssembly", "ReadyToRunInfo.EntryPointToMethodDescMap": "`HashMap` of entry point addresses to `MethodDesc` pointers", - "ReadyToRunInfo.HotColdMap": "Pointer to an array of 32-bit integers - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80)", + "ReadyToRunInfo.HotColdMap": "Pointer to an array of 32-bit integers; present only with `FEATURE_COLD_R2R_CODE` - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80)", "ReadyToRunInfo.ImportSections": "Pointer to the array of ReadyToRun import sections", "ReadyToRunInfo.LoadedImageBase": "Base address of the loaded R2R image", "ReadyToRunInfo.MinVirtualIP": "Base virtual IP assigned to the ReadyToRun module on WebAssembly", - "ReadyToRunInfo.NumHotColdMap": "Number of entries in the `HotColdMap`", + "ReadyToRunInfo.NumHotColdMap": "Number of entries in the `HotColdMap`; absent without `FEATURE_COLD_R2R_CODE`, meaning no hot/cold mapping", "ReadyToRunInfo.NumImportSections": "Number of ReadyToRun import sections", "ReadyToRunInfo.NumRuntimeFunctions": "Number of `RuntimeFunctions`", "ReadyToRunInfo.ReadyToRunHeader": "Pointer to the ReadyToRunHeader", diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index d51e67d6e868f3..58c306e138f2d2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -56,7 +56,7 @@ public override bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer TargetNUInt relativeOffset = new TargetNUInt(addr - startAddress); // Take hot/cold splitting into account for the relative offset - if (_hotCold.TryGetColdFunctionIndex(r2rInfo.NumHotColdMap, r2rInfo.HotColdMap, index, out uint coldFunctionIndex)) + if (_hotCold.TryGetColdFunctionIndex(r2rInfo.NumHotColdMap ?? 0, r2rInfo.HotColdMap, index, out uint coldFunctionIndex)) { Debug.Assert(coldFunctionIndex < r2rInfo.NumRuntimeFunctions); Data.RuntimeFunction coldFunction = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldFunctionIndex); @@ -94,7 +94,7 @@ public override void GetMethodRegionInfo( if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer codeBase, out uint index)) return; - if (_hotCold.TryGetColdFunctionIndex(r2rInfo.NumHotColdMap, r2rInfo.HotColdMap, index, r2rInfo.NumRuntimeFunctions, out uint coldStartIdx, out uint coldEndIdx)) + if (_hotCold.TryGetColdFunctionIndex(r2rInfo.NumHotColdMap ?? 0, r2rInfo.HotColdMap, index, r2rInfo.NumRuntimeFunctions, out uint coldStartIdx, out uint coldEndIdx)) { Data.RuntimeFunction coldStartFunc = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldStartIdx); Data.RuntimeFunction coldEndFunc = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldEndIdx); @@ -245,7 +245,7 @@ private bool GetRuntimeFunction( private uint AdjustRuntimeFunctionIndexForHotCold(Data.ReadyToRunInfo r2rInfo, uint index) { // Look up index in hot/cold map - if the function is in the cold part, get the index of the hot part. - index = _hotCold.GetHotFunctionIndex(r2rInfo.NumHotColdMap, r2rInfo.HotColdMap, index); + index = _hotCold.GetHotFunctionIndex(r2rInfo.NumHotColdMap ?? 0, r2rInfo.HotColdMap, index); Debug.Assert(index < r2rInfo.NumRuntimeFunctions); return index; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index f78a9cc7d967d5..1a31b56ada3611 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -199,8 +199,6 @@ private static RangeSection FindVirtualIPRangeSection( TargetPointer virtualIPRangeListAddress, TargetCodePointer virtualIP) { - const int MaxVirtualIPRangeNodes = 1024; - if (virtualIPRangeListAddress == TargetPointer.Null) return new RangeSection(); @@ -209,9 +207,7 @@ private static RangeSection FindVirtualIPRangeSection( TargetPointer current = target.ReadPointer(virtualIPRangeListAddress); HashSet visited = new(); Data.RangeSection? matchingRange = null; - int count = 0; - - while (current != TargetPointer.Null && count < MaxVirtualIPRangeNodes) + while (current != TargetPointer.Null) { if (!visited.Add(current)) return new RangeSection(); @@ -227,33 +223,31 @@ private static RangeSection FindVirtualIPRangeSection( return new RangeSection(); } - Data.Module module = target.ProcessedData.GetOrAdd(range.R2RModule); - if (module.ReadyToRunInfo == TargetPointer.Null) - return new RangeSection(); - - Data.ReadyToRunInfo r2rInfo = target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); - if (r2rInfo.MinVirtualIP is not TargetPointer minVirtualIP - || minVirtualIP != range.RangeBegin - || r2rInfo.LoadedImageBase == TargetPointer.Null) - { - return new RangeSection(); - } - if (range.RangeBegin <= virtualIP.Value && virtualIP.Value < range.RangeEndOpen) { if (matchingRange is not null) return new RangeSection(); + // Registration publishes the node before setting MinVirtualIP. Only + // the candidate module must be initialized for this lookup to succeed. + Data.Module module = target.ProcessedData.GetOrAdd(range.R2RModule); + if (module.ReadyToRunInfo == TargetPointer.Null) + return new RangeSection(); + + Data.ReadyToRunInfo r2rInfo = target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + if (r2rInfo.MinVirtualIP is not TargetPointer minVirtualIP + || minVirtualIP != range.RangeBegin + || r2rInfo.LoadedImageBase == TargetPointer.Null) + { + return new RangeSection(); + } + matchingRange = range; } current = node.Next; - count++; } - if (current != TargetPointer.Null) - return new RangeSection(); - return matchingRange is null ? new RangeSection() : new RangeSection(matchingRange); } catch (VirtualReadException) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index 01a0427b92fc6f..2dd672cb40887e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -11,7 +11,8 @@ internal sealed partial class ReadyToRunInfo : IData [Field] public partial TargetPointer CompositeInfo { get; } [Field] public partial TargetPointer ReadyToRunHeader { get; } [Field] public partial uint NumRuntimeFunctions { get; } - [Field] public partial uint NumHotColdMap { get; } + // Absent when the runtime is built without FEATURE_COLD_R2R_CODE. + [Field] public partial uint? NumHotColdMap { get; } [Field] public partial TargetPointer DelayLoadMethodCallThunks { get; } [Field] public partial TargetPointer DebugInfoSection { get; } [Field] public partial TargetPointer ExceptionInfoSection { get; } @@ -41,8 +42,9 @@ private partial TargetPointer InitRuntimeFunctions(Target target, TargetPointer private partial TargetPointer InitHotColdMap(Target target, TargetPointer address) { Target.TypeInfo type = target.GetTypeInfo(DataType.ReadyToRunInfo); - Debug.Assert(NumHotColdMap % 2 == 0, "Hot/cold map should have an even number of entries (pairs of hot/cold runtime function indexes)"); - return NumHotColdMap > 0 + uint count = NumHotColdMap ?? 0; + Debug.Assert(count % 2 == 0, "Hot/cold map should have an even number of entries (pairs of hot/cold runtime function indexes)"); + return count > 0 ? target.ReadPointerField(address, type, nameof(HotColdMap)) : TargetPointer.Null; } diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index bc3a99ea474b89..5981cf7f8d503d 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -57,18 +57,21 @@ internal static Target CreateTarget( internal static Target CreateTarget( MockExecutionManagerBuilder emBuilder, RuntimeInfoOperatingSystem operatingSystem = RuntimeInfoOperatingSystem.Windows, - RuntimeInfoArchitecture? targetArchitecture = null) + RuntimeInfoArchitecture? targetArchitecture = null, + Action? configureTarget = null) => CreateTarget( emBuilder, operatingSystem, targetArchitecture, - []); + [], + configureTarget); private static Target CreateTarget( MockExecutionManagerBuilder emBuilder, RuntimeInfoOperatingSystem operatingSystem, RuntimeInfoArchitecture? targetArchitecture, - (string Name, ulong Value)[] additionalGlobals) + (string Name, ulong Value)[] additionalGlobals, + Action? configureTarget = null) { var arch = emBuilder.Builder.TargetTestHelpers.Arch; RuntimeInfoArchitecture architecture = targetArchitecture ?? (arch.Is64Bit @@ -78,15 +81,16 @@ private static Target CreateTarget( runtimeInfo.Setup(r => r.GetTargetOperatingSystem()).Returns(operatingSystem); runtimeInfo.Setup(r => r.GetTargetArchitecture()).Returns(architecture); - return new TestPlaceholderTarget.Builder(arch) + var targetBuilder = new TestPlaceholderTarget.Builder(arch) .UseReader(emBuilder.Builder.GetMemoryContext().ReadFromTarget) .AddTypes(CreateContractTypes(emBuilder)) .AddGlobals(emBuilder.Globals) .AddGlobals(additionalGlobals) .AddContract(version: emBuilder.Version) .AddMockContract(Mock.Of()) - .AddMockContract(runtimeInfo) - .Build(); + .AddMockContract(runtimeInfo); + configureTarget?.Invoke(targetBuilder); + return targetBuilder.Build(); } private static IExecutionManager CreateExecutionManagerContract( @@ -96,7 +100,8 @@ private static IExecutionManager CreateExecutionManagerContract( ulong allCodeHeaps = 0, RuntimeInfoArchitecture? targetArchitecture = null) { - MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange, allCodeHeaps); + MockExecutionManagerBuilder emBuilder = new(version, arch, MockExecutionManagerBuilder.DefaultAllocationRange, allCodeHeaps, + isWasm: targetArchitecture == RuntimeInfoArchitecture.Wasm); configure?.Invoke(emBuilder); Target target = CreateTarget(emBuilder, RuntimeInfoOperatingSystem.Windows, targetArchitecture); return target.Contracts.ExecutionManager; @@ -355,6 +360,10 @@ public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() r2rInfo.LoadedImageBase = LoadedImageBase; runtimeFunctionsAddress = r2rInfo.RuntimeFunctions; runtimeFunctionSize = emBuilder.RuntimeFunctionLayout.Size; + Assert.Equal(8, runtimeFunctionSize); + Assert.DoesNotContain(emBuilder.RuntimeFunctionLayout.Fields, field => field.Name == "EndAddress"); + Assert.DoesNotContain(emBuilder.ReadyToRunInfoLayout.Fields, + field => field.Name is "NumHotColdMap" or "HotColdMap" or "DelayLoadMethodCallThunks"); MockHashMapBuilder hashMapBuilder = new(emBuilder.Builder); hashMapBuilder.PopulatePtrMap( @@ -395,7 +404,8 @@ public void GetGCInfo_R2R_WasmVirtualIPUsesLoadedImageBase() MockExecutionManagerBuilder emBuilder = new( "c1", wasmArch, - MockExecutionManagerBuilder.DefaultAllocationRange); + MockExecutionManagerBuilder.DefaultAllocationRange, + isWasm: true); MockExecutionManagerBuilder.JittedCodeRange virtualIPRange = emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, 0x400); MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([FunctionBeginAddress], []); @@ -418,10 +428,16 @@ public void GetGCInfo_R2R_WasmVirtualIPUsesLoadedImageBase() MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address); + IGCInfoHandle gcInfoHandle = Mock.Of(); + Mock gcInfoContract = new(MockBehavior.Strict); + gcInfoContract.Setup(c => c.DecodePlatformSpecificGCInfo(new TargetPointer(LoadedImageBase + UnwindDataRva + 3), 5)) + .Returns(gcInfoHandle); + gcInfoContract.Setup(c => c.GetCodeLength(gcInfoHandle)).Returns(32u); Target target = CreateTarget( emBuilder, RuntimeInfoOperatingSystem.Windows, - RuntimeInfoArchitecture.Wasm); + RuntimeInfoArchitecture.Wasm, + targetBuilder => targetBuilder.AddMockContract(gcInfoContract)); IExecutionManager em = target.Contracts.ExecutionManager; CodeBlockHandle? handle = em.GetCodeBlockHandle(new TargetCodePointer(VirtualIPRangeStart + FunctionBeginAddress)); @@ -431,6 +447,10 @@ public void GetGCInfo_R2R_WasmVirtualIPUsesLoadedImageBase() Assert.Equal(new TargetPointer(LoadedImageBase + UnwindDataRva + 3), gcInfo); Assert.Equal(5u, gcVersion); Assert.Equal(ExpectedGCInfoByte, target.Read(gcInfo)); + em.GetMethodRegionInfo(handle.Value, out uint hotSize, out TargetPointer coldStart, out uint coldSize); + Assert.Equal(32u, hotSize); + Assert.Equal(TargetPointer.Null, coldStart); + Assert.Equal(0u, coldSize); } [Fact] @@ -477,6 +497,114 @@ public void GetCodeKind_R2R_WasmVirtualIPRangeList_RejectsBoundariesAndKeepsAdja Assert.Equal(CodeKind.Unknown, em.GetCodeKind(new TargetCodePointer(SecondRangeStart + RangeSize))); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool funclet) + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + const ulong VirtualIPRangeStart = 0x8001_0001; + const uint RootBeginAddress = 0x100; + const uint FuncletBeginAddress = 0x120; + const uint LoadedImageBase = 0x0090_0000; + const uint DebugInfoRva = 0x80; + const ulong MethodDescAddress = 0x0101_aaa0; + const ulong JitManagerAddress = 0x000b_ff00; + const byte ExpectedDebugInfoByte = 0xa5; + + MockExecutionManagerBuilder emBuilder = new( + "c1", wasmArch, MockExecutionManagerBuilder.DefaultAllocationRange, isWasm: true); + var range = emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, 0x400); + MockReadyToRunInfo info = emBuilder.AddReadyToRunInfo([0x80, RootBeginAddress, 0x8000_0000 | FuncletBeginAddress], []); + info.MinVirtualIP = VirtualIPRangeStart; + info.LoadedImageBase = LoadedImageBase; + new MockHashMapBuilder(emBuilder.Builder).PopulatePtrMap( + info.EntryPointToMethodDescMapAddress, [(VirtualIPRangeStart + RootBeginAddress, MethodDescAddress)]); + MockLoaderModule module = emBuilder.AddReadyToRunModule(info.Address); + emBuilder.AddVirtualIPRangeSection(range, JitManagerAddress, module.Address); + emBuilder.SetDebugInfoSection(info, DebugInfoRva, 5); + emBuilder.Builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = LoadedImageBase + DebugInfoRva, + // NativeArray: three entries, one-byte block offset, leaf for index 1, no lookback. + Data = [0x18, 0x01, 0x08, 0x00, ExpectedDebugInfoByte], + Name = "WASM debug info", + }); + Dictionary exceptionTypes = + AddWasmExceptionInfo(emBuilder, info, LoadedImageBase, RootBeginAddress); + + MethodDescHandle methodHandle = new(new TargetPointer(MethodDescAddress)); + TargetPointer methodTable = new(0x0102_0000); + ITypeHandle typeHandle = new TargetTypeHandle(methodTable); + Mock rts = new(MockBehavior.Strict); + rts.Setup(r => r.GetMethodDescHandle(new TargetPointer(MethodDescAddress))).Returns(methodHandle); + rts.Setup(r => r.GetMethodTable(methodHandle)).Returns(methodTable); + rts.Setup(r => r.GetTypeHandle(methodTable)).Returns(typeHandle); + rts.Setup(r => r.GetModule(typeHandle)).Returns(new TargetPointer(module.Address)); + Target target = CreateTarget(emBuilder, RuntimeInfoOperatingSystem.Windows, RuntimeInfoArchitecture.Wasm, + targetBuilder => targetBuilder.AddTypes(exceptionTypes).AddMockContract(rts)); + IExecutionManager em = target.Contracts.ExecutionManager; + + CodeBlockHandle? handle = em.GetCodeBlockHandle( + new TargetCodePointer(VirtualIPRangeStart + (funclet ? FuncletBeginAddress : RootBeginAddress))); + Assert.NotNull(handle); + TargetPointer debugInfo = em.GetDebugInfo(handle.Value, out bool hasFlagByte); + Assert.False(hasFlagByte); + Assert.Equal(new TargetPointer(LoadedImageBase + DebugInfoRva + 4), debugInfo); + Assert.Equal(ExpectedDebugInfoByte, target.Read(debugInfo)); + ExceptionClauseInfo clause = Assert.Single(em.GetExceptionClauses(handle.Value)); + Assert.Equal(ExceptionClauseInfo.ExceptionClauseFlags.Finally, clause.ClauseType); + Assert.Equal(2u, clause.TryStartPC); + Assert.Equal(16u, clause.TryEndPC); + Assert.Equal(FuncletBeginAddress - RootBeginAddress, clause.HandlerStartPC); + Assert.Equal(48u, clause.HandlerEndPC); + Assert.Null(clause.ClassToken); + Assert.Null(clause.FilterOffset); + } + + private static Dictionary AddWasmExceptionInfo( + MockExecutionManagerBuilder emBuilder, MockReadyToRunInfo info, uint imageBase, uint methodRva) + { + const uint CoreInfoRva = 0x200; + const uint CoreHeaderRva = 0x210; + const uint ExceptionTableRva = 0x300; + const uint ClauseRva = 0x340; + const uint ClauseSize = 24; + Dictionary types = []; + info.Composite = imageBase + CoreInfoRva; + AddWords(imageBase + CoreInfoRva, DataType.ReadyToRunCoreInfo, + [new("Header", DataType.pointer)], [imageBase + CoreHeaderRva]); + AddWords(imageBase + CoreHeaderRva, DataType.ReadyToRunCoreHeader, + [new("NumberOfSections", DataType.uint32)], [1]); + AddWords(imageBase + CoreHeaderRva + 4, DataType.ReadyToRunSection, + [new("Type", DataType.uint32), new("Section", DataType.ImageDataDirectory, 8)], + [104, ExceptionTableRva, 16]); + AddWords(imageBase + ExceptionTableRva, DataType.ExceptionLookupTableEntry, + [new("MethodStartRVA", DataType.uint32), new("ExceptionInfoRVA", DataType.uint32)], + [methodRva, ClauseRva, uint.MaxValue, ClauseRva + ClauseSize]); + AddWords(imageBase + ClauseRva, DataType.R2RExceptionClause, + [new("Flags", DataType.uint32), new("TryStartPC", DataType.uint32), new("TryEndPC", DataType.uint32), + new("HandlerStartPC", DataType.uint32), new("HandlerEndPC", DataType.uint32), new("ClassToken", DataType.uint32)], + [2, 2, 16, 32, 48, 0]); + return types; + + void AddWords(uint address, DataType type, TargetTestHelpers.Field[] fields, uint[] words) + { + TargetTestHelpers helpers = emBuilder.Builder.TargetTestHelpers; + var layout = helpers.LayoutFields(TargetTestHelpers.FieldLayout.Packed, fields); + types[type] = new Target.TypeInfo { Fields = layout.Fields, Size = layout.Stride }; + byte[] data = new byte[words.Length * sizeof(uint)]; + for (int i = 0; i < words.Length; i++) + helpers.Write(data.AsSpan(i * sizeof(uint), sizeof(uint)), words[i]); + emBuilder.Builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = address, + Data = data, + Name = type.ToString(), + }); + } + } + [Fact] public void GetCodeKind_R2R_WasmVirtualIPMissingFromList_DoesNotUseRangeSectionMap() { @@ -515,7 +643,6 @@ public void GetCodeKind_R2R_WasmVirtualIPMissingFromList_DoesNotUseRangeSectionM [InlineData("inverted-range")] [InlineData("null-module")] [InlineData("overlap")] - [InlineData("excessive-nodes")] public void GetCodeKind_R2R_WasmVirtualIPCorruptList_FailsClosed(string corruption) { MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; @@ -574,36 +701,6 @@ public void GetCodeKind_R2R_WasmVirtualIPCorruptList_FailsClosed(string corrupti _ = emBuilder.AddVirtualIPRangeSection(virtualIPRange, JitManagerAddress, r2rModule.Address, first.Address); break; } - case "excessive-nodes": - { - MockVirtualIPRangeSection matchingTail = - emBuilder.AddVirtualIPRangeSection( - virtualIPRange, - JitManagerAddress, - r2rModule.Address, - registerAsHead: false); - ulong head = matchingTail.Address; - for (int i = 0; i < 1024; i++) - { - ulong rangeStart = 0x8003_0001ul + ((ulong)i * VirtualIPRangeSize); - MockExecutionManagerBuilder.JittedCodeRange excessiveRange = - emBuilder.AllocateJittedCodeRange(rangeStart, VirtualIPRangeSize); - MockReadyToRunInfo excessiveInfo = emBuilder.AddReadyToRunInfo([0], []); - excessiveInfo.MinVirtualIP = rangeStart; - excessiveInfo.LoadedImageBase = 0x00a0_0000ul + ((ulong)i * VirtualIPRangeSize); - MockLoaderModule excessiveModule = emBuilder.AddReadyToRunModule(excessiveInfo.Address); - MockVirtualIPRangeSection node = - emBuilder.AddVirtualIPRangeSection( - excessiveRange, - JitManagerAddress, - excessiveModule.Address, - head, - registerAsHead: false); - head = node.Address; - } - emBuilder.SetVirtualIPRangeListHead(head); - break; - } default: throw new InvalidOperationException(corruption); } @@ -614,6 +711,95 @@ public void GetCodeKind_R2R_WasmVirtualIPCorruptList_FailsClosed(string corrupti Assert.Equal(CodeKind.Unknown, em.GetCodeKind(new TargetCodePointer(ProbeVirtualIP))); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetMethodDesc_R2R_WasmVirtualIPUnrelatedRegistration(bool registrationComplete) + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + const ulong InitializedRangeStart = 0x8001_0001; + const ulong PendingRangeStart = 0x8002_0001; + const ulong MethodDescAddress = 0x0101_aaa0; + const ulong JitManagerAddress = 0x000b_ff00; + ulong initializedModule = 0; + + IExecutionManager em = CreateExecutionManagerContract("c1", wasmArch, emBuilder => + { + var initializedRange = emBuilder.AllocateJittedCodeRange(InitializedRangeStart, 0x200); + MockReadyToRunInfo initializedInfo = emBuilder.AddReadyToRunInfo([0], []); + initializedInfo.MinVirtualIP = InitializedRangeStart; + initializedInfo.LoadedImageBase = 0x0090_0000; + new MockHashMapBuilder(emBuilder.Builder).PopulatePtrMap( + initializedInfo.EntryPointToMethodDescMapAddress, [(InitializedRangeStart, MethodDescAddress)]); + initializedModule = emBuilder.AddReadyToRunModule(initializedInfo.Address).Address; + MockVirtualIPRangeSection tail = emBuilder.AddVirtualIPRangeSection( + initializedRange, JitManagerAddress, initializedModule, registerAsHead: false); + + var pendingRange = emBuilder.AllocateJittedCodeRange(PendingRangeStart, 0x200); + MockReadyToRunInfo pendingInfo = emBuilder.AddReadyToRunInfo([0], []); + pendingInfo.MinVirtualIP = registrationComplete ? PendingRangeStart : 0; + pendingInfo.LoadedImageBase = 0x00a0_0000; + MockLoaderModule pendingModule = emBuilder.AddReadyToRunModule(pendingInfo.Address); + emBuilder.AddVirtualIPRangeSection(pendingRange, JitManagerAddress, pendingModule.Address, tail.Address); + }, targetArchitecture: RuntimeInfoArchitecture.Wasm); + + CodeBlockHandle? handle = em.GetCodeBlockHandle(new TargetCodePointer(InitializedRangeStart)); + Assert.NotNull(handle); + Assert.Equal(new TargetPointer(MethodDescAddress), em.GetMethodDesc(handle.Value)); + Assert.Equal(new TargetPointer(initializedModule), em.FindReadyToRunModule(new TargetPointer(InitializedRangeStart))); + Assert.Equal(registrationComplete ? CodeKind.ReadyToRun : CodeKind.Unknown, + em.GetCodeKind(new TargetCodePointer(PendingRangeStart))); + } + + [Theory] + [InlineData(1024, false)] + [InlineData(1025, false)] + [InlineData(2, true)] + [InlineData(1025, true)] + public void FindReadyToRunModule_WasmVirtualIPLongList(int count, bool cycle) + { + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + const ulong FirstRangeStart = 0x8001_0001; + const uint RangeSize = 0x200; + const ulong JitManagerAddress = 0x000b_ff00; + ulong firstModule = 0; + ulong lastModule = 0; + + IExecutionManager em = CreateExecutionManagerContract("c1", wasmArch, emBuilder => + { + ulong head = 0; + MockVirtualIPRangeSection? tail = null; + for (int i = 0; i < count; i++) + { + ulong rangeStart = FirstRangeStart + (ulong)i * RangeSize; + var range = emBuilder.AllocateJittedCodeRange(rangeStart, RangeSize); + MockReadyToRunInfo info = emBuilder.AddReadyToRunInfo([0], []); + info.MinVirtualIP = rangeStart; + info.LoadedImageBase = 0x0090_0000 + (ulong)i * RangeSize; + MockLoaderModule module = emBuilder.AddReadyToRunModule(info.Address); + MockVirtualIPRangeSection node = emBuilder.AddVirtualIPRangeSection( + range, JitManagerAddress, module.Address, head, registerAsHead: false); + head = node.Address; + if (i == 0) + { + tail = node; + firstModule = module.Address; + } + lastModule = module.Address; + } + if (cycle) + tail!.Next = head; + emBuilder.SetVirtualIPRangeListHead(head); + }, targetArchitecture: RuntimeInfoArchitecture.Wasm); + + Assert.Equal(new TargetPointer(cycle ? 0 : lastModule), + em.FindReadyToRunModule(new TargetPointer(FirstRangeStart + (ulong)(count - 1) * RangeSize))); + Assert.Equal(new TargetPointer(cycle ? 0 : firstModule), + em.FindReadyToRunModule(new TargetPointer(FirstRangeStart))); + Assert.Equal(TargetPointer.Null, + em.FindReadyToRunModule(new TargetPointer(FirstRangeStart + (ulong)count * RangeSize))); + } + [Fact] public void GetMethodDesc_R2R_WasmFuncletUsesMaskedBeginAddressAndControllingMethodDesc() { diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs index d453aa660bbdc5..ee65bd0d29b02b 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -441,25 +441,35 @@ internal sealed class MockReadyToRunInfo : TypedView private const string NumImportSectionsFieldName = "NumImportSections"; private const string MinVirtualIPFieldName = "MinVirtualIP"; - public static Layout CreateLayout(MockTarget.Architecture architecture, int hashMapStride) - => new SequentialLayoutBuilder("ReadyToRunInfo", architecture) + public static Layout CreateLayout(MockTarget.Architecture architecture, int hashMapStride, bool isWasm = false) + { + SequentialLayoutBuilder builder = new SequentialLayoutBuilder("ReadyToRunInfo", architecture) .AddPointerField(ReadyToRunHeaderFieldName) .AddPointerField(CompositeInfoFieldName) .AddUInt32Field(NumRuntimeFunctionsFieldName) - .AddPointerField(RuntimeFunctionsFieldName) - .AddUInt32Field(NumHotColdMapFieldName) - .AddPointerField(HotColdMapFieldName) - .AddPointerField(DelayLoadMethodCallThunksFieldName) + .AddPointerField(RuntimeFunctionsFieldName); + if (!isWasm) + { + builder.AddUInt32Field(NumHotColdMapFieldName) + .AddPointerField(HotColdMapFieldName) + .AddPointerField(DelayLoadMethodCallThunksFieldName); + } + builder .AddPointerField(DebugInfoSectionFieldName) .AddPointerField(ExceptionInfoSectionFieldName) .AddPointerField(ImportSectionsFieldName) .AddUInt32Field(NumImportSectionsFieldName) .AddField(EntryPointToMethodDescMapFieldName, hashMapStride) .AddPointerField(LoadedImageBaseFieldName) - .AddPointerField(CompositeFieldName) - // WASM-only: base virtual IP for the module's ReadyToRun functions (nullable field). - .AddPointerField(MinVirtualIPFieldName) - .Build(); + .AddPointerField(CompositeFieldName); + if (isWasm) + { + builder.AddPointerField(MinVirtualIPFieldName); + } + return builder.Build(); + } + + public bool HasHotColdMap => Array.Exists(Layout.Fields, static f => f.Name == NumHotColdMapFieldName); public ulong CompositeInfo { @@ -503,6 +513,18 @@ public ulong ExceptionInfoSection set => WritePointerField(ExceptionInfoSectionFieldName, value); } + public ulong DebugInfoSection + { + get => ReadPointerField(DebugInfoSectionFieldName); + set => WritePointerField(DebugInfoSectionFieldName, value); + } + + public ulong Composite + { + get => ReadPointerField(CompositeFieldName); + set => WritePointerField(CompositeFieldName, value); + } + public ulong DelayLoadMethodCallThunks { get => ReadPointerField(DelayLoadMethodCallThunksFieldName); @@ -738,18 +760,18 @@ internal readonly struct JittedCodeRange private readonly int _rangeSectionMapMaxSetBit; private ulong _virtualIPRangeListGlobalAddress; - internal MockExecutionManagerBuilder(string version, MockTarget.Architecture arch, AllocationRange allocationRange, ulong allCodeHeaps = 0) - : this(version, new MockMemorySpace.Builder(new TargetTestHelpers(arch)), allocationRange, allCodeHeaps) + internal MockExecutionManagerBuilder(string version, MockTarget.Architecture arch, AllocationRange allocationRange, ulong allCodeHeaps = 0, bool isWasm = false) + : this(version, new MockMemorySpace.Builder(new TargetTestHelpers(arch)), allocationRange, allCodeHeaps, isWasm) { } - internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder builder, AllocationRange allocationRange, ulong allCodeHeaps = 0) + internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder builder, AllocationRange allocationRange, ulong allCodeHeaps = 0, bool isWasm = false) { ArgumentNullException.ThrowIfNull(builder); Version = version; Builder = builder; - _runtimeFunctions = new MockRuntimeFunctionsBuilder(builder); + _runtimeFunctions = new MockRuntimeFunctionsBuilder(builder, includeEndAddress: !isWasm); _rangeSectionMapAllocator = Builder.CreateAllocator(allocationRange.RangeSectionMapStart, allocationRange.RangeSectionMapEnd); _nibbleMapAllocator = Builder.CreateAllocator(allocationRange.NibbleMapStart, allocationRange.NibbleMapEnd); _allocator = Builder.CreateAllocator(allocationRange.ExecutionManagerStart, allocationRange.ExecutionManagerEnd); @@ -774,7 +796,7 @@ internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder bui HostCodeHeapLayout = MockHostCodeHeap.CreateLayout(architecture); RealCodeHeaderLayout = MockRealCodeHeader.CreateLayout(architecture); InterpreterRealCodeHeaderLayout = MockInterpreterRealCodeHeader.CreateLayout(architecture); - ReadyToRunInfoLayout = MockReadyToRunInfo.CreateLayout(architecture, hashMapStride); + ReadyToRunInfoLayout = MockReadyToRunInfo.CreateLayout(architecture, hashMapStride, isWasm); ReadyToRunHeaderLayout = MockReadyToRunHeader.CreateLayout(architecture); EEJitManagerLayout = MockEEJitManager.CreateLayout(architecture); DynamicFunctionTableLayout = MockDynamicFunctionTable.CreateLayout(architecture); @@ -1087,8 +1109,15 @@ public MockReadyToRunInfo AddReadyToRunInfo(uint[] runtimeFunctions, uint[] hotC readyToRunInfo.CompositeInfo = readyToRunInfo.Address; readyToRunInfo.NumRuntimeFunctions = checked((uint)runtimeFunctions.Length); readyToRunInfo.RuntimeFunctions = runtimeFunctionsAddress; - readyToRunInfo.NumHotColdMap = checked((uint)hotColdMap.Length); - readyToRunInfo.HotColdMap = hotColdMapAddress; + if (readyToRunInfo.HasHotColdMap) + { + readyToRunInfo.NumHotColdMap = checked((uint)hotColdMap.Length); + readyToRunInfo.HotColdMap = hotColdMapAddress; + } + else if (hotColdMap.Length != 0) + { + throw new InvalidOperationException("Hot/cold code is not supported by this target."); + } return readyToRunInfo; } @@ -1118,6 +1147,14 @@ public void SetDelayLoadMethodCallThunks(MockReadyToRunInfo readyToRunInfo, uint readyToRunInfo.DelayLoadMethodCallThunks = imageDataDir.Address; } + public void SetDebugInfoSection(MockReadyToRunInfo readyToRunInfo, uint rva, uint size) + { + MockImageDataDirectory directory = AllocateAndCreate(ImageDataDirectoryLayout, "DebugInfoSection"); + directory.VirtualAddress = rva; + directory.Size = size; + readyToRunInfo.DebugInfoSection = directory.Address; + } + public MockLoaderModule AddReadyToRunModule(ulong readyToRunInfoAddress) { MockLoaderModule module = AllocateAndCreate(ModuleLayout, "R2R Module"); diff --git a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs index 9e4ff20497ca4b..4554ee3d5fa69e 100644 --- a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs @@ -33,7 +33,7 @@ private static TestPlaceholderTarget CreateTarget(bool emptyList = false) int hashMapStride = MockHashMap.CreateLayout(WasmArch).Size; var moduleLayout = MockLoaderModule.CreateLayout(WasmArch); - var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride); + var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride, isWasm: true); var runtimeFunctionLayout = helpers.LayoutFields([ new("BeginAddress", DataType.uint32), new("UnwindData", DataType.uint32), @@ -146,7 +146,7 @@ private static (TestPlaceholderTarget Target, ulong FrameAddress) CreateDispatch int hashMapStride = MockHashMap.CreateLayout(WasmArch).Size; var moduleLayout = MockLoaderModule.CreateLayout(WasmArch); - var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride); + var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride, isWasm: true); var runtimeFunctionLayout = helpers.LayoutFields([ new("BeginAddress", DataType.uint32), new("UnwindData", DataType.uint32), From 7c8c9d8ec61b3bd0916a29feb853ef3961653e48 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 19:24:31 -0500 Subject: [PATCH 03/14] Bound cDAC virtual-IP lookup with an explicit reader budget Allow up to 65,536 nodes while retaining full-list ambiguity and cycle checks. Reject an over-budget chain before reading its next node, even after a match. Document the budget as reader policy and cover exact-budget success, budget+1 rejection, and the read boundary with a compact fixture. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 9 ++++- .../ExecutionManager/ExecutionManagerCore.cs | 5 ++- .../ExecutionManager/ExecutionManagerTests.cs | 40 +++++++++++++++++++ .../MockDescriptors.ExecutionManager.cs | 33 +++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 629316c1d307af..edd22322934f1f 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -159,7 +159,14 @@ Within a range section fragment, a [nibble map](#nibblemap) structure is used to WebAssembly ReadyToRun code uses encoded virtual IPs rather than linear-memory code addresses. These ranges are registered in `VirtualIPRangeList`, not the range section map. A virtual-IP -lookup walks the intrusive list with cycle detection and no fixed node-count limit. Invalid +lookup walks the intrusive list with cycle detection and a per-lookup budget of 65,536 nodes. +The budget bounds traversal work and visited-set growth for corrupt chains of distinct nodes; +it is a reader resource policy, not a native registration limit or proof of corruption. +It provides 64 times the headroom of the original 1,024-node budget while remaining finite. +Exactly 65,536 nodes may be traversed successfully. If the last permitted node has a non-null +`Next`, lookup fails closed before dereferencing that next node, even if a matching range has +already been found. This intentionally rejects longer lists, including otherwise valid ones. +Traversal continues after a match to detect ambiguity within the budget. Invalid range structures, cycles, or multiple ranges containing the requested IP fail closed, and an encoded virtual IP that is absent from the list does not fall through to the real-address map. Module and ReadyToRun metadata are validated only for a range containing the requested IP. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 1a31b56ada3611..f96184dd5e5023 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -199,6 +199,9 @@ private static RangeSection FindVirtualIPRangeSection( TargetPointer virtualIPRangeListAddress, TargetCodePointer virtualIP) { + // Reader resource budget, not a limit imposed by native range registration. + const int MaxVirtualIPRangeNodes = 65_536; + if (virtualIPRangeListAddress == TargetPointer.Null) return new RangeSection(); @@ -209,7 +212,7 @@ private static RangeSection FindVirtualIPRangeSection( Data.RangeSection? matchingRange = null; while (current != TargetPointer.Null) { - if (!visited.Add(current)) + if (visited.Count == MaxVirtualIPRangeNodes || !visited.Add(current)) return new RangeSection(); Data.VirtualIPRangeSection node = target.ProcessedData.GetOrAdd(current); diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index 5981cf7f8d503d..b63ecf1d315c9b 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -851,6 +851,46 @@ public void GetMethodDesc_R2R_WasmFuncletUsesMaskedBeginAddressAndControllingMet Assert.Equal(new TargetPointer(runtimeFunctionsAddress + (ulong)runtimeFunctionSize), em.GetUnwindInfo(funclet.Value)); } + [Theory] + [InlineData(65_536, true)] + [InlineData(65_537, false)] + public void FindReadyToRunModule_WasmVirtualIPTraversalBudget(int count, bool withinBudget) + { + const int NodeBudget = 65_536; + const ulong FirstRangeStart = 0x8001_0001; + const uint RangeSize = 0x200; + MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + var allocationRange = MockExecutionManagerBuilder.DefaultAllocationRange with + { + ExecutionManagerStart = 0x0100_0000, + ExecutionManagerEnd = 0x0400_0000, + }; + MockExecutionManagerBuilder emBuilder = new("c1", wasmArch, allocationRange, isWasm: true); + (ulong nodes, ulong firstModule, ulong lastModule) = + emBuilder.AddVirtualIPRangeSectionArray(count, FirstRangeStart, RangeSize, 0x000b_ff00); + int nodeSize = emBuilder.VirtualIPRangeSectionLayout.Size; + int highestNodeRead = -1; + MockMemorySpace.MemoryContext memory = emBuilder.Builder.GetMemoryContext(); + Target target = CreateTarget(emBuilder, RuntimeInfoOperatingSystem.Windows, RuntimeInfoArchitecture.Wasm, + targetBuilder => targetBuilder.UseReader((address, buffer) => + { + if (address >= nodes && address < nodes + (ulong)(count * nodeSize)) + highestNodeRead = Math.Max(highestNodeRead, (int)((address - nodes) / (ulong)nodeSize)); + return memory.ReadFromTarget(address, buffer); + })); + IExecutionManager em = target.Contracts.ExecutionManager; + + TargetPointer firstResult = em.FindReadyToRunModule(new TargetPointer(FirstRangeStart)); + Assert.Equal(NodeBudget - 1, highestNodeRead); + Assert.Equal(new TargetPointer(withinBudget ? firstModule : 0), firstResult); + Assert.Equal(new TargetPointer(withinBudget ? lastModule : 0), + em.FindReadyToRunModule(new TargetPointer(FirstRangeStart + (ulong)(count - 1) * RangeSize))); + Assert.Equal(NodeBudget - 1, highestNodeRead); + Assert.Equal(withinBudget ? CodeKind.ReadyToRun : CodeKind.Unknown, + em.GetCodeKind(new TargetCodePointer(FirstRangeStart))); + Assert.Equal(NodeBudget - 1, highestNodeRead); + } + [Theory] [MemberData(nameof(StdArchAllVersions))] public void GetMethodDesc_R2R_MultipleRuntimeFunctions(string version, MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs index ee65bd0d29b02b..b7b4505bb79d82 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -1155,6 +1155,39 @@ public void SetDebugInfoSection(MockReadyToRunInfo readyToRunInfo, uint rva, uin readyToRunInfo.DebugInfoSection = directory.Address; } + public (ulong NodesAddress, ulong FirstModule, ulong LastModule) AddVirtualIPRangeSectionArray( + int count, ulong rangeStart, uint rangeSize, ulong jitManager) + { + ArgumentOutOfRangeException.ThrowIfLessThan(count, 2); + MockMemorySpace.HeapFragment nodes = _allocator.Allocate( + checked((ulong)count * (ulong)VirtualIPRangeSectionLayout.Size), "VirtualIPRangeSections"); + MockReadyToRunInfo firstInfo = AddReadyToRunInfo([0], []); + firstInfo.MinVirtualIP = rangeStart; + firstInfo.LoadedImageBase = 0x1000_0000; + MockLoaderModule firstModule = AddReadyToRunModule(firstInfo.Address); + MockReadyToRunInfo lastInfo = AddReadyToRunInfo([0], []); + lastInfo.MinVirtualIP = rangeStart + (ulong)(count - 1) * rangeSize; + lastInfo.LoadedImageBase = 0x2000_0000; + MockLoaderModule lastModule = AddReadyToRunModule(lastInfo.Address); + for (int i = 0; i < count; i++) + { + ulong nodeAddress = nodes.Address + (ulong)(i * VirtualIPRangeSectionLayout.Size); + Memory nodeMemory = nodes.Data.AsMemory(i * VirtualIPRangeSectionLayout.Size, VirtualIPRangeSectionLayout.Size); + MockVirtualIPRangeSection node = VirtualIPRangeSectionLayout.Create(nodeMemory, nodeAddress); + MockRangeSection range = RangeSectionLayout.Create( + nodeMemory.Slice(0, RangeSectionLayout.Size), node.RangeSectionAddress); + range.RangeBegin = rangeStart + (ulong)i * rangeSize; + range.RangeEndOpen = range.RangeBegin + rangeSize; + range.JitManager = jitManager; + // Only the head and tail are queried; unrelated ranges need no distinct module metadata. + range.R2RModule = i + 1 == count ? lastModule.Address : firstModule.Address; + range.Flags = VirtualIPRangeSectionFlag; + node.Next = i + 1 == count ? 0 : nodeAddress + (ulong)VirtualIPRangeSectionLayout.Size; + } + SetVirtualIPRangeListHead(nodes.Address); + return (nodes.Address, firstModule.Address, lastModule.Address); + } + public MockLoaderModule AddReadyToRunModule(ulong readyToRunInfoAddress) { MockLoaderModule module = AllocateAndCreate(ModuleLayout, "R2R Module"); From be16f01f18a3902daa79e27d860e77396d7f2fec Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 15 Sep 2026 13:41:46 -0500 Subject: [PATCH 04/14] Test WASM R2R filter funclet classification Cover the shared native and cDAC invariant that a filter clause offset equals the flagged funclet start relative to its controlling method. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ExecutionManager/ExecutionManagerTests.cs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index b63ecf1d315c9b..0aac043728971a 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -498,9 +498,10 @@ public void GetCodeKind_R2R_WasmVirtualIPRangeList_RejectsBoundariesAndKeepsAdja } [Theory] - [InlineData(false)] - [InlineData(true)] - public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool funclet) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool funclet, bool filter) { MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; const ulong VirtualIPRangeStart = 0x8001_0001; @@ -531,7 +532,13 @@ public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool fun Name = "WASM debug info", }); Dictionary exceptionTypes = - AddWasmExceptionInfo(emBuilder, info, LoadedImageBase, RootBeginAddress); + AddWasmExceptionInfo( + emBuilder, + info, + LoadedImageBase, + RootBeginAddress, + filter, + FuncletBeginAddress - RootBeginAddress); MethodDescHandle methodHandle = new(new TargetPointer(MethodDescAddress)); TargetPointer methodTable = new(0x0102_0000); @@ -553,17 +560,25 @@ public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool fun Assert.Equal(new TargetPointer(LoadedImageBase + DebugInfoRva + 4), debugInfo); Assert.Equal(ExpectedDebugInfoByte, target.Read(debugInfo)); ExceptionClauseInfo clause = Assert.Single(em.GetExceptionClauses(handle.Value)); - Assert.Equal(ExceptionClauseInfo.ExceptionClauseFlags.Finally, clause.ClauseType); + Assert.Equal( + filter ? ExceptionClauseInfo.ExceptionClauseFlags.Filter : ExceptionClauseInfo.ExceptionClauseFlags.Finally, + clause.ClauseType); Assert.Equal(2u, clause.TryStartPC); Assert.Equal(16u, clause.TryEndPC); - Assert.Equal(FuncletBeginAddress - RootBeginAddress, clause.HandlerStartPC); - Assert.Equal(48u, clause.HandlerEndPC); + Assert.Equal(filter ? 48u : FuncletBeginAddress - RootBeginAddress, clause.HandlerStartPC); + Assert.Equal(filter ? 64u : 48u, clause.HandlerEndPC); Assert.Null(clause.ClassToken); - Assert.Null(clause.FilterOffset); + Assert.Equal(filter ? FuncletBeginAddress - RootBeginAddress : null, clause.FilterOffset); + Assert.Equal(filter, em.IsFilterFunclet(handle.Value)); } private static Dictionary AddWasmExceptionInfo( - MockExecutionManagerBuilder emBuilder, MockReadyToRunInfo info, uint imageBase, uint methodRva) + MockExecutionManagerBuilder emBuilder, + MockReadyToRunInfo info, + uint imageBase, + uint methodRva, + bool filter, + uint funcletOffset) { const uint CoreInfoRva = 0x200; const uint CoreHeaderRva = 0x210; @@ -585,7 +600,9 @@ public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool fun AddWords(imageBase + ClauseRva, DataType.R2RExceptionClause, [new("Flags", DataType.uint32), new("TryStartPC", DataType.uint32), new("TryEndPC", DataType.uint32), new("HandlerStartPC", DataType.uint32), new("HandlerEndPC", DataType.uint32), new("ClassToken", DataType.uint32)], - [2, 2, 16, 32, 48, 0]); + filter + ? [1, 2, 16, 48, 64, funcletOffset] + : [2, 2, 16, funcletOffset, 48, 0]); return types; void AddWords(uint address, DataType type, TargetTestHelpers.Field[] fields, uint[] words) From 443fb559f4771c3fbf31a1e85d07ef9aebfd10cc Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 15 Sep 2026 20:28:19 -0500 Subject: [PATCH 05/14] Fix cDAC WASM filter funclet classification Map the executable filter entry to its containing runtime function before comparing funclet starts, matching native WASM behavior while preserving non-WASM offset comparisons. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 2 +- .../ExecutionManager/ExecutionManagerCore.cs | 19 ++++++++++++++++++- .../ExecutionManager/ExecutionManagerTests.cs | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index edd22322934f1f..ad46552ce9ce2e 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -574,7 +574,7 @@ There are two distinct clause data types. JIT-compiled code uses `EEExceptionCla After obtaining the clause array bounds, the common iteration logic classifies each clause by its flags. The native `COR_ILEXCEPTION_CLAUSE` flags are bit flags: `Filter` (0x1), `Finally` (0x2), `Fault` (0x4). If none are set, the clause is `Typed`. For typed clauses, if the `CachedClass` flag (0x10000000) is set (JIT-only, used for dynamic methods), the union field contains a resolved `TypeHandle` pointer; the clause is a catch-all if this pointer equals the `ObjectMethodTable` global. Otherwise, the union field is a metadata `ClassToken`. To determine whether a typed clause is a catch-all handler, the `ClassToken` (which may be a `TypeDef` or `TypeRef`) is resolved to a `MethodTable` via the `Loader` contract's module lookup maps (`TypeDefToMethodTable` or `TypeRefToMethodTable`) and compared against the `ObjectMethodTable` global. For typed clauses without a cached type handle, the module address is resolved by walking `CodeBlockHandle` -> `MethodDesc` -> `MethodTable` -> `TypeHandle` -> `Module` via the `RuntimeTypeSystem` contract. -`IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. +`IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method. On WebAssembly, `FilterOffset` identifies the executable filter entry, which can follow a synthetic funclet prolog. The reader resolves the code address at `methodStart + FilterOffset` and checks whether its containing runtime function has the same funclet start as the queried code block. On other targets, the filter offset is compared directly with the funclet's relative offset. `IExecutionManager.GetStackParameterSize` returns the size (in bytes) of stack-passed parameters at the call to the method described by the code block handle. It mirrors the native `EECodeManager::GetStackParameterSize`: it returns 0 for funclets and for non-x86 targets. On x86, it returns 0 for methods using the varargs calling convention (which are caller-popped), otherwise it returns the argument size encoded in the GC info header. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index f96184dd5e5023..7d17a2d6d371bd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -443,8 +443,25 @@ bool IExecutionManager.IsFilterFunclet(CodeBlockHandle codeInfoHandle) List clauses = eman.GetExceptionClauses(codeInfoHandle); foreach (ExceptionClauseInfo clause in clauses) { - if (clause.ClauseType == ExceptionClauseInfo.ExceptionClauseFlags.Filter && clause.FilterOffset == funcletStartOffset) + if (clause.ClauseType != ExceptionClauseInfo.ExceptionClauseFlags.Filter + || clause.FilterOffset is not uint filterOffset) + { + continue; + } + + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.Wasm) + { + TargetCodePointer filterAddress = new(info.StartAddress.Value + filterOffset); + if (eman.GetCodeBlockHandle(filterAddress) is CodeBlockHandle filterCodeInfoHandle + && eman.GetFuncletStartAddress(filterCodeInfoHandle) == funcletStartAddress) + { + return true; + } + } + else if (filterOffset == funcletStartOffset) + { return true; + } } return false; diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index 0aac043728971a..96b431efefb441 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -568,7 +568,7 @@ public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool fun Assert.Equal(filter ? 48u : FuncletBeginAddress - RootBeginAddress, clause.HandlerStartPC); Assert.Equal(filter ? 64u : 48u, clause.HandlerEndPC); Assert.Null(clause.ClassToken); - Assert.Equal(filter ? FuncletBeginAddress - RootBeginAddress : null, clause.FilterOffset); + Assert.Equal(filter ? FuncletBeginAddress - RootBeginAddress + 2 : null, clause.FilterOffset); Assert.Equal(filter, em.IsFilterFunclet(handle.Value)); } @@ -601,7 +601,7 @@ public void GetDebugInfoAndExceptionClauses_R2R_WasmUsesLoadedImageBase(bool fun [new("Flags", DataType.uint32), new("TryStartPC", DataType.uint32), new("TryEndPC", DataType.uint32), new("HandlerStartPC", DataType.uint32), new("HandlerEndPC", DataType.uint32), new("ClassToken", DataType.uint32)], filter - ? [1, 2, 16, 48, 64, funcletOffset] + ? [1, 2, 16, 48, 64, funcletOffset + 2] : [2, 2, 16, funcletOffset, 48, 0]); return types; From 61d603933a643fe29dc674f3e011560d19ea2fe5 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 2 Sep 2026 09:28:31 -0500 Subject: [PATCH 06/14] [wasm] Preserve R2R variable debug info Preserve variable debug information produced by RyuJIT for ReadyToRun WebAssembly code, including scope ranges across relooper block ordering and packed wasm local register locations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegen.h | 6 ++ src/coreclr/jit/compiler.cpp | 7 -- src/coreclr/jit/scopeinfo.cpp | 86 +++++++++++++++++-- .../TestCases/R2RTestSuites.cs | 33 ++++++- .../TestCases/Webcil/WasmWebcilModule.cs | 7 ++ .../DebugInfo.cs | 25 +++++- 6 files changed, 147 insertions(+), 17 deletions(-) diff --git a/src/coreclr/jit/codegen.h b/src/coreclr/jit/codegen.h index 2020048e2acc3f..af165a682b65fa 100644 --- a/src/coreclr/jit/codegen.h +++ b/src/coreclr/jit/codegen.h @@ -744,6 +744,12 @@ class CodeGen final : public CodeGenInterface IL_OFFSET siLastEndOffs; // IL offset of the (exclusive) end of the last block processed +#if defined(TARGET_WASM) + // The relooper can reorder and duplicate blocks, so wasm cannot discover + // scopes with the monotonic enter/exit cursors used by other targets. + bool* siWasmOpenedScopes; +#endif // defined(TARGET_WASM) + /* XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index e20d08a7be1b2e..013c8e0bf0d605 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -2898,13 +2898,6 @@ void Compiler::compInitOptions(JitFlags* jitFlags) opts.compScopeInfo = opts.compDbgInfo; -#ifdef TARGET_WASM - // Wasm uses virtual registers that cannot be encoded in the - // ICorDebugInfo register scheme, and there is no native debugger - // to consume scope info, so disable it entirely. - opts.compScopeInfo = false; -#endif - #ifdef LATE_DISASM codeGen->getDisAssembler().disOpenForLateDisAsm(info.compMethodName, info.compClassName, info.compMethodInfo->args.pSig); diff --git a/src/coreclr/jit/scopeinfo.cpp b/src/coreclr/jit/scopeinfo.cpp index edae8a122f9693..ae7ee24d155f4d 100644 --- a/src/coreclr/jit/scopeinfo.cpp +++ b/src/coreclr/jit/scopeinfo.cpp @@ -208,6 +208,30 @@ void CodeGenInterface::siVarLoc::storeVariableInRegisters(regNumber reg, regNumb // Note: mask registers (K0-K7) and XMM16+ are accepted but will produce // VLT_INVALID since they can't be encoded in debug info. +#if defined(TARGET_WASM) + if (reg == REG_NA) + { + vlType = VLT_INVALID; + return; + } + + assert(genIsValidReg(reg)); + + if (otherReg == REG_NA) + { + vlType = VLT_REG; + vlReg.vlrReg = reg; + } + else + { + assert(genIsValidReg(otherReg)); + vlType = VLT_REG_REG; + vlRegReg.vlrrReg1 = reg; + vlRegReg.vlrrReg2 = otherReg; + } + return; +#endif // defined(TARGET_WASM) + if (otherReg == REG_NA) { if (genIsValidFloatReg(reg)) @@ -471,6 +495,12 @@ void CodeGenInterface::siVarLoc::siFillStackVarLoc( void CodeGenInterface::siVarLoc::siFillRegisterVarLoc( const LclVarDsc* varDsc, var_types type, regNumber baseReg, int offset, bool isFramePointerUsed) { +#if defined(TARGET_WASM) + this->vlType = VLT_REG; + this->vlReg.vlrReg = varDsc->GetRegNum(); + return; +#endif // defined(TARGET_WASM) + switch (type) { case TYP_INT: @@ -1598,6 +1628,14 @@ void CodeGen::siInit() siLastEndOffs = 0; m_compiler->compResetScopeLists(); + +#if defined(TARGET_WASM) + siWasmOpenedScopes = nullptr; + if (m_compiler->info.compVarScopesCount > 0) + { + siWasmOpenedScopes = new (m_compiler, CMK_DebugInfo) bool[m_compiler->info.compVarScopesCount](); + } +#endif // defined(TARGET_WASM) } /***************************************************************************** @@ -1682,17 +1720,51 @@ void CodeGen::siBeginBlock(BasicBlock* block) // void CodeGen::siOpenScopesForNonTrackedVars(const BasicBlock* block, unsigned int lastBlockILEndOffset) { + unsigned int beginOffs = block->bbCodeOffs; + #if defined(TARGET_WASM) - // TODO-WASM: Wasm structured control flow - // requirements are incompatible with debug codegen's - // desire to keep blocks in increasing IL offset - // order. Figure out the proper scope manipulations. - // + // Scan all scopes directly because the relooper does not emit blocks in + // increasing IL offset order. + if (m_compiler->opts.OptimizationDisabled()) + { + unsigned int endOffs = block->bbCodeOffsEnd; + + for (unsigned i = 0; i < m_compiler->info.compVarScopesCount; i++) + { + VarScopeDsc* varScope = &m_compiler->info.compVarScopes[i]; + + if (siWasmOpenedScopes[i]) + { + continue; + } + + if ((varScope->vsdLifeBeg >= endOffs) || (varScope->vsdLifeEnd <= beginOffs)) + { + continue; + } + + siWasmOpenedScopes[i] = true; + + LclVarDsc* lclVarDsc = m_compiler->lvaGetDesc(varScope->vsdVarNum); + + // Only report locals that were referenced, if we're not doing debug codegen + if (m_compiler->opts.compDbgCode || (lclVarDsc->lvRefCnt() > 0)) + { + JITDUMP("Scope info: opening scope, LVnum=%u [%03X..%03X)\n", varScope->vsdLVnum, varScope->vsdLifeBeg, + varScope->vsdLifeEnd); + + varLiveKeeper->siStartVariableLiveRange(lclVarDsc, varScope->vsdVarNum); + } + else + { + JITDUMP("Skipping open scope for V%02u, unreferenced\n", varScope->vsdVarNum); + } + } + } + return; #endif // defined(TARGET_WASM) - unsigned int beginOffs = block->bbCodeOffs; - // There aren't any tracked locals. // // For debuggable or minopts code, scopes can begin only on block boundaries. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 262e9de11c1605..85f46fb4fa0acd 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -126,12 +126,28 @@ static void Validate(ReadyToRunReader reader) List methods = R2RAssert.GetAllMethods(reader); Assert.True(methods.Exists(method => method.SignatureString.Contains("AddIntegers", StringComparison.Ordinal))); + ReadyToRunMethod addDoubles = Assert.Single(methods, method => + method.SignatureString.Contains("AddDoubles", StringComparison.Ordinal)); + const int WasmRegTypeShift = 29; + const uint F64WasmValueType = 4; + List doubleVariables = addDoubles.RuntimeFunctions + .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) + .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) + .ToList(); + Assert.DoesNotContain(doubleVariables, + variable => variable.VariableLocation.VarLocType is VarLocType.VLT_REG_FP or VarLocType.VLT_FPSTK); + Assert.Contains(doubleVariables, variable => + variable.VariableLocation.VarLocType == VarLocType.VLT_REG && + (uint)variable.VariableLocation.Data1 >> WasmRegTypeShift == F64WasmValueType); // Reads static data, so the JIT materializes the image base via a well-known-global global.get. Assert.True(methods.Exists(method => method.SignatureString.Contains("SumStaticData", StringComparison.Ordinal))); // Has a try/finally, so the JIT materializes the table base via a well-known-global global.get. - Assert.True(methods.Exists(method => - method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal))); + ReadyToRunMethod sumWithFinally = Assert.Single(methods, method => + method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal)); + Assert.Contains(sumWithFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.DebugInfo is not null && + runtimeFunction.DebugInfo.VariablesList.Exists(variable => variable.Variable.Type == VariableType.Local)); // Has a catch clause, so the JIT emits a try_table catch_ref that references the // imported restore-context exception tag. Assert.True(methods.Exists(method => @@ -411,6 +427,19 @@ static void Validate(ReadyToRunReader reader) } } + [Theory] + [InlineData(2, "ambient SP")] + [InlineData(0x20000001, "$1 (i32)")] + [InlineData(0x40000002, "$2 (i64)")] + [InlineData(0x60000003, "$3 (f32)")] + [InlineData(unchecked((int)0x80000004), "$4 (f64)")] + [InlineData(unchecked((int)0xA0000005), "$5 (v128)")] + [InlineData(unchecked((int)0xC0000006), "$6 (exnref)")] + public void WasmDebugRegisterIsDecoded(int register, string expected) + { + Assert.Equal(expected, DebugInfo.GetPlatformSpecificRegister(WasmMachine.Wasm32, register)); + } + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsNotWasmTarget))] public void RuntimeFunctionsSectionSizeExcludesSentinel() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs index e84e3fdd8f5d3d..204b026268da07 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs @@ -17,6 +17,13 @@ public static int AddIntegers(int left, int right) return left + right; } + [MethodImpl(MethodImplOptions.NoOptimization)] + public static double AddDoubles(double left, double right) + { + double result = left + right; + return result; + } + // Reads static data, which forces the JIT to materialize the imageBase address via a // 'global.get' of the wasm imageBase well-known global. public static int SumStaticData(int index) diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs index 58cb620fe98558..768731c4303b63 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs @@ -87,12 +87,35 @@ public static string GetPlatformSpecificRegister(Machine machine, int regnum) case Machine.RiscV64: return ((RiscV64.Registers)regnum).ToString(); case WasmMachine.Wasm32: - return $"NYI '{regnum}'"; // WASM-TODO Implement this correctly. + return GetWasmRegister(regnum); default: throw new NotImplementedException($"No implementation for machine type {machine}."); } } + private static string GetWasmRegister(int regnum) + { + const int WasmRegTypeShift = 29; + const uint WasmRegIndexMask = (1u << WasmRegTypeShift) - 1; + + return regnum switch + { + 0 => "PC", + 1 => "REGNUM_COUNT", + 2 => "ambient SP", + _ => ((uint)regnum >> WasmRegTypeShift) switch + { + 1 => $"${(uint)regnum & WasmRegIndexMask} (i32)", + 2 => $"${(uint)regnum & WasmRegIndexMask} (i64)", + 3 => $"${(uint)regnum & WasmRegIndexMask} (f32)", + 4 => $"${(uint)regnum & WasmRegIndexMask} (f64)", + 5 => $"${(uint)regnum & WasmRegIndexMask} (v128)", + 6 => $"${(uint)regnum & WasmRegIndexMask} (exnref)", + _ => $"Unknown '{regnum}'", + }, + }; + } + private void EnsureInitialized() { if (_boundsList != null) From 8194fec029588b785fbd7fb4175cdc9fac4bb192 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 01:10:07 -0500 Subject: [PATCH 07/14] [wasm] Exclude portable entry pointer from debug variables The end-to-end variable-debug-info validation exposed the hidden wasm portable-entry-pointer argument as a source local. The argument is appended after user arguments, but unlike the wasm stack-pointer argument it was not recorded or excluded by compMap2ILvarNum. AddDoubles therefore reported the hidden i32 argument as source local 0, alongside the real f64 parameters. Record the argument's local number when it is created, map it to UNKNOWN_ILNUM, and account for it when mapping later internal locals back to IL variable numbers. Replace the count-only wasm R2R checks with complete exact records for the AddDoubles parameters and SumWithFinally local: variable identity, native range, location kind, packed wasm local, and frame-pointer-relative offset. Mutate the local-index bits of one packed register and prove the exact oracle rejects the corrupted record. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/compiler.h | 3 +- src/coreclr/jit/lclvars.cpp | 16 +- .../TestCases/R2RTestSuites.cs | 142 +++++++++++++++++- 3 files changed, 149 insertions(+), 12 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 5e0dbba6988618..cee15dcf93e6b1 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -4386,7 +4386,8 @@ class Compiler #endif // TARGET_X86 #if defined(TARGET_WASM) - unsigned lvaWasmSpArg = BAD_VAR_NUM; // lcl var index of Wasm stack pointer arg + unsigned lvaWasmSpArg = BAD_VAR_NUM; // lcl var index of Wasm stack pointer arg + unsigned lvaWasmPortableEntryPtrArg = BAD_VAR_NUM; // lcl var index of Wasm portable entry point arg unsigned lvaWasmVirtualIP = BAD_VAR_NUM; // Wasm virtual IP slot unsigned lvaWasmFunctionIndex = BAD_VAR_NUM; // Wasm function index slot unsigned lvaWasmResumeIP = BAD_VAR_NUM; // Wasm catch resumption IP slot diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index 87a2f95eba7cc5..d3a91e7ee73895 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -556,10 +556,11 @@ void Compiler::lvaInitWasmPortableEntryPtr(unsigned* curVarNum) { if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_PORTABLE_ENTRY_POINTS)) { - LclVarDsc* varDsc = lvaGetDesc(*curVarNum); - varDsc->lvType = TYP_I_IMPL; - varDsc->lvIsParam = 1; - varDsc->lvOnFrame = true; + LclVarDsc* varDsc = lvaGetDesc(*curVarNum); + varDsc->lvType = TYP_I_IMPL; + varDsc->lvIsParam = 1; + varDsc->lvOnFrame = true; + lvaWasmPortableEntryPtrArg = *curVarNum; (*curVarNum)++; } } @@ -1276,7 +1277,7 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const } #if defined(TARGET_WASM) - if (varNum == lvaWasmSpArg) + if ((varNum == lvaWasmSpArg) || (varNum == lvaWasmPortableEntryPtrArg)) { return (unsigned)ICorDebugInfo::UNKNOWN_ILNUM; } @@ -1314,6 +1315,11 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const { varNum--; } + + if ((lvaWasmPortableEntryPtrArg != BAD_VAR_NUM) && (originalVarNum > lvaWasmPortableEntryPtrArg)) + { + varNum--; + } #endif if (varNum >= info.compLocalsCount) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 85f46fb4fa0acd..275de8e98f2d37 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -134,20 +134,129 @@ static void Validate(ReadyToRunReader reader) .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) .ToList(); + Assert.Equal(4, doubleVariables.Count); Assert.DoesNotContain(doubleVariables, variable => variable.VariableLocation.VarLocType is VarLocType.VLT_REG_FP or VarLocType.VLT_FPSTK); - Assert.Contains(doubleVariables, variable => - variable.VariableLocation.VarLocType == VarLocType.VLT_REG && - (uint)variable.VariableLocation.Data1 >> WasmRegTypeShift == F64WasmValueType); + NativeVarInfo leftInLocal = Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x22, + locationType: VarLocType.VLT_REG, + data1: unchecked((int)0x80000001), + data2: 0, + data3: 0)); + Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x22, + endOffset: 0x34, + locationType: VarLocType.VLT_STK2, + data1: 2, + data2: 0x18, + data3: 0)); + Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x0, + endOffset: 0x22, + locationType: VarLocType.VLT_REG, + data1: unchecked((int)0x80000002), + data2: 0, + data3: 0)); + Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x22, + endOffset: 0x34, + locationType: VarLocType.VLT_STK2, + data1: 2, + data2: 0x10, + data3: 0)); + Assert.All( + doubleVariables.Where(variable => variable.VariableLocation.VarLocType == VarLocType.VLT_REG), + variable => Assert.Equal(F64WasmValueType, (uint)variable.VariableLocation.Data1 >> WasmRegTypeShift)); + Assert.DoesNotContain(doubleVariables, variable => variable.Variable.Type == VariableType.Local); + + // Prove the exact assertion is sensitive to a garbled decoder rather than merely + // counting entries. Mutate the local-index bits in the packed register and verify the + // known-good record no longer matches. + NativeVarInfo mutatedLeft = leftInLocal; + VarLoc mutatedLocation = mutatedLeft.VariableLocation; + mutatedLocation.Data1++; + mutatedLeft.VariableLocation = mutatedLocation; + Assert.NotEqual(leftInLocal.VariableLocation.Data1, mutatedLeft.VariableLocation.Data1); + Assert.False(IsExactVariable( + mutatedLeft, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x22, + locationType: VarLocType.VLT_REG, + data1: unchecked((int)0x80000001), + data2: 0, + data3: 0)); // Reads static data, so the JIT materializes the image base via a well-known-global global.get. Assert.True(methods.Exists(method => method.SignatureString.Contains("SumStaticData", StringComparison.Ordinal))); // Has a try/finally, so the JIT materializes the table base via a well-known-global global.get. ReadyToRunMethod sumWithFinally = Assert.Single(methods, method => method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal)); - Assert.Contains(sumWithFinally.RuntimeFunctions, runtimeFunction => - runtimeFunction.DebugInfo is not null && - runtimeFunction.DebugInfo.VariablesList.Exists(variable => variable.Variable.Type == VariableType.Local)); + RuntimeFunction sumWithFinallyRoot = Assert.Single( + sumWithFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.DebugInfo is not null); + Assert.Equal(3, sumWithFinallyRoot.DebugInfo!.VariablesList.Count); + Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x3B, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1F2, + locationType: VarLocType.VLT_STK, + data1: 2, + data2: 0x2C, + data3: 0)); + Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1F2, + locationType: VarLocType.VLT_STK, + data1: 2, + data2: 0x24, + data3: 0)); + Assert.Single(sumWithFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); // Has a catch clause, so the JIT emits a try_table catch_ref that references the // imported restore-context exception tag. Assert.True(methods.Exists(method => @@ -155,6 +264,27 @@ runtimeFunction.DebugInfo is not null && Assert.True(WasmR2RAssert.WasmIndexSpacesHaveExpectedEntries(webcilReader, out string indexDiagnostic), indexDiagnostic); + static bool IsExactVariable( + NativeVarInfo variable, + uint variableNumber, + VariableType variableType, + int variableIndex, + uint startOffset, + uint endOffset, + VarLocType locationType, + int data1, + int data2, + int data3) + => variable.VariableNumber == variableNumber + && variable.Variable.Type == variableType + && variable.Variable.Index == variableIndex + && variable.StartOffset == startOffset + && variable.EndOffset == endOffset + && variable.VariableLocation.VarLocType == locationType + && variable.VariableLocation.Data1 == data1 + && variable.VariableLocation.Data2 == data2 + && variable.VariableLocation.Data3 == data3; + // The wasm JIT references the ABI well-known globals via maximally padded WASM_GLOBAL_INDEX_LEB // relocations that the R2R object writer must self-resolve to the fixed global // indices and shrink down to their minimal size. Verify the emitted code contains a correctly self-resolved 'global.get' for the From a1e6820b5d074badc59e71903759581c904578b5 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 12:45:40 -0500 Subject: [PATCH 08/14] [wasm] Centralize variable debug register encoding constants Define the packed WASM debug-register bit layout in ICorDebugInfo and have the JIT derive its register masks from that shared encoding contract. Assert that the JIT register representation and WasmValueType count remain compatible with the debug-info format. Document that the static ReadyToRun reader's compiled-in shift must move with a versioned R2R debug-info format change, since it has no live target descriptor from which to discover a different layout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/cordebuginfo.h | 9 +++++++++ src/coreclr/jit/registeropswasm.cpp | 6 ++++-- .../aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/coreclr/inc/cordebuginfo.h b/src/coreclr/inc/cordebuginfo.h index bc0d5cc5c9a69f..eb867c9711ddc8 100644 --- a/src/coreclr/inc/cordebuginfo.h +++ b/src/coreclr/inc/cordebuginfo.h @@ -11,6 +11,15 @@ class ICorDebugInfo { public: +#ifdef TARGET_WASM + // WASM variable locations encode a JIT-local index and a JIT WasmValueType in the 32-bit + // RegNum payload. These constants are part of the debug-info encoding consumed by cDAC and + // ILCompiler.Reflection.ReadyToRun. + static constexpr uint32_t WASM_REG_TYPE_BITS = 3; + static constexpr uint32_t WASM_REG_TYPE_SHIFT = 32 - WASM_REG_TYPE_BITS; + static constexpr uint32_t WASM_VALUE_TYPE_COUNT = 7; +#endif // TARGET_WASM + /*----------------------------- Boundary-info ---------------------------*/ enum MappingTypes diff --git a/src/coreclr/jit/registeropswasm.cpp b/src/coreclr/jit/registeropswasm.cpp index 112520dd9928b1..e352a084db8003 100644 --- a/src/coreclr/jit/registeropswasm.cpp +++ b/src/coreclr/jit/registeropswasm.cpp @@ -7,12 +7,14 @@ #endif using RegNumUnderlyingType = regNumberSmall; -static const RegNumUnderlyingType WASM_REG_TYPE_BITS = 3; -static const RegNumUnderlyingType WASM_REG_TYPE_SHIFT = 8 * sizeof(RegNumUnderlyingType) - WASM_REG_TYPE_BITS; +static const RegNumUnderlyingType WASM_REG_TYPE_BITS = ICorDebugInfo::WASM_REG_TYPE_BITS; +static const RegNumUnderlyingType WASM_REG_TYPE_SHIFT = ICorDebugInfo::WASM_REG_TYPE_SHIFT; static const RegNumUnderlyingType WASM_REG_TYPE_MASK = ~0u << WASM_REG_TYPE_SHIFT; static const unsigned WASM_LOCAL_INDEX_LIMIT = WASM_REG_TYPE_MASK; static_assert(sizeof(RegNumUnderlyingType) >= sizeof(unsigned)); +static_assert(WASM_REG_TYPE_SHIFT == (8 * sizeof(RegNumUnderlyingType) - WASM_REG_TYPE_BITS)); +static_assert(static_cast(WasmValueType::Count) == ICorDebugInfo::WASM_VALUE_TYPE_COUNT); static_assert(((static_cast(WasmValueType::Count) - 1) >> WASM_REG_TYPE_BITS) == 0); //------------------------------------------------------------------------ diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs index 768731c4303b63..bab9e18b8aab47 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs @@ -95,6 +95,9 @@ public static string GetPlatformSpecificRegister(Machine machine, int regnum) private static string GetWasmRegister(int regnum) { + // Keep in sync with ICorDebugInfo::WASM_REG_TYPE_SHIFT in cordebuginfo.h. A future + // width change requires a versioned ReadyToRun debug-info format change because this + // static image reader has no live target descriptor from which to discover it. const int WasmRegTypeShift = 29; const uint WasmRegIndexMask = (1u << WasmRegTypeShift) - 1; From 6e6fc95a0c20eaa780ababacb8161c804ac02aef Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 13:45:16 -0500 Subject: [PATCH 09/14] [wasm] Advertise variable debug register encoding Publish the shared WASM register type shift and value-type count through the target data descriptor so version-skewed readers can reject incompatible variable debug information. Document the producer-owned encoding and extend the static ReadyToRun reader coverage for reserved and unsupported value-type codes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 36 +++++++++++++++++++ .../TestCases/R2RTestSuites.cs | 2 ++ .../vm/datadescriptor/datadescriptor.inc | 2 ++ 3 files changed, 40 insertions(+) diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index cf83e203cac90d..c3c40da8c5e833 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -223,6 +223,42 @@ Each variable entry in the Vars section is nibble-encoded as follows: Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`. +### WebAssembly Variable Register Encoding + +WASM has no physical registers. RyuJIT packs a `(local index, debug value type)` tuple into the +32-bit `regNumber` payload, and that packed value appears in every register field of the Vars +stream: + +```text +packedRegister = localIndex | ((uint)debugValueType << WasmDebugRegisterTypeShift) +``` + +The encoding uses the following values: + +| Value type | Encoded value | +| --- | --- | +| `Invalid` | `0` | +| `I32` | `1` | +| `I64` | `2` | +| `F32` | `3` | +| `F64` | `4` | +| `V128` | `5` | +| `ExnRef` | `6` | + +The target advertises `WasmDebugRegisterTypeShift` and `WasmDebugValueTypeCount` as `uint8` +numeric data descriptor globals. These values define how to separate the local index from the +debug value type. A reader must reject an unsupported or missing encoding rather than fall back +to a compiled-in shift and plausibly decode the wrong local or type. + +Debug value type `0` is reserved so that small raw values remain available for pseudo-registers +such as `REGNUM_AMBIENT_SP`. A packed value whose value type is `0` or greater than or equal to +`WasmDebugValueTypeCount` does not name a local. + +`WasmDebugValueTypeCount` is JIT debug-encoding vocabulary, not the complete WebAssembly +specification type set. Managed references currently use the JIT's machine `I32`/`I64` +representation; the encoding does not independently identify a managed GC reference. A future +bit-width or value-count change requires a format-aware, versioned reader update. + ### Async Suspension Point APIs We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 275de8e98f2d37..a4f991ccf85422 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -559,12 +559,14 @@ static void Validate(ReadyToRunReader reader) [Theory] [InlineData(2, "ambient SP")] + [InlineData(3, "Unknown '3'")] [InlineData(0x20000001, "$1 (i32)")] [InlineData(0x40000002, "$2 (i64)")] [InlineData(0x60000003, "$3 (f32)")] [InlineData(unchecked((int)0x80000004), "$4 (f64)")] [InlineData(unchecked((int)0xA0000005), "$5 (v128)")] [InlineData(unchecked((int)0xC0000006), "$6 (exnref)")] + [InlineData(unchecked((int)0xE0000001), "Unknown '-536870911'")] public void WasmDebugRegisterIsDecoded(int register, string expected) { Assert.Equal(expected, DebugInfo.GetPlatformSpecificRegister(WasmMachine.Wasm32, register)); diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 1a3f8c17921666..a70aecea52b55d 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1699,6 +1699,8 @@ CDAC_GLOBAL_STRING(Architecture, loongarch64) CDAC_GLOBAL_STRING(Architecture, riscv64) #elif defined(TARGET_WASM) CDAC_GLOBAL_STRING(Architecture, wasm) +CDAC_GLOBAL(WasmDebugRegisterTypeShift, T_UINT8, ICorDebugInfo::WASM_REG_TYPE_SHIFT) +CDAC_GLOBAL(WasmDebugValueTypeCount, T_UINT8, ICorDebugInfo::WASM_VALUE_TYPE_COUNT) #else #error TARGET_{ARCH} define is not recognized by the cDAC. Update this switch and the enum values in IRuntimeInfo.cs #endif From 4d5cb3919cd2d6a4c63f12f735c63e7621ea2696 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 15:13:49 -0500 Subject: [PATCH 10/14] [wasm] Validate frame-resident GC variable debug info Add a no-opt object local that remains live across a GC call in a finally funclet. Assert its IL class type, complete ReadyToRun variable tuples, frame-relative GC slot, and safepoint coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 90 ++++++++++++++++++- .../TestCases/Webcil/WasmWebcilModule.cs | 33 +++++++ 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index a4f991ccf85422..9b4b9dbed37920 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -130,6 +130,7 @@ static void Validate(ReadyToRunReader reader) method.SignatureString.Contains("AddDoubles", StringComparison.Ordinal)); const int WasmRegTypeShift = 29; const uint F64WasmValueType = 4; + const int WasmEncodedFrameBase = 2; List doubleVariables = addDoubles.RuntimeFunctions .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) @@ -158,7 +159,7 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x22, endOffset: 0x34, locationType: VarLocType.VLT_STK2, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x18, data3: 0)); Assert.Single(doubleVariables, variable => @@ -182,7 +183,7 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x22, endOffset: 0x34, locationType: VarLocType.VLT_STK2, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x10, data3: 0)); Assert.All( @@ -240,7 +241,7 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x3B, endOffset: 0x1F2, locationType: VarLocType.VLT_STK, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x2C, data3: 0)); Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => @@ -252,11 +253,92 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x3B, endOffset: 0x1F2, locationType: VarLocType.VLT_STK, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x24, data3: 0)); Assert.Single(sumWithFinally.RuntimeFunctions, runtimeFunction => runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + ReadyToRunMethod gcLocalAcrossFinally = Assert.Single(methods, method => + method.SignatureString.Contains("GcLocalAcrossFinally", StringComparison.Ordinal)); + Assert.Equal( + new[] { "Webcil.WasmWebcilModule+GcMarker", "int" }, + gcLocalAcrossFinally.LocalSignature); + RuntimeFunction gcLocalRoot = Assert.Single( + gcLocalAcrossFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.DebugInfo is not null); + Assert.Equal(4, gcLocalRoot.DebugInfo!.VariablesList.Count); + Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x3B, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1B6, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x2C, + data3: 0)); + NativeVarInfo gcMarker = Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1B6, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x24, + data3: 0)); + Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 1, + startOffset: 0x3B, + endOffset: 0x1B6, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x20, + data3: 0)); + ILCompiler.Reflection.ReadyToRun.Amd64.GcInfo gcInfo = + Assert.IsType(gcLocalAcrossFinally.GcInfo); + Assert.Equal(2u, gcInfo.SlotTable.NumSlots); + Assert.Equal(0u, gcInfo.SlotTable.NumRegisters); + Assert.Equal(0u, gcInfo.SlotTable.NumStackSlots); + Assert.Equal(2u, gcInfo.SlotTable.NumUntracked); + ILCompiler.Reflection.ReadyToRun.Amd64.GcSlotTable.GcSlot markerGcSlot = Assert.Single( + gcInfo.SlotTable.GcSlots, + slot => slot.StackSlot?.SpOffset == gcMarker.VariableLocation.Data2); + Assert.Equal(GcStackSlotBase.GC_FRAMEREG_REL, markerGcSlot.StackSlot.Base); + Assert.Equal(GcSlotFlags.GC_SLOT_PINNED | GcSlotFlags.GC_SLOT_UNTRACKED, markerGcSlot.Flags); + Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + + // Wasm frame homes are addressed through the producer's logical frame pointer. The + // current wire encoding collapses REG_FPBASE, REG_SPBASE, and REGNUM_AMBIENT_SP to 2, + // so the consumer must distinguish FP from SP using the live frame context. + DebugInfoBoundsEntry collectCall = Assert.Single( + gcLocalRoot.DebugInfo.BoundsList, + bound => bound.ILOffset == 0x10); + Assert.Equal(0x161u, collectCall.NativeOffset); + Assert.Equal(SourceTypes.StackEmpty, collectCall.SourceTypes); + Assert.True(gcMarker.StartOffset <= collectCall.NativeOffset); + Assert.True(collectCall.NativeOffset < gcMarker.EndOffset); // Has a catch clause, so the JIT emits a try_table catch_ref that references the // imported restore-context exception tag. Assert.True(methods.Exists(method => diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs index 204b026268da07..aa25f80544c4a9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs @@ -11,6 +11,16 @@ public static class WasmWebcilModule private static readonly int[] s_primes = new int[] { 3, 5, 7, 11, 13 }; private static int s_counter; + private sealed class GcMarker + { + public readonly int Value; + + public GcMarker(int value) + { + Value = value; + } + } + [MethodImpl(MethodImplOptions.NoInlining)] public static int AddIntegers(int left, int right) { @@ -55,6 +65,29 @@ public static int SumWithFinally(int index) return total + s_counter; } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static int GcLocalAcrossFinally(int value) + { + GcMarker marker = new(value); + try + { + return marker.Value; + } + finally + { + // Keep the newly allocated object live in the parent frame across an actual GC + // reached through the finally funclet. + CollectAtGcSafepoint(); + GC.KeepAlive(marker); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void CollectAtGcSafepoint() + { + GC.Collect(); + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static int CatchException(int value) { From a7a349ecf4dc27b4d4ff1472f270d483c2eee38b Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 16:43:57 -0500 Subject: [PATCH 11/14] [wasm] Validate optimized and frame variable records Add exact optimized tracked-variable coverage, frame-base ABI variations including localloc and funclets, and same-type GC slot identity with a legitimate null reference. Pin the current stack VarLoc base encoding and document that absolute frame reconstruction is independent of unstable wasm local indices. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 25 + src/coreclr/jit/scopeinfo.cpp | 8 + .../TestCases/R2RTestSuites.cs | 470 +++++++++++++++++- .../TestCases/Webcil/WasmWebcilModule.cs | 77 +++ 4 files changed, 573 insertions(+), 7 deletions(-) diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index c3c40da8c5e833..f079e1d7e084a3 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -259,6 +259,31 @@ specification type set. Managed references currently use the JIT's machine `I32` representation; the encoding does not independently identify a managed GC reference. A future bit-width or value-count change requires a format-aware, versioned reader update. +### WebAssembly Stack Base Encoding + +WASM `VLT_STK` and `VLT_STK2` records currently encode base register `2`. +`REG_FPBASE`, `REG_SPBASE`, and `REGNUM_AMBIENT_SP` all have that value on this target, so the +debug record identifies a logical frame-relative stack home; it does not identify a particular +WebAssembly engine local. + +The absolute logical frame address is reconstructed by the runtime stack-walk and unwind +protocol from shadow-stack linear memory. The engine's current per-function SP/FP local allocation +is a separate code-generation detail: + +* Frame access allocates an FP value when a method has frame locals, uses `localloc`, or has + funclets. +* Without `localloc`, the root function's FP aliases its SP, including methods that make calls. +* `localloc` gives the root a distinct FP so later SP movement does not change frame-relative + addresses. +* Funclets receive a distinct parent establishing FP; with `localloc`, that remains the root's + pre-adjustment frame base. + +The numeric WebAssembly local indices holding those values can vary with function parameters and +compiler-created locals and are not part of this debug-info format. Readers must not infer the +logical frame address from a hardcoded `$varN`, and the producer does not advertise SP/FP engine +local indices. A future producer that changes stack records away from base `2` requires a +coordinated, fail-loud reader update. + ### Async Suspension Point APIs We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list. diff --git a/src/coreclr/jit/scopeinfo.cpp b/src/coreclr/jit/scopeinfo.cpp index ae7ee24d155f4d..dad46b01f0ca55 100644 --- a/src/coreclr/jit/scopeinfo.cpp +++ b/src/coreclr/jit/scopeinfo.cpp @@ -57,6 +57,14 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX #include "emit.h" #include "codegen.h" +#if defined(TARGET_WASM) +// Stack VarLoc records use the target register-number convention, not packed wasm local indices. +static_assert(REG_FPBASE == REG_SPBASE); +static_assert(REG_FPBASE == REG_NA); +static_assert(static_cast(REG_NA) == static_cast(ICorDebugInfo::REGNUM_AMBIENT_SP)); +static_assert(static_cast(ICorDebugInfo::REGNUM_AMBIENT_SP) == 2); +#endif // defined(TARGET_WASM) + //============================================================================ // siVarLoc functions //============================================================================ diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 9b4b9dbed37920..9cd397b4c0c075 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using ILCompiler.ObjectWriter; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; @@ -119,6 +120,16 @@ public void WasmWebcilModule() static void Validate(ReadyToRunReader reader) { + const byte WasmI32 = 0x7F; + const byte WasmLocalGet = 0x20; + const byte WasmLocalSet = 0x21; + const byte WasmLocalTee = 0x22; + const byte WasmI32Load = 0x28; + const byte WasmI32Const = 0x41; + const byte WasmI32Sub = 0x6B; + const byte WasmI64 = 0x7E; + const int WasmEncodedFrameBase = 2; + var webcilReader = Assert.IsType(reader.CompositeReader); Assert.True(webcilReader.IsWasmWrapped); Assert.Equal(WasmMachine.Wasm32, reader.Machine); @@ -126,11 +137,184 @@ static void Validate(ReadyToRunReader reader) List methods = R2RAssert.GetAllMethods(reader); Assert.True(methods.Exists(method => method.SignatureString.Contains("AddIntegers", StringComparison.Ordinal))); + ReadyToRunMethod optimizedTrackedVariables = Assert.Single(methods, method => + method.SignatureString.Contains("OptimizedTrackedVariables", StringComparison.Ordinal)); + Assert.Equal(new[] { "int", "int" }, optimizedTrackedVariables.LocalSignature); + RuntimeFunction optimizedRoot = Assert.Single(optimizedTrackedVariables.RuntimeFunctions); + Assert.Equal(5, optimizedRoot.DebugInfo!.VariablesList.Count); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x3C, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x0, + endOffset: 0x3C, + locationType: VarLocType.VLT_REG, + data1: 0x20000002, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x59, + endOffset: 0x74, + locationType: VarLocType.VLT_REG, + data1: 0x20000002, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x2F, + endOffset: 0x51, + locationType: VarLocType.VLT_REG, + data1: 0x20000004, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x59, + endOffset: 0x74, + locationType: VarLocType.VLT_REG, + data1: 0x20000004, + data2: 0, + data3: 0)); + Assert.DoesNotContain( + optimizedRoot.DebugInfo.VariablesList, + variable => variable.Variable.Type == VariableType.Local && variable.Variable.Index == 1); + WebcilImageReader.WasmFunctionInfo optimizedBody = ResolveWasmBody(reader, webcilReader, optimizedRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32, WasmI32 }, optimizedBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (3, WasmI32) }, optimizedBody.Locals); + AssertWasmInstructionPrefix( + optimizedBody, + [WasmLocalGet, 0, WasmI32Const, 0x10, WasmI32Sub, WasmLocalTee, 0]); + ReadyToRunMethod leafFrameLocal = Assert.Single(methods, method => + method.SignatureString.Contains("LeafFrameLocal", StringComparison.Ordinal)); + Assert.Equal(new[] { "int" }, leafFrameLocal.LocalSignature); + RuntimeFunction leafRoot = Assert.Single(leafFrameLocal.RuntimeFunctions); + Assert.Equal(3, leafRoot.DebugInfo!.VariablesList.Count); + Assert.Single(leafRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x27, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(leafRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x27, + endOffset: 0x43, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x1C, + data3: 0)); + Assert.Single(leafRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x27, + endOffset: 0x43, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x14, + data3: 0)); + WebcilImageReader.WasmFunctionInfo leafBody = ResolveWasmBody(reader, webcilReader, leafRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, leafBody.ParamTypes); + Assert.Empty(leafBody.Locals); + AssertWasmContainsInstructions( + leafBody, + [WasmLocalGet, 0, WasmI32Load, 0, 0x14]); + ReadyToRunMethod locallocFrameLocal = Assert.Single(methods, method => + method.SignatureString.Contains("LocallocFrameLocal", StringComparison.Ordinal)); + Assert.Equal(new[] { "int" }, locallocFrameLocal.LocalSignature); + RuntimeFunction locallocRoot = Assert.Single(locallocFrameLocal.RuntimeFunctions); + Assert.Equal(3, locallocRoot.DebugInfo!.VariablesList.Count); + Assert.Single(locallocRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x41, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(locallocRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x13F, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x1C, + data3: 0)); + Assert.Single(locallocRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x13F, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x14, + data3: 0)); + WebcilImageReader.WasmFunctionInfo locallocRootBody = + ResolveWasmBody(reader, webcilReader, locallocRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, locallocRootBody.ParamTypes); + Assert.Equal( + new (uint Count, byte ValType)[] { (3, WasmI32), (1, WasmI64) }, + locallocRootBody.Locals); + AssertWasmContainsInstructions( + locallocRootBody, + [WasmLocalGet, 0, WasmLocalSet, 3]); + AssertWasmContainsInstructions( + locallocRootBody, + [WasmLocalGet, 3, WasmI32Load, 0, 0x1C]); ReadyToRunMethod addDoubles = Assert.Single(methods, method => method.SignatureString.Contains("AddDoubles", StringComparison.Ordinal)); const int WasmRegTypeShift = 29; const uint F64WasmValueType = 4; - const int WasmEncodedFrameBase = 2; List doubleVariables = addDoubles.RuntimeFunctions .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) @@ -211,8 +395,30 @@ static void Validate(ReadyToRunReader reader) data2: 0, data3: 0)); // Reads static data, so the JIT materializes the image base via a well-known-global global.get. - Assert.True(methods.Exists(method => - method.SignatureString.Contains("SumStaticData", StringComparison.Ordinal))); + ReadyToRunMethod sumStaticData = Assert.Single(methods, method => + method.SignatureString.Contains("SumStaticData", StringComparison.Ordinal)); + RuntimeFunction sumStaticRoot = Assert.Single(sumStaticData.RuntimeFunctions); + Assert.Single(sumStaticRoot.DebugInfo!.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0xC1, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + WebcilImageReader.WasmFunctionInfo sumStaticBody = ResolveWasmBody(reader, webcilReader, sumStaticRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, sumStaticBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (7, WasmI32) }, sumStaticBody.Locals); + AssertWasmInstructionPrefix( + sumStaticBody, + [WasmLocalGet, 0, WasmI32Const, 0x20, WasmI32Sub, WasmLocalTee, 0]); + AssertWasmContainsInstructions( + sumStaticBody, + [WasmLocalGet, 0, WasmI32Load, 0, 0x14]); // Has a try/finally, so the JIT materializes the table base via a well-known-global global.get. ReadyToRunMethod sumWithFinally = Assert.Single(methods, method => method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal)); @@ -326,12 +532,12 @@ static void Validate(ReadyToRunReader reader) slot => slot.StackSlot?.SpOffset == gcMarker.VariableLocation.Data2); Assert.Equal(GcStackSlotBase.GC_FRAMEREG_REL, markerGcSlot.StackSlot.Base); Assert.Equal(GcSlotFlags.GC_SLOT_PINNED | GcSlotFlags.GC_SLOT_UNTRACKED, markerGcSlot.Flags); - Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => + RuntimeFunction gcLocalFunclet = Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); - // Wasm frame homes are addressed through the producer's logical frame pointer. The - // current wire encoding collapses REG_FPBASE, REG_SPBASE, and REGNUM_AMBIENT_SP to 2, - // so the consumer must distinguish FP from SP using the live frame context. + // The target reconstructs the absolute logical frame from shadow-stack memory and + // unwind data. The debug-info wire encoding separately collapses REG_FPBASE, + // REG_SPBASE, and REGNUM_AMBIENT_SP to 2; it does not name a V8 local. DebugInfoBoundsEntry collectCall = Assert.Single( gcLocalRoot.DebugInfo.BoundsList, bound => bound.ILOffset == 0x10); @@ -339,6 +545,210 @@ static void Validate(ReadyToRunReader reader) Assert.Equal(SourceTypes.StackEmpty, collectCall.SourceTypes); Assert.True(gcMarker.StartOffset <= collectCall.NativeOffset); Assert.True(collectCall.NativeOffset < gcMarker.EndOffset); + WebcilImageReader.WasmFunctionInfo gcLocalRootBody = + ResolveWasmBody(reader, webcilReader, gcLocalRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, gcLocalRootBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (2, WasmI32) }, gcLocalRootBody.Locals); + AssertWasmContainsInstructions( + gcLocalRootBody, + [WasmLocalGet, 0, WasmI32Load, 0, 0x24]); + WebcilImageReader.WasmFunctionInfo gcLocalFuncletBody = + ResolveWasmBody(reader, webcilReader, gcLocalFunclet); + Assert.Equal(new byte[] { WasmI32, WasmI32 }, gcLocalFuncletBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (1, WasmI32) }, gcLocalFuncletBody.Locals); + AssertWasmInstructionPrefix( + gcLocalFuncletBody, + [WasmLocalGet, 0, WasmI32Const, 0x10, WasmI32Sub, WasmLocalSet, 0]); + AssertWasmContainsInstructions( + gcLocalFuncletBody, + [WasmLocalGet, 1, WasmI32Load, 0, 0x24]); + ReadyToRunMethod locallocAcrossFinally = Assert.Single(methods, method => + method.SignatureString.Contains("LocallocAcrossFinally", StringComparison.Ordinal)); + Assert.Equal(new[] { "int", "int*", "int" }, locallocAcrossFinally.LocalSignature); + RuntimeFunction locallocFinallyRoot = Assert.Single( + locallocAcrossFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.DebugInfo is not null); + Assert.Equal(5, locallocFinallyRoot.DebugInfo!.VariablesList.Count); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x41, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x2C, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x24, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 1, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x20, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 3, + variableType: VariableType.Local, + variableIndex: 2, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x1C, + data3: 0)); + RuntimeFunction locallocFinallyFunclet = Assert.Single( + locallocAcrossFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + WebcilImageReader.WasmFunctionInfo locallocFinallyRootBody = + ResolveWasmBody(reader, webcilReader, locallocFinallyRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, locallocFinallyRootBody.ParamTypes); + Assert.Equal( + new (uint Count, byte ValType)[] { (3, WasmI32), (1, WasmI64) }, + locallocFinallyRootBody.Locals); + AssertWasmContainsInstructions( + locallocFinallyRootBody, + [WasmLocalGet, 0, WasmLocalSet, 3]); + AssertWasmContainsInstructions( + locallocFinallyRootBody, + [WasmLocalGet, 3, WasmI32Load, 0, 0x2C]); + WebcilImageReader.WasmFunctionInfo locallocFinallyFuncletBody = + ResolveWasmBody(reader, webcilReader, locallocFinallyFunclet); + Assert.Equal(new byte[] { WasmI32, WasmI32 }, locallocFinallyFuncletBody.ParamTypes); + Assert.Equal( + new (uint Count, byte ValType)[] { (2, WasmI32), (1, WasmI64) }, + locallocFinallyFuncletBody.Locals); + AssertWasmContainsInstructions( + locallocFinallyFuncletBody, + [WasmLocalGet, 1, WasmI32Load, 0, 0x18]); + ReadyToRunMethod gcSlotIdentity = Assert.Single(methods, method => + method.SignatureString.Contains("GcSlotIdentity", StringComparison.Ordinal)); + Assert.Equal( + new[] + { + "Webcil.WasmWebcilModule+GcMarker", + "Webcil.WasmWebcilModule+GcMarker", + "Webcil.WasmWebcilModule+GcMarker", + "int", + }, + gcSlotIdentity.LocalSignature); + byte[] gcSlotIdentityIL = GetMethodILBytes(gcSlotIdentity); + Assert.Equal(0x57, gcSlotIdentityIL.Length); + Assert.Equal(0x1F, gcSlotIdentityIL[0x00]); + Assert.Equal(17, gcSlotIdentityIL[0x01]); + Assert.Equal(0x0A, gcSlotIdentityIL[0x07]); + Assert.Equal(0x1F, gcSlotIdentityIL[0x12]); + Assert.Equal(29, gcSlotIdentityIL[0x13]); + Assert.Equal(0x0B, gcSlotIdentityIL[0x19]); + Assert.Equal(0x14, gcSlotIdentityIL[0x24]); + Assert.Equal(0x0C, gcSlotIdentityIL[0x25]); + RuntimeFunction gcSlotRoot = Assert.Single(gcSlotIdentity.RuntimeFunctions); + Assert.Equal(4, gcSlotRoot.DebugInfo!.VariablesList.Count); + // The minopts wasm scope latch opens each synthesized source scope once and closes it + // at method end, so these distinct IL locals currently share one exact native range. + NativeVarInfo firstMarker = Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x48, + data3: 0)); + NativeVarInfo secondMarker = Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 1, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x44, + data3: 0)); + NativeVarInfo nullMarker = Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 2, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x40, + data3: 0)); + Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 3, + variableType: VariableType.Local, + variableIndex: 3, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x3C, + data3: 0)); + Assert.NotEqual(firstMarker.VariableLocation.Data2, secondMarker.VariableLocation.Data2); + ILCompiler.Reflection.ReadyToRun.Amd64.GcInfo slotIdentityGcInfo = + Assert.IsType(gcSlotIdentity.GcInfo); + Assert.Equal(6u, slotIdentityGcInfo.SlotTable.NumSlots); + foreach (NativeVarInfo marker in new[] { firstMarker, secondMarker, nullMarker }) + { + ILCompiler.Reflection.ReadyToRun.Amd64.GcSlotTable.GcSlot slot = Assert.Single( + slotIdentityGcInfo.SlotTable.GcSlots, + candidate => candidate.StackSlot?.SpOffset == marker.VariableLocation.Data2); + Assert.Equal(GcStackSlotBase.GC_FRAMEREG_REL, slot.StackSlot.Base); + Assert.Equal(GcSlotFlags.GC_SLOT_PINNED | GcSlotFlags.GC_SLOT_UNTRACKED, slot.Flags); + } + DebugInfoBoundsEntry slotIdentityGcCall = Assert.Single( + gcSlotRoot.DebugInfo.BoundsList, + bound => bound.ILOffset == 0x2D); + Assert.Equal(0x18Cu, slotIdentityGcCall.NativeOffset); + Assert.All( + new[] { firstMarker, secondMarker, nullMarker }, + marker => + { + Assert.True(marker.StartOffset <= slotIdentityGcCall.NativeOffset); + Assert.True(slotIdentityGcCall.NativeOffset < marker.EndOffset); + }); // Has a catch clause, so the JIT emits a try_table catch_ref that references the // imported restore-context exception tag. Assert.True(methods.Exists(method => @@ -367,6 +777,52 @@ static bool IsExactVariable( && variable.VariableLocation.Data2 == data2 && variable.VariableLocation.Data3 == data3; + static WebcilImageReader.WasmFunctionInfo ResolveWasmBody( + ReadyToRunReader reader, + WebcilImageReader webcilReader, + RuntimeFunction runtimeFunction) + { + uint tableIndex = checked(reader.WasmMinFunctionTableIndex + (uint)runtimeFunction.Id); + int functionIndex = webcilReader.GetFunctionIndexFromTableIndex(tableIndex); + Assert.True(functionIndex >= 0, $"Could not resolve wasm table index {tableIndex} to a function body."); + WebcilImageReader.WasmFunctionInfo? body = webcilReader.GetWasmFunctionBody(functionIndex); + Assert.True(body is not null, $"Wasm function body {functionIndex} was not found."); + return body.Value; + } + + static byte[] GetMethodILBytes(ReadyToRunMethod method) + { + MethodDefinition methodDefinition = + method.ComponentReader.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)method.MethodHandle); + byte[]? ilBytes = null; + method.ComponentReader.GetSectionData( + methodDefinition.RelativeVirtualAddress, + sectionData => ilBytes = MethodBodyBlock.Create(sectionData).GetILBytes()); + return Assert.IsType(ilBytes); + } + + static void AssertWasmInstructionPrefix( + WebcilImageReader.WasmFunctionInfo body, + ReadOnlySpan expected) + { + ReadOnlySpan instructions = + body.Image.AsSpan(body.InstructionOffset, body.InstructionLength); + Assert.True( + instructions.StartsWith(expected), + $"Expected wasm instruction prefix {Convert.ToHexString(expected)}, actual {Convert.ToHexString(instructions[..Math.Min(instructions.Length, expected.Length)])}."); + } + + static void AssertWasmContainsInstructions( + WebcilImageReader.WasmFunctionInfo body, + ReadOnlySpan expected) + { + ReadOnlySpan instructions = + body.Image.AsSpan(body.InstructionOffset, body.InstructionLength); + Assert.True( + instructions.IndexOf(expected) >= 0, + $"Expected wasm instruction sequence {Convert.ToHexString(expected)}."); + } + // The wasm JIT references the ABI well-known globals via maximally padded WASM_GLOBAL_INDEX_LEB // relocations that the R2R object writer must self-resolve to the fixed global // indices and shrink down to their minimal size. Verify the emitted code contains a correctly self-resolved 'global.get' for the diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs index aa25f80544c4a9..b0103d62338172 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs @@ -27,6 +27,36 @@ public static int AddIntegers(int left, int right) return left + right; } + [MethodImpl(MethodImplOptions.NoInlining)] + public static int OptimizedTrackedVariables(int left, int right) + { + int sum = left + right; + if (sum < 0) + { + return AddIntegers(sum, right); + } + + int difference = left - right; + return AddIntegers(sum, difference); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static unsafe int LeafFrameLocal(int value) + { + int local = value + 1; + int* address = &local; + return *address * 2; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static unsafe int LocallocFrameLocal(int value) + { + int length = (value & 3) + 1; + int* storage = stackalloc int[length]; + *storage = value; + return *storage; + } + [MethodImpl(MethodImplOptions.NoOptimization)] public static double AddDoubles(double left, double right) { @@ -88,6 +118,53 @@ public static void CollectAtGcSafepoint() GC.Collect(); } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static unsafe int LocallocAcrossFinally(int value) + { + int length = (value & 3) + 1; + int* storage = stackalloc int[length]; + *storage = value; + try + { + return *storage; + } + finally + { + CollectAtGcSafepoint(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static int GcSlotIdentity() + { + GcMarker first = new(17); + if (first.Value == 0) + { + return 0; + } + + GcMarker second = new(29); + if (second.Value == 0) + { + return 0; + } + + GcMarker? empty = null; + TouchMarkerSlot(ref empty); + + CollectAtGcSafepoint(); + int result = (first.Value * 100) + second.Value; + GC.KeepAlive(first); + GC.KeepAlive(second); + GC.KeepAlive(empty); + return result; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TouchMarkerSlot(ref GcMarker? marker) + { + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static int CatchException(int value) { From 756dd2055b8d1ed8fd26856242e4da1834cb0ce7 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 12:46:19 -0500 Subject: [PATCH 12/14] [cdac][wasm] Maintain frame context and expose function identity Keep the WASM stack-walk context's SP, virtual IP, and logical frame pointer coherent with the native RtlVirtualUnwind and Frame::UpdateRegDisplay paths. - Derive IP and logical FP for the R2R InlinedCallFrame marker. - Use TransitionBlock's saved R2R SP, including lazy return-address recovery, and avoid treating the generic fallback argument area as a shadow frame. - Copy the full serialized WASM context for software exception frames. - Detect reverse P/Invoke from GC info so native caller bytes are never probed as a possible R2R shadow frame; retain caller SP with IP/FP cleared. - Expose a documented StackWalk function identity containing the raw shared table index, owning module, image runtime-function index, and nullable funclet classification. Reject native/interpreter handles before reading shadow-frame memory while preserving a known raw index when range lookup itself fails. Add native-layout transition fixtures, real adjacent root/funclet range coverage, parent/nested/terminator/localloc unwind coverage, and false-frame reverse-P/Invoke tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 43 +- docs/design/datacontracts/StackWalk.md | 60 ++- .../data-descriptor-meanings.json | 10 +- .../vm/datadescriptor/datadescriptor.inc | 3 + .../Contracts/IGCInfo.cs | 3 +- .../Contracts/IStackWalk.cs | 30 ++ .../Contracts/GCInfo/GCInfoDecoder.cs | 3 +- .../PlatformTraits/Wasm32GCInfoTraits.cs | 43 ++ .../StackWalk/Context/Wasm/WasmR2RInfo.cs | 28 ++ .../StackWalk/Context/Wasm/WasmUnwinder.cs | 155 +++++++- .../StackWalk/Context/WasmContext.cs | 7 +- .../StackWalk/FrameHandling/FrameHelpers.cs | 8 + .../FrameHandling/WasmFrameHandler.cs | 69 +++- .../Contracts/StackWalk/StackWalk_1.cs | 41 ++ .../CoreCLRContracts.cs | 1 + .../Data/Frames/TransitionBlock.cs | 3 + .../MockDescriptors/MockDescriptors.Frame.cs | 38 ++ .../cdac/tests/UnitTests/StackWalkTests.cs | 71 ++++ .../cdac/tests/UnitTests/WasmMockTarget.cs | 302 +++++++++++++++ .../cdac/tests/UnitTests/WasmR2RInfoTests.cs | 178 ++++++++- .../cdac/tests/UnitTests/WasmUnwinderTests.cs | 366 +++++++++++++++++- 21 files changed, 1439 insertions(+), 23 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/Wasm32GCInfoTraits.cs create mode 100644 src/native/managed/cdac/tests/UnitTests/WasmMockTarget.cs diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index ec8e820e248f84..d3154f04f8b9ff 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -2,7 +2,7 @@ This contract is for fetching information related to GCInfo associated with native code. -The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is partially supported by this contract. +The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is partially supported by this contract. WebAssembly uses the common format with identity normalization for code offsets, stack slots, stack-base registers, and stack-area sizes. ## APIs of contract @@ -91,7 +91,8 @@ public readonly record struct GCInfoHeader( uint GSCookieValidRangeEnd, // End (exclusive) of the GS cookie valid range SpecialSlot? PSPSym, // PSP sym stack slot, or null if none SpecialSlot? GenericsInstContext, // Generics instantiation context stack slot, or null if none - GenericsContextKind GenericsInstContextKind); // Kind of the generics instantiation context + GenericsContextKind GenericsInstContextKind, // Kind of the generics instantiation context + bool HasReversePInvokeFrame); // True if unwinding leaves managed code through reverse P/Invoke // Unified lifetime (live code range) of a GC slot, register or stack. public readonly record struct GCSlotLifetime( @@ -380,9 +381,41 @@ Slots use delta encoding where consecutive entries encode only the difference fr | **Stack Slot** | `offset >> 2` | `offset << 2` | | **Stack Area Size** | `size >> 2` | `size << 2` | -#### Interpreter (WASM / FEATURE_INTERPRETER) - -The interpreter uses a platform-independent encoding where all normalization and denormalization functions are identity (no transformation). This encoding is used for WASM targets (where `TargetGcInfoEncoding` is `InterpreterGcInfoEncoding`) and on any architecture when `FEATURE_INTERPRETER` is enabled. +#### WebAssembly (WASM32) + +WebAssembly ReadyToRun code uses the native `Wasm32GcInfoEncoding` from +`src/coreclr/inc/gcinfotypes.h`. Its normalization and denormalization functions are identity +operations. + +| Encoding Base | Value | +| --- | --- | +| `GENERICS_INST_CONTEXT_STACK_SLOT_ENCBASE` | 6 | +| `GS_COOKIE_STACK_SLOT_ENCBASE` | 6 | +| `CODE_LENGTH_ENCBASE` | 6 | +| `STACK_BASE_REGISTER_ENCBASE` | 3 | +| `SIZE_OF_STACK_AREA_ENCBASE` | 6 | +| `SIZE_OF_EDIT_AND_CONTINUE_PRESERVED_AREA_ENCBASE` | 3 | +| `REVERSE_PINVOKE_FRAME_ENCBASE` | 6 | +| `NUM_REGISTERS_ENCBASE` | 3 | +| `NUM_STACK_SLOTS_ENCBASE` | 5 | +| `NUM_UNTRACKED_SLOTS_ENCBASE` | 5 | +| `NORM_PROLOG_SIZE_ENCBASE` | 4 | +| `NORM_EPILOG_SIZE_ENCBASE` | 3 | +| `INTERRUPTIBLE_RANGE_DELTA1_ENCBASE` | 5 | +| `INTERRUPTIBLE_RANGE_DELTA2_ENCBASE` | 5 | +| `REGISTER_ENCBASE` | 3 | +| `REGISTER_DELTA_ENCBASE` | 3 | +| `STACK_SLOT_ENCBASE` | 6 | +| `STACK_SLOT_DELTA_ENCBASE` | 4 | +| `NUM_SAFE_POINTS_ENCBASE` | 4 | +| `NUM_INTERRUPTIBLE_RANGES_ENCBASE` | 1 | + +`HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA` is false. RyuJIT does not keep GC references in WASM +locals, so any malformed register slot is treated as scratch/volatile. + +#### Interpreter (FEATURE_INTERPRETER) + +The interpreter uses a platform-independent encoding where all normalization and denormalization functions are identity (no transformation). This encoding is used by interpreted methods regardless of target architecture. | Encoding Base | Value | Purpose | | --- | --- | --- | diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 4e0c27ef77a50f..e07582da667148 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -159,6 +159,10 @@ Unwinding call frames on the stack usually requires an OS specific implementatio | `FramedMethodFrame` | `TransitionBlockPtr` | `pointer` | Pointer to Frame's TransitionBlock | | `FuncEvalFrame` | `DebuggerEvalPtr` | `pointer` | Pointer to the Frame's DebuggerEval object | | `FuncEvalFrame` | `ReturnAddress` | `CodePointer` | Return address of the frame | +| `FunctionTableIndexRangeSection` | `MinFunctionTableIndex` | `uint32` | First runtime-global shared function-table index owned by the R2R module | +| `FunctionTableIndexRangeSection` | `Next` | `pointer` | Pointer to the next registered WASM R2R function-table range | +| `FunctionTableIndexRangeSection` | `NumRuntimeFunctions` | `uint32` | Number of consecutive RUNTIME_FUNCTION entries owned by the R2R module | +| `FunctionTableIndexRangeSection` | `R2RModule` | `pointer` | Pointer to the Module that owns this function-table range | | `GCFrame` | `GCFlags` | `uint32` | GC_CALL_* promotion flags applied when reporting the protected slots | | `GCFrame` | `Next` | `pointer` | Pointer to the next GCFrame toward the top of the chain | | `GCFrame` | `NumObjRefs` | `uint32` | Count of protected object reference slots starting at ObjRefs | @@ -185,8 +189,14 @@ Unwinding call frames on the stack usually requires an OS specific implementatio | `PInvokeCalliFrame` | `VASigCookiePtr` | `pointer` | Pointer to the varargs signature cookie for the unmanaged call | | `ReadyToRunInfo` | `ImportSections` | `pointer` | Pointer to the array of ReadyToRun import sections | | `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image | +| `ReadyToRunInfo` | `MinVirtualIP` | `pointer` | Base virtual IP assigned to the ReadyToRun module on WebAssembly | | `ReadyToRunInfo` | `NumImportSections` | `uint32` | Number of ReadyToRun import sections | +| `ReadyToRunInfo` | `NumRuntimeFunctions` | `uint32` | Number of `RuntimeFunctions` | +| `ReadyToRunInfo` | `RuntimeFunctions` | `pointer` | Pointer to an array of `RuntimeFunctions` - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontyperuntimefunctions) | | `ResumableFrame` | `TargetContextPtr` | `pointer` | Pointer to the Frame's Target Context | +| `RuntimeFunction` | *(type size)* | `uint32` | Size of a runtime function entry in bytes | +| `RuntimeFunction` | `BeginAddress` | `uint32` | Begin address of the function. On ARM32, bit 0 is the Thumb bit; on WebAssembly, bit 31 marks a funclet and is excluded from address arithmetic. | +| `RuntimeFunction` | `UnwindData` | `uint32` | Pointer to the unwind info for the function | | `SoftwareExceptionFrame` | `ReturnAddress` | `CodePointer` | Return address saved in Frame | | `SoftwareExceptionFrame` | `TargetContext` | `pointer` | Context object saved in Frame | | `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters | @@ -203,6 +213,7 @@ Unwinding call frames on the stack usually requires an OS specific implementatio | `TransitionBlock` | `CalleeSavedRegisters` | `pointer` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | | `TransitionBlock` | `FirstGCRefMapSlot` | `pointer` | Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset | | `TransitionBlock` | `ReturnAddress` | `CodePointer` | Return address associated with the TransitionBlock | +| `TransitionBlock` | `StackPointer` | `pointer` | WASM R2R shadow-stack pointer saved by the transition helper | | `VASigCookie` | `SizeOfArgs` | `uint32` | Total size in bytes of the varargs argument area; used on x86 to locate the argument base | ### Global variables used @@ -211,6 +222,7 @@ Unwinding call frames on the stack usually requires an OS specific implementatio | --- | --- | --- | | `Identifier` *(name pattern)* | `pointer` | Per-frame-type sentinel address used to identify and classify runtime frames | | `Architecture` | `string` | Target architecture | +| `FunctionTableIndexRangeList` | `pointer` | Pointer to the head pointer of the registered WASM R2R function-table range list | | `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address | ### Contracts used @@ -439,6 +451,9 @@ Most of the handlers are implemented in `BaseFrameHandler`. Platform specific co InlinedCallFrames store and update only the IP, SP, and FP of a given context. If the stored IP (CallerReturnAddress) is 0 then the InlinedCallFrame does not have an active call and should not update the context. * On ARM, the InlinedCallFrame stores the value of the SP after the prolog (`SPAfterProlog`) to allow unwinding for functions with stackalloc. When a function uses stackalloc, the CallSiteSP can already have been adjusted. This value should be placed in R9. +* On WASM, `CallerReturnAddress == INLINED_PINVOKE_FROM_R2R` is an active-frame marker rather than + an IP. Derive SP from `CallSiteSP`, derive the virtual IP from the R2R shadow frame, and derive + logical FP with the wasm funclet-aware frame-pointer walk. **Return Address**: `CallerReturnAddress`, but only when the frame has an active call (i.e., `CallerReturnAddress != 0`). Returns null otherwise. @@ -446,6 +461,9 @@ InlinedCallFrames store and update only the IP, SP, and FP of a given context. I SoftwareExceptionFrames store a copy of the context struct. The IP, SP, and all ABI specified (platform specific) callee-saved registers are copied from the stored context to the working context. +On WASM the serialized `WasmContext` already contains the coherent synthetic SP/IP/FP state, so +copy the full context rather than only IP/SP plus a hardware callee-saved register set. + **Return Address**: Read from the `ReturnAddress` field on the frame. #### TransitionFrame @@ -455,8 +473,16 @@ TransitionFrames hold a pointer to a `TransitionBlock`. The TransitionBlock hold When updating the context from a TransitionFrame, the IP, SP, and all ABI specified callee-saved registers are copied over. * On ARM, the additional register values stored in `ArgumentRegisters` are copied over. The `TransitionBlock` holds a pointer to the `ArgumentRegister` struct containing these values. +* On WASM, when `TransitionBlock.StackPointer` is non-null, it points into the R2R shadow stack. + If `ReturnAddress` is still zero, derive it lazily from that SP, matching + `FramedMethodFrame::GetTransitionBlock_Impl`. Use the saved SP and derive logical FP only when + both the saved SP and a resolved virtual IP are available. Otherwise use the generic + `TransitionBlock + sizeof(TransitionBlock)` fallback SP and clear FP; the fallback address is an + argument area, not an R2R shadow frame. -**Return Address**: Read from `TransitionBlock.ReturnAddress`. This applies to all frame types that use the TransitionFrame mechanism. +**Return Address**: Read from `TransitionBlock.ReturnAddress`. On WASM, a zero return address with a +non-null saved stack pointer is resolved lazily from the R2R shadow frame. This applies to all frame +types that use the TransitionFrame mechanism. The following Frame types also use this mechanism: * FramedMethodFrame @@ -659,10 +685,42 @@ TargetPointer GetStackPointer(IStackDataFrameHandle stackDataFrameHandle) `GetContextFramePointer` returns the base pointer register from the current frame's context: EBP on x86, RBP on x64, and the platform frame-pointer register on other architectures. +On WASM this is the logical R2R frame pointer maintained in `WasmContext.InterpreterFP`. A root +function uses its own frame base. A funclet uses the containing method's establishing frame, +resolved by unwinding through containing funclets or the `CallFuncletWith[out]Throwable` +terminator. SP, IP, and FP are updated as one context state during virtual unwind and explicit-frame +seeding. + ```csharp TargetPointer GetContextFramePointer(IStackDataFrameHandle stackDataFrameHandle) ``` +`GetWasmFunctionIdentity` returns the raw runtime-global function-table index stored in a WASM R2R +shadow frame. When that index belongs to a registered R2R range, the result also contains the +owning Module, the RUNTIME_FUNCTION index within that image, and nullable funclet classification. +The raw index is preserved even when range resolution fails. + +The API is valid only for `Frameless` frames whose code kind is `ReadyToRun`. It rejects native +markers and interpreter frames before reading the stack pointer, because those addresses can +contain bytes that resemble a valid shadow frame. + +WebAssembly engine function indices are module-local. A CDP consumer must use the owning module +and image-relative runtime-function index to select the correct wasm script and translate through +that image's element section. It must not interpret the raw shared-table index as a V8 +`func_index`. + +```csharp +public readonly struct WasmFunctionIdentity +{ + public uint FunctionTableIndex { get; init; } + public TargetPointer? Module { get; init; } + public uint? RuntimeFunctionIndex { get; init; } + public bool? IsFunclet { get; init; } +} + +WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle) +``` + Each `IStackDataFrameHandle` also exposes `IsInterrupted` and `HasFaulted`. `IsInterrupted` is true when the current managed frame was reached through an exception Frame. `HasFaulted` is true when that exception Frame is a `FaultingExceptionFrame` whose saved context still has `CONTEXT_EXCEPTION_ACTIVE` set. `WalkStackReferences` walks the entire managed stack and enumerates all live GC references at each frame. It returns a list of `StackReferenceData` describing each GC-tracked slot (its address, whether it's an interior pointer, and the register/stack location). When `resolveInteriorPointers` is true, interior references are resolved to their containing objects. This API is the primary consumer for `SOSDacImpl.GetStackReferences`. diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 65d3c9fc03a6a7..5fe5b0758f9a6d 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -199,6 +199,10 @@ "FramedMethodFrame.TransitionBlockPtr": "Pointer to Frame's TransitionBlock", "FuncEvalFrame.DebuggerEvalPtr": "Pointer to the Frame's DebuggerEval object", "FuncEvalFrame.ReturnAddress": "Return address of the frame", + "FunctionTableIndexRangeSection.MinFunctionTableIndex": "First runtime-global shared function-table index owned by the R2R module", + "FunctionTableIndexRangeSection.Next": "Pointer to the next registered WASM R2R function-table range", + "FunctionTableIndexRangeSection.NumRuntimeFunctions": "Number of consecutive RUNTIME_FUNCTION entries owned by the R2R module", + "FunctionTableIndexRangeSection.R2RModule": "Pointer to the Module that owns this function-table range", "GCAllocContext.AllocBytes": "Number of bytes allocated on SOH by this context", "GCAllocContext.AllocBytesLoh": "Number of bytes allocated not on SOH by this context", "GCAllocContext.Limit": "Allocation limit pointer", @@ -684,6 +688,7 @@ "TransitionBlock.CalleeSavedRegisters": "Platform specific CalleeSavedRegisters struct associated with the TransitionBlock", "TransitionBlock.FirstGCRefMapSlot": "Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset", "TransitionBlock.ReturnAddress": "Return address associated with the TransitionBlock", + "TransitionBlock.StackPointer": "WASM R2R shadow-stack pointer saved by the transition helper", "TransitionBlock.Size": "Size in bytes of the transition block, used to restore the caller's stack pointer", "TypedByRef.Data": "Managed pointer (the byref) stored in a System.TypedReference value", "TypedByRef.Type": "Raw TypeHandle pointer of the referent type", @@ -756,6 +761,7 @@ "FeatureOnStackReplacement": "Present (nonzero) when on-stack replacement is enabled", "FeaturePortableEntrypoints": "Present (nonzero) when portable entrypoints are enabled", "FeatureWebcil": "Present (nonzero) when Webcil is enabled", + "FunctionTableIndexRangeList": "Pointer to the head pointer of the registered WASM R2R function-table range list", "FieldOffsetBigRVA": "Sentinel value of FieldDesc::DWord2 indicating the field is an RVA static whose offset is too large to encode in the bitfield; the real offset must be read from the field's metadata (FieldDefinition.GetRelativeVirtualAddress).", "FieldOffsetDynamicRVA": "Sentinel FieldDesc offset for an EnC-added RVA field whose enclosing type is not yet loaded", "FieldOffsetNewEnc": "Sentinel offset value stored in FieldDesc::DWord2 for added fields whose storage has not yet been allocated.", @@ -864,6 +870,8 @@ "ThinlockThreadIdDispenser": "Pointer to the dispenser of thin-lock thread IDs", "ThreadStore": "Pointer to the runtime thread store", "TotalCpuCount": "Number of available processors", - "TotalGenerationCount": "The total number of generations in the GC" + "TotalGenerationCount": "The total number of generations in the GC", + "WasmDebugRegisterTypeShift": "Bit position at which the JIT debug-register encoding stores its WASM value type", + "WasmDebugValueTypeCount": "Number of values in the JIT's WASM debug-register value-type encoding" } } diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index a70aecea52b55d..9559b6da3ce09c 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1298,6 +1298,9 @@ CDAC_TYPE_END(InterpreterFrame) CDAC_TYPE_BEGIN(TransitionBlock) CDAC_TYPE_SIZE(sizeof(TransitionBlock)) CDAC_TYPE_FIELD(TransitionBlock, TYPE(CodePointer), ReturnAddress, offsetof(TransitionBlock, m_ReturnAddress)) +#ifdef TARGET_WASM +CDAC_TYPE_FIELD(TransitionBlock, T_POINTER, StackPointer, offsetof(TransitionBlock, m_StackPointer)) +#endif // TARGET_WASM CDAC_TYPE_FIELD(TransitionBlock, TYPE(CalleeSavedRegisters), CalleeSavedRegisters, offsetof(TransitionBlock, m_calleeSavedRegisters)) // Offset to argument registers and first GCRefMap slot (platform-specific) #if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs index 8820b2d2d85604..4482ef185e1b12 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs @@ -35,7 +35,8 @@ public readonly record struct GCInfoHeader( uint GSCookieValidRangeEnd, SpecialSlot? PSPSym, SpecialSlot? GenericsInstContext, - GenericsContextKind GenericsInstContextKind); + GenericsContextKind GenericsInstContextKind, + bool HasReversePInvokeFrame = false); /// Unified lifetime of a GC slot (register or stack). /// True if the slot is a CPU register; false if it is a stack location. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index f86ca12ee4c699..c0d60b1fb188ba 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -84,6 +84,35 @@ public record struct DebuggerEvalData( uint MethodToken, TargetPointer AssemblyPtr); +/// +/// Identifies the WebAssembly function represented by a stack frame. +/// +/// +/// +/// is the raw runtime-global shared-table index stored in the +/// CoreCLR WASM shadow frame. When the index maps to a registered ReadyToRun range, +/// , , and identify +/// the owning image entry. +/// +/// +/// A WebAssembly engine's function index is module-local. Consumers must use +/// together with and the image's element +/// section to translate to an engine function index; the raw shared-table index alone is not an +/// engine function index. +/// +/// +public readonly struct WasmFunctionIdentity +{ + /// The raw runtime-global function-table index stored in the frame. + public uint FunctionTableIndex { get; init; } + /// The owning ReadyToRun module, or null when the raw index is not registered. + public TargetPointer? Module { get; init; } + /// The RUNTIME_FUNCTION index within , or null when unresolved. + public uint? RuntimeFunctionIndex { get; init; } + /// Whether the resolved runtime function is a funclet, or null when unresolved. + public bool? IsFunclet { get; init; } +} + public interface IStackWalk : IContract { static string IContract.Name => nameof(StackWalk); @@ -106,6 +135,7 @@ public interface IStackWalk : IContract byte[] GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags) => throw new NotImplementedException(); TargetPointer GetFuncletRootId(IStackDataFrameHandle stackDataFrameHandle, out uint parentNativeOffset) => throw new NotImplementedException(); TargetPointer GetExactGenericArgsToken(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); + WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); } public struct StackWalk : IStackWalk diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index 26beed0ee971be..f5ace142bd0a81 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -546,7 +546,8 @@ public GCInfoHeader GetHeader() GSCookieValidRangeEnd: gsCookie.HasValue ? _validRangeEnd : 0, PSPSym: pspSym, GenericsInstContext: genericsInstContext, - GenericsInstContextKind: genericsContextKind); + GenericsInstContextKind: genericsContextKind, + HasReversePInvokeFrame: _reversePInvokeFrameStackSlot != TTraits.NO_REVERSE_PINVOKE_FRAME); } public uint GetCodeLength() diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/Wasm32GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/Wasm32GCInfoTraits.cs new file mode 100644 index 00000000000000..b9ad010b7c8927 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/Wasm32GCInfoTraits.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; + +internal class Wasm32GCInfoTraits : IGCInfoTraits +{ + public static uint DenormalizeStackBaseRegister(uint reg) => reg; + public static uint DenormalizeCodeLength(uint len) => len; + public static uint NormalizeCodeLength(uint len) => len; + public static uint DenormalizeCodeOffset(uint offset) => offset; + public static uint NormalizeCodeOffset(uint offset) => offset; + public static int DenormalizeStackSlot(int x) => x; + public static uint DenormalizeSizeOfStackArea(uint size) => size; + + public static int GENERICS_INST_CONTEXT_STACK_SLOT_ENCBASE => 6; + public static int GS_COOKIE_STACK_SLOT_ENCBASE => 6; + public static int CODE_LENGTH_ENCBASE => 6; + public static int STACK_BASE_REGISTER_ENCBASE => 3; + public static int SIZE_OF_STACK_AREA_ENCBASE => 6; + public static int SIZE_OF_EDIT_AND_CONTINUE_PRESERVED_AREA_ENCBASE => 3; + public static int SIZE_OF_EDIT_AND_CONTINUE_FIXED_STACK_FRAME_ENCBASE => 0; + public static int REVERSE_PINVOKE_FRAME_ENCBASE => 6; + public static int NUM_REGISTERS_ENCBASE => 3; + public static int NUM_STACK_SLOTS_ENCBASE => 5; + public static int NUM_UNTRACKED_SLOTS_ENCBASE => 5; + public static int NORM_PROLOG_SIZE_ENCBASE => 4; + public static int NORM_EPILOG_SIZE_ENCBASE => 3; + public static int INTERRUPTIBLE_RANGE_DELTA1_ENCBASE => 5; + public static int INTERRUPTIBLE_RANGE_DELTA2_ENCBASE => 5; + public static int REGISTER_ENCBASE => 3; + public static int REGISTER_DELTA_ENCBASE => 3; + public static int STACK_SLOT_ENCBASE => 6; + public static int STACK_SLOT_DELTA_ENCBASE => 4; + public static int NUM_SAFE_POINTS_ENCBASE => 4; + public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 1; + + public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => false; + + // RyuJIT does not place GC references in WASM locals. If a malformed encoding reports a + // register slot, treat it as volatile so it is never reported for a non-leaf frame. + public static bool IsScratchRegister(uint regNum) => true; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs index f18d6c4bdf7fb6..90b6445c3041ae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs @@ -56,6 +56,28 @@ private Data.ReadyToRunInfo GetReadyToRunInfo(Data.FunctionTableIndexRangeSectio private Data.RuntimeFunction GetRuntimeFunction(Data.ReadyToRunInfo r2rInfo, uint localIndex) => _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, localIndex); + public bool TryGetFunctionIdentity( + uint functionTableIndex, + out TargetPointer module, + out uint runtimeFunctionIndex, + out bool isFunclet) + { + module = TargetPointer.Null; + runtimeFunctionIndex = 0; + isFunclet = false; + + Data.FunctionTableIndexRangeSection? section = FindSection(functionTableIndex); + if (section is null) + return false; + + runtimeFunctionIndex = functionTableIndex - section.MinFunctionTableIndex; + Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(section); + Data.RuntimeFunction runtimeFunction = GetRuntimeFunction(r2rInfo, runtimeFunctionIndex); + module = section.R2RModule; + isFunclet = _runtimeFunctions.IsFunclet(runtimeFunction); + return true; + } + public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP) { baseVirtualIP = 0; @@ -99,4 +121,10 @@ public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDa unwindDataAddress = new TargetPointer(r2rInfo.LoadedImageBase.Value + runtimeFunction.UnwindData); return true; } + + // Mirrors ExecutionManager::IsFuncletFunctionIndex. + public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet) + { + return TryGetFunctionIdentity(functionTableIndex, out _, out _, out isFunclet); + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs index f5d7c9049348c5..740337aab2c14d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs @@ -26,6 +26,13 @@ internal interface IWasmR2RInfo /// frame size. Returns false when the index does not map to a known R2R function. /// bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress); + + /// + /// Reports whether an R2R function table entry is a funclet rather than a method's root + /// function (ExecutionManager::IsFuncletFunctionIndex). Returns false when the index + /// does not map to a known R2R function. + /// + bool TryIsFunclet(uint functionTableIndex, out bool isFunclet); } /// @@ -133,11 +140,17 @@ public TargetCodePointer GetVirtualIP(TargetPointer sp) /// /// Advances by one R2R frame and produces the caller's virtual IP, - /// mirroring WasmUnwindStackFrameCore. Returns false when the R2R walk terminates - /// (no R2R frame at ), in which case is set to - /// . + /// mirroring WasmUnwindStackFrameCore. Returns false when the R2R walk cannot advance + /// (no R2R frame at , missing unwind data, or a zero frame size), in + /// which case is set to . Returns true + /// with the caller stack pointer preserved and set to + /// when unwinding succeeds but the caller is not + /// ReadyToRun code. /// - public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip) + public bool TryUnwindOneFrame( + ref TargetPointer sp, + TargetCodePointer controlPC, + out TargetCodePointer ip) { ip = TargetCodePointer.Null; if (!TryGetFramePointer(sp, out TargetPointer frameBase)) @@ -161,19 +174,143 @@ public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip) return false; } + bool callerIsNative = false; + if (controlPC != TargetCodePointer.Null && + _r2rInfo.TryIsFunclet(functionIndex, out bool isFunclet) && + !isFunclet && + _target.Contracts.ExecutionManager.GetCodeBlockHandle(controlPC) is CodeBlockHandle codeBlock) + { + _target.Contracts.ExecutionManager.GetGCInfo( + codeBlock, + out TargetPointer gcInfoAddress, + out uint gcVersion); + IGCInfoHandle gcInfoHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo( + gcInfoAddress, + gcVersion); + callerIsNative = _target.Contracts.GCInfo.GetHeader(gcInfoHandle).HasReversePInvokeFrame; + } + sp = new TargetPointer(frameBase.Value + frameSize); - ip = GetVirtualIP(sp); - if (ip == TargetCodePointer.Null) + ip = callerIsNative ? TargetCodePointer.Null : GetVirtualIP(sp); + return true; + } + + public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip) + => TryUnwindOneFrame(ref sp, TargetCodePointer.Null, out ip); + + /// + /// Returns the R2R function table entry index recorded in the frame at . + /// Returns false when there is no R2R frame there. + /// + public bool TryGetFunctionIndex(TargetPointer sp, out uint functionIndex) + { + functionIndex = 0; + if (!TryGetFramePointer(sp, out TargetPointer frameBase)) + return false; + + functionIndex = _target.Read(frameBase.Value + FunctionIndexOffset); + return true; + } + + /// + /// Advances by one R2R frame and returns the caller's stack pointer, + /// without requiring the caller to be R2R-generated code. This is the frame-size half of + /// WasmUnwindStackFrameCore, which needs in + /// order to inspect a synthetic frame. + /// + private bool TryUnwindToCallerStackPointer(TargetPointer sp, out TargetPointer callerSp) + { + callerSp = TargetPointer.Null; + if (!TryGetFramePointer(sp, out TargetPointer frameBase)) + return false; + + uint functionIndex = _target.Read(frameBase.Value + FunctionIndexOffset); + if (!_r2rInfo.TryGetUnwindData(functionIndex, out TargetPointer unwindData)) + return false; + + uint frameSize = DecodeULEB128(unwindData.Value); + if (frameSize == 0) { - // The caller is not R2R-generated code (an interpreter transition or the stack top); - // the R2R walk is exhausted. - sp = TargetPointer.Null; + // A zero frame size makes no progress; refuse rather than risk an unbounded walk. return false; } + callerSp = new TargetPointer(frameBase.Value + frameSize); return true; } + /// + /// Returns the logical (establishing) frame pointer for the frame at , + /// mirroring GetWasmFramePointerFromStackPointer in + /// src/coreclr/vm/wasm/helpers.cpp. + /// + /// + /// For a method's root function this is simply its own frame base. For a funclet it is the + /// frame base of the method that established the frame, because a funclet's WASM frame pointer + /// local refers to the fixed portion of its parent's frame rather than its own + /// (WasmRegAlloc in src/coreclr/jit/regallocwasm.cpp). Reaching it means + /// unwinding out of the funclet, either to its containing function or to the synthetic + /// frame that CallFuncletWith[out]Throwable pushes, + /// which carries the establishing frame pointer beside the marker. + /// + public bool TryGetLogicalFramePointer(TargetPointer sp, out TargetPointer framePointer) + { + framePointer = TargetPointer.Null; + + // The native original recurses until it reaches a non-funclet frame or the CallFunclet + // terminator, relying on the stack being finite. The cDAC reads potentially untrustworthy + // memory, so require strictly increasing stack pointers instead: every unwind step must + // move toward the caller, which guarantees termination without capping legitimate funclet + // nesting depth. The iteration count is a backstop only. + const int MaxUnwindSteps = 4096; + TargetPointer current = sp; + + for (int i = 0; i < MaxUnwindSteps; i++) + { + if (!TryGetFramePointer(current, out TargetPointer frameBase)) + return false; + + uint functionIndex = _target.Read(frameBase.Value + FunctionIndexOffset); + + // Native ExecutionManager::IsFuncletFunctionIndex returns FALSE when the function + // index matches no range section, and GetWasmFramePointerFromStackPointer then treats + // the frame as a root function. We deliberately fail instead: an index that belongs to + // no R2R range section is not R2R code, so it has no variable debug info to resolve + // against, and reporting its raw frame base would be a guess. + if (!_r2rInfo.TryIsFunclet(functionIndex, out bool isFunclet)) + return false; + + if (!isFunclet) + { + framePointer = frameBase; + return true; + } + + if (!TryUnwindToCallerStackPointer(current, out TargetPointer callerSp)) + return false; + + if (callerSp.Value <= current.Value) + { + // No progress toward the caller; refuse rather than risk an unbounded walk. + return false; + } + + if (_target.Read(callerSp.Value + FunctionIndexOffset) == TerminateR2RStackWalk) + { + // The funclet was invoked by the VM through CallFuncletWith[out]Throwable, so + // unwinding terminates at that synthetic frame before reaching the method's own + // frame. Recover the establishing frame pointer stored beside the marker. + framePointer = GetEstablishingFramePointerFromTerminator(callerSp); + return true; + } + + // The funclet was called by its containing method or funclet; keep walking out. + current = callerSp; + } + + return false; + } + // Standard little-endian base-128 varint, matching the native DecodeULEB128AsU32. A ULEB128 // uint32 is at most 5 bytes (5 * 7 = 35 >= 32 bits); a longer encoding is malformed. private uint DecodeULEB128(ulong address) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs index 24ac092843f18b..18f1400fe7beda 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs @@ -84,15 +84,20 @@ public void Unwind(Target target) // the caller falls back to the explicit Frame chain / interpreter frame chain. Wasm.WasmUnwinder unwinder = new(target, new Wasm.WasmR2RInfo(target)); TargetPointer sp = StackPointer; - if (unwinder.TryUnwindOneFrame(ref sp, out TargetCodePointer ip)) + if (unwinder.TryUnwindOneFrame(ref sp, InstructionPointer, out TargetCodePointer ip)) { StackPointer = sp; InstructionPointer = ip; + FramePointer = ip != TargetCodePointer.Null && + unwinder.TryGetLogicalFramePointer(sp, out TargetPointer fp) + ? fp + : TargetPointer.Null; } else { StackPointer = TargetPointer.Null; InstructionPointer = TargetCodePointer.Null; + FramePointer = TargetPointer.Null; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index b1c59af11e2e8a..1de5fc3fd10fde 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -235,6 +235,14 @@ public TargetCodePointer GetReturnAddress(Data.Frame frame) case FrameType.DynamicHelperFrame: Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frame.Address); Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + if (tb.ReturnAddress == TargetCodePointer.Null && + _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.Wasm && + tb.StackPointer is TargetPointer stackPointer && + stackPointer != TargetPointer.Null) + { + Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target)); + return unwinder.GetVirtualIP(stackPointer); + } return tb.ReturnAddress; // SoftwareExceptionFrame: stored m_ReturnAddress diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs index 35ffcfc3b4f430..09dc2851a5fb5a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs @@ -24,11 +24,25 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal sealed class WasmFrameHandler(Target target, ContextHolder contextHolder) : BaseFrameHandler(target, contextHolder), IPlatformFrameHandler { + // INLINED_PINVOKE_FROM_R2R from src/coreclr/vm/frames.h. WASM has no native return address + // for an R2R inline P/Invoke, so the native frame stores this marker and derives IP/FP from SP. + private const uint InlinedPInvokeFromR2R = 1; + private readonly ContextHolder _holder = contextHolder; public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame) { - base.HandleInlinedCallFrame(inlinedCallFrame); + if (inlinedCallFrame.CallerReturnAddress == TargetCodePointer.Null) + return; + + if (inlinedCallFrame.CallerReturnAddress == InlinedPInvokeFromR2R) + { + SetR2RContext(inlinedCallFrame.CallSiteSP); + } + else + { + base.HandleInlinedCallFrame(inlinedCallFrame); + } // When the frame directly above this P/Invoke transition is an InterpreterFrame, stash its // address in the synthetic first-argument register so the subsequent interpreter virtual @@ -43,6 +57,59 @@ public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame) } } + public override void HandleSoftwareExceptionFrame(SoftwareExceptionFrame softwareExceptionFrame) + { + // The serialized WASM T_CONTEXT carries SP/IP/FP together. The base implementation copies + // only IP/SP plus hardware callee-saved registers; WASM has no such register dictionary. + _holder.ReadFromAddress(_target, softwareExceptionFrame.TargetContext); + } + + public override void HandleTransitionFrame(FramedMethodFrame framedMethodFrame) + { + Data.TransitionBlock transitionBlock = _target.ProcessedData.GetOrAdd( + framedMethodFrame.TransitionBlockPtr); + TargetPointer savedStackPointer = transitionBlock.StackPointer ?? TargetPointer.Null; + bool hasR2RStackPointer = savedStackPointer != TargetPointer.Null; + + TargetCodePointer instructionPointer = transitionBlock.ReturnAddress; + if (instructionPointer == TargetCodePointer.Null && hasR2RStackPointer) + { + Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target)); + instructionPointer = unwinder.GetVirtualIP(savedStackPointer); + } + + _holder.Context.InstructionPointer = instructionPointer; + _holder.Context.StackPointer = hasR2RStackPointer && instructionPointer != TargetCodePointer.Null + ? savedStackPointer + : framedMethodFrame.TransitionBlockPtr + Data.TransitionBlock.GetSize(_target); + + if (hasR2RStackPointer && instructionPointer != TargetCodePointer.Null) + { + Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target)); + if (unwinder.TryGetLogicalFramePointer(savedStackPointer, out TargetPointer fp)) + { + _holder.Context.FramePointer = fp; + return; + } + } + + _holder.Context.FramePointer = TargetPointer.Null; + } + public void HandleHijackFrame(HijackFrame frame) => throw new PlatformNotSupportedException("HijackFrame handling is not supported on WASM."); + + private void SetR2RContext(TargetPointer stackPointer) + { + Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target)); + TargetCodePointer instructionPointer = unwinder.GetVirtualIP(stackPointer); + if (instructionPointer == TargetCodePointer.Null) + throw new InvalidOperationException($"Failed to resolve WASM R2R virtual IP from stack pointer {stackPointer}."); + if (!unwinder.TryGetLogicalFramePointer(stackPointer, out TargetPointer framePointer)) + throw new InvalidOperationException($"Failed to resolve WASM R2R frame pointer from stack pointer {stackPointer}."); + + _holder.Context.StackPointer = stackPointer; + _holder.Context.InstructionPointer = instructionPointer; + _holder.Context.FramePointer = framePointer; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index 98c76b52196364..0695f606147dff 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -9,6 +9,7 @@ using System.Reflection.Metadata.Ecma335; using System.Runtime.InteropServices; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; @@ -1096,6 +1097,46 @@ TargetPointer IStackWalk.GetStackPointer(IStackDataFrameHandle stackDataFrameHan return handle.Context.StackPointer; } + WasmFunctionIdentity IStackWalk.GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle) + { + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() != RuntimeInfoArchitecture.Wasm) + throw new PlatformNotSupportedException("WASM function identity is only available on WASM targets."); + + StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + if (handle.State != StackWalkState.Frameless || + _eman.GetCodeKind(handle.Context.InstructionPointer) != CodeKind.ReadyToRun) + { + throw new InvalidOperationException( + "WASM function identity is only available for ReadyToRun frameless frames."); + } + + WasmR2RInfo r2rInfo = new(_target); + WasmUnwinder unwinder = new(_target, r2rInfo); + + if (!unwinder.TryGetFunctionIndex(handle.Context.StackPointer, out uint functionTableIndex)) + throw new InvalidOperationException("The stack frame does not contain a WASM R2R function-table index."); + + if (!r2rInfo.TryGetFunctionIdentity( + functionTableIndex, + out TargetPointer module, + out uint runtimeFunctionIndex, + out bool isFunclet)) + { + return new WasmFunctionIdentity + { + FunctionTableIndex = functionTableIndex, + }; + } + + return new WasmFunctionIdentity + { + FunctionTableIndex = functionTableIndex, + Module = module, + RuntimeFunctionIndex = runtimeFunctionIndex, + IsFunclet = isFunclet, + }; + } + TargetPointer IStackWalk.GetFuncletRootId(IStackDataFrameHandle stackDataFrameHandle, out uint parentNativeOffset) { StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index e9b8312f726eab..3b18d1217c8dce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -67,6 +67,7 @@ public static void Register(ContractRegistry registry) RuntimeInfoArchitecture.Arm => new GCInfo_1(t), RuntimeInfoArchitecture.LoongArch64 => new GCInfo_1(t), RuntimeInfoArchitecture.RiscV64 => new GCInfo_1(t), + RuntimeInfoArchitecture.Wasm => new GCInfo_1(t), _ => default(GCInfo), }; }); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index 0bc4da6f58800e..85fe9870d79314 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -8,6 +8,9 @@ internal partial class TransitionBlock : IData { [Field] public partial TargetCodePointer ReturnAddress { get; } + // WASM-only: the R2R shadow-stack pointer saved by transition helpers. + [Field] public partial TargetPointer? StackPointer { get; } + [FieldAddress] public partial TargetPointer CalleeSavedRegisters { get; } diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs index 99aaabb822883d..d318ea65a21a89 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs @@ -86,6 +86,44 @@ public ulong MethodDescPtr get => ReadPointerField(MethodDescPtrFieldName); set => WritePointerField(MethodDescPtrFieldName, value); } + + public ulong TransitionBlockPtr + { + get => ReadPointerField(TransitionBlockPtrFieldName); + set => WritePointerField(TransitionBlockPtrFieldName, value); + } +} + +internal sealed class MockWasmTransitionBlock : TypedView +{ + private const string ReturnAddressFieldName = "ReturnAddress"; + private const string StackPointerFieldName = "StackPointer"; + private const string CalleeSavedRegistersFieldName = "CalleeSavedRegisters"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + { + LayoutBuilder builder = new("TransitionBlock", architecture) + { + Size = 2 * sizeof(uint), + }; + return builder + .AddField(ReturnAddressFieldName, 0, sizeof(uint)) + .AddField(CalleeSavedRegistersFieldName, 0, sizeof(uint)) + .AddField(StackPointerFieldName, sizeof(uint), sizeof(uint)) + .Build(); + } + + public ulong ReturnAddress + { + get => ReadCodePointerField(ReturnAddressFieldName); + set => WriteCodePointerField(ReturnAddressFieldName, value); + } + + public ulong StackPointer + { + get => ReadPointerField(StackPointerFieldName); + set => WritePointerField(StackPointerFieldName, value); + } } internal sealed class MockInterpMethodContextFrame : TypedView diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index 7ef05e11299899..575013d722da46 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -812,6 +812,77 @@ public void UpdateContextFromFrame_WasmInlinedCallFrame_SeedsContextFromCallSite Assert.Equal(calleeSavedFP, context.FramePointer.Value); } + [Fact] + public void UpdateContextFromFrame_WasmR2RInlinedCallFrame_DerivesInstructionAndFramePointersFromStack() + { + const ulong callSiteSP = 0x0020_0000; + (Target target, ulong inlinedCallFrameAddress) = + WasmMockTarget.CreateWithR2RInlinedCallFrame( + callSiteSP, + WasmMockTarget.FunctionTableIndex); + + ContextHolder context = new(); + FrameHelpers frameHelpers = new(target); + Data.Frame frame = target.ProcessedData.GetOrAdd(inlinedCallFrameAddress); + frameHelpers.UpdateContextFromFrame(frame, context); + + Assert.Equal(callSiteSP, context.StackPointer.Value); + Assert.Equal( + WasmMockTarget.MinVirtualIP + WasmMockTarget.FunctionBeginAddress + 6, + context.InstructionPointer.Value); + Assert.Equal(callSiteSP, context.FramePointer.Value); + } + + [Fact] + public void UpdateContextFromFrame_WasmR2RTransitionFrame_UsesSavedStackAndDerivesFramePointer() + { + const ulong savedStackPointer = 0x0020_0000; + (Target target, ulong transitionFrameAddress) = + WasmMockTarget.CreateWithR2RTransitionFrame( + savedStackPointer, + WasmMockTarget.FunctionTableIndex); + + ContextHolder context = new(); + FrameHelpers frameHelpers = new(target); + Data.Frame frame = target.ProcessedData.GetOrAdd(transitionFrameAddress); + frameHelpers.UpdateContextFromFrame(frame, context); + + Assert.Equal(savedStackPointer, context.StackPointer.Value); + Assert.Equal( + WasmMockTarget.MinVirtualIP + WasmMockTarget.FunctionBeginAddress + 6, + context.InstructionPointer.Value); + Assert.Equal(savedStackPointer, context.FramePointer.Value); + } + + [Fact] + public void UpdateContextFromFrame_WasmTransitionFrameWithoutSavedR2RStack_UsesFallbackAndClearsFramePointer() + { + const ulong savedStackPointer = 0x0020_0000; + const uint nativeReturnAddress = 0x0004_2000; + (Target target, ulong transitionFrameAddress) = + WasmMockTarget.CreateWithR2RTransitionFrame( + savedStackPointer, + WasmMockTarget.FunctionTableIndex); + + Data.FramedMethodFrame framedMethodFrame = + target.ProcessedData.GetOrAdd(transitionFrameAddress); + target.Write(framedMethodFrame.TransitionBlockPtr, nativeReturnAddress); + target.WritePointer( + framedMethodFrame.TransitionBlockPtr + sizeof(uint), + TargetPointer.Null); + + ContextHolder context = new(); + FrameHelpers frameHelpers = new(target); + Data.Frame frame = target.ProcessedData.GetOrAdd(transitionFrameAddress); + frameHelpers.UpdateContextFromFrame(frame, context); + + Assert.Equal(nativeReturnAddress, context.InstructionPointer.Value); + Assert.Equal( + framedMethodFrame.TransitionBlockPtr.Value + Data.TransitionBlock.GetSize(target), + context.StackPointer.Value); + Assert.Equal(TargetPointer.Null, context.FramePointer); + } + // The WasmContext mirrors the native wasm T_CONTEXT (src/coreclr/pal/inc/pal.h): five // 32-bit slots (ContextFlags, InterpreterWalkFramePointer, InterpreterSP/FP/IP). Verify the // serialized size and that the synthetic first-argument register (InterpreterWalkFramePointer) diff --git a/src/native/managed/cdac/tests/UnitTests/WasmMockTarget.cs b/src/native/managed/cdac/tests/UnitTests/WasmMockTarget.cs new file mode 100644 index 00000000000000..ced73a0ffd5cbd --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/WasmMockTarget.cs @@ -0,0 +1,302 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Moq; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Builds a WASM carrying enough of the ReadyToRun function-table state for +/// WasmR2RInfo / WasmUnwinder to run, plus a real ReadyToRun frame in linear memory. +/// +internal static class WasmMockTarget +{ + // WASM is a 32-bit little-endian target. + private static readonly MockTarget.Architecture WasmArch = new() { IsLittleEndian = true, Is64Bit = false }; + + internal const uint MinFunctionTableIndex = 5; + internal const uint FunctionTableIndex = 5; // localIndex 0 + internal const uint FuncletFunctionTableIndex = 6; // localIndex 1 + internal const ulong MinVirtualIP = 0x0005_0000; + internal const ulong LoadedImageBase = 0x0090_0000; + internal const uint FunctionBeginAddress = 0x100; + internal const uint FrameSize = 0x20; + + // Where the synthetic CallFunclet terminator stores the establishing (method) frame pointer. + internal const ulong EstablishingFramePointer = 0x0030_0000; + + // TERMINATE_R2R_STACK_WALK, from src/coreclr/vm/wasm/callhelpers.hpp. + private const uint TerminateR2RStackWalk = 1; + + /// + /// Creates a target whose frame at belongs to + /// . When is set, the frame is a + /// funclet and the frame one above it is the synthetic CallFunclet + /// terminator carrying . + /// + internal static Target Create( + ulong frameAddress, + uint functionIndex, + bool isFunclet, + bool funcletCalledByParent = false, + CodeKind codeKind = CodeKind.ReadyToRun) + => CreateCore( + frameAddress, + functionIndex, + isFunclet, + funcletCalledByParent, + addR2RInlinedCallFrame: false, + codeKind: codeKind, + out _); + + internal static (Target Target, ulong InlinedCallFrameAddress) CreateWithR2RInlinedCallFrame( + ulong frameAddress, + uint functionIndex) + { + Target target = CreateCore( + frameAddress, + functionIndex, + isFunclet: false, + funcletCalledByParent: false, + addR2RInlinedCallFrame: true, + codeKind: CodeKind.ReadyToRun, + out ulong inlinedCallFrameAddress); + return (target, inlinedCallFrameAddress); + } + + internal static (Target Target, ulong TransitionFrameAddress) CreateWithR2RTransitionFrame( + ulong frameAddress, + uint functionIndex) + { + Target target = CreateCore( + frameAddress, + functionIndex, + isFunclet: false, + funcletCalledByParent: false, + addR2RInlinedCallFrame: false, + addR2RTransitionFrame: true, + codeKind: CodeKind.ReadyToRun, + out _, + out ulong transitionFrameAddress); + return (target, transitionFrameAddress); + } + + private static Target CreateCore( + ulong frameAddress, + uint functionIndex, + bool isFunclet, + bool funcletCalledByParent, + bool addR2RInlinedCallFrame, + CodeKind codeKind, + out ulong inlinedCallFrameAddress) + => CreateCore( + frameAddress, + functionIndex, + isFunclet, + funcletCalledByParent, + addR2RInlinedCallFrame, + addR2RTransitionFrame: false, + codeKind: codeKind, + out inlinedCallFrameAddress, + out _); + + private static Target CreateCore( + ulong frameAddress, + uint functionIndex, + bool isFunclet, + bool funcletCalledByParent, + bool addR2RInlinedCallFrame, + bool addR2RTransitionFrame, + CodeKind codeKind, + out ulong inlinedCallFrameAddress, + out ulong transitionFrameAddress) + { + inlinedCallFrameAddress = 0; + transitionFrameAddress = 0; + TargetTestHelpers helpers = new(WasmArch); + TestPlaceholderTarget.Builder targetBuilder = new(WasmArch); + MockMemorySpace.Builder builder = targetBuilder.MemoryBuilder; + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0018_0000); + + int hashMapStride = MockHashMap.CreateLayout(WasmArch).Size; + var moduleLayout = MockLoaderModule.CreateLayout(WasmArch); + var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride, isWasm: true); + var runtimeFunctionLayout = helpers.LayoutFields([ + new("BeginAddress", DataType.uint32), + new("UnwindData", DataType.uint32), + ]); + var rangeSectionLayout = helpers.LayoutFields([ + new("MinFunctionTableIndex", DataType.uint32), + new("NumRuntimeFunctions", DataType.uint32), + new("R2RModule", DataType.pointer), + new("Next", DataType.pointer), + ]); + + // The unwind blob is a ULEB128 fixed frame size. FrameSize is < 0x80 so it is one byte. + // It must live at a positive RVA from LoadedImageBase, since RuntimeFunction.UnwindData is + // an image-relative offset. + const uint UnwindDataRva = 0x40; + builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = LoadedImageBase + UnwindDataRva, + Data = [(byte)FrameSize], + Name = "UnwindData", + }); + + uint runtimeFunctionCount = funcletCalledByParent ? 2u : 1u; + MockMemorySpace.HeapFragment runtimeFuncFrag = allocator.Allocate( + runtimeFunctionLayout.Stride * runtimeFunctionCount, + "RuntimeFunctions"); + + void WriteRuntimeFunction(uint index, uint beginAddress, bool funclet) + { + int entryOffset = checked((int)(index * runtimeFunctionLayout.Stride)); + // RUNTIME_FUNCTION__IsFunclet: the funclet flag is the high bit of BeginAddress. + helpers.Write( + runtimeFuncFrag.Data.AsSpan().Slice( + entryOffset + runtimeFunctionLayout.Fields["BeginAddress"].Offset, + sizeof(uint)), + funclet ? beginAddress | 0x80000000u : beginAddress); + helpers.Write( + runtimeFuncFrag.Data.AsSpan().Slice( + entryOffset + runtimeFunctionLayout.Fields["UnwindData"].Offset, + sizeof(uint)), + UnwindDataRva); + } + + if (funcletCalledByParent) + { + WriteRuntimeFunction(0, FunctionBeginAddress, funclet: false); + WriteRuntimeFunction(1, FunctionBeginAddress + 0x20, funclet: true); + } + else + { + WriteRuntimeFunction(0, FunctionBeginAddress, isFunclet); + } + + MockReadyToRunInfo r2rInfo = r2rInfoLayout.Create(allocator.Allocate((ulong)r2rInfoLayout.Size, "ReadyToRunInfo")); + r2rInfo.CompositeInfo = r2rInfo.Address; + r2rInfo.NumRuntimeFunctions = runtimeFunctionCount; + r2rInfo.RuntimeFunctions = runtimeFuncFrag.Address; + r2rInfo.LoadedImageBase = LoadedImageBase; + r2rInfo.MinVirtualIP = MinVirtualIP; + + MockLoaderModule module = moduleLayout.Create(allocator.Allocate((ulong)moduleLayout.Size, "Module")); + module.ReadyToRunInfo = r2rInfo.Address; + + MockMemorySpace.HeapFragment sectionFrag = allocator.Allocate(rangeSectionLayout.Stride, "FunctionTableIndexRangeSection"); + var secFields = rangeSectionLayout.Fields; + helpers.Write(sectionFrag.Data.AsSpan().Slice(secFields["MinFunctionTableIndex"].Offset, sizeof(uint)), MinFunctionTableIndex); + helpers.Write( + sectionFrag.Data.AsSpan().Slice(secFields["NumRuntimeFunctions"].Offset, sizeof(uint)), + runtimeFunctionCount); + helpers.WritePointer(sectionFrag.Data.AsSpan().Slice(secFields["R2RModule"].Offset, helpers.PointerSize), module.Address); + helpers.WritePointer(sectionFrag.Data.AsSpan().Slice(secFields["Next"].Offset, helpers.PointerSize), 0ul); + + // The global points at a slot holding the list head, not at the head itself. + MockMemorySpace.HeapFragment slotFrag = allocator.Allocate((uint)helpers.PointerSize, "FunctionTableIndexRangeListSlot"); + helpers.WritePointer(slotFrag.Data.AsSpan().Slice(0, helpers.PointerSize), sectionFrag.Address); + + // The frame itself: [0] = function index, [4] = function-local virtual IP / 2. + byte[] frame = new byte[FrameSize]; + helpers.Write(frame.AsSpan(0, sizeof(uint)), functionIndex); + helpers.Write(frame.AsSpan(4, sizeof(uint)), 3u); + builder.AddHeapFragment(new MockMemorySpace.HeapFragment { Address = frameAddress, Data = frame, Name = "frame" }); + + if (funcletCalledByParent) + { + byte[] callerFrame = new byte[FrameSize]; + helpers.Write(callerFrame.AsSpan(0, sizeof(uint)), FunctionTableIndex); + helpers.Write(callerFrame.AsSpan(4, sizeof(uint)), 3u); + builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = frameAddress + FrameSize, + Data = callerFrame, + Name = "parentFrame", + }); + } + else if (isFunclet) + { + // Unwinding the funclet lands here: the synthetic CallFuncletWith[out]Throwable frame, + // which stores the establishing frame pointer one pointer-sized slot after the marker. + byte[] terminator = new byte[FrameSize]; + helpers.Write(terminator.AsSpan(0, sizeof(uint)), TerminateR2RStackWalk); + helpers.WritePointer(terminator.AsSpan((int)helpers.PointerSize, helpers.PointerSize), EstablishingFramePointer); + builder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = frameAddress + FrameSize, + Data = terminator, + Name = "callFuncletTerminator", + }); + } + + var types = new Dictionary + { + [DataType.RuntimeFunction] = new() { Fields = runtimeFunctionLayout.Fields, Size = runtimeFunctionLayout.Stride }, + [DataType.ReadyToRunInfo] = TargetTestHelpers.CreateTypeInfo(r2rInfoLayout), + [DataType.Module] = TargetTestHelpers.CreateTypeInfo(moduleLayout), + [DataType.FunctionTableIndexRangeSection] = new() { Fields = rangeSectionLayout.Fields, Size = rangeSectionLayout.Stride }, + }; + + if (addR2RInlinedCallFrame) + { + MockFrameBuilder frameBuilder = new(builder); + MockInlinedCallFrame inlinedFrame = frameBuilder.AddInlinedCallFrame( + callerReturnAddress: 1, // INLINED_PINVOKE_FROM_R2R + datum: 0, + callSiteSP: frameAddress, + calleeSavedFP: 0); + inlinedCallFrameAddress = inlinedFrame.Address; + + types.Add(DataType.Frame, TargetTestHelpers.CreateTypeInfo(frameBuilder.FrameLayout)); + types.Add(DataType.InlinedCallFrame, TargetTestHelpers.CreateTypeInfo(frameBuilder.InlinedCallFrameLayout)); + targetBuilder.AddGlobals( + ("InlinedCallFrameIdentifier", MockFrameBuilder.InlinedCallFrameIdentifierValue)); + } + + if (addR2RTransitionFrame) + { + MockFrameBuilder frameBuilder = new(builder); + Layout transitionBlockLayout = + MockWasmTransitionBlock.CreateLayout(WasmArch); + MockWasmTransitionBlock transitionBlock = transitionBlockLayout.Create( + allocator.Allocate((ulong)transitionBlockLayout.Size, "TransitionBlock")); + // Match the native lazy state: the return address starts at zero and is derived from + // the saved R2R stack pointer by FramedMethodFrame::GetTransitionBlock_Impl. + transitionBlock.ReturnAddress = 0; + transitionBlock.StackPointer = frameAddress; + + MockFramedMethodFrame transitionFrame = frameBuilder.AddFramedMethodFrame(methodDescPtr: 0); + transitionFrame.TransitionBlockPtr = transitionBlock.Address; + transitionFrameAddress = transitionFrame.Address; + + types.Add(DataType.Frame, TargetTestHelpers.CreateTypeInfo(frameBuilder.FrameLayout)); + types.Add(DataType.FramedMethodFrame, TargetTestHelpers.CreateTypeInfo(frameBuilder.FramedMethodFrameLayout)); + types.Add(DataType.TransitionBlock, TargetTestHelpers.CreateTypeInfo(transitionBlockLayout)); + targetBuilder.AddGlobals( + ("FramedMethodFrameIdentifier", MockFrameBuilder.FramedMethodFrameIdentifierValue)); + } + + Mock executionManager = new(); + executionManager + .Setup(e => e.GetCodeKind(It.IsAny())) + .Returns(codeKind); + + return targetBuilder + .AddTypes(types) + .AddGlobals( + ("FunctionTableIndexRangeList", slotFrag.Address), + ("WasmDebugRegisterTypeShift", 29), + ("WasmDebugValueTypeCount", 7)) + .AddGlobalStrings((Constants.Globals.Architecture, nameof(RuntimeInfoArchitecture.Wasm).ToLowerInvariant())) + .AddContract(version: "c1") + .AddContract(version: "c1") + .AddMockContract(executionManager.Object) + .AddMockContract(Mock.Of()) + .Build(); + } +} diff --git a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs index 4554ee3d5fa69e..ec09a0e19b4385 100644 --- a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using System.Reflection; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -21,10 +24,35 @@ public class WasmR2RInfoTests private const uint FunctionBeginAddress = 0x100; private const uint FunctionUnwindData = 0x40; + private static IStackDataFrameHandle CreateStackDataFrameHandle( + IPlatformAgnosticContext context, + StackWalkState state = StackWalkState.Frameless) + { + Type handleType = typeof(WasmR2RInfo).Assembly.GetType( + "Microsoft.Diagnostics.DataContractReader.Contracts.StackWalk_1+StackDataFrameHandle", + throwOnError: true)!; + return (IStackDataFrameHandle)Activator.CreateInstance( + handleType, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + [ + context, + state, + TargetPointer.Null, + default(ThreadData), + false, + false, + false, + false, + false, + ], + culture: null)!; + } + // Builds a target whose FunctionTableIndexRangeList global points at a *slot* (pointer-to-pointer), // matching the CDAC_GLOBAL_POINTER contract. WasmR2RInfo must dereference the slot to reach the // list head; walking from the slot address directly reads garbage and finds nothing. - private static TestPlaceholderTarget CreateTarget(bool emptyList = false) + private static TestPlaceholderTarget CreateTarget(bool emptyList = false, bool isFunclet = false) { TargetTestHelpers helpers = new(WasmArch); var targetBuilder = new TestPlaceholderTarget.Builder(WasmArch); @@ -46,7 +74,8 @@ private static TestPlaceholderTarget CreateTarget(bool emptyList = false) ]); var runtimeFuncFrag = allocator.Allocate(runtimeFunctionLayout.Stride, "RuntimeFunction"); - helpers.Write(runtimeFuncFrag.Data.AsSpan().Slice(runtimeFunctionLayout.Fields["BeginAddress"].Offset, sizeof(uint)), FunctionBeginAddress); + // RUNTIME_FUNCTION__IsFunclet: the funclet flag is the high bit of BeginAddress (clrnt.h). + helpers.Write(runtimeFuncFrag.Data.AsSpan().Slice(runtimeFunctionLayout.Fields["BeginAddress"].Offset, sizeof(uint)), isFunclet ? FunctionBeginAddress | 0x80000000u : FunctionBeginAddress); helpers.Write(runtimeFuncFrag.Data.AsSpan().Slice(runtimeFunctionLayout.Fields["UnwindData"].Offset, sizeof(uint)), FunctionUnwindData); MockReadyToRunInfo r2rInfo = r2rInfoLayout.Create(allocator.Allocate((ulong)r2rInfoLayout.Size, "ReadyToRunInfo")); @@ -103,6 +132,151 @@ public void TryGetUnwindData_ReturnsImageBasePlusUnwindData() Assert.Equal(LoadedImageBase + FunctionUnwindData, unwindData.Value); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TryGetFunctionIdentity_ReturnsOwningModuleRuntimeFunctionAndFuncletState(bool isFunclet) + { + WasmR2RInfo info = new(CreateTarget(isFunclet: isFunclet)); + + Assert.True(info.TryGetFunctionIdentity( + FunctionTableIndex, + out TargetPointer module, + out uint runtimeFunctionIndex, + out bool actualIsFunclet)); + + Assert.NotEqual(TargetPointer.Null, module); + Assert.Equal(0u, runtimeFunctionIndex); + Assert.Equal(isFunclet, actualIsFunclet); + } + + [Fact] + public void TryGetFunctionIdentity_IndexNotInAnySection_PreservesCallerOwnedRawIndex() + { + WasmR2RInfo info = new(CreateTarget()); + + Assert.False(info.TryGetFunctionIdentity( + FunctionTableIndex + 100, + out TargetPointer module, + out uint runtimeFunctionIndex, + out bool isFunclet)); + Assert.Equal(TargetPointer.Null, module); + Assert.Equal(0u, runtimeFunctionIndex); + Assert.False(isFunclet); + } + + [Theory] + [InlineData(false, FunctionTableIndex, 0u)] + [InlineData(true, FunctionTableIndex, 0u)] + public void StackWalk_GetWasmFunctionIdentity_ReturnsResolvedIdentity( + bool isFunclet, + uint functionTableIndex, + uint expectedRuntimeFunctionIndex) + { + const ulong FrameAddress = 0x0020_0000; + Target target = WasmMockTarget.Create(FrameAddress, functionTableIndex, isFunclet); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.StackPointer = new TargetPointer(FrameAddress); + context.InstructionPointer = new TargetCodePointer(MinVirtualIP + FunctionBeginAddress); + IStackDataFrameHandle frame = CreateStackDataFrameHandle(context); + + WasmFunctionIdentity identity = target.Contracts.StackWalk.GetWasmFunctionIdentity(frame); + + Assert.Equal(functionTableIndex, identity.FunctionTableIndex); + Assert.NotNull(identity.Module); + Assert.NotEqual(TargetPointer.Null, identity.Module.Value); + Assert.Equal(expectedRuntimeFunctionIndex, identity.RuntimeFunctionIndex); + Assert.Equal(isFunclet, identity.IsFunclet); + } + + [Fact] + public void StackWalk_GetWasmFunctionIdentity_UnregisteredIndexPreservesRawIdentity() + { + const ulong FrameAddress = 0x0020_0000; + const uint UnregisteredFunctionIndex = 0x1234; + Target target = WasmMockTarget.Create(FrameAddress, UnregisteredFunctionIndex, isFunclet: false); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.StackPointer = new TargetPointer(FrameAddress); + context.InstructionPointer = new TargetCodePointer(MinVirtualIP + FunctionBeginAddress); + IStackDataFrameHandle frame = CreateStackDataFrameHandle(context); + + WasmFunctionIdentity identity = target.Contracts.StackWalk.GetWasmFunctionIdentity(frame); + + Assert.Equal(UnregisteredFunctionIndex, identity.FunctionTableIndex); + Assert.Null(identity.Module); + Assert.Null(identity.RuntimeFunctionIndex); + Assert.Null(identity.IsFunclet); + } + + [Fact] + public void StackWalk_GetWasmFunctionIdentity_MissingStackPointerThrows() + { + Target target = WasmMockTarget.Create(0x0020_0000, FunctionTableIndex, isFunclet: false); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.InstructionPointer = new TargetCodePointer(MinVirtualIP + FunctionBeginAddress); + IStackDataFrameHandle frame = CreateStackDataFrameHandle(context); + + Assert.Throws( + () => target.Contracts.StackWalk.GetWasmFunctionIdentity(frame)); + } + + [Fact] + public void StackWalk_GetWasmFunctionIdentity_NativeMarkerRejectsFalseFrameBytes() + { + const ulong FrameAddress = 0x0020_0000; + Target target = WasmMockTarget.Create(FrameAddress, FunctionTableIndex, isFunclet: false); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.StackPointer = new TargetPointer(FrameAddress); + context.InstructionPointer = TargetCodePointer.Null; + IStackDataFrameHandle frame = CreateStackDataFrameHandle( + context, + StackWalkState.NativeMarker); + + Assert.Throws( + () => target.Contracts.StackWalk.GetWasmFunctionIdentity(frame)); + } + + [Fact] + public void StackWalk_GetWasmFunctionIdentity_InterpreterFrameRejectsShadowLikeBytes() + { + const ulong FrameAddress = 0x0020_0000; + Target target = WasmMockTarget.Create( + FrameAddress, + FunctionTableIndex, + isFunclet: false, + codeKind: CodeKind.Interpreter); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.StackPointer = new TargetPointer(FrameAddress); + context.InstructionPointer = new TargetCodePointer(MinVirtualIP + FunctionBeginAddress); + IStackDataFrameHandle frame = CreateStackDataFrameHandle(context); + + Assert.Throws( + () => target.Contracts.StackWalk.GetWasmFunctionIdentity(frame)); + } + + /// + /// Mirrors ExecutionManager::IsFuncletFunctionIndex, which reads + /// RUNTIME_FUNCTION__IsFunclet — the high bit of BeginAddress. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TryIsFunclet_ReadsHighBitOfBeginAddress(bool isFunclet) + { + WasmR2RInfo info = new(CreateTarget(isFunclet: isFunclet)); + + Assert.True(info.TryIsFunclet(FunctionTableIndex, out bool actual)); + Assert.Equal(isFunclet, actual); + } + + [Fact] + public void TryIsFunclet_IndexNotInAnySection_ReturnsFalse() + { + WasmR2RInfo info = new(CreateTarget()); + + Assert.False(info.TryIsFunclet(FunctionTableIndex + 100, out _)); + } + [Fact] public void TryGetVirtualIPBase_IndexNotInAnySection_ReturnsFalse() { diff --git a/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs index cd215f35489e87..6a48d5a845d922 100644 --- a/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Moq; using Xunit; namespace Microsoft.Diagnostics.DataContractReader.Tests; @@ -27,6 +31,7 @@ private sealed class FakeWasmR2RInfo : IWasmR2RInfo { public Dictionary VirtualIpBases { get; } = new(); public Dictionary UnwindData { get; } = new(); + public HashSet Funclets { get; } = new(); public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP) => VirtualIpBases.TryGetValue(functionTableIndex, out baseVirtualIP); @@ -41,13 +46,29 @@ public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDa unwindDataAddress = TargetPointer.Null; return false; } + + // A function index is "known" here if it has unwind data or a virtual IP base registered. + public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet) + { + isFunclet = Funclets.Contains(functionTableIndex); + return UnwindData.ContainsKey(functionTableIndex) + || VirtualIpBases.ContainsKey(functionTableIndex) + || isFunclet; + } } - private static TestPlaceholderTarget CreateTarget(MockMemorySpace.HeapFragment[] fragments) + private static TestPlaceholderTarget CreateTarget( + MockMemorySpace.HeapFragment[] fragments, + IExecutionManager? executionManager = null, + IGCInfo? gcInfo = null) { TestPlaceholderTarget.Builder builder = new(WasmArch); foreach (MockMemorySpace.HeapFragment fragment in fragments) builder.MemoryBuilder.AddHeapFragment(fragment); + if (executionManager is not null) + builder.AddMockContract(executionManager); + if (gcInfo is not null) + builder.AddMockContract(gcInfo); return builder.Build(); } @@ -64,6 +85,51 @@ private static MockMemorySpace.HeapFragment Frame(ulong address, uint functionIn private static MockMemorySpace.HeapFragment Blob(ulong address, byte[] uleb128, string name) => new() { Address = address, Data = uleb128, Name = name }; + [Fact] + public void GCInfo_WasmUsesPlatformDecoder() + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(WasmArch) + .AddGlobalStrings((Constants.Globals.Architecture, "wasm")) + .AddContract(version: "c1") + .AddContract(version: "c1") + .Build(); + + IGCInfoHandle handle = target.Contracts.GCInfo.DecodePlatformSpecificGCInfo( + new TargetPointer(BlobsBase), gcVersion: 5); + + Assert.Contains("Wasm32GCInfoTraits", handle.GetType().FullName); + } + + [Fact] + public void GCInfo_WasmTraitsMatchNativeEncoding() + { + Assert.Equal(6, Wasm32GCInfoTraits.GENERICS_INST_CONTEXT_STACK_SLOT_ENCBASE); + Assert.Equal(6, Wasm32GCInfoTraits.GS_COOKIE_STACK_SLOT_ENCBASE); + Assert.Equal(6, Wasm32GCInfoTraits.CODE_LENGTH_ENCBASE); + Assert.Equal(3, Wasm32GCInfoTraits.STACK_BASE_REGISTER_ENCBASE); + Assert.Equal(6, Wasm32GCInfoTraits.SIZE_OF_STACK_AREA_ENCBASE); + Assert.Equal(3, Wasm32GCInfoTraits.SIZE_OF_EDIT_AND_CONTINUE_PRESERVED_AREA_ENCBASE); + Assert.Equal(6, Wasm32GCInfoTraits.REVERSE_PINVOKE_FRAME_ENCBASE); + Assert.Equal(3, Wasm32GCInfoTraits.NUM_REGISTERS_ENCBASE); + Assert.Equal(5, Wasm32GCInfoTraits.NUM_STACK_SLOTS_ENCBASE); + Assert.Equal(5, Wasm32GCInfoTraits.NUM_UNTRACKED_SLOTS_ENCBASE); + Assert.Equal(4, Wasm32GCInfoTraits.NORM_PROLOG_SIZE_ENCBASE); + Assert.Equal(3, Wasm32GCInfoTraits.NORM_EPILOG_SIZE_ENCBASE); + Assert.Equal(5, Wasm32GCInfoTraits.INTERRUPTIBLE_RANGE_DELTA1_ENCBASE); + Assert.Equal(5, Wasm32GCInfoTraits.INTERRUPTIBLE_RANGE_DELTA2_ENCBASE); + Assert.Equal(3, Wasm32GCInfoTraits.REGISTER_ENCBASE); + Assert.Equal(3, Wasm32GCInfoTraits.REGISTER_DELTA_ENCBASE); + Assert.Equal(6, Wasm32GCInfoTraits.STACK_SLOT_ENCBASE); + Assert.Equal(4, Wasm32GCInfoTraits.STACK_SLOT_DELTA_ENCBASE); + Assert.Equal(4, Wasm32GCInfoTraits.NUM_SAFE_POINTS_ENCBASE); + Assert.Equal(1, Wasm32GCInfoTraits.NUM_INTERRUPTIBLE_RANGES_ENCBASE); + Assert.False(Wasm32GCInfoTraits.HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA); + Assert.Equal(7u, Wasm32GCInfoTraits.DenormalizeStackBaseRegister(7)); + Assert.Equal(0x24, Wasm32GCInfoTraits.DenormalizeStackSlot(0x24)); + Assert.Equal(0x18Cu, Wasm32GCInfoTraits.DenormalizeCodeOffset(0x18C)); + Assert.Equal(0x2B1u, Wasm32GCInfoTraits.DenormalizeCodeLength(0x2B1)); + } + [Fact] public void TryGetFramePointer_NormalFrame_ReturnsSelf() { @@ -173,6 +239,29 @@ public void TryUnwindOneFrame_AdvancesBySingleByteFrameSize_AndYieldsCallerVirtu Assert.Equal(VirtualIpBase + 14, ip.Value); // caller local VIP 7*2 } + [Fact] + public void TryUnwindOneFrame_CallerWithoutVirtualIp_PreservesCallerStackPointer() + { + const uint leafFrameSize = 0x20; + ulong callerBase = FramesBase + leafFrameSize; + + FakeWasmR2RInfo info = new(); + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 3, "leaf"), + Frame(callerBase, FuncIndexCaller, 7, "nonR2RCaller"), + Blob(BlobsBase, [(byte)leafFrameSize], "leafUnwind"), + ]); + WasmUnwinder unwinder = new(target, info); + + TargetPointer sp = new(FramesBase); + Assert.True(unwinder.TryUnwindOneFrame(ref sp, out TargetCodePointer ip)); + Assert.Equal(callerBase, sp.Value); + Assert.Equal(TargetCodePointer.Null, ip); + } + [Fact] public void TryUnwindOneFrame_DecodesMultiByteFrameSize() { @@ -197,6 +286,59 @@ public void TryUnwindOneFrame_DecodesMultiByteFrameSize() Assert.Equal(callerBase, sp.Value); } + [Fact] + public void TryUnwindOneFrame_ReversePInvoke_DoesNotProbeNativeCallerAsShadowFrame() + { + const uint frameSize = 0x20; + const ulong controlPc = VirtualIpBase + 6; + ulong nativeCallerSp = FramesBase + frameSize; + + FakeWasmR2RInfo info = new(); + info.UnwindData[FuncIndexLeaf] = BlobsBase; + // Deliberately make native caller bytes look like a valid R2R frame. Without the + // reverse-P/Invoke guard, GetVirtualIP would manufacture this false managed caller. + info.VirtualIpBases[FuncIndexCaller] = VirtualIpBase + 0x100; + + Mock executionManager = new(); + executionManager + .Setup(e => e.GetCodeBlockHandle(new TargetCodePointer(controlPc))) + .Returns(new CodeBlockHandle(new TargetPointer(0x30000))); + TargetPointer gcInfoAddress = new(0x40000); + uint gcVersion = 5; + executionManager + .Setup(e => e.GetGCInfo( + It.IsAny(), + out gcInfoAddress, + out gcVersion)); + + IGCInfoHandle gcInfoHandle = Mock.Of(); + Mock gcInfo = new(); + gcInfo + .Setup(g => g.DecodePlatformSpecificGCInfo(gcInfoAddress, gcVersion)) + .Returns(gcInfoHandle); + gcInfo + .Setup(g => g.GetHeader(gcInfoHandle)) + .Returns(default(GCInfoHeader) with { HasReversePInvokeFrame = true }); + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 3, "reversePInvoke"), + Frame(nativeCallerSp, FuncIndexCaller, 2, "nativeBytesResemblingR2R"), + Blob(BlobsBase, [(byte)frameSize], "frameSize"), + ], + executionManager.Object, + gcInfo.Object); + WasmUnwinder unwinder = new(target, info); + + TargetPointer sp = new(FramesBase); + Assert.True(unwinder.TryUnwindOneFrame( + ref sp, + new TargetCodePointer(controlPc), + out TargetCodePointer ip)); + Assert.Equal(nativeCallerSp, sp.Value); + Assert.Equal(TargetCodePointer.Null, ip); + } + [Fact] public void TryUnwindOneFrame_AtTerminator_ReturnsFalse() { @@ -264,4 +406,226 @@ public void TryUnwindOneFrame_MalformedUleb128_Throws() } private const uint StackWalkSentinelIndirect = 0; + private const uint StackWalkSentinelTerminate = 1; + + private const uint FuncIndexFunclet = 12; + private const uint FuncIndexNestedFunclet = 13; + + // A ULEB128-encoded frame size, used as the whole unwind blob. + private static byte[] FrameSize(uint size) + { + List bytes = new(); + do + { + byte b = (byte)(size & 0x7F); + size >>= 7; + if (size != 0) + b |= 0x80; + bytes.Add(b); + } + while (size != 0); + return bytes.ToArray(); + } + + [Fact] + public void TryGetLogicalFramePointer_RootFunction_ReturnsOwnFrameBase() + { + FakeWasmR2RInfo info = new(); + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexLeaf, 3, "root"), + Blob(BlobsBase, FrameSize(0x20), "rootFrameSize"), + ]); + WasmUnwinder unwinder = new(target, info); + + Assert.True(unwinder.TryGetLogicalFramePointer(new TargetPointer(FramesBase), out TargetPointer fp)); + Assert.Equal(FramesBase, fp.Value); + } + + /// + /// A funclet called directly by its containing method: unwinding out of the funclet lands on + /// the method's own frame, whose base is the establishing frame pointer. + /// + [Fact] + public void TryGetLogicalFramePointer_FuncletCalledByParent_ReturnsParentFrameBase() + { + const uint FuncletFrameSize = 0x20; + ulong parentFrame = FramesBase + FuncletFrameSize; + + FakeWasmR2RInfo info = new(); + info.Funclets.Add(FuncIndexFunclet); + info.UnwindData[FuncIndexFunclet] = BlobsBase; + info.UnwindData[FuncIndexCaller] = BlobsBase + 0x10; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexFunclet, 1, "funclet"), + Frame(parentFrame, FuncIndexCaller, 5, "parent"), + Blob(BlobsBase, FrameSize(FuncletFrameSize), "funcletFrameSize"), + Blob(BlobsBase + 0x10, FrameSize(0x40), "parentFrameSize"), + ]); + WasmUnwinder unwinder = new(target, info); + + Assert.True(unwinder.TryGetLogicalFramePointer(new TargetPointer(FramesBase), out TargetPointer fp)); + Assert.Equal(parentFrame, fp.Value); + } + + [Fact] + public void WasmContext_UnwindFromFuncletCalledByParent_SetsCallerStackInstructionAndFramePointers() + { + ulong funcletFrame = FramesBase; + ulong parentFrame = FramesBase + WasmMockTarget.FrameSize; + Target target = WasmMockTarget.Create( + funcletFrame, + WasmMockTarget.FuncletFunctionTableIndex, + isFunclet: true, + funcletCalledByParent: true); + + WasmR2RInfo r2rInfo = new(target); + Assert.True(r2rInfo.TryGetFunctionIdentity( + WasmMockTarget.FuncletFunctionTableIndex, + out TargetPointer module, + out uint runtimeFunctionIndex, + out bool isFunclet)); + Assert.NotEqual(TargetPointer.Null, module); + Assert.Equal(1u, runtimeFunctionIndex); + Assert.True(isFunclet); + + // The funclet and its root resolve to the same virtual-IP base. This exercises the + // backward walk over two real RUNTIME_FUNCTION entries without treating the fixture's + // synthetic MinVirtualIP as captured-world truth. + Assert.True(r2rInfo.TryGetVirtualIPBase( + WasmMockTarget.FunctionTableIndex, + out ulong rootVirtualIpBase)); + Assert.True(r2rInfo.TryGetVirtualIPBase( + WasmMockTarget.FuncletFunctionTableIndex, + out ulong funcletVirtualIpBase)); + Assert.Equal(rootVirtualIpBase, funcletVirtualIpBase); + + WasmContext context = new() + { + StackPointer = new TargetPointer(funcletFrame), + InstructionPointer = new TargetCodePointer(funcletVirtualIpBase + 6), + FramePointer = new TargetPointer(funcletFrame), + }; + + context.Unwind(target); + + Assert.Equal(parentFrame, context.StackPointer.Value); + Assert.Equal(rootVirtualIpBase + 6, context.InstructionPointer.Value); + Assert.Equal(parentFrame, context.FramePointer.Value); + } + + /// + /// A funclet invoked by the VM through CallFuncletWith[out]Throwable: unwinding terminates at + /// the synthetic TERMINATE_R2R_STACK_WALK frame, which carries the establishing frame pointer + /// one pointer-sized slot after the marker. + /// + [Fact] + public void TryGetLogicalFramePointer_FuncletCalledByVM_RecoversEstablishingFramePointer() + { + const uint FuncletFrameSize = 0x20; + ulong terminatorFrame = FramesBase + FuncletFrameSize; + ulong establishingFp = FramesBase + 0x1000; + + TargetTestHelpers helpers = new(WasmArch); + byte[] terminator = new byte[16]; + helpers.Write(terminator.AsSpan(0, sizeof(uint)), StackWalkSentinelTerminate); + helpers.WritePointer(terminator.AsSpan((int)helpers.PointerSize, helpers.PointerSize), establishingFp); + + FakeWasmR2RInfo info = new(); + info.Funclets.Add(FuncIndexFunclet); + info.UnwindData[FuncIndexFunclet] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexFunclet, 1, "funclet"), + new MockMemorySpace.HeapFragment { Address = terminatorFrame, Data = terminator, Name = "terminator" }, + Blob(BlobsBase, FrameSize(FuncletFrameSize), "funcletFrameSize"), + ]); + WasmUnwinder unwinder = new(target, info); + + Assert.True(unwinder.TryGetLogicalFramePointer(new TargetPointer(FramesBase), out TargetPointer fp)); + Assert.Equal(establishingFp, fp.Value); + } + + /// + /// A handler nested inside another funclet unwinds through both before reaching the method. + /// + [Fact] + public void TryGetLogicalFramePointer_NestedFunclets_WalksOutToMethod() + { + const uint InnerFrameSize = 0x10; + const uint OuterFrameSize = 0x20; + ulong outerFunclet = FramesBase + InnerFrameSize; + ulong parentFrame = outerFunclet + OuterFrameSize; + + FakeWasmR2RInfo info = new(); + info.Funclets.Add(FuncIndexNestedFunclet); + info.Funclets.Add(FuncIndexFunclet); + info.UnwindData[FuncIndexNestedFunclet] = BlobsBase; + info.UnwindData[FuncIndexFunclet] = BlobsBase + 0x10; + info.UnwindData[FuncIndexCaller] = BlobsBase + 0x20; + + TestPlaceholderTarget target = CreateTarget( + [ + Frame(FramesBase, FuncIndexNestedFunclet, 1, "innerFunclet"), + Frame(outerFunclet, FuncIndexFunclet, 2, "outerFunclet"), + Frame(parentFrame, FuncIndexCaller, 5, "parent"), + Blob(BlobsBase, FrameSize(InnerFrameSize), "innerFrameSize"), + Blob(BlobsBase + 0x10, FrameSize(OuterFrameSize), "outerFrameSize"), + Blob(BlobsBase + 0x20, FrameSize(0x40), "parentFrameSize"), + ]); + WasmUnwinder unwinder = new(target, info); + + Assert.True(unwinder.TryGetLogicalFramePointer(new TargetPointer(FramesBase), out TargetPointer fp)); + Assert.Equal(parentFrame, fp.Value); + } + + /// + /// A localloc frame indirects to its real base before anything else is read, so the logical + /// frame pointer must be the indirected base rather than the stack pointer. + /// + [Fact] + public void TryGetLogicalFramePointer_LocallocRootFunction_ReturnsIndirectedBase() + { + TargetTestHelpers helpers = new(WasmArch); + ulong realFp = FramesBase + 0x100; + + byte[] indirect = new byte[16]; + helpers.Write(indirect.AsSpan(0, sizeof(uint)), StackWalkSentinelIndirect); + helpers.WritePointer(indirect.AsSpan((int)helpers.PointerSize, helpers.PointerSize), realFp); + + FakeWasmR2RInfo info = new(); + info.UnwindData[FuncIndexLeaf] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget( + [ + new MockMemorySpace.HeapFragment { Address = FramesBase, Data = indirect, Name = "locallocSp" }, + Frame(realFp, FuncIndexLeaf, 3, "realFrame"), + Blob(BlobsBase, FrameSize(0x20), "frameSize"), + ]); + WasmUnwinder unwinder = new(target, info); + + Assert.True(unwinder.TryGetLogicalFramePointer(new TargetPointer(FramesBase), out TargetPointer fp)); + Assert.Equal(realFp, fp.Value); + } + + [Fact] + public void TryGetFunctionIndex_ReportsIndexAndFuncletFlag() + { + FakeWasmR2RInfo info = new(); + info.Funclets.Add(FuncIndexFunclet); + info.UnwindData[FuncIndexFunclet] = BlobsBase; + + TestPlaceholderTarget target = CreateTarget([Frame(FramesBase, FuncIndexFunclet, 1, "funclet")]); + WasmUnwinder unwinder = new(target, info); + + Assert.True(unwinder.TryGetFunctionIndex(new TargetPointer(FramesBase), out uint functionIndex)); + Assert.Equal(FuncIndexFunclet, functionIndex); + Assert.True(info.TryIsFunclet(functionIndex, out bool isFunclet)); + Assert.True(isFunclet); + } } From b9f7b830b04cf144eb65f3973df0fb8fd7993cda Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 12:47:48 -0500 Subject: [PATCH 13/14] [cdac][wasm] Decode ReadyToRun variable locations Decode the WASM register representation produced by ReadyToRun debug info as symbolic WasmLocal/WasmLocalPair locations with an encoding-specific value type. Engine-owned locals remain symbolic; stack locations resolve from the logical frame pointer already maintained by the StackWalk context. Advertise the register type shift and supported value count as WASM cDAC data descriptor globals. Require and validate those values before reading a non-empty variable stream so a reader/target format mismatch fails explicitly instead of plausibly decoding the wrong local or type. Reject malformed packed registers and impossible shift/count combinations. Keep current stack encoding exact: base register 2 means context FP. Remove speculative decoded stack-base locals whose values the reader could not honor. Preserve the original packed VarLoc through DacDbi for consumers that need the ICorDebugInfo representation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 73 ++- .../Contracts/IDebugInfo.cs | 67 +++ .../Constants.cs | 2 + .../Contracts/DebugInfo/DebugInfoHelpers.cs | 107 +++- .../Contracts/DebugInfo/DebugInfo_1.cs | 44 +- .../ClrDataFrame.cs | 68 ++- .../Dbi/DacDbiImpl.NativeCodeInfo.cs | 9 + .../tests/UnitTests/WasmDebugInfoTests.cs | 516 ++++++++++++++++++ 8 files changed, 869 insertions(+), 17 deletions(-) create mode 100644 src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index f079e1d7e084a3..7c7c28d979cec5 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -44,7 +44,10 @@ _None._ ### Global variables used -_None._ +| Global | Type | Meaning | +| --- | --- | --- | +| `WasmDebugRegisterTypeShift` | `uint8` | Bit position at which the JIT debug-register encoding stores its WASM value type | +| `WasmDebugValueTypeCount` | `uint8` | Number of values in the JIT's WASM debug-register value-type encoding | ### Contracts used @@ -173,6 +176,30 @@ public enum DebugVarLocKind DoubleStack, FloatingPointStack, FixedVarArg, + // The variable lives in a WebAssembly local (see "WebAssembly Variable Locations" below). + WasmLocal, + // The variable spans two WebAssembly locals. + WasmLocalPair, +} + +// A value type in the JIT's WebAssembly debug-register encoding. +public enum WasmDebugValueType : uint +{ + Invalid = 0, + I32 = 1, + I64 = 2, + F32 = 3, + F64 = 4, + V128 = 5, + ExnRef = 6, + Count = 7, +} + +// Identifies a WebAssembly local by index and value type. +public readonly struct WasmLocalInfo +{ + public uint Index { get; init; } + public WasmDebugValueType ValueType { get; init; } } public readonly struct DebugVarInfo @@ -192,6 +219,9 @@ public readonly struct DebugVarInfo public uint FloatingPointStackRegister { get; init; } public uint FixedVarArgOffset { get; init; } public uint CallReturnValueILOffset { get; init; } + // WASM only; null on every other architecture. + public WasmLocalInfo? WasmLocal { get; init; } + public WasmLocalInfo? WasmLocal2 { get; init; } } // Given a code pointer, return the variable location info for the method. @@ -284,6 +314,47 @@ logical frame address from a hardcoded `$varN`, and the producer does not advert local indices. A future producer that changes stack records away from base `2` requires a coordinated, fail-loud reader update. +### WebAssembly Variable Locations + +On WASM the contract therefore reports: + +* `VLT_REG` / `VLT_REG_BYREF` as `DebugVarLocKind.WasmLocal`, and `VLT_REG_REG` as + `DebugVarLocKind.WasmLocalPair`. WASM locals are engine-private frame state: they are not in + linear memory and cannot be read through the data target, so the contract names them rather than + resolving them. A consumer attached to the WASM engine (for example over the Chrome DevTools + Protocol) can fetch the value from the local index. +* Stack-based kinds unchanged, because their storage is linear memory and is readable. Only the + base register differs, as described below. + +The frame pointer to add the offset to is the *logical* frame pointer, which for a method's root +function is its own frame base and for a funclet is the parent method's frame base. +`WasmContext.Unwind` and wasm explicit-frame seeding populate it by mirroring +`RtlVirtualUnwind` / `GetWasmFramePointerFromStackPointer` in +`src/coreclr/vm/wasm/helpers.cpp`; variable resolution consumes that context value rather than +independently unwinding again. + +The JIT never emits `VLT_REG_FP` or `VLT_FPSTK` on WASM; `f32` and `f64` locals are reported as +`VLT_REG` with the value type in the packed bits, so `IsFloatingPoint` is not set for them. + +`DebugVarInfo` exposes decoded register locations in `WasmLocal` and `WasmLocal2`, corresponding to +`Register` and `Register2`. Both are null on every other architecture. Stack base register `2` is +not a WASM local descriptor. + +The managed cDAC reader reports engine-private WASM register locals with no native locations. +Existing `ClrDataValue` behavior then reports those values unavailable (`GetNumLocations == 0`, +`GetBytes`/`GetAddress` fail) instead of fabricating a register value of zero. + +There is no native DAC build on WASM, so live `_legacyImpl` comparison assertions cannot validate +this representation. DacDbi coverage is structural managed unit coverage. + +WASM local index spaces are per function, and a method's funclets are separate WASM functions from +its root (`WasmRegAlloc` in `src/coreclr/jit/regallocwasm.cpp`). Variable ranges, by contrast, are +method-relative, and a funclet's virtual IP is rebased onto its root function. A consumer must +therefore establish which WASM function the current virtual IP belongs to before interpreting a +local index. `GetMethodVarInfo` obtains the method-relative offset from the ReadyToRun CodeBlock's +`ExecutionManager.GetRelativeOffset` result; it does not validate the portable-entrypoint +`MethodDesc` through `CodeVersions`. + ### Async Suspension Point APIs We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs index f40f8479efb53d..5b72b380c1477c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs @@ -48,6 +48,62 @@ public enum DebugVarLocKind DoubleStack, FloatingPointStack, FixedVarArg, + /// + /// The variable lives in a WebAssembly local. WASM locals are engine-private frame state: + /// they are not in linear memory and cannot be read through the data target. Consumers with + /// access to the WASM engine (for example a debugger attached over the Chrome DevTools + /// Protocol) can fetch the value using . + /// + WasmLocal, + /// + /// The variable spans two WebAssembly locals, described by + /// and . + /// + WasmLocalPair, +} + +/// +/// A value type in the JIT's WebAssembly debug-register encoding. +/// This is encoding vocabulary, not the WebAssembly specification's complete or stable type set. +/// +public enum WasmDebugValueType : uint +{ + /// Reserved so that small packed values can encode pseudo-registers. + Invalid = 0, + I32 = 1, + I64 = 2, + F32 = 3, + F64 = 4, + V128 = 5, + ExnRef = 6, + Count = 7, +} + +/// +/// Identifies a WebAssembly local by index and value type. +/// +/// +/// +/// On WASM, RyuJIT has no physical registers. It packs a (local index, value type) tuple +/// into regNumber using the target-described WasmDebugRegisterTypeShift +/// (MakeWasmReg in src/coreclr/jit/registeropswasm.cpp). That packed value is what +/// lands in the ICorDebugInfo variable-location stream. is the index +/// the emitted local.get / local.set instructions use directly, so it can be handed +/// to a WASM engine as-is after selecting the correct module/function. +/// +/// +/// WASM local index spaces are per function, and a method's funclets are separate WASM functions +/// from its root (see WasmRegAlloc in src/coreclr/jit/regallocwasm.cpp). Variable +/// ranges, by contrast, are method-relative. A consumer must therefore know which WASM function +/// the current virtual IP belongs to before interpreting . +/// +/// +public readonly struct WasmLocalInfo +{ + /// The WASM local index, as used by local.get / local.set. + public uint Index { get; init; } + /// The JIT debug-encoding value type of the local. + public WasmDebugValueType ValueType { get; init; } } /// @@ -84,6 +140,17 @@ public readonly struct DebugVarInfo /// the call site whose return value this entry describes. Zero for all other entries. /// public uint CallReturnValueILOffset { get; init; } + + /// + /// On WASM, decoded into a local index and value type. + /// Null on every other architecture. + /// + public WasmLocalInfo? WasmLocal { get; init; } + /// + /// On WASM, decoded into a local index and value type. + /// Null on every other architecture. + /// + public WasmLocalInfo? WasmLocal2 { get; init; } } /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 0cdf1d40014021..ad14e7994566b8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -14,6 +14,8 @@ public static class Globals public const string FinalizerThread = nameof(FinalizerThread); public const string VirtualIPRangeList = nameof(VirtualIPRangeList); public const string FunctionTableIndexRangeList = nameof(FunctionTableIndexRangeList); + public const string WasmDebugRegisterTypeShift = nameof(WasmDebugRegisterTypeShift); + public const string WasmDebugValueTypeCount = nameof(WasmDebugValueTypeCount); public const string GCThread = nameof(GCThread); public const string Debugger = nameof(Debugger); public const string DebuggerPatchTable = nameof(DebuggerPatchTable); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs index 5b52d110947a98..b65fdfc3ceeb94 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs @@ -15,6 +15,40 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; /// internal static class DebugInfoHelpers { + internal readonly struct WasmDebugInfoEncoding + { + public byte RegisterTypeShift { get; } + public byte ValueTypeCount { get; } + + public WasmDebugInfoEncoding(byte registerTypeShift, byte valueTypeCount) + { + if (registerTypeShift is 0 or >= 32) + throw new NotSupportedException($"Unsupported WASM debug register type shift {registerTypeShift}."); + if (valueTypeCount != (byte)WasmDebugValueType.Count) + throw new NotSupportedException( + $"Unsupported WASM debug value type count {valueTypeCount}; reader supports {(byte)WasmDebugValueType.Count}."); + uint encodableTypeCount = 1u << (32 - registerTypeShift); + if (encodableTypeCount < valueTypeCount) + throw new NotSupportedException( + $"WASM debug register type shift {registerTypeShift} cannot encode {valueTypeCount} value types."); + + RegisterTypeShift = registerTypeShift; + ValueTypeCount = valueTypeCount; + } + } + + internal static WasmDebugInfoEncoding GetWasmDebugInfoEncoding(Target target) + { + if (!target.TryReadGlobal(Constants.Globals.WasmDebugRegisterTypeShift, out byte? registerTypeShift) || + !target.TryReadGlobal(Constants.Globals.WasmDebugValueTypeCount, out byte? valueTypeCount)) + { + throw new InvalidOperationException( + "The target does not describe its WASM variable debug-register encoding."); + } + + return new WasmDebugInfoEncoding(registerTypeShift.Value, valueTypeCount.Value); + } + /// /// Mirrors ICorDebugInfo::VarLocType from cordebuginfo.h. /// Describes how a variable is stored at a particular point in native code. @@ -109,7 +143,10 @@ internal static IEnumerable DoBounds(NativeReader nativeReader, u /// public entries directly. /// Mirrors the native DoNativeVarInfo/TransferReader logic from debuginfostore.cpp. /// - internal static IEnumerable DoVars(NativeReader nativeReader, bool isX86) + internal static IEnumerable DoVars( + NativeReader nativeReader, + bool isX86, + WasmDebugInfoEncoding? wasmEncoding = null) { NibbleReader reader = new(nativeReader, 0); @@ -137,7 +174,7 @@ internal static IEnumerable DoVars(NativeReader nativeReader, bool if (locType is VarLocType.VLT_INVALID or VarLocType.VLT_COUNT) continue; - yield return locType switch + DebugVarInfo info = locType switch { VarLocType.VLT_REG => new DebugVarInfo { @@ -199,7 +236,73 @@ internal static IEnumerable DoVars(NativeReader nativeReader, bool StartOffset = startOffset, EndOffset = endOffset, VarNumber = varNumber, CallReturnValueILOffset = callReturnValueILOffset, }, }; + + yield return wasmEncoding is WasmDebugInfoEncoding encoding + ? ApplyWasmProjection(info, encoding) + : info; + } + } + + /// + /// Decodes a packed WASM regNumber into a local index and value type. Returns null for + /// values that do not name a local, which is the reserved pseudo-register range + /// (PC, REGNUM_COUNT, REGNUM_AMBIENT_SP) and any out-of-range value type. + /// + internal static WasmLocalInfo? DecodeWasmRegister( + uint packedRegister, + WasmDebugInfoEncoding encoding) + { + uint valueType = packedRegister >> encoding.RegisterTypeShift; + if (valueType is (uint)WasmDebugValueType.Invalid || + valueType >= encoding.ValueTypeCount) + { + return null; } + + return new WasmLocalInfo + { + Index = packedRegister & ((1u << encoding.RegisterTypeShift) - 1), + ValueType = (WasmDebugValueType)valueType, + }; + } + + /// + /// Reinterprets an architecture-neutral for WASM, where the JIT's + /// "registers" are packed WASM local descriptors rather than physical registers. + /// + private static DebugVarInfo ApplyWasmProjection( + DebugVarInfo info, + WasmDebugInfoEncoding encoding) + { + WasmLocalInfo? local = DecodeWasmRegister(info.Register, encoding); + WasmLocalInfo? local2 = DecodeWasmRegister(info.Register2, encoding); + + // Promote the kinds whose storage is entirely WASM locals, so that consumers written + // against a real register file cannot mistake them for readable registers. Stack-based + // kinds keep their kind: their storage is linear memory, which is readable; only the + // base register is a WASM local. + DebugVarLocKind kind = info.Kind switch + { + DebugVarLocKind.Register => local is not null + ? DebugVarLocKind.WasmLocal + : throw new InvalidOperationException( + $"Invalid WASM debug register encoding 0x{info.Register:X8}."), + DebugVarLocKind.RegisterRegister => local is not null && local2 is not null + ? DebugVarLocKind.WasmLocalPair + : throw new InvalidOperationException( + $"Invalid WASM debug register pair encoding 0x{info.Register:X8}, 0x{info.Register2:X8}."), + DebugVarLocKind.RegisterStack or DebugVarLocKind.StackRegister when local is null => + throw new InvalidOperationException( + $"Invalid WASM debug register encoding 0x{info.Register:X8}."), + _ => info.Kind, + }; + + return info with + { + Kind = kind, + WasmLocal = local, + WasmLocal2 = local2, + }; } private static int ReadEncodedStackOffset(NibbleReader reader, bool isX86) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs index 14d78f9f09e4ae..090c21bd3cb624 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_1.cs @@ -123,17 +123,7 @@ IEnumerable IDebugInfo.GetMethodVarInfo(TargetCodePointer pCode, o throw new InvalidOperationException($"No CodeBlockHandle found for native code {pCode}."); TargetPointer debugInfo = _eman.GetDebugInfo(cbh, out bool _); - // Compute code offset from the method's native code entry point, not from the code block start. - // GetStartAddress returns the start of the current code block (which may be a funclet for exception - // handlers). Variable location offsets are always relative to the method entry point, so we must use - // GetNativeCode from the NativeCodeVersion, matching the native DAC's GetMethodVarInfo which uses - // NativeCodeVersion::GetNativeCode() for this purpose - ICodeVersions cv = _target.Contracts.CodeVersions; - NativeCodeVersionHandle ncvh = cv.GetNativeCodeVersionForIP(pCode); - if (!ncvh.Valid) - throw new InvalidOperationException($"No NativeCodeVersion found for native code {pCode}."); - TargetCodePointer nativeCodeStart = cv.GetNativeCode(ncvh); - codeOffset = (uint)(CodePointerUtils.AddressFromCodePointer(pCode, _target) - CodePointerUtils.AddressFromCodePointer(nativeCodeStart, _target)); + codeOffset = GetMethodCodeOffset(pCode, cbh); if (debugInfo == TargetPointer.Null) return []; @@ -142,14 +132,42 @@ IEnumerable IDebugInfo.GetMethodVarInfo(TargetCodePointer pCode, o if (chunks.VarsSize > 0) { - bool isX86 = _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86; + RuntimeInfoArchitecture arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); + bool isX86 = arch == RuntimeInfoArchitecture.X86; + DebugInfoHelpers.WasmDebugInfoEncoding? wasmEncoding = null; + if (arch == RuntimeInfoArchitecture.Wasm) + wasmEncoding = DebugInfoHelpers.GetWasmDebugInfoEncoding(_target); + NativeReader varsNativeReader = new(new TargetStream(_target, chunks.VarsStart, chunks.VarsSize), _target.IsLittleEndian); - return DebugInfoHelpers.DoVars(varsNativeReader, isX86); + return DebugInfoHelpers.DoVars(varsNativeReader, isX86, wasmEncoding); } return []; } + internal uint GetMethodCodeOffset(TargetCodePointer pCode, CodeBlockHandle cbh) + { + // WASM ReadyToRun CodeBlocks carry a controlling-method-relative offset, including when + // pCode is inside a funclet. Use that existing ExecutionManager result directly; the + // generic CodeVersions path validates a MethodDesc shape that portable entrypoints do not + // expose. + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.Wasm) + return checked((uint)_eman.GetRelativeOffset(cbh).Value); + + // Compute code offset from the method's native code entry point, not from the code block + // start. On other architectures GetStartAddress may be the current funclet, while variable + // locations are relative to the method entry point. Match the native DAC's use of + // NativeCodeVersion::GetNativeCode(). + ICodeVersions cv = _target.Contracts.CodeVersions; + NativeCodeVersionHandle ncvh = cv.GetNativeCodeVersionForIP(pCode); + if (!ncvh.Valid) + throw new InvalidOperationException($"No NativeCodeVersion found for native code {pCode}."); + TargetCodePointer nativeCodeStart = cv.GetNativeCode(ncvh); + return (uint)( + CodePointerUtils.AddressFromCodePointer(pCode, _target) - + CodePointerUtils.AddressFromCodePointer(nativeCodeStart, _target)); + } + IReadOnlyList IDebugInfo.GetAsyncSuspensionPoints(TargetCodePointer pCode) { if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs index 3053371d06df1f..10331486424dad 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs @@ -943,13 +943,16 @@ private static NativeVarLocation[] FindAndResolveVarLocation( /// Resolves a DebugVarInfo entry to physical NativeVarLocation(s) /// using the given CPU context. Mirrors the native NativeVarLocations() from util.cpp. /// - private static NativeVarLocation[] ResolveVarLocation( + internal static NativeVarLocation[] ResolveVarLocation( DebugVarInfo varInfo, IPlatformAgnosticContext context, Target target) { int pointerSize = target.PointerSize; + if (target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.Wasm) + return ResolveWasmVarLocation(varInfo, context, target, pointerSize); + return (varInfo.Kind, varInfo.IsByRef) switch { (DebugVarLocKind.Register, false) => @@ -989,6 +992,69 @@ private static NativeVarLocation[] ResolveVarLocation( }; } + /// + /// Resolves a variable location on WASM. + /// + /// + /// WASM has no register file — .TryReadRegister(int) always + /// fails — and a WASM "register" in debug info is not a register at all but a packed + /// (local index, value type) tuple naming a WASM local. WASM locals are engine-private frame + /// state: they are not in linear memory and cannot be read through the data target, so they + /// are reported as having no location rather than resolved. Routing them through + /// would instead silently report the value 0 for every variable. + /// Stack slots, by contrast, live in linear memory and are resolved here against the frame's + /// logical frame pointer. + /// + private static NativeVarLocation[] ResolveWasmVarLocation( + DebugVarInfo varInfo, + IPlatformAgnosticContext context, + Target target, + int pointerSize) + { + // On WASM, REG_FPBASE, REG_SPBASE (both REG_NA == REG_COUNT == 2) and + // ICorDebugInfo::REGNUM_AMBIENT_SP (also 2) all collapse to this single value, so it is the + // only base register a JIT-emitted WASM stack location can carry. + const uint WasmFrameBaseRegister = 2; + switch (varInfo.Kind) + { + case DebugVarLocKind.Stack: + case DebugVarLocKind.DoubleStack: + break; + default: + // WasmLocal, WasmLocalPair, and the mixed register/stack kinds all have at least + // part of their storage in a WASM local, which is unreadable from here. + return []; + } + + // Determine the base the stack offset is relative to. + // + // RyuJIT's getSiVarLoc always passes REG_FPBASE or REG_SPBASE as the base register, and on + // WASM both are REG_NA (src/coreclr/jit/targetwasm.h). REG_NA is REG_COUNT, which is 2 on + // WASM because registerwasm.h defines only REG_STK. siFillStackVarLoc may additionally + // rewrite the base to ICorDebugInfo::REGNUM_AMBIENT_SP, which is also 2 on WASM + // (src/coreclr/inc/cordebuginfo.h). So every JIT-emitted WASM stack location carries base + // register 2, and the two cases are indistinguishable. + // + // WASM codegen addresses the frame exclusively through the frame-pointer local -- see + // genZeroInitFrame's "Wasm locals are at non-negative offsets from FP" assert and the + // GetFramePointerRegIndex() uses throughout codegenwasm.cpp -- so base 2 means the logical + // frame pointer stored in the frame's stack-walk context. + if (varInfo.BaseRegister != WasmFrameBaseRegister) + return []; + + TargetPointer framePointer = context.FramePointer; + if (framePointer == TargetPointer.Null) + return []; + + ulong address = (ulong)((long)framePointer.Value + varInfo.StackOffset); + ulong size = varInfo.Kind == DebugVarLocKind.DoubleStack ? 2 * (ulong)pointerSize : (ulong)pointerSize; + + if (varInfo.IsByRef) + address = DereferenceOrZero(target, address); + + return [new NativeVarLocation { AddressOrValue = address, Size = size, IsRegisterValue = false }]; + } + private static NativeVarLocation[] ResolveRegByRef(IPlatformAgnosticContext context, Target target, uint register, int pointerSize) { // The register holds a pointer to the variable in target memory. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.NativeCodeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.NativeCodeInfo.cs index 3db90d49d72b94..56ba653f32e44e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.NativeCodeInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.NativeCodeInfo.cs @@ -54,6 +54,13 @@ internal static VarLoc ConvertToVarLoc(DebugVarInfo varInfo) (DebugVarLocKind.Register, false, false) => VarLocType.VLT_REG, (DebugVarLocKind.Register, false, true) => VarLocType.VLT_REG_FP, (DebugVarLocKind.Register, true, _) => VarLocType.VLT_REG_BYREF, + // WASM locals are reported to DacDbi in their original ICorDebugInfo form. VarLoc is a + // mirror of ICorDebugInfo::VarLoc, and the JIT encodes a WASM local as VLT_REG whose + // register number is the packed (local index, value type) tuple. Preserving that keeps + // parity with the native DAC, which passes the JIT's encoding through untouched. + (DebugVarLocKind.WasmLocal, false, _) => VarLocType.VLT_REG, + (DebugVarLocKind.WasmLocal, true, _) => VarLocType.VLT_REG_BYREF, + (DebugVarLocKind.WasmLocalPair, _, _) => VarLocType.VLT_REG_REG, (DebugVarLocKind.Stack, false, _) => VarLocType.VLT_STK, (DebugVarLocKind.Stack, true, _) => VarLocType.VLT_STK_BYREF, (DebugVarLocKind.RegisterRegister, _, _) => VarLocType.VLT_REG_REG, @@ -68,6 +75,7 @@ internal static VarLoc ConvertToVarLoc(DebugVarInfo varInfo) switch (varInfo.Kind) { case DebugVarLocKind.Register: + case DebugVarLocKind.WasmLocal: loc.vlrReg = varInfo.Register; break; case DebugVarLocKind.Stack: @@ -75,6 +83,7 @@ internal static VarLoc ConvertToVarLoc(DebugVarInfo varInfo) loc.vlsOffset = varInfo.StackOffset; break; case DebugVarLocKind.RegisterRegister: + case DebugVarLocKind.WasmLocalPair: loc.vlrrReg1 = varInfo.Register; loc.vlrrReg2 = varInfo.Register2; break; diff --git a/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs new file mode 100644 index 00000000000000..04650af8169a8f --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs @@ -0,0 +1,516 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ILCompiler.Reflection.ReadyToRun; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Tests for decoding and resolving WASM variable debug information. +/// +/// +/// On WASM a debug-info "register" is not a register. RyuJIT packs a +/// (local index, wasm value type) tuple into regNumber +/// (src/coreclr/jit/registeropswasm.cpp), and the resulting wasm local is engine-private +/// frame state that is not in linear memory. exposes no indexed +/// register file, so a naive resolution reports the value 0 for every variable. +/// +public unsafe class WasmDebugInfoTests +{ + private const int WasmRegTypeShift = 29; + private const byte WasmDebugValueTypeCount = (byte)WasmDebugValueType.Count; + private static DebugInfoHelpers.WasmDebugInfoEncoding CurrentEncoding + => new(WasmRegTypeShift, WasmDebugValueTypeCount); + + // The base register RyuJIT emits for every WASM stack location. REG_FPBASE and REG_SPBASE are + // both REG_NA on WASM (targetwasm.h), REG_NA is REG_COUNT which is 2 because registerwasm.h + // defines only REG_STK, and ICorDebugInfo::REGNUM_AMBIENT_SP is also 2 (cordebuginfo.h). + private const uint JitEmittedWasmStackBaseRegister = 2; + + private static uint PackWasmRegister(uint index, uint valueType) + => index | (valueType << WasmRegTypeShift); + + private static WasmLocalInfo? DecodeWasmRegister(uint packed) + => DebugInfoHelpers.DecodeWasmRegister(packed, CurrentEncoding); + + private static Target CreateTarget( + RuntimeInfoArchitecture targetArch, + bool is64Bit, + bool includeWasmEncoding = true) + { + TestPlaceholderTarget.Builder builder = new( + new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = is64Bit }); + builder + .AddGlobalStrings((Constants.Globals.Architecture, targetArch.ToString().ToLowerInvariant())) + .AddContract(version: "c1"); + + if (targetArch == RuntimeInfoArchitecture.Wasm && includeWasmEncoding) + { + builder.AddGlobals( + (Constants.Globals.WasmDebugRegisterTypeShift, WasmRegTypeShift), + (Constants.Globals.WasmDebugValueTypeCount, WasmDebugValueTypeCount)); + } + + return builder.Build(); + } + + public static TheoryData WasmPackedRegisters() => new() + { + { 1u, 1u }, // $1 (i32) + { 2u, 2u }, // $2 (i64) + { 3u, 3u }, // $3 (f32) + { 4u, 4u }, // $4 (f64) + { 5u, 5u }, // $5 (v128) + { 6u, 6u }, // $6 (exnref) + }; + + [Fact] + public void GetMethodCodeOffset_WasmUsesExecutionManagerRelativeOffset() + { + const uint MethodOffset = 0x18C; + CodeBlockHandle codeBlock = new(new TargetPointer(0x7000)); + Mock executionManager = new(); + executionManager.Setup(e => e.GetRelativeOffset(codeBlock)).Returns(new TargetNUInt(MethodOffset)); + + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder( + new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = false }) + .AddGlobalStrings((Constants.Globals.Architecture, "wasm")) + .AddContract(version: "c1") + .AddMockContract(executionManager) + .Build(); + DebugInfo_1 debugInfo = new(target); + + uint offset = debugInfo.GetMethodCodeOffset( + new TargetCodePointer(0x8001_028F), + codeBlock); + + Assert.Equal(MethodOffset, offset); + executionManager.Verify(e => e.GetRelativeOffset(codeBlock), Times.Once); + } + + /// + /// A WASM local must not resolve to a fabricated location. Before this guard existed, + /// ReadRegister fell through to return 0 for every packed WASM register, + /// silently reporting the value 0 as though it had been read successfully. + /// + [Theory] + [MemberData(nameof(WasmPackedRegisters))] + public void ResolveVarLocation_WasmRegister_ReportsNoLocation(uint localIndex, uint valueType) + { + Target target = CreateTarget(RuntimeInfoArchitecture.Wasm, is64Bit: false); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + + uint packed = PackWasmRegister(localIndex, valueType); + + // Both the projected kind produced by the contract on WASM and the raw architecture-neutral + // kind must refuse to resolve. + foreach (DebugVarLocKind kind in new[] { DebugVarLocKind.WasmLocal, DebugVarLocKind.Register }) + { + DebugVarInfo varInfo = new() + { + StartOffset = 0, + EndOffset = 0x100, + VarNumber = 0, + Kind = kind, + Register = packed, + WasmLocal = DecodeWasmRegister(packed), + }; + + Assert.Empty(ClrDataFrame.ResolveVarLocation(varInfo, context, target)); + } + } + + /// + /// A WASM stack location whose base register is neither a decodable WASM local nor the WASM + /// frame-base sentinel has no frame pointer to resolve against and must not produce an address. + /// + [Theory] + [InlineData(0u)] // REGNUM_PC + [InlineData(1u)] // REGNUM_COUNT + public void ResolveVarLocation_WasmStackWithNonLocalBase_ReportsNoLocation(uint baseRegister) + { + Target target = CreateTarget(RuntimeInfoArchitecture.Wasm, is64Bit: false); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + + DebugVarInfo varInfo = new() + { + StartOffset = 0, + EndOffset = 0x100, + VarNumber = 0, + Kind = DebugVarLocKind.Stack, + BaseRegister = baseRegister, + StackOffset = 0x10, + }; + + Assert.Empty(ClrDataFrame.ResolveVarLocation(varInfo, context, target)); + } + + /// + /// The WASM guard must be scoped to WASM. Architectures with a real register file keep + /// resolving register locations exactly as before. + /// + [Fact] + public void ResolveVarLocation_NonWasmRegister_StillResolves() + { + Target target = CreateTarget(RuntimeInfoArchitecture.X64, is64Bit: true); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + + DebugVarInfo varInfo = new() + { + StartOffset = 0, + EndOffset = 0x100, + VarNumber = 0, + Kind = DebugVarLocKind.Register, + Register = 0, + }; + + NativeVarLocation[] locations = ClrDataFrame.ResolveVarLocation(varInfo, context, target); + + NativeVarLocation location = Assert.Single(locations); + Assert.True(location.IsRegisterValue); + } + + public static TheoryData DecodableRegisters() => new() + { + { PackWasmRegister(1, 1), 1u, WasmDebugValueType.I32 }, + { PackWasmRegister(2, 2), 2u, WasmDebugValueType.I64 }, + { PackWasmRegister(3, 3), 3u, WasmDebugValueType.F32 }, + { PackWasmRegister(4, 4), 4u, WasmDebugValueType.F64 }, + { PackWasmRegister(5, 5), 5u, WasmDebugValueType.V128 }, + { PackWasmRegister(6, 6), 6u, WasmDebugValueType.ExnRef }, + // Index 0 and the largest representable index must round-trip too. + { PackWasmRegister(0, 1), 0u, WasmDebugValueType.I32 }, + { PackWasmRegister((1u << WasmRegTypeShift) - 1, 4), (1u << WasmRegTypeShift) - 1, WasmDebugValueType.F64 }, + }; + + [Theory] + [MemberData(nameof(DecodableRegisters))] + public void DecodeWasmRegister_DecodesIndexAndValueType(uint packed, uint expectedIndex, WasmDebugValueType expectedType) + { + WasmLocalInfo? local = DecodeWasmRegister(packed); + + Assert.NotNull(local); + Assert.Equal(expectedIndex, local.Value.Index); + Assert.Equal(expectedType, local.Value.ValueType); + } + + /// + /// Value type 0 is reserved by MakeWasmReg so that small raw values remain available + /// for pseudo-registers. Those must not decode as local 0/1/2. + /// + [Theory] + [InlineData(0u)] // PC + [InlineData(1u)] // REGNUM_COUNT + [InlineData(2u)] // REGNUM_AMBIENT_SP + [InlineData(0x1FFFFFFFu)] // largest value with value type 0 + [InlineData(0xE0000000u)] // value type 7, out of range + [InlineData(0xFFFFFFFFu)] // value type 7, out of range + public void DecodeWasmRegister_NonLocalValues_ReturnNull(uint packed) + { + Assert.Null(DecodeWasmRegister(packed)); + } + + [Theory] + [InlineData(0)] + [InlineData(30)] + [InlineData(31)] + [InlineData(32)] + public void WasmDebugInfoEncoding_UnsupportedShift_Throws(byte shift) + { + Assert.Throws( + () => new DebugInfoHelpers.WasmDebugInfoEncoding(shift, WasmDebugValueTypeCount)); + } + + [Theory] + [InlineData(6)] + [InlineData(8)] + public void WasmDebugInfoEncoding_MismatchedValueTypeCount_Throws(byte valueTypeCount) + { + Assert.Throws( + () => new DebugInfoHelpers.WasmDebugInfoEncoding(WasmRegTypeShift, valueTypeCount)); + } + + [Fact] + public void GetWasmDebugInfoEncoding_TargetGlobalsPresent_ReturnsAdvertisedEncoding() + { + Target target = CreateTarget(RuntimeInfoArchitecture.Wasm, is64Bit: false); + + DebugInfoHelpers.WasmDebugInfoEncoding encoding = + DebugInfoHelpers.GetWasmDebugInfoEncoding(target); + + Assert.Equal(WasmRegTypeShift, encoding.RegisterTypeShift); + Assert.Equal(WasmDebugValueTypeCount, encoding.ValueTypeCount); + } + + [Fact] + public void GetWasmDebugInfoEncoding_TargetGlobalsMissing_Throws() + { + Target target = CreateTarget( + RuntimeInfoArchitecture.Wasm, + is64Bit: false, + includeWasmEncoding: false); + + Assert.Throws( + () => DebugInfoHelpers.GetWasmDebugInfoEncoding(target)); + } + + [Theory] + [InlineData(0u)] + [InlineData(0xE0000000u)] + public void DoVars_WasmInvalidRegisterEncoding_Throws(uint packedRegister) + { + byte[] encoded = EncodeNibbleUInts( + 1, + unchecked(0u - MaxILNum), + 0, + 1, + VLT_REG, + packedRegister); + NativeReader reader = new(new MemoryStream(encoded)); + + Assert.Throws( + () => DebugInfoHelpers.DoVars(reader, isX86: false, CurrentEncoding).ToList()); + } + + private const uint MaxILNum = unchecked((uint)-6); + + // VarLocType values from ICorDebugInfo (cordebuginfo.h), in declaration order. + private const uint VLT_REG = 0; + private const uint VLT_STK = 3; + private const uint VLT_REG_REG = 5; + + /// + /// A vars blob holding three entries: an f64 in a single WASM local, an i32 pair spanning two + /// WASM locals, and a stack slot based off the WASM frame-pointer local. + /// + private static byte[] EncodeWasmVarsBlob() => EncodeNibbleUInts( + 3, + // var 0: [0x00, 0x10) in WASM local $4 (f64) + unchecked(0u - MaxILNum), 0x00, 0x10, VLT_REG, PackWasmRegister(4, (uint)WasmDebugValueType.F64), + // var 1: [0x10, 0x30) spanning WASM locals $7 and $8 (i32) + unchecked(1u - MaxILNum), 0x10, 0x20, VLT_REG_REG, PackWasmRegister(7, (uint)WasmDebugValueType.I32), PackWasmRegister(8, (uint)WasmDebugValueType.I32), + // var 2: [0x30, 0x40) at [logical FP + 0x18]; WASM stack bases encode as register 2. + // The signed offset is encoded with the sign in bit 0. + unchecked(2u - MaxILNum), 0x30, 0x10, VLT_STK, JitEmittedWasmStackBaseRegister, 0x18 << 1); + + [Fact] + public void DoVars_Wasm_PromotesKindsAndDecodesLocals() + { + NativeReader reader = new(new MemoryStream(EncodeWasmVarsBlob())); + List result = new(DebugInfoHelpers.DoVars(reader, isX86: false, CurrentEncoding)); + + Assert.Collection( + result, + varInfo => + { + Assert.Equal(DebugVarLocKind.WasmLocal, varInfo.Kind); + Assert.NotNull(varInfo.WasmLocal); + Assert.Equal(4u, varInfo.WasmLocal.Value.Index); + Assert.Equal(WasmDebugValueType.F64, varInfo.WasmLocal.Value.ValueType); + }, + varInfo => + { + Assert.Equal(DebugVarLocKind.WasmLocalPair, varInfo.Kind); + Assert.NotNull(varInfo.WasmLocal); + Assert.NotNull(varInfo.WasmLocal2); + Assert.Equal(7u, varInfo.WasmLocal.Value.Index); + Assert.Equal(8u, varInfo.WasmLocal2.Value.Index); + Assert.Equal(WasmDebugValueType.I32, varInfo.WasmLocal2.Value.ValueType); + }, + varInfo => + { + // Stack slots live in linear memory, so the kind and encoded base are unchanged. + Assert.Equal(DebugVarLocKind.Stack, varInfo.Kind); + Assert.Null(varInfo.WasmLocal); + Assert.Equal(JitEmittedWasmStackBaseRegister, varInfo.BaseRegister); + Assert.Equal(0x18, varInfo.StackOffset); + }); + } + + /// + /// The identical blob decoded as a non-WASM target must keep the architecture-neutral kinds + /// and decode no locals, so the projection cannot leak into other architectures. + /// + [Fact] + public void DoVars_NonWasm_LeavesKindsAndLocalsAlone() + { + NativeReader reader = new(new MemoryStream(EncodeWasmVarsBlob())); + List result = new(DebugInfoHelpers.DoVars(reader, isX86: false)); + + Assert.Collection( + result, + varInfo => + { + Assert.Equal(DebugVarLocKind.Register, varInfo.Kind); + Assert.Null(varInfo.WasmLocal); + }, + varInfo => + { + Assert.Equal(DebugVarLocKind.RegisterRegister, varInfo.Kind); + Assert.Null(varInfo.WasmLocal); + Assert.Null(varInfo.WasmLocal2); + }, + varInfo => + { + Assert.Equal(DebugVarLocKind.Stack, varInfo.Kind); + }); + } + + /// + /// DacDbi's VarLoc mirrors ICorDebugInfo::VarLoc, where a WASM local is encoded + /// as VLT_REG whose register number is the packed (local index, value type) tuple. + /// The projected WASM kinds must therefore round-trip to the original ICorDebugInfo form + /// rather than degrading to VLT_INVALID, which would lose the local index entirely. + /// + [Fact] + public void ConvertToVarLoc_WasmLocal_RoundTripsToRegisterForms() + { + uint packed = PackWasmRegister(4, (uint)WasmDebugValueType.F64); + uint packed2 = PackWasmRegister(5, (uint)WasmDebugValueType.I32); + + VarLoc single = DacDbiImpl.ConvertToVarLoc(new DebugVarInfo + { + Kind = DebugVarLocKind.WasmLocal, + Register = packed, + WasmLocal = DecodeWasmRegister(packed), + }); + Assert.Equal(VarLocType.VLT_REG, single.vlType); + Assert.Equal(packed, single.vlrReg); + + VarLoc byref = DacDbiImpl.ConvertToVarLoc(new DebugVarInfo + { + Kind = DebugVarLocKind.WasmLocal, + IsByRef = true, + Register = packed, + WasmLocal = DecodeWasmRegister(packed), + }); + Assert.Equal(VarLocType.VLT_REG_BYREF, byref.vlType); + Assert.Equal(packed, byref.vlrReg); + + VarLoc pair = DacDbiImpl.ConvertToVarLoc(new DebugVarInfo + { + Kind = DebugVarLocKind.WasmLocalPair, + Register = packed, + Register2 = packed2, + WasmLocal = DecodeWasmRegister(packed), + WasmLocal2 = DecodeWasmRegister(packed2), + }); + Assert.Equal(VarLocType.VLT_REG_REG, pair.vlType); + Assert.Equal(packed, pair.vlrrReg1); + Assert.Equal(packed2, pair.vlrrReg2); + } + + /// + /// The success path: a WASM stack slot resolves to logical frame pointer + StackOffset + /// in linear memory. This exercises ResolveWasmVarLocation end to end through the real + /// WasmR2RInfo / WasmUnwinder, rather than only asserting the refusal paths. + /// + [Theory] + [InlineData(0x18, false)] + [InlineData(0x00, false)] + [InlineData(0x18, true)] + public void ResolveVarLocation_WasmStack_ResolvesAgainstLogicalFramePointer(int stackOffset, bool isDoubleStack) + { + const ulong FrameAddress = 0x0020_0000; + const uint FunctionIndex = WasmMockTarget.FunctionTableIndex; + + Target target = WasmMockTarget.Create(FrameAddress, FunctionIndex, isFunclet: false); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.StackPointer = new TargetPointer(FrameAddress); + context.FramePointer = new TargetPointer(FrameAddress); + + // The base register RyuJIT actually emits for a WASM stack slot: REG_FPBASE / REG_SPBASE + // are REG_NA (== REG_COUNT == 2), and REGNUM_AMBIENT_SP is also 2. + DebugVarInfo varInfo = new() + { + StartOffset = 0, + EndOffset = 0x100, + VarNumber = 0, + Kind = isDoubleStack ? DebugVarLocKind.DoubleStack : DebugVarLocKind.Stack, + BaseRegister = JitEmittedWasmStackBaseRegister, + StackOffset = stackOffset, + }; + + NativeVarLocation location = Assert.Single(ClrDataFrame.ResolveVarLocation(varInfo, context, target)); + + Assert.False(location.IsRegisterValue); + Assert.Equal(FrameAddress + (ulong)stackOffset, location.AddressOrValue); + Assert.Equal(isDoubleStack ? 8u : 4u, location.Size); + } + + /// + /// For a funclet frame the WASM frame-pointer local refers to the parent method's frame, so the + /// resolved address must be based on the establishing frame pointer recovered by unwinding out + /// of the funclet — not on the funclet's own frame base. + /// + [Fact] + public void ResolveVarLocation_WasmStackInFunclet_ResolvesAgainstEstablishingFrame() + { + const ulong FuncletFrame = 0x0020_0000; + const int StackOffset = 0x18; + + Target target = WasmMockTarget.Create(FuncletFrame, WasmMockTarget.FunctionTableIndex, isFunclet: true); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.StackPointer = new TargetPointer(FuncletFrame); + context.FramePointer = new TargetPointer(WasmMockTarget.EstablishingFramePointer); + + DebugVarInfo varInfo = new() + { + StartOffset = 0, + EndOffset = 0x100, + VarNumber = 0, + Kind = DebugVarLocKind.Stack, + BaseRegister = JitEmittedWasmStackBaseRegister, + StackOffset = StackOffset, + }; + + NativeVarLocation location = Assert.Single(ClrDataFrame.ResolveVarLocation(varInfo, context, target)); + + Assert.False(location.IsRegisterValue); + Assert.Equal(WasmMockTarget.EstablishingFramePointer + StackOffset, location.AddressOrValue); + Assert.NotEqual(FuncletFrame + StackOffset, location.AddressOrValue); + } + + /// + /// Nibble encoder matching CoreCLR's NibbleWriter::WriteEncodedU32: three value bits per + /// nibble, high bit set on every nibble but the last, most significant group first, low nibble + /// of each byte used before the high nibble. + /// + private static byte[] EncodeNibbleUInts(params uint[] values) + { + List nibbles = new(); + Span groups = stackalloc byte[11]; + foreach (uint value in values) + { + int groupCount = 0; + uint remaining = value; + do + { + groups[groupCount++] = (byte)(remaining & 7); + remaining >>= 3; + } + while (remaining != 0); + + for (int i = groupCount - 1; i >= 0; i--) + { + byte continuation = i == 0 ? (byte)0 : (byte)8; + nibbles.Add((byte)(groups[i] | continuation)); + } + } + + byte[] bytes = new byte[(nibbles.Count + 1) / 2]; + for (int i = 0; i < nibbles.Count; i++) + bytes[i / 2] |= (byte)(nibbles[i] << (4 * (i & 1))); + return bytes; + } +} From 11664425ab87901b1965f7cf1365ebc9a076f325 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 16:32:19 -0500 Subject: [PATCH 14/14] [cdac][wasm] Handle unavailable variable values Keep engine-private WASM locals unavailable rather than fabricating a zero value, and distinguish an unreadable VLT_STK_BYREF pointer from a legitimate stack-homed null reference. The native DAC retains one logical location when indirect pointer reads fail, but its subsequent address-zero memory read fails on native platforms. WASM linear address zero is readable, so carrying that fallback forward can return successful irrelevant bytes. Preserve the logical location count while making all address/value/object access report CORDBG_E_READVIRTUAL_FAILURE when the indirection is unreadable or resolves to null. Non-WASM dereference behavior remains unchanged. Exact controls cover an unreadable byref, a null byref pointer, a readable null reference, and a symbolic engine local with no physical location. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 5 + .../ClrDataFrame.cs | 18 +- .../ClrDataValue.cs | 23 ++ .../tests/UnitTests/WasmDebugInfoTests.cs | 238 ++++++++++++++++++ 4 files changed, 283 insertions(+), 1 deletion(-) diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index 7c7c28d979cec5..aa28a162dc664e 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -344,6 +344,11 @@ The managed cDAC reader reports engine-private WASM register locals with no nati Existing `ClrDataValue` behavior then reports those values unavailable (`GetNumLocations == 0`, `GetBytes`/`GetAddress` fail) instead of fabricating a register value of zero. +An unreadable or null WASM `VLT_STK_BYREF` indirection preserves one logical location, matching +the native DAC's location count, but its address/value/object accessors fail with +`CORDBG_E_READVIRTUAL_FAILURE`. This distinguishes a failed indirection from a direct +stack-homed null reference, which remains one readable location containing zero. + There is no native DAC build on WASM, so live `_legacyImpl` comparison assertions cannot validate this representation. DacDbi coverage is structural managed unit coverage. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs index 10331486424dad..860dbc17d2ce14 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs @@ -540,6 +540,7 @@ private ClrDataValue CreateValueFromDebugInfo( AddressOrValue = locations[0].AddressOrValue, Size = (ulong)typeSize, IsRegisterValue = locations[0].IsRegisterValue, + HasReadFailure = locations[0].HasReadFailure, }, ]; } @@ -1050,7 +1051,22 @@ private static NativeVarLocation[] ResolveWasmVarLocation( ulong size = varInfo.Kind == DebugVarLocKind.DoubleStack ? 2 * (ulong)pointerSize : (ulong)pointerSize; if (varInfo.IsByRef) - address = DereferenceOrZero(target, address); + { + if (!target.TryReadPointer(address, out TargetPointer byRefAddress) || + byRefAddress == TargetPointer.Null) + { + return + [ + new NativeVarLocation + { + AddressOrValue = 0, + Size = (ulong)pointerSize, + HasReadFailure = true, + }, + ]; + } + address = byRefAddress.Value; + } return [new NativeVarLocation { AddressOrValue = address, Size = size, IsRegisterValue = false }]; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataValue.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataValue.cs index ac8bd2f40fbb79..83cdc6c48e6900 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataValue.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataValue.cs @@ -25,6 +25,7 @@ public readonly struct NativeVarLocation public ulong AddressOrValue { get; init; } public ulong Size { get; init; } public bool IsRegisterValue { get; init; } + public bool HasReadFailure { get; init; } } [GeneratedComClass] @@ -111,6 +112,7 @@ int IXCLRDataValue.GetAddress(ClrDataAddress* address) { throw new InvalidCastException(); // E_NOINTERFACE } + ThrowIfLocationReadFailed(); *address = _locations[0].AddressOrValue; } @@ -175,6 +177,8 @@ int IXCLRDataValue.GetBytes(uint bufLen, uint* dataSize, byte* buffer) if (_totalSize == 0) throw new InvalidCastException(); // E_NOINTERFACE + ThrowIfLocationReadFailed(); + if (dataSize is not null) *dataSize = (uint)_totalSize; @@ -463,6 +467,8 @@ int IXCLRDataValue.GetAssociatedValue(DacComNullableByRef assocV } NativeVarLocation currentLocation = _locations[0]; + ThrowIfLocationReadFailed(); + ulong address = currentLocation.IsRegisterValue ? currentLocation.AddressOrValue : _target.ReadPointer(new TargetPointer(currentLocation.AddressOrValue)).Value; @@ -512,6 +518,7 @@ int IXCLRDataValue.GetAssociatedType(DacComNullableByRef } else if ((_flags & (uint)ClrDataValueFlag.IS_ARRAY) != 0) { + ThrowIfLocationReadFailed(); IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; TargetPointer methodTable = _target.Contracts.Object.GetMethodTableAddress(_baseAddress); typeHandle = rts.GetTypeParam(rts.GetTypeHandle(methodTable)); @@ -544,6 +551,7 @@ int IXCLRDataValue.GetString(uint bufLen, uint* strLen, char* str) if ((_flags & (uint)ClrDataValueFlag.IS_STRING) == 0) throw new ArgumentException(); + ThrowIfLocationReadFailed(); string value = _target.Contracts.Object.GetStringValue(_baseAddress); OutputBufferHelpers.CopyStringToBuffer(str, bufLen, strLen, value); if (str is null || bufLen < value.Length + 1) @@ -585,6 +593,7 @@ int IXCLRDataValue.GetArrayProperties(uint* rank, uint* totalElements, uint numD if ((_flags & (uint)ClrDataValueFlag.IS_ARRAY) == 0) throw new ArgumentException(); + ThrowIfLocationReadFailed(); IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; ITypeHandle arrayType = rts.GetTypeHandle(_target.Contracts.Object.GetMethodTableAddress(_baseAddress)); rts.IsArray(arrayType, out uint arrayRank); @@ -645,6 +654,7 @@ int IXCLRDataValue.GetArrayElement(uint numInd, [In, MarshalUsing(CountElementNa if ((_flags & (uint)ClrDataValueFlag.IS_ARRAY) == 0) throw new ArgumentException(); + ThrowIfLocationReadFailed(); IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; ITypeHandle arrayType = rts.GetTypeHandle(_target.Contracts.Object.GetMethodTableAddress(_baseAddress)); if (!rts.IsArray(arrayType, out uint rank) || numInd != rank) @@ -984,6 +994,7 @@ private ClrDataValue CreateFieldValue( } else { + ThrowIfLocationReadFailed(); uint offset = rts.GetFieldDescOffset(fieldDesc, fieldDefinition); ulong objectOffset = rts.IsValueType(enclosingType) ? 0 : (ulong)_target.PointerSize; address = new TargetPointer(checked(_baseAddress + objectOffset + offset)); @@ -1153,6 +1164,15 @@ int IXCLRDataValue.GetNumLocations(uint* numLocs) return hr; } + private void ThrowIfLocationReadFailed() + { + foreach (NativeVarLocation location in _locations) + { + if (location.HasReadFailure) + throw new VirtualReadException("The variable's indirect location could not be read."); + } + } + int IXCLRDataValue.GetLocationByIndex(uint loc, uint* flags, ClrDataAddress* arg) { using Lock.Scope scope = _apiLock.EnterScope(); @@ -1166,6 +1186,9 @@ int IXCLRDataValue.GetLocationByIndex(uint loc, uint* flags, ClrDataAddress* arg throw new ArgumentException(); NativeVarLocation location = _locations[loc]; + if (location.HasReadFailure) + throw new VirtualReadException("The variable's indirect location could not be read."); + *flags = location.IsRegisterValue ? ClrDataVLocFlag.CLRDATA_VLOC_REGISTER : ClrDataVLocFlag.CLRDATA_VLOC_MEMORY; *arg = location.IsRegisterValue ? 0 : location.AddressOrValue; } diff --git a/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs index 04650af8169a8f..ba977c905e3fd0 100644 --- a/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/WasmDebugInfoTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using ILCompiler.Reflection.ReadyToRun; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -481,6 +482,243 @@ public void ResolveVarLocation_WasmStackInFunclet_ResolvesAgainstEstablishingFra Assert.NotEqual(FuncletFrame + StackOffset, location.AddressOrValue); } + /// + /// Characterizes the pre-existing behavior that the WASM consumer + /// relies on: a symbolic engine local resolves to no native locations, and reads from that + /// empty value fail rather than returning a plausible zero. + /// + [Fact] + public unsafe void ClrDataValue_NoLocations_FailsInsteadOfReturningZero() + { + Target target = CreateTarget(RuntimeInfoArchitecture.Wasm, is64Bit: false); + + ClrDataValue value = new( + target, + TargetPointer.Null, + flags: (uint)ClrDataValueFlag.IS_PRIMITIVE, + typeHandle: null, + baseAddress: TargetPointer.Null, + locations: [], + legacyImpl: null, + apiLock: new Lock()); + + IXCLRDataValue dataValue = value; + + uint numLocs; + Assert.Equal(HResults.S_OK, dataValue.GetNumLocations(&numLocs)); + Assert.Equal(0u, numLocs); + + // GetBytes must not succeed with a zero-filled buffer. + byte[] buffer = new byte[8]; + uint dataSize; + int hr; + fixed (byte* pBuffer = buffer) + { + hr = dataValue.GetBytes((uint)buffer.Length, &dataSize, pBuffer); + } + Assert.True(hr < 0, $"GetBytes should fail for a value with no locations, got 0x{hr:X8}"); + + // GetAddress must not report address 0 as though it were a real address. + ClrDataAddress address; + Assert.True(dataValue.GetAddress(&address) < 0, "GetAddress should fail for a value with no locations"); + } + + [Fact] + public void ResolveVarLocation_WasmStackByRefUnreadable_PreservesFailedLocation() + { + const ulong FrameAddress = 0x0020_0000; + Target target = CreateWasmTargetWithZeroPage(); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.FramePointer = new TargetPointer(FrameAddress); + + DebugVarInfo varInfo = new() + { + Kind = DebugVarLocKind.Stack, + IsByRef = true, + BaseRegister = JitEmittedWasmStackBaseRegister, + StackOffset = 0x24, + }; + + NativeVarLocation location = Assert.Single(ClrDataFrame.ResolveVarLocation(varInfo, context, target)); + + Assert.True(location.HasReadFailure); + Assert.False(location.IsRegisterValue); + Assert.Equal(0u, location.AddressOrValue); + Assert.Equal(4u, location.Size); + } + + [Fact] + public unsafe void ClrDataValue_WasmStackByRefUnreadable_FailsWithoutBecomingZero() + { + const ulong FrameAddress = 0x0020_0000; + Target target = CreateWasmTargetWithZeroPage(); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.FramePointer = new TargetPointer(FrameAddress); + DebugVarInfo varInfo = new() + { + Kind = DebugVarLocKind.Stack, + IsByRef = true, + BaseRegister = JitEmittedWasmStackBaseRegister, + StackOffset = 0x24, + }; + NativeVarLocation[] locations = ClrDataFrame.ResolveVarLocation(varInfo, context, target); + + IXCLRDataValue dataValue = new ClrDataValue( + target, + TargetPointer.Null, + flags: (uint)ClrDataValueFlag.IS_REFERENCE, + typeHandle: new TargetTypeHandle(new TargetPointer(0x0030_0000)), + baseAddress: TargetPointer.Null, + locations, + legacyImpl: null, + apiLock: new Lock()); + + uint numLocs; + Assert.Equal(HResults.S_OK, dataValue.GetNumLocations(&numLocs)); + Assert.Equal(1u, numLocs); + + byte[] buffer = [0xCC, 0xCC, 0xCC, 0xCC]; + uint dataSize = 0; + int hr; + fixed (byte* pBuffer = buffer) + { + hr = dataValue.GetBytes((uint)buffer.Length, &dataSize, pBuffer); + } + Assert.Equal(CorDbgHResults.CORDBG_E_READVIRTUAL_FAILURE, hr); + Assert.Equal(new byte[] { 0xCC, 0xCC, 0xCC, 0xCC }, buffer); + + ClrDataAddress address; + Assert.Equal(CorDbgHResults.CORDBG_E_READVIRTUAL_FAILURE, dataValue.GetAddress(&address)); + + uint flags; + ClrDataAddress location; + Assert.Equal( + CorDbgHResults.CORDBG_E_READVIRTUAL_FAILURE, + dataValue.GetLocationByIndex(0, &flags, &location)); + + DacComNullableByRef associatedValue = new(isNullRef: false); + Assert.Equal( + CorDbgHResults.CORDBG_E_READVIRTUAL_FAILURE, + dataValue.GetAssociatedValue(associatedValue)); + } + + [Fact] + public unsafe void ClrDataValue_WasmStackNullReference_RemainsReadable() + { + const ulong FrameAddress = 0x0020_0000; + const int StackOffset = 0x24; + const ulong SlotAddress = 0x0020_0024; + Target target = CreateWasmTargetWithZeroPage( + new MockMemorySpace.HeapFragment + { + Address = SlotAddress, + Data = new byte[4], + Name = "null reference slot", + }); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.FramePointer = new TargetPointer(FrameAddress); + DebugVarInfo varInfo = new() + { + Kind = DebugVarLocKind.Stack, + BaseRegister = JitEmittedWasmStackBaseRegister, + StackOffset = StackOffset, + }; + NativeVarLocation[] locations = ClrDataFrame.ResolveVarLocation(varInfo, context, target); + + IXCLRDataValue dataValue = new ClrDataValue( + target, + TargetPointer.Null, + flags: (uint)ClrDataValueFlag.IS_REFERENCE, + typeHandle: null, + baseAddress: new TargetPointer(SlotAddress), + locations, + legacyImpl: null, + apiLock: new Lock()); + + uint numLocs; + Assert.Equal(HResults.S_OK, dataValue.GetNumLocations(&numLocs)); + Assert.Equal(1u, numLocs); + + byte[] buffer = [0xCC, 0xCC, 0xCC, 0xCC]; + uint dataSize; + fixed (byte* pBuffer = buffer) + { + Assert.Equal(HResults.S_OK, dataValue.GetBytes((uint)buffer.Length, &dataSize, pBuffer)); + } + Assert.Equal(4u, dataSize); + Assert.Equal(new byte[4], buffer); + + ClrDataAddress address; + Assert.Equal(HResults.S_OK, dataValue.GetAddress(&address)); + Assert.Equal(SlotAddress, (ulong)address); + } + + [Fact] + public unsafe void ClrDataValue_WasmStackByRefNullPointer_FailsWithoutReadingZeroPage() + { + const ulong FrameAddress = 0x0020_0000; + const int StackOffset = 0x24; + Target target = CreateWasmTargetWithZeroPage( + new MockMemorySpace.HeapFragment + { + Address = FrameAddress + StackOffset, + Data = new byte[4], + Name = "null byref pointer slot", + }); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.FramePointer = new TargetPointer(FrameAddress); + DebugVarInfo varInfo = new() + { + Kind = DebugVarLocKind.Stack, + IsByRef = true, + BaseRegister = JitEmittedWasmStackBaseRegister, + StackOffset = StackOffset, + }; + NativeVarLocation[] locations = ClrDataFrame.ResolveVarLocation(varInfo, context, target); + Assert.True(Assert.Single(locations).HasReadFailure); + + IXCLRDataValue dataValue = new ClrDataValue( + target, + TargetPointer.Null, + flags: (uint)ClrDataValueFlag.IS_REFERENCE, + typeHandle: null, + baseAddress: TargetPointer.Null, + locations, + legacyImpl: null, + apiLock: new Lock()); + + byte[] buffer = [0xCC, 0xCC, 0xCC, 0xCC]; + uint dataSize; + fixed (byte* pBuffer = buffer) + { + Assert.Equal( + CorDbgHResults.CORDBG_E_READVIRTUAL_FAILURE, + dataValue.GetBytes((uint)buffer.Length, &dataSize, pBuffer)); + } + Assert.Equal(new byte[] { 0xCC, 0xCC, 0xCC, 0xCC }, buffer); + } + + private static Target CreateWasmTargetWithZeroPage(params MockMemorySpace.HeapFragment[] fragments) + { + TestPlaceholderTarget.Builder builder = new( + new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = false }); + builder + .AddGlobalStrings((Constants.Globals.Architecture, "wasm")) + .AddGlobals( + (Constants.Globals.WasmDebugRegisterTypeShift, WasmRegTypeShift), + (Constants.Globals.WasmDebugValueTypeCount, WasmDebugValueTypeCount)) + .AddContract(version: "c1"); + builder.MemoryBuilder.AddHeapFragment( + new MockMemorySpace.HeapFragment + { + Address = 0, + Data = new byte[64], + Name = "readable wasm zero page", + }); + builder.MemoryBuilder.AddHeapFragments(fragments); + return builder.Build(); + } + /// /// Nibble encoder matching CoreCLR's NibbleWriter::WriteEncodedU32: three value bits per /// nibble, high bit set on every nibble but the last, most significant group first, low nibble