Fix cross-bitness address truncation in ClrDataAddress conversions#1423
Conversation
|
Follow-up to #1421 — we missed this case. |
Cross-bitness dump reading (e.g., 32-bit host reading 64-bit dumps) is not yet supported by ClrMD due to ClrDataAddress truncation issues. Skip these tests with a clear message referencing microsoft/clrmd#1423 instead of letting them fail with confusing errors. The skip logic compares the host's IntPtr.Size against the dump's architecture from dump-info.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reading 64-bit dumps from a 32-bit host is not yet supported by ClrMD due to ClrDataAddress truncation issues (microsoft/clrmd#1423). Skip these tests with a clear message instead of letting them fail. The reverse (64-bit host reading 32-bit dumps) works fine and is not skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary Skip cDAC dump tests when the host pointer size doesn't match the dump's architecture (e.g., 32-bit host reading 64-bit dumps). Cross-bitness dump reading is not yet supported by ClrMD due to `ClrDataAddress` truncation issues being fixed in microsoft/clrmd#1423. ## Changes - **DumpTestBase.cs**: Added cross-bitness check in `EvaluateSkipAttributes`. When `IntPtr.Size` doesn't match the dump architecture from `dump-info.json`, the test is skipped with a clear message referencing the upstream fix. ## Impact This prevents confusing test failures on x86 Helix legs when they attempt to load x64 dumps (and vice versa). Once microsoft/clrmd#1423 is merged and the ClrMD package is updated, this skip can be removed. Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Let's separate out the This is a pretty messy change for something I don't really care if it works or not (32bit process debugging an x64 process). I don't mind supporting it if it's something you all want, but I want to make sure it doesn't overcomplicate the code. |
|
This will be incredibly tricky to get right. I think I'd like to see a more explicit converter class which we create once when the DacLibrary. This converter can hang off of DacLibrary but should be passed to ClrDataProcess and all SosDac instances. This way we have an encapsulated/explicit way of handling this conversion throughout. Danger: Danger: ClrDataAddress value parameters (which is basically every dac method) also expects a sign extended value at the boundary. The current code works (probably by accident), so we have to be careful not to break it. For example: inline TADDR CLRDATA_ADDRESS_TO_TADDR(CLRDATA_ADDRESS cdAddr)
{
#ifndef HOST_64BIT
INT64 iSignedAddr = (INT64)cdAddr;
if (iSignedAddr > INT_MAX || iSignedAddr < INT_MIN)
DacError(E_INVALIDARG);
#endif
return (TADDR)cdAddr;
}Walk 0x80000000 (a valid 3-GB-range 32-bit target address) through this:
Below is a rough sketch of what I'm thinking, though this is untested. You can modify this design if you want, but the key points:
(Or feel free to come up with something that works better. Just keep in mind that touching this fragile system is a hot stove of pain. You are more than welcome to tackle this, we just need to make sure it's right with the legacy dac and test it as best we can.) /// <summary>
/// A wrapper around the raw 64-bit value produced by the DAC to represent an address. Due to historic/legacy
/// reasons, some DACs (legacy DACs) produce a sign-extended 64-bit value even when the target process is 32-bit.
/// This struct encapsulates that value and provides a converter to handle both legacy and modern DACs correctly.
/// </summary>
/// <param name="_value">The raw 64 bits produced by the DAC. May be sign-extended from a 32-bit target address
/// when the backing DAC is a legacy DAC.</param>
[StructLayout(LayoutKind.Sequential)]
[DebuggerDisplay("{Value,h}")]
internal readonly struct ClrDataAddress(ulong _value) : IEquatable<ClrDataAddress>
{
public bool IsNull => _value == 0;
public bool Equals(ClrDataAddress other) => _value == other._value;
public override bool Equals(object? obj) => obj is ClrDataAddress cda && Equals(cda);
public override int GetHashCode() => _value.GetHashCode();
public static bool operator ==(ClrDataAddress left, ClrDataAddress right) => left.Equals(right);
public static bool operator !=(ClrDataAddress left, ClrDataAddress right) => !left.Equals(right);
public override string ToString() => $"0x{_value:x}";
/// <summary>
/// Constructs a ClrDataAddress.Converter for the given DAC configuration.
/// </summary>
/// <param name="_signExtend">Should be false if cDac, true otherwise.</param>
/// <param name="_pointerSize">The pointer size of the target process.</param>
public sealed class Converter(bool _signExtend, int _pointerSize)
{
/// <summary>
/// The target pointer size this converter was built for, in bytes.
/// </summary>
public int PointerSize => _pointerSize;
/// <summary>
/// True if this converter un-sign-extends legacy DAC output. Exposed
/// primarily for diagnostics/asserts; callers should normally just call
/// <see cref="ToAddress"/> / <see cref="FromAddress"/>.
/// </summary>
public bool IsLegacy => _signExtend;
/// <summary>
/// Returns the un-sign-extended value of a ClrDataAddress to pass to a dac function.
/// </summary>
public ulong ToInteropAddress(ClrDataAddress cda) => cda._value;
/// <summary>
/// Converts a DAC-produced address to the real target address that
/// the rest of ClrMD should see.
/// </summary>
public ulong ToAddress(ClrDataAddress cda)
{
if (!_signExtend)
return cda.Value;
return _pointerSize == 4 ? (uint)cda.Value : cda.Value;
}
/// <summary>
/// Converts a canonical target address to the bit pattern the DAC
/// expects back on its interface.
/// </summary>
public ClrDataAddress FromAddress(ulong address)
{
if (!_signExtend)
return new ClrDataAddress(address);
return _pointerSize == 4
? new ClrDataAddress(unchecked((ulong)(long)(int)address))
: new ClrDataAddress(address);
}
}
} |
Agreed that this is messy and that 32 bit processes debugging a 64 bit process is not an important scenario. The larger reason I want to fix this is supporting 64 bit processes debugging a 32 bit dump. Today it mostly works, but only when the 32 bit address is not in the top half of the address (and the top half of bits are filled with
We handle this in the cDAC by using custom source generated marshallers to marshal the
The cDAC maintains the sign-extension of the legacy DAC APIs which use ClrDataAddress, so there shouldn't be any behavioral differences between legacy and 'modern' ISOSDacInterface APIs. I appreciate the feedback. I'll rework this to try and make it cleaner using the converter approach. Thanks. |
e0e322e to
f47dd17
Compare
|
Update: Rebased onto latest New in this push:
Validation:
|
f47dd17 to
9b7ffbb
Compare
leculver
left a comment
There was a problem hiding this comment.
Just some pre-review things I found.
|
Oh, I think SOSDac12.cs was entirely missed? |
- Flip all by-value ClrDataAddress params in vtable signatures to ulong (annotated /*ClrDataAddress*/) so the ABI boundary is a pure blittable primitive on every platform. - Wrapper methods call .ToInteropAddress() at call sites; ref/out/pointer forms are unchanged (ABI-safe as-is). - Add TargetProperties struct and ClrDataAddress conversion helpers (FromAddress, ToAddress, FromInteropAddress, ToInteropAddress). - Remove redundant TargetProperties target = _target local copies across all DacImplementation files (use _target field directly). - Replace ClrDataAddress.FromAddress(0, _target) with default. - Revise ClrDataAddress XML doc to document the ABI boundary policy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
17f4a60 to
57d49a1
Compare
Add missing /*ClrDataAddress*/ annotations on GetMethodDescData ip param and GetSyncBlockCleanupData addr param to match sospriv.idl. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
57d49a1 to
35fc125
Compare
Replace all 'default' usages that represent a null ClrDataAddress with the more descriptive ClrDataAddress.Null property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add comprehensive block comment to ClrDataAddress.cs explaining: - How the DAC sign-extends 32-bit addresses on the wire - The C++ conversion macros (TO_CDADDR, CLRDATA_ADDRESS_TO_TADDR) - The equivalent C# cast chains in FromAddress/ToAddress - Why vtable slots use ulong instead of the struct (ABI safety) - The full data flow from DacImplementation through the vtable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarifies that the input is a clean target-process address (not sign-extended), and the method produces a sign-extended ClrDataAddress for the DAC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7cae7a0 to
6acabb6
Compare
- Store TargetProperties directly instead of DacLibrary - Remove unnecessary null-conditional and ArgumentNullException (codebase has nullable enabled, library param is non-null) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
leculver
left a comment
There was a problem hiding this comment.
One issue and three nitpicks.
It will likely be a day or two before I hit approve, I have to cleanly get the 4.0 change through.
- Add unchecked to ToAddress cast (prevents OverflowException) - Inline ccwAddr/rcwAddr locals in DacComHelpers - Remove dead DacLibrary null checks and unused _library fields across SosDac6/8/12/13Old/14/16 and ClrDataProcess - Remove ?. null-conditional on library.OwningLibrary (nullable enabled) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
leculver
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the hard work on this one!
- Remove library?. null-conditional in ClrStackWalk - Replace last default with ClrDataAddress.Null in DacTypeHelpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6ef25e9 to
e6637e5
Compare
Update ClrMD to the first stable release with cross-bitness address truncation fixes (microsoft/clrmd#1423). This version correctly handles sign-extended CLRDATA_ADDRESS values on 32-bit targets, enabling 32-bit hosts to read 64-bit dumps. Remove the cross-bitness dump test skip added in #127118 — ClrMD now supports this scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update ClrMD to the first stable release with cross-bitness address truncation fixes ([microsoft/clrmd#1423](microsoft/clrmd#1423)). ## Changes - **`eng/Versions.props`**: Update `MicrosoftDiagnosticsRuntimeVersion` from `4.0.0-beta.26210.1` to `4.0.722401` (stable release) - **`DumpTestBase.cs`**: Remove the cross-bitness dump test skip added in #127118 — ClrMD 4.0.722401 now correctly handles sign-extended `CLRDATA_ADDRESS` values, enabling 32-bit hosts to read 64-bit dumps ## What was fixed in ClrMD ClrMD 4.0.722401 includes: - `ClrDataAddress` type with explicit sign-extension handling (`FromTargetAddress` / `ToAddress`) - ABI-safe COM vtable marshalling (addresses as `ulong` primitives, not structs) - `unchecked` casts to prevent `OverflowException` on sign-extended 32-bit addresses - Full verification against `sospriv.idl` in dotnet/runtime ## Cross-platform verification xplat cDAC dump tests pass with the skip removed: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1398682&view=ms.vss-test-web.build-test-results-tab Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Refactor
ClrDataAddressto correctly handle sign-extended addresses from the DAC and eliminate cross-platform ABI ambiguity at the COM interop boundary. This prevents silent address truncation when inspecting 32-bit targets and ensures correct marshalling on all platforms.Problem
ClrDataAddressused implicit operators that converted throughnint/nuint:operator ulong:(nuint)Value— truncates 64-bit addresses to 32 bits on x86 hostsoperator ClrDataAddress(ulong):(nint)value— same truncation on inputThis corrupts all addresses when a 32-bit host reads a 64-bit dump, causing:
GetExportSymbolAddress()returning 0PEImagebeing nullCould not find DotNetRuntimeContractDescriptorerrorsAdditionally, the DAC's sign-extended 32-bit addresses (e.g.,
0xFFFFFFFF_80123456) causedOverflowExceptionin checked arithmetic contexts inDacDataTargetCOM.ReadVirtual.ClrDataAddress Rules
1. Sign Extension
The legacy DAC sign-extends 32-bit target addresses on the wire:
0x80123456becomes0xFFFFFFFF_80123456(CLRDATA_ADDRESS)FromAddress(addr, target)applies sign extension:(ulong)(long)(int)(uint)addrToAddress(target)strips it:(ulong)(uint)_value2. ABI Boundary — Managed-Only Type
ClrDataAddressnever crosses the ABI boundary by value. Vtable slots use plainulong(annotated/*ClrDataAddress*/) because single-field struct passing differs across platform ABIs. Managed wrappers convert via:addr.ToInteropAddress()— unwrap to raw ulong (no transform)ClrDataAddress.FromInteropAddress(raw)— wrap from raw ulong (no transform)Pointer forms (
ref/out/ClrDataAddress*) are safe as-is and preserved in vtable signatures.3. Confinement
ClrDataAddressexists only inDacInterface/(vtable wrappers, struct definitions) andDacImplementation/(conversion boundary). It never leaks intoAbstractDac/,Implementation/, or the public API. The rest of ClrMD uses plainulongfor all addresses.4. Data Flow
Changes
ClrDataAddress API
ClrDataAddressandulongToAddress(TargetProperties)/FromAddress(ulong, TargetProperties)— sign-extension-aware conversionsToInteropAddress()/FromInteropAddress(ulong)— raw wire-value helpers for the ABI boundaryClrDataAddress.Nullproperty — replacesdefaultfor clarityIsNull,IEquatable<ClrDataAddress>, hexDebuggerDisplayTargetPropertiesstruct — holds pointer size, threaded through DAC layerVtable Signatures (all SosDac files)
Flipped every by-value
ClrDataAddressparameter toulong /*ClrDataAddress*/:SosDac.cs— 48 vtable slots, 47 wrapper call sitesSosDac6.cs,SosDac8.cs,SosDac13.cs,SosDac14.cs,SOSDac13Old.csClrDataProcess.cs— includingCLRDATA_ENUMhandle correctionsDacDataTargetCOM.cs— address-typed slots annotatedVerified slot-by-slot against
sospriv.idlin dotnet/runtime.DacImplementation Conversion Sites
ClrDataAddress.FromAddress(ulong, _target)to wrap addresses for DAC calls.ToAddress(_target)to unwrapdefaultClrDataAddress usages withClrDataAddress.NullTargetProperties target = _targetlocal copiesInbound DAC Callback Fix
DacDataTargetCOM.ReadVirtual: wrap(long)addressinunchecked(...)to preventOverflowExceptionon sign-extended 32-bit addressesTesting
netstandard2.0andnet10.0SingleFileBasicArrayTestsfailures unrelated)OverflowExceptioninClrDataAddress.ToAddressfor checked context — pre-existing, fix pending (unchecked((uint)_value))Follow-up from #1421
This PR addresses the broader
ClrDataAddressdesign issue discovered after #1421 (IntPtr.Size fixes) was merged.