From 0444fc383539e6b34558d144cbce90aa3d6f1c45 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 13 Apr 2026 16:18:00 -0400 Subject: [PATCH] Add debug type validation for global reads in cDAC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add DataDescriptorTypeValidation as a shared helper in Abstractions for validating that data descriptor type annotations match the C# read type. Used by both field extensions and global reads. - AssertPrimitiveType — validates primitive field/global types - AssertPointerType — validates pointer field types - AssertGlobalType — validates ReadGlobal calls; pointer-like globals (pointer, nuint, nint) must use ReadGlobalPointer instead - AssertGlobalPointerType — validates ReadGlobalPointer calls - TargetFieldExtensions delegates to shared helpers instead of duplicating validation logic - ContractDescriptorTarget uses TryReadGlobalRaw internally to separate raw reads from validated reads All methods are [Conditional(DEBUG)] and fully elided in release. Contributes to #126749 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DataDescriptorTypeValidation.cs | 98 +++++++++++++++++++ .../TargetFieldExtensions.cs | 60 ++---------- .../ContractDescriptorTarget.cs | 27 +++-- 3 files changed, 123 insertions(+), 62 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataDescriptorTypeValidation.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataDescriptorTypeValidation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataDescriptorTypeValidation.cs new file mode 100644 index 00000000000000..27ad7fc1413a5f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataDescriptorTypeValidation.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Numerics; + +namespace Microsoft.Diagnostics.DataContractReader; + +/// +/// Debug-only helpers that validate cDAC type annotations match the C# read type. +/// In release builds, all methods are completely elided by the . +/// +public static class DataDescriptorTypeValidation +{ + /// + /// Assert that a declared field type name is compatible with the C# primitive integer type . + /// + [Conditional("DEBUG")] + public static void AssertPrimitiveType(string? typeName, string context) + where T : unmanaged, IBinaryInteger, IMinMaxValue + { + Debug.Assert( + typeName is null or "" || IsCompatiblePrimitiveType(typeName), + $"Type mismatch reading {context}: declared as '{typeName}', reading as {typeof(T).Name}"); + } + + /// + /// Assert that a declared field type name is "pointer" (or absent). + /// + [Conditional("DEBUG")] + public static void AssertPointerType(string? typeName, string context) + { + Debug.Assert( + typeName is null or "" or "pointer", + $"Type mismatch reading {context}: declared as '{typeName}', expected pointer"); + } + + /// + /// Assert that a global's declared type is compatible with ReadGlobal<T>. + /// Pointer-like types (nuint, nint, pointer) must use ReadGlobalPointer instead. + /// String-typed globals are allowed since they may carry dual numeric/string values. + /// + [Conditional("DEBUG")] + public static void AssertGlobalType(string? typeName, string globalName) + where T : struct, INumber + { + if (typeName is null or "" or "string") + return; + + bool compatible = typeName switch + { + "uint8" => typeof(T) == typeof(byte), + "int8" => typeof(T) == typeof(sbyte), + "uint16" => typeof(T) == typeof(ushort), + "int16" => typeof(T) == typeof(short), + "uint32" => typeof(T) == typeof(uint), + "int32" => typeof(T) == typeof(int), + "uint64" => typeof(T) == typeof(ulong), + "int64" => typeof(T) == typeof(long), + "bool" => typeof(T) == typeof(byte), + _ => false, + }; + + Debug.Assert(compatible, + $"Type mismatch reading global '{globalName}': declared as '{typeName}', reading as {typeof(T).Name}. " + + $"Pointer-like globals (pointer, nuint, nint) should be read via ReadGlobalPointer."); + } + + /// + /// Assert that a global's declared type is compatible with ReadGlobalPointer. + /// Accepts pointer, nuint, nint, or absent type information. + /// + [Conditional("DEBUG")] + public static void AssertGlobalPointerType(string? typeName, string globalName) + { + Debug.Assert( + typeName is null or "" or "pointer" or "nuint" or "nint" or "string", + $"Type mismatch reading global '{globalName}' as pointer: declared as '{typeName}', expected pointer/nuint/nint"); + } + + public static bool IsCompatiblePrimitiveType(string typeName) + where T : unmanaged, IBinaryInteger, IMinMaxValue + { + return typeName switch + { + "uint8" => typeof(T) == typeof(byte), + "int8" => typeof(T) == typeof(sbyte), + "uint16" => typeof(T) == typeof(ushort), + "int16" => typeof(T) == typeof(short), + "uint32" => typeof(T) == typeof(uint), + "int32" => typeof(T) == typeof(int), + "uint64" => typeof(T) == typeof(ulong), + "int64" => typeof(T) == typeof(long), + "bool" => typeof(T) == typeof(byte), + _ => false, + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs index 007af1845cb798..7e2343bbf75f4c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs @@ -21,8 +21,7 @@ public static T ReadField(this Target target, ulong address, Target.TypeInfo where T : unmanaged, IBinaryInteger, IMinMaxValue { Target.FieldInfo field = typeInfo.Fields[fieldName]; - AssertPrimitiveType(field, fieldName); - + DataDescriptorTypeValidation.AssertPrimitiveType(field.TypeName, $"field '{fieldName}'"); return target.Read(address + (ulong)field.Offset); } @@ -36,8 +35,7 @@ public static T ReadFieldOrDefault(this Target target, ulong address, Target. if (!typeInfo.Fields.TryGetValue(fieldName, out Target.FieldInfo field)) return defaultValue; - AssertPrimitiveType(field, fieldName); - + DataDescriptorTypeValidation.AssertPrimitiveType(field.TypeName, $"field '{fieldName}'"); return target.Read(address + (ulong)field.Offset); } @@ -47,8 +45,7 @@ public static T ReadFieldOrDefault(this Target target, ulong address, Target. public static TargetPointer ReadPointerField(this Target target, ulong address, Target.TypeInfo typeInfo, string fieldName) { Target.FieldInfo field = typeInfo.Fields[fieldName]; - AssertPointerType(field, fieldName); - + DataDescriptorTypeValidation.AssertPointerType(field.TypeName, $"field '{fieldName}'"); return target.ReadPointer(address + (ulong)field.Offset); } @@ -61,8 +58,7 @@ public static TargetPointer ReadPointerFieldOrNull(this Target target, ulong add if (!typeInfo.Fields.TryGetValue(fieldName, out Target.FieldInfo field)) return TargetPointer.Null; - AssertPointerType(field, fieldName); - + DataDescriptorTypeValidation.AssertPointerType(field.TypeName, $"field '{fieldName}'"); return target.ReadPointer(address + (ulong)field.Offset); } @@ -75,7 +71,6 @@ public static TargetNUInt ReadNUIntField(this Target target, ulong address, Targ Debug.Assert( field.TypeName is null or "" or "nuint", $"Type mismatch reading field '{fieldName}': declared as '{field.TypeName}', expected nuint"); - return target.ReadNUInt(address + (ulong)field.Offset); } @@ -88,13 +83,11 @@ public static TargetCodePointer ReadCodePointerField(this Target target, ulong a Debug.Assert( field.TypeName is null or "" or "CodePointer", $"Type mismatch reading field '{fieldName}': declared as '{field.TypeName}', expected CodePointer"); - return target.ReadCodePointer(address + (ulong)field.Offset); } /// /// Read a field that contains an inline Data struct type, with type validation. - /// Returns the data object created by . /// public static T ReadDataField(this Target target, ulong address, Target.TypeInfo typeInfo, string fieldName) where T : IData @@ -103,25 +96,21 @@ public static T ReadDataField(this Target target, ulong address, Target.TypeI Debug.Assert( field.TypeName is null or "" || field.TypeName == typeof(T).Name, $"Type mismatch reading field '{fieldName}': declared as '{field.TypeName}', reading as {typeof(T).Name}"); - return target.ProcessedData.GetOrAdd(address + (ulong)field.Offset); } /// /// Read a field that contains a pointer to a Data struct type, with type validation. - /// Reads the pointer, then creates the data object via . /// Returns null if the pointer is null. /// public static T? ReadDataFieldPointer(this Target target, ulong address, Target.TypeInfo typeInfo, string fieldName) where T : IData { Target.FieldInfo field = typeInfo.Fields[fieldName]; - AssertPointerType(field, fieldName); - + DataDescriptorTypeValidation.AssertPointerType(field.TypeName, $"field '{fieldName}'"); TargetPointer pointer = target.ReadPointer(address + (ulong)field.Offset); if (pointer == TargetPointer.Null) return default; - return target.ProcessedData.GetOrAdd(pointer); } @@ -135,47 +124,10 @@ public static T ReadDataField(this Target target, ulong address, Target.TypeI if (!typeInfo.Fields.TryGetValue(fieldName, out Target.FieldInfo field)) return default; - AssertPointerType(field, fieldName); - + DataDescriptorTypeValidation.AssertPointerType(field.TypeName, $"field '{fieldName}'"); TargetPointer pointer = target.ReadPointer(address + (ulong)field.Offset); if (pointer == TargetPointer.Null) return default; - return target.ProcessedData.GetOrAdd(pointer); } - - [Conditional("DEBUG")] - private static void AssertPrimitiveType(Target.FieldInfo field, string fieldName) - where T : unmanaged, IBinaryInteger, IMinMaxValue - { - Debug.Assert( - field.TypeName is null or "" || IsCompatiblePrimitiveType(field.TypeName), - $"Type mismatch reading field '{fieldName}': declared as '{field.TypeName}', reading as {typeof(T).Name}"); - } - - [Conditional("DEBUG")] - private static void AssertPointerType(Target.FieldInfo field, string fieldName) - { - Debug.Assert( - field.TypeName is null or "" or "pointer", - $"Type mismatch reading field '{fieldName}': declared as '{field.TypeName}', expected pointer"); - } - - private static bool IsCompatiblePrimitiveType(string typeName) - where T : unmanaged, IBinaryInteger, IMinMaxValue - { - return typeName switch - { - "uint8" => typeof(T) == typeof(byte), - "int8" => typeof(T) == typeof(sbyte), - "uint16" => typeof(T) == typeof(ushort), - "int16" => typeof(T) == typeof(short), - "uint32" => typeof(T) == typeof(uint), - "int32" => typeof(T) == typeof(int), - "uint64" => typeof(T) == typeof(ulong), - "int64" => typeof(T) == typeof(long), - "bool" => typeof(T) == typeof(byte), - _ => false, - }; - } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 71786f8d9a7d30..967b2036955e5b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -706,15 +706,13 @@ public override bool TryReadGlobal(string name, [NotNullWhen(true)] out T? va public bool TryReadGlobal(string name, [NotNullWhen(true)] out T? value, out string? type) where T : struct, INumber { - value = null; - type = null; - if (!_globals.TryGetValue(name, out GlobalValue global) || global.NumericValue is null) + if (!TryReadGlobalRaw(name, out ulong? rawValue, out type)) { - // Not found or does not contain a numeric value + value = null; return false; } - type = global.Type; - value = T.CreateChecked(global.NumericValue.Value); + DataDescriptorTypeValidation.AssertGlobalType(type, name); + value = T.CreateChecked(rawValue.Value); return true; } @@ -735,10 +733,11 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out TargetPointer? value, out string? type) { value = null; - if (!TryReadGlobal(name, out ulong? innerValue, out type)) + if (!TryReadGlobalRaw(name, out ulong? rawValue, out type)) return false; - value = new TargetPointer(innerValue.Value); + DataDescriptorTypeValidation.AssertGlobalPointerType(type, name); + value = new TargetPointer(rawValue.Value); return true; } @@ -753,6 +752,18 @@ public TargetPointer ReadGlobalPointer(string name, out string? type) return value.Value; } + private bool TryReadGlobalRaw(string name, [NotNullWhen(true)] out ulong? value, out string? type) + { + value = null; + type = null; + if (!_globals.TryGetValue(name, out GlobalValue global) || global.NumericValue is null) + return false; + + type = global.Type; + value = global.NumericValue; + return true; + } + public override string ReadGlobalString(string name) => ReadStringGlobal(name, out _);