Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates a DEBUG-time assertion in GetMethodTableName to include additional diagnostic details when the legacy and new implementations disagree.
Changes:
- Adds a formatted message to a
Debug.Assertcomparingneededsizes and method table names.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…er.Legacy/SOSDacImpl.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| if (!(pNeeded is null || *pNeeded == neededLocal)) | ||
| { | ||
| string localNameString = new(mtNameLocal, 0, localNameLength); | ||
| string nameString = mtName is null ? string.Empty : new(mtName, 0, nameLength); | ||
| Debug.Fail($"local name = {localNameString}, name = {nameString}"); | ||
| } |
There was a problem hiding this comment.
The pNeeded validation uses a manual if + Debug.Fail(...), which is inconsistent with the surrounding pattern in this file (other callsites use Debug.Assert(pNeeded == null || *pNeeded == neededLocal, ...)). Consider switching back to Debug.Assert with a message and include the mismatched *pNeeded/neededLocal values (and optionally count) so the failure is actionable.
| if (!(pNeeded is null || *pNeeded == neededLocal)) | |
| { | |
| string localNameString = new(mtNameLocal, 0, localNameLength); | |
| string nameString = mtName is null ? string.Empty : new(mtName, 0, nameLength); | |
| Debug.Fail($"local name = {localNameString}, name = {nameString}"); | |
| } | |
| string localNameString = new(mtNameLocal, 0, localNameLength); | |
| string nameString = mtName is null ? string.Empty : new(mtName, 0, nameLength); | |
| Debug.Assert( | |
| pNeeded is null || *pNeeded == neededLocal, | |
| $"pNeeded mismatch: actual={(pNeeded is null ? "null" : (*pNeeded).ToString())}, expected={neededLocal}, count={count}, local name = {localNameString}, name = {nameString}"); |
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
| @@ -2946,6 +2948,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.TypeHandle methodTableHandle = typeSystemContract.GetTypeHandle(mt.ToTargetPointer(_target, overrideCheck: true)); | |||
| if (typeSystemContract.IsFreeObjectMethodTable(methodTableHandle)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "Free"); | |||
| } | |||
There was a problem hiding this comment.
counttest is declared as int, but it's passed to APIs that require uint (OutputBufferHelpers.CopyStringToBuffer(..., uint bufferSize, ...) and ISOSDacInterface.GetMethodTableName(..., uint count, ...)). This won’t compile without an explicit cast; consider making this a const uint (or uint countTest = 4096u) to match the API surface.
| @@ -2946,6 +2948,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.TypeHandle methodTableHandle = typeSystemContract.GetTypeHandle(mt.ToTargetPointer(_target, overrideCheck: true)); | |||
| if (typeSystemContract.IsFreeObjectMethodTable(methodTableHandle)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "Free"); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, "Free"); | |||
| } | |||
| else | |||
| @@ -2954,6 +2960,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePointer); | |||
| if (!loader.TryGetLoadedImageContents(moduleHandle, out _, out _, out _)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "<Unloaded Type>"); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, "<Unloaded Type>"); | |||
| } | |||
| else | |||
| @@ -2972,6 +2982,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| methodTableName.Append(fallbackName); | |||
| } | |||
| } | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, methodTableName.ToString()); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, methodTableName.ToString()); | |||
| } | |||
There was a problem hiding this comment.
mtNametest/counttest and the extra CopyStringToBuffer calls are executed in all builds, but they are only used by the #if DEBUG validation logic later. This adds extra allocations and string copies to a potentially hot path; consider moving the temp buffer + copies under #if DEBUG (or using a stackalloc/Span-based approach guarded by DEBUG).
| @@ -2946,6 +2948,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.TypeHandle methodTableHandle = typeSystemContract.GetTypeHandle(mt.ToTargetPointer(_target, overrideCheck: true)); | |||
| if (typeSystemContract.IsFreeObjectMethodTable(methodTableHandle)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "Free"); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, "Free"); | |||
| } | |||
| else | |||
| @@ -2954,6 +2960,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePointer); | |||
| if (!loader.TryGetLoadedImageContents(moduleHandle, out _, out _, out _)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "<Unloaded Type>"); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, "<Unloaded Type>"); | |||
| } | |||
| else | |||
| @@ -2972,6 +2982,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| methodTableName.Append(fallbackName); | |||
| } | |||
| } | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, methodTableName.ToString()); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, methodTableName.ToString()); | |||
| } | |||
There was a problem hiding this comment.
mtNametest / counttest are allocated and used unconditionally (outside #if DEBUG), but they only appear to support the DEBUG-only legacy validation later. This adds extra buffer allocation and extra CopyStringToBuffer calls (and an extra ToString() allocation) to the release path. Consider moving the test buffer + its writes under #if DEBUG (or removing entirely) and caching methodTableName.ToString() into a local string once before copying to buffers.
| char[] mtNameLocal = new char[counttest]; | ||
| uint neededLocal; | ||
| int hrLocal; | ||
| fixed (char* ptr = mtNameLocal) | ||
| { | ||
| hrLocal = _legacyImpl.GetMethodTableName(mt, count, ptr, &neededLocal); | ||
| hrLocal = _legacyImpl.GetMethodTableName(mt, counttest, ptr, &neededLocal); |
There was a problem hiding this comment.
In the #if DEBUG block, new char[counttest] uses a uint length, which does not compile. Use an int length (e.g., const int bufferLen = 4096; and allocate new char[bufferLen]) and pass the corresponding uint to the DAC API.
…er.Legacy/SOSDacImpl.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| @@ -2946,6 +2948,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.TypeHandle methodTableHandle = typeSystemContract.GetTypeHandle(mt.ToTargetPointer(_target, overrideCheck: true)); | |||
| if (typeSystemContract.IsFreeObjectMethodTable(methodTableHandle)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "Free"); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, "Free"); | |||
| } | |||
| else | |||
| @@ -2954,6 +2960,10 @@ int ISOSDacInterface.GetMethodTableName(ClrDataAddress mt, uint count, char* mtN | |||
| Contracts.ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePointer); | |||
| if (!loader.TryGetLoadedImageContents(moduleHandle, out _, out _, out _)) | |||
| { | |||
| fixed (char* pMtNametest = mtNametest) | |||
| { | |||
| OutputBufferHelpers.CopyStringToBuffer(pMtNametest, counttest, pNeeded, "<Unloaded Type>"); | |||
| } | |||
| OutputBufferHelpers.CopyStringToBuffer(mtName, count, pNeeded, "<Unloaded Type>"); | |||
There was a problem hiding this comment.
mtNametest/counttest are allocated unconditionally and the method now performs extra CopyStringToBuffer calls into this temporary buffer in all builds. This adds per-call allocations and duplicate work, and also writes pNeeded twice. If this buffer is only meant for DEBUG validation, move the temp buffer + copy under #if DEBUG (or remove entirely).
| char[] mtNameLocal = new char[counttest]; | ||
| uint neededLocal; | ||
| int hrLocal; | ||
| fixed (char* ptr = mtNameLocal) | ||
| { | ||
| hrLocal = _legacyImpl.GetMethodTableName(mt, count, ptr, &neededLocal); | ||
| hrLocal = _legacyImpl.GetMethodTableName(mt, counttest, ptr, &neededLocal); | ||
| } | ||
| Debug.ValidateHResult(hr, hrLocal); | ||
| if (hr == HResults.S_OK) | ||
| { | ||
| Debug.Assert(pNeeded == null || *pNeeded == neededLocal); | ||
| Debug.Assert(mtName == null || new ReadOnlySpan<char>(mtNameLocal, 0, (int)neededLocal - 1).SequenceEqual(new string(mtName))); | ||
| int maxComparableLength = 4095; | ||
| ReadOnlySpan<char> localComparableSpan = mtNameLocal.AsSpan(0, maxComparableLength); | ||
| int localNameLength = localComparableSpan.IndexOf('\0'); | ||
| if (localNameLength < 0) | ||
| { | ||
| localNameLength = maxComparableLength; | ||
| } | ||
|
|
||
| int nameLength = 0; | ||
| if (mtName is not null) | ||
| { | ||
| ReadOnlySpan<char> comparableSpan = mtNametest.AsSpan(0, maxComparableLength); | ||
| nameLength = comparableSpan.IndexOf('\0'); | ||
| if (nameLength < 0) | ||
| { | ||
| nameLength = maxComparableLength; | ||
| } | ||
| } | ||
|
|
||
| if (!(pNeeded is null || *pNeeded == neededLocal)) | ||
| { | ||
| string localNameString = new(mtNameLocal, 0, localNameLength); | ||
| string nameString = mtNametest is null ? string.Empty : new(mtNametest, 0, nameLength); | ||
| Debug.Fail($"local name = {localNameString}, name = {nameString}, neededlocal = {neededLocal}, pneeded = {(pNeeded == null ? "null" : *pNeeded)}"); | ||
| } |
There was a problem hiding this comment.
The DEBUG cross-check no longer validates that the produced name matches the legacy DAC output (it only checks pNeeded), and it uses a hard-coded counttest when calling _legacyImpl instead of using the caller-provided count. This deviates from the established pattern in this file (e.g., GetAppDomainName at SOSDacImpl.cs:284-300) and can mask output mismatches. Consider restoring name comparison and passing count through, while making the comparison robust against truncation (e.g., compare up to the first \0 or Math.Min(count, neededLocal)).
| if (!(pNeeded is null || *pNeeded == neededLocal)) | ||
| { | ||
| string localNameString = new(mtNameLocal, 0, localNameLength); | ||
| string nameString = mtNametest is null ? string.Empty : new(mtNametest, 0, nameLength); |
There was a problem hiding this comment.
mtNametest is a local array and can never be null, so mtNametest is null ? ... is dead code. If the intent is to avoid reading from an uninitialized buffer, gate on whether the temp buffer was actually populated or (preferably) build the string from the actual output pointer (mtName) when non-null.
| string nameString = mtNametest is null ? string.Empty : new(mtNametest, 0, nameLength); | |
| string nameString = mtName is null ? string.Empty : new(mtNametest, 0, nameLength); |
No description provided.