From 49c1b1557e6c50c5aea38de3517b6a0f63126a9e Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Nov 2026 15:07:47 -0600 Subject: [PATCH 1/4] cDAC: resolve ReadyToRunInfo.LoadedImageBase through CompositeInfo Component assemblies of a multi-assembly composite R2R image never set m_pLoadedImageBase (only the composite ReadyToRunInfo does), so the cDAC read 0 for them. This rejected the composite's Wasm virtual-IP range section, stopping stack walks at the first explicit frame, and broke image-base-relative reads (unwind/GC/debug/EH info, GCRefMap lookup). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Data/ReadyToRunInfo.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 511065f743fe44..ed5669434a9554 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 @@ -17,7 +17,6 @@ internal sealed partial class ReadyToRunInfo : IData [Field] public partial TargetPointer? DelayLoadMethodCallThunks { get; } [Field] public partial TargetPointer DebugInfoSection { get; } [Field] public partial TargetPointer ExceptionInfoSection { get; } - [Field] public partial TargetPointer LoadedImageBase { get; } [Field] public partial TargetPointer Composite { get; } [Field] public partial uint NumImportSections { get; } @@ -27,6 +26,7 @@ internal sealed partial class ReadyToRunInfo : IData [CustomInit(nameof(InitHotColdMap))] public partial TargetPointer HotColdMap { get; } [CustomInit(nameof(InitImportSections))] public partial TargetPointer ImportSections { get; } [CustomInit(nameof(InitEntryPointToMethodDescMap))] public partial TargetPointer EntryPointToMethodDescMap { get; } + [CustomInit(nameof(InitLoadedImageBase))] public partial TargetPointer LoadedImageBase { get; } [DataDescriptorDependency(nameof(NumRuntimeFunctions), "uint32")] [DataDescriptorDependency(nameof(RuntimeFunctions), "pointer")] @@ -68,4 +68,14 @@ private partial TargetPointer InitEntryPointToMethodDescMap(Target target, Targe // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) return CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; } + + [DataDescriptorDependency(nameof(CompositeInfo), "pointer")] + [DataDescriptorDependency(nameof(LoadedImageBase), "pointer")] + private partial TargetPointer InitLoadedImageBase(Target target, TargetPointer address) + { + // Component assemblies of a multi-assembly composite image do not record the image base; + // read it from the composite info (set to itself for non-component images). + Target.TypeInfo type = target.GetTypeInfo(DataType.ReadyToRunInfo); + return target.ReadPointerField(CompositeInfo, type, nameof(LoadedImageBase)); + } } From 172d7a6133c1ce69576f421f72f03125dc8d43f2 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Nov 2026 15:09:49 -0600 Subject: [PATCH 2/4] cDAC: test and document composite ReadyToRunInfo image base resolution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 4 ++-- docs/design/datacontracts/StackWalk.md | 4 ++-- .../ExecutionManager/ExecutionManagerTests.cs | 19 +++++++++++++++---- .../cdac/tests/UnitTests/StackWalkTests.cs | 6 ++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index fa614820343781..bda78e52a0efd1 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -270,7 +270,7 @@ virtual-IP ranges bypass native thunk classification. | `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; 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` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image; unset for component assemblies of a composite image, so it is read from the `CompositeInfo` | | `ReadyToRunInfo` | `MinVirtualIP` | `pointer` | Base virtual IP assigned to the ReadyToRun module on WebAssembly | | `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` | @@ -398,7 +398,7 @@ 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 codeBase = /* RangeSection.RangeBegin, or ReadyToRunInfo.MinVirtualIP on WebAssembly */; - TargetPointer loadedImageBase = /* ReadyToRunInfo.LoadedImageBase on WebAssembly, otherwise codeBase */; + TargetPointer loadedImageBase = /* LoadedImageBase of r2rInfo.CompositeInfo on WebAssembly, otherwise codeBase */; TargetPointer relativeAddr = addr - codeBase; TargetPointer runtimeFunctions = Target.ReadPointer(r2rInfo + /* ReadyToRunInfo::RuntimeFunctions offset */); diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index f31c38a1d6010a..29e7bc0728e3c0 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -208,7 +208,7 @@ On ARM64, the `pac` unwind opcode strips pointer authentication bits from `Lr` u | `PInvokeCalliFrame` | `VASigCookiePtr` | `pointer` | Pointer to the varargs signature cookie for the unmanaged call | | `PortableEntryPoint` | `ActualCode` | `pointer` | Native code for the entrypoint; on WebAssembly R2R code this is a function-table index (only defined if `FeaturePortableEntrypoints` is enabled) | | `ReadyToRunInfo` | `ImportSections` | `pointer` | Pointer to the array of ReadyToRun import sections | -| `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image | +| `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image; unset for component assemblies of a composite image, so it is read from the `CompositeInfo` | | `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` | @@ -873,7 +873,7 @@ GCRefMap resolution from a frame's `Indirection` pointer proceeds as follows: 1. Call `FindReadyToRunModule(indirection)` (see [ExecutionManager contract](./ExecutionManager.md)) to find the ReadyToRun module containing the import slot. 2. Load the module's `ReadyToRunInfo` to access the import section array. -3. Compute the RVA of the indirection address: `rva = indirection - imageBase`. +3. Compute the RVA of the indirection address: `rva = indirection - imageBase`, where `imageBase` is the `LoadedImageBase` of the `ReadyToRunInfo`'s `CompositeInfo`. 4. Search through `READYTORUN_IMPORT_SECTION` entries to find the section containing the RVA. 5. Compute the slot index within the section: `index = (rva - sectionVA) / entrySize`. 6. If the section has the `GCRefMap` flag (`0x0008`), read the DWORD at `imageBase + Signatures + index * 4`. This RVA points directly to the GCRefMap prefix of the import signature. diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index 25ae3b178f2e86..f02bc29965930b 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -441,8 +441,10 @@ int ReadWithReadableZeroPage(ulong address, Span buffer) } } - [Fact] - public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape(bool compositeComponent) { MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; @@ -466,7 +468,16 @@ public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() emBuilder.AllocateJittedCodeRange(VirtualIPRangeStart, VirtualIPRangeSize); MockReadyToRunInfo r2rInfo = emBuilder.AddReadyToRunInfo([0x80, FunctionBeginAddress, 0x200], []); r2rInfo.MinVirtualIP = VirtualIPRangeStart; - r2rInfo.LoadedImageBase = LoadedImageBase; + + // A component assembly of a composite image leaves LoadedImageBase unset; the image base + // and entry point map belong to the composite image's ReadyToRunInfo. + MockReadyToRunInfo compositeInfo = r2rInfo; + if (compositeComponent) + { + compositeInfo = emBuilder.AddReadyToRunInfo([0x80, FunctionBeginAddress, 0x200], []); + r2rInfo.CompositeInfo = compositeInfo.Address; + } + compositeInfo.LoadedImageBase = LoadedImageBase; runtimeFunctionsAddress = r2rInfo.RuntimeFunctions; runtimeFunctionSize = emBuilder.RuntimeFunctionLayout.Size; Assert.Equal(8, runtimeFunctionSize); @@ -476,7 +487,7 @@ public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() MockHashMapBuilder hashMapBuilder = new(emBuilder.Builder); hashMapBuilder.PopulatePtrMap( - r2rInfo.EntryPointToMethodDescMapAddress, + compositeInfo.EntryPointToMethodDescMapAddress, [(CapturedVirtualIP, ExpectedMethodDescAddress)]); MockLoaderModule r2rModule = emBuilder.AddReadyToRunModule(r2rInfo.Address); diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index de49bf32c3cb27..f75b973ae55faa 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -1260,10 +1260,11 @@ private static (TestPlaceholderTarget Target, ContextHolder Context) byte[] module = new byte[4]; helpers.Write(module, 0x4000u); builder.MemoryBuilder.AddHeapFragment(new() { Address = 0x3000, Data = module, Name = "Module" }); - byte[] r2rInfo = new byte[12]; + byte[] r2rInfo = new byte[16]; helpers.Write(r2rInfo.AsSpan(0), ImageBase); helpers.Write(r2rInfo.AsSpan(4), 1u); helpers.Write(r2rInfo.AsSpan(8), 0x5000u); + helpers.Write(r2rInfo.AsSpan(12), 0x4000u); builder.MemoryBuilder.AddHeapFragment(new() { Address = 0x4000, Data = r2rInfo, Name = "ReadyToRun info" }); byte[] importSection = new byte[20]; helpers.Write(importSection.AsSpan(0), ImportSectionRva); @@ -1340,7 +1341,8 @@ private static (TestPlaceholderTarget Target, ContextHolder Context) (nameof(X86Context.Ebp), 0), (nameof(X86Context.Ebx), 4), (nameof(X86Context.Edi), 8), (nameof(X86Context.Esi), 12)), [DataType.Module] = CreateTypeInfo((nameof(Data.Module.ReadyToRunInfo), 0)), [DataType.ReadyToRunInfo] = CreateTypeInfo( - (nameof(Data.ReadyToRunInfo.LoadedImageBase), 0), (nameof(Data.ReadyToRunInfo.NumImportSections), 4), (nameof(Data.ReadyToRunInfo.ImportSections), 8)), + (nameof(Data.ReadyToRunInfo.LoadedImageBase), 0), (nameof(Data.ReadyToRunInfo.NumImportSections), 4), (nameof(Data.ReadyToRunInfo.ImportSections), 8), + (nameof(Data.ReadyToRunInfo.CompositeInfo), 12)), }; if (methodDescMap) { From 165d005762df6f3c08f6862c2cebc8102239e149 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Nov 2026 16:25:04 -0600 Subject: [PATCH 3/4] cDAC: test composite component GCRefMap image base Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../cdac/tests/UnitTests/StackWalkTests.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index f75b973ae55faa..b5b852d038fcaf 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -24,13 +24,14 @@ public unsafe class StackWalkTests private const uint X86TransitionBlockSize = 7 * sizeof(uint); [Theory] - [InlineData(false)] - [InlineData(true)] - public void FindGCRefMap_ResolvesImportRecord(bool inlineGCRefMap) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FindGCRefMap_ResolvesImportRecord(bool inlineGCRefMap, bool compositeComponent) { (TestPlaceholderTarget target, _) = CreateX86TransitionFrameTarget( nameof(Data.ExternalMethodFrame), gcRefMap: [0x83, 0x05], hasMethodDesc: false, - inlineGCRefMap: inlineGCRefMap); + inlineGCRefMap: inlineGCRefMap, compositeComponent: compositeComponent); TargetPointer map = new FrameHelpers(target).FindGCRefMap(new TargetPointer(0x100108)); @@ -1208,7 +1209,8 @@ private static (TestPlaceholderTarget Target, ContextHolder Context) string frameType, byte[]? gcRefMap, bool hasMethodDesc, byte[]? signatureBlob = null, uint? representativeMethodDescAddress = null, bool hasUnresolvedIndirection = false, RuntimeInfoArchitecture architecture = RuntimeInfoArchitecture.X86, bool hasCodeData = true, - bool hasGCRefMapField = true, bool? valueType = null, bool unboxing = false, bool inlineGCRefMap = true) + bool hasGCRefMapField = true, bool? valueType = null, bool unboxing = false, bool inlineGCRefMap = true, + bool compositeComponent = false) { MockTarget.Architecture arch = new() { IsLittleEndian = true, Is64Bit = false }; TestPlaceholderTarget.Builder builder = new(arch); @@ -1261,11 +1263,19 @@ private static (TestPlaceholderTarget Target, ContextHolder Context) helpers.Write(module, 0x4000u); builder.MemoryBuilder.AddHeapFragment(new() { Address = 0x3000, Data = module, Name = "Module" }); byte[] r2rInfo = new byte[16]; - helpers.Write(r2rInfo.AsSpan(0), ImageBase); + // A component assembly of a composite image leaves LoadedImageBase unset; the composite's info holds it. + helpers.Write(r2rInfo.AsSpan(0), compositeComponent ? 0u : ImageBase); helpers.Write(r2rInfo.AsSpan(4), 1u); helpers.Write(r2rInfo.AsSpan(8), 0x5000u); - helpers.Write(r2rInfo.AsSpan(12), 0x4000u); + helpers.Write(r2rInfo.AsSpan(12), compositeComponent ? 0x4100u : 0x4000u); builder.MemoryBuilder.AddHeapFragment(new() { Address = 0x4000, Data = r2rInfo, Name = "ReadyToRun info" }); + if (compositeComponent) + { + byte[] compositeInfo = new byte[16]; + helpers.Write(compositeInfo.AsSpan(0), ImageBase); + helpers.Write(compositeInfo.AsSpan(12), 0x4100u); + builder.MemoryBuilder.AddHeapFragment(new() { Address = 0x4100, Data = compositeInfo, Name = "Composite ReadyToRun info" }); + } byte[] importSection = new byte[20]; helpers.Write(importSection.AsSpan(0), ImportSectionRva); helpers.Write(importSection.AsSpan(4), 3u * sizeof(uint)); From 4dec2dcdffdacd686635c6486d7a5e8a3f81547c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Nov 2026 17:06:09 -0600 Subject: [PATCH 4/4] cDAC: regenerate usage docs from data-descriptor-meanings.json Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 1 + docs/design/datacontracts/data-descriptor-meanings.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 29e7bc0728e3c0..c05770adf5628a 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -207,6 +207,7 @@ On ARM64, the `pac` unwind opcode strips pointer authentication bits from `Lr` u | `Object` | `m_pMethTab` | `pointer` | Method table for the object | | `PInvokeCalliFrame` | `VASigCookiePtr` | `pointer` | Pointer to the varargs signature cookie for the unmanaged call | | `PortableEntryPoint` | `ActualCode` | `pointer` | Native code for the entrypoint; on WebAssembly R2R code this is a function-table index (only defined if `FeaturePortableEntrypoints` is enabled) | +| `ReadyToRunInfo` | `CompositeInfo` | `pointer` | Pointer to composite R2R info - or itself for non-composite | | `ReadyToRunInfo` | `ImportSections` | `pointer` | Pointer to the array of ReadyToRun import sections | | `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image; unset for component assemblies of a composite image, so it is read from the `CompositeInfo` | | `ReadyToRunInfo` | `MinVirtualIP` | `pointer` | Base virtual IP assigned to the ReadyToRun module on WebAssembly | diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index cfadd9c2d1b0e8..ee23b38a31ccd9 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -534,7 +534,7 @@ "ReadyToRunInfo.EntryPointToMethodDescMap": "`HashMap` of entry point addresses to `MethodDesc` pointers", "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.LoadedImageBase": "Base address of the loaded R2R image; unset for component assemblies of a composite image, so it is read from the `CompositeInfo`", "ReadyToRunInfo.MinVirtualIP": "Base virtual IP assigned to the ReadyToRun module on WebAssembly", "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",