diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml index f84d7c74c99f..6ef9b033ae30 100644 --- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml +++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml @@ -870,6 +870,7 @@ + diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs index b22a234ad178..f1f243a075fa 100644 --- a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs +++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs @@ -23,6 +23,18 @@ internal partial class ProbeExpressionParser private const string RedactedValueDump = "{redacted}"; private const string BackingFieldSuffix = ">k__BackingField"; + // A type in CoreLib is never customer code, and a subclass of a type outside CoreLib is never in CoreLib. + private static readonly Assembly CoreLibAssembly = typeof(object).Assembly; + + // CoreLib exceptions whose Message formats values that can be customer objects: ReflectionTypeLoadException reads + // each loader exception's Message, and SwitchExpressionException formats the unmatched value. .NET Framework has no + // SwitchExpressionException, so it's looked up by name and can be null. + private static readonly Type ReflectionTypeLoadExceptionType = typeof(ReflectionTypeLoadException); + private static readonly Type SwitchExpressionExceptionType = CoreLibAssembly.GetType("System.Runtime.CompilerServices.SwitchExpressionException"); + + // Read as a field, so a Message override never runs. + private static readonly FieldInfo ExceptionMessageField = typeof(Exception).GetField("_message", BindingFlags.Instance | BindingFlags.NonPublic); + private static FieldInfo[] GetSafeFieldsForDump(FieldInfo[] fields) { var writeIndex = 0; @@ -103,60 +115,202 @@ private static bool IsRedactedRuntimeValue(object value, Type declaredType) return runtimeType != declaredType && ShouldRedactDumpValue(null, runtimeType); } + // Only called when the declared type is safe to call ToString on. A value type or an exact match is + // decided by the declared type. No subclass of a non-sealed allowed type is allowed itself, so any + // other runtime type is a subclass whose override must not run. + private static bool CanCallToStringOnRuntimeType(object value, Type declaredType) + { + return value.GetType() == declaredType || declaredType.IsValueType; + } + + // Message and StackTrace are virtual, so they are only called on a CoreLib exception whose Message doesn't + // format customer values. + private static bool CanCallExceptionMembers(Type exceptionType) + { + return ReferenceEquals(exceptionType.Assembly, CoreLibAssembly) && + !ReferenceEquals(exceptionType, ReflectionTypeLoadExceptionType) && + !ReferenceEquals(exceptionType, SwitchExpressionExceptionType); + } + + private static string DumpException(Exception exception, string prefix) + { + var runtimeType = exception.GetType(); + return CanCallExceptionMembers(runtimeType) + ? $"{prefix}{runtimeType.FullName}, {exception.Message}, {exception.StackTrace}" + : DumpExceptionMessageField(exception, prefix); + } + + // Shows the runtime type and the message the exception was constructed with, without calling any member. + private static string DumpExceptionMessageField(Exception exception, string prefix) + { + var typeName = exception.GetType().FullName; + return ExceptionMessageField?.GetValue(exception) is string message + ? $"{prefix}{typeName}, {message}" + : $"{prefix}{typeName}"; + } + + private static Expression IsNullExpression(Expression expression) + { + // Expression.Equal could bind to a customer equality operator + if (!expression.Type.IsValueType) + { + return Expression.ReferenceEqual(expression, Expression.Constant(null)); + } + + return Nullable.GetUnderlyingType(expression.Type) is null + ? Expression.Constant(false) + : Expression.Not(Expression.Property(expression, nameof(Nullable.HasValue))); + } + private Expression DumpExpression(Expression expression, List scopeMembers) { - if (ShouldRedactDumpValue(null, expression.Type)) + var type = expression.Type; + if (ShouldRedactDumpValue(null, type)) { return Expression.Constant(RedactedValueDump); } - if (expression.Type == ProbeExpressionParserHelper.UndefinedValueType) + if (type == ProbeExpressionParserHelper.UndefinedValueType) { return Expression.Constant(UndefinedValueDump); } - if (Datadog.Trace.Debugger.Helpers.TypeExtensions.IsSimple(expression.Type) || - Redaction.AllowedTypesSafeToCallToString.Contains(expression.Type)) + // A sealed or value type is its own runtime type, so its dump is decided here and needs no runtime check. + var isExactType = type.IsValueType || type.IsSealed; + var getTypeMethod = GetMethodByReflection(typeof(object), nameof(object.GetType), Type.EmptyTypes); + var toStringMethod = GetMethodByReflection(typeof(object), nameof(object.ToString), Type.EmptyTypes); + + if (Datadog.Trace.Debugger.Helpers.TypeExtensions.IsSimple(type) || + Redaction.AllowedTypesSafeToCallToString.Contains(type)) { - return Expression.Call(expression, GetMethodByReflection(typeof(object), nameof(object.ToString), Type.EmptyTypes)); - } + if (isExactType) + { + return Expression.Call(expression, toStringMethod); + } - var ifNull = Expression.Equal(expression, Expression.Constant(null)); + // No subclass of a non-sealed allowed type is allowed itself, so only an exact match may run ToString. + return DumpRuntimeValueExpression( + expression, + Expression.Constant("null"), + value => DumpIfRuntimeTypeAllows( + Expression.ReferenceEqual(Expression.Call(value, getTypeMethod), Expression.Constant(type, typeof(Type))), + Expression.Call(value, toStringMethod), + value, + scopeMembers), + scopeMembers); + } - if (IsSafeException(expression.Type)) + if (typeof(Exception).IsAssignableFrom(type)) { - // for known Exception types we can assume it's safe to call .Message - // whereas the others might have overriden it in a way that could cause side effects - var stringConcat = GetMethodByReflection(typeof(string), nameof(string.Concat), new[] { typeof(object[]) }); - var typeNameExpression = Expression.Constant(expression.Type.FullName, typeof(string)); - var exceptionAsString = Expression.Call( - stringConcat, - Expression.NewArrayInit( - typeof(string), - typeNameExpression, - Expression.Constant(", "), - Expression.Property(expression, nameof(Exception.Message)), - Expression.Constant(", "), - Expression.Property(expression, nameof(Exception.StackTrace)))); - - return Expression.Condition(ifNull, typeNameExpression, exceptionAsString); + var typeNameExpression = Expression.Constant(type.FullName, typeof(string)); + + // Message and StackTrace are virtual, so DumpException calls them only when CanCallExceptionMembers allows it. + // A type outside CoreLib has no subclass in CoreLib, so that check only runs for a non-sealed CoreLib type. + if (isExactType && CanCallExceptionMembers(type)) + { + var stringConcat = GetMethodByReflection(typeof(string), nameof(string.Concat), new[] { typeof(object[]) }); + var exceptionAsString = Expression.Call( + stringConcat, + Expression.NewArrayInit( + typeof(string), + typeNameExpression, + Expression.Constant(", "), + Expression.Property(expression, nameof(Exception.Message)), + Expression.Constant(", "), + Expression.Property(expression, nameof(Exception.StackTrace)))); + + return Expression.Condition(IsNullExpression(expression), typeNameExpression, exceptionAsString); + } + + var dumpExceptionMethod = GetMethodByReflection( + typeof(ProbeExpressionParser), + !isExactType && ReferenceEquals(type.Assembly, CoreLibAssembly) ? nameof(DumpException) : nameof(DumpExceptionMessageField), + [typeof(Exception), typeof(string)]); + Func dumpException = exception => Expression.Call( + dumpExceptionMethod, + Expression.Convert(exception, typeof(Exception)), + Expression.Constant(null, typeof(string))); + + return isExactType + ? Expression.Condition(IsNullExpression(expression), typeNameExpression, dumpException(expression)) + : DumpRuntimeValueExpression(expression, typeNameExpression, dumpException, scopeMembers); } - Expression dumpExpression; - if (IsDictionaryDumpType(expression.Type)) + if (IsDictionaryDumpType(type)) { - dumpExpression = DumpDictionaryExpression(expression, scopeMembers); + return Expression.Condition(IsNullExpression(expression), Expression.Constant("null"), DumpDictionaryExpression(expression, scopeMembers)); } - else if (IsSafeCollection(expression.Type)) + + if (type.IsArray) { - dumpExpression = DumpCollectionExpression(expression, scopeMembers); + // An array's runtime type is always an array, whose enumerator can't be customer code. + return Expression.Condition(IsNullExpression(expression), Expression.Constant("null"), DumpCollectionExpression(expression, scopeMembers)); } - else + + if (IsSafeCollection(type)) { - dumpExpression = DumpFieldsExpression(expression, scopeMembers); + // Enumerate only the collection types snapshot capture supports, so a customer enumerator never runs. + if (isExactType) + { + return Expression.Condition( + IsNullExpression(expression), + Expression.Constant("null"), + Redaction.IsSupportedCollection(type) ? DumpCollectionExpression(expression, scopeMembers) : DumpFieldsExpression(expression, scopeMembers)); + } + + // Every type on the collection allowlist other than arrays, which are handled above, derives directly from + // object, so a declared class can only hold a supported runtime type by holding exactly itself. Only an + // interface needs the runtime allowlist. + if (!type.IsInterface && !Redaction.IsSupportedCollection(type)) + { + return DumpRuntimeValueExpression(expression, Expression.Constant("null"), value => DumpFieldsExpression(value, scopeMembers), scopeMembers); + } + + return DumpRuntimeValueExpression( + expression, + Expression.Constant("null"), + value => DumpIfRuntimeTypeAllows( + type.IsInterface + ? Expression.Call(GetMethodByReflection(typeof(Redaction), nameof(Redaction.IsSupportedCollection), [typeof(object)]), value) + : Expression.ReferenceEqual(Expression.Call(value, getTypeMethod), Expression.Constant(type, typeof(Type))), + DumpCollectionExpression(Expression.Convert(value, type), scopeMembers), + value, + scopeMembers), + scopeMembers); } - return Expression.Condition(ifNull, Expression.Constant("null"), dumpExpression); + return isExactType + ? Expression.Condition(IsNullExpression(expression), Expression.Constant("null"), DumpFieldsExpression(expression, scopeMembers)) + : DumpRuntimeValueExpression(expression, Expression.Constant("null"), value => DumpFieldsExpression(value, scopeMembers), scopeMembers); + } + + // Reads the value once and redacts it when its runtime type, which a non-sealed declared type can't tell, is + // redacted. IsRedactedRuntimeValue skips the lookup when the runtime type is the declared type. + private BlockExpression DumpRuntimeValueExpression( + Expression expression, + Expression nullDump, + Func dumpValue, + List scopeMembers) + { + var value = Expression.Variable(typeof(object), "dumpValue"); + scopeMembers.Add(value); + var isRedacted = Expression.Call( + GetMethodByReflection(typeof(ProbeExpressionParser), nameof(IsRedactedRuntimeValue), [typeof(object), typeof(Type)]), + value, + Expression.Constant(expression.Type, typeof(Type))); + + return Expression.Block( + Expression.Assign(value, Expression.Convert(expression, typeof(object))), + Expression.Condition( + Expression.ReferenceEqual(value, Expression.Constant(null)), + nullDump, + Expression.Condition(isRedacted, Expression.Constant(RedactedValueDump), dumpValue(value)))); + } + + // Any runtime type that isAllowed rejects gets its fields dumped, the same output a root local of that type gets. + private ConditionalExpression DumpIfRuntimeTypeAllows(Expression isAllowed, Expression dumpAllowed, Expression value, List scopeMembers) + { + return Expression.Condition(isAllowed, dumpAllowed, DumpFieldsExpression(value, scopeMembers)); } private BlockExpression DumpDictionaryExpression(Expression dictionary, List scopeMembers) @@ -181,7 +335,7 @@ private BlockExpression DumpDictionaryExpression(Expression dictionary, List), nameof(DumpCollection), [typeof(object), typeof(Type)]), value, Expression.Constant(typeof(IDictionary))); - var dumpFields = DumpFieldsExpression(Expression.Convert(value, dictionary.Type), scopeMembers); + var dumpFields = DumpFieldsExpression(value, scopeMembers); var dump = Expression.Condition(isSupportedDictionary, dumpEntries, dumpFields); return Expression.Block( @@ -365,22 +519,20 @@ private string DumpObject(object value, Type type, string name, int depth = 0) } // only one level depth of collection - if (depth == 0 && (IsTypeSupportIndex(type, out var assignableFrom) || IsGenericDictionaryInterface(type))) + // The declared type doesn't tell which collection the field holds, so enumerate only the runtime types + // snapshot capture supports and a customer or wrapped enumerator never runs. Those allow lists are + // cheap, so check them before the declared type, whose check looks up interfaces. + if (depth == 0 && + (value is null || Redaction.IsSupportedDictionary(value) || Redaction.IsSupportedCollection(value)) && + (IsTypeSupportIndex(type, out var assignableFrom) || IsGenericDictionaryInterface(type))) { - assignableFrom ??= typeof(IDictionary); - // The declared type may be an interface. Redact from the runtime type before enumerating. if (IsRedactedRuntimeValue(value, type)) { return hasName ? $"{name}={RedactedValueDump}" : RedactedValueDump; } - // The declared type doesn't tell which dictionary the field holds. Enumerate only the dictionary - // types snapshot capture supports, so a customer or wrapped enumerator never runs. - if (assignableFrom != typeof(IDictionary) || value is null || Redaction.IsSupportedDictionary(value)) - { - return DumpCollection(value, assignableFrom); - } + return DumpCollection(value, assignableFrom ?? typeof(IDictionary)); } if (hasName) @@ -393,15 +545,19 @@ private string DumpObject(object value, Type type, string name, int depth = 0) return $"{name}{UndefinedValueDump}"; } - if (IsSafeException(type)) + if (typeof(Exception).IsAssignableFrom(type)) { - return value is not Exception ex - ? $"{name}{type?.FullName}" - : $"{name}{ex.GetType().FullName}, {ex.Message}, {ex.StackTrace}"; + if (value is not Exception ex) + { + return $"{name}{type.FullName}"; + } + + // The declared type may be a non-sealed exception. Redact from the runtime type before reading the message. + return IsRedactedRuntimeValue(value, type) ? $"{name}{RedactedValueDump}" : DumpException(ex, name); } - return Redaction.IsSafeToCallToString(type) ? - $"{name}{value?.ToString() ?? type.FullName}" : + return value is not null && Redaction.IsSafeToCallToString(type) && CanCallToStringOnRuntimeType(value, type) ? + $"{name}{value.ToString()}" : $"{name}{type?.FullName}"; } diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs index 7d4197f9c210..2d8b748f3117 100644 --- a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs +++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs @@ -593,11 +593,6 @@ private ParameterExpression GetParameterExpression(List par } } - private bool IsSafeException(Type type) - { - return typeof(Exception).IsAssignableFrom(type) && IsMicrosoftType(type); - } - private bool IsMicrosoftType(Type type) { var @namespace = type.Namespace; diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs b/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs index 76af369ef7a3..e39f083f363e 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs @@ -15,6 +15,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Xml; using AgileObjects.ReadableExpressions; using Datadog.Trace.Debugger; using Datadog.Trace.Debugger.Configurations.Models; @@ -42,6 +43,8 @@ public class DebuggerExpressionLanguageTests private const string DefaultJsonTemplate = @"{""Ignore"": ""Ignore""}"; + private const string CustomerDumpMarker = "DD_CUSTOMER_DUMP_MARKER"; + private const string ConditionsFolder = "Conditions"; private const string TemplatesFolder = "Templates"; @@ -3459,6 +3462,239 @@ public void ProbeExpressionEvaluator_TemplateDump_DoesNotEnumerateCustomDictiona result.Errors.Should().BeNullOrEmpty(); } + [Theory] + [InlineData("_items")] + [InlineData("_itemsInterface")] + [InlineData("_readOnlyItems")] + [InlineData("_headers")] + [InlineData("_uri")] + public void ProbeExpressionEvaluator_TemplateDump_GetMemberDumpsFieldsOfCustomerRuntimeType(string memberName) + { + // getmember keeps the declared type, which allows enumeration or ToString, but the runtime + // type is a customer subclass whose members throw, so only its fields are dumped. + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(CustomerRuntimeTypesHolder), new CustomerRuntimeTypesHolder(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be("k__BackingField=1"); + result.Errors.Should().BeNullOrEmpty(); + } + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_FieldsOfCustomerRuntimeTypeDumpDeclaredTypeName() + { + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(CustomerRuntimeTypesHolder), new CustomerRuntimeTypesHolder(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().StartWith("_items=System.Collections.Generic.List`1"); + result.Template.Should().Contain(", _itemsInterface=System.Collections.Generic.IList`1"); + result.Template.Should().Contain(", _readOnlyItems=System.Collections.Generic.IReadOnlyList`1"); + result.Template.Should().Contain(", _headers=System.Collections.Generic.Dictionary`2"); + result.Template.Should().EndWith(", _uri=System.Uri"); + result.Template.Should().NotContain(CustomerDumpMarker); + result.Errors.Should().BeNullOrEmpty(); + } + + [Theory] + [InlineData("items", "[a, b]")] + [InlineData("uri", "http://localhost/")] + [InlineData("xmlError", "System.Xml.XmlException, bad xml")] + public void ProbeExpressionEvaluator_TemplateDump_GetMemberDumpsAllowedRuntimeType(string memberName, string expected) + { + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(AllowedRuntimeTypesHolder), new AllowedRuntimeTypesHolder(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be(expected); + result.Errors.Should().BeNullOrEmpty(); + } + + [Theory] + [InlineData("coreLib", "System.InvalidOperationException, operation failed, ")] + [InlineData("customer", "Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+ThrowingMessageException, customer message")] + [InlineData("customerWithoutMessage", "Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+CustomerException")] + [InlineData("outsideCoreLib", "System.Xml.XmlException, bad xml")] + public void ProbeExpressionEvaluator_TemplateDump_GetMemberExceptionField(string memberName, string expected) + { + // getmember keeps the declared Exception type. Only a CoreLib runtime type may run Message and + // StackTrace; any other shows its runtime type and the message field, so the throwing overrides never run. + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldsHolder), new DumpExceptionFieldsHolder(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be(expected); + result.Errors.Should().BeNullOrEmpty(); + } + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_ExceptionFieldsCallMembersOnlyOnCoreLibRuntimeType() + { + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldsHolder), new DumpExceptionFieldsHolder(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be( + "coreLib=System.InvalidOperationException, operation failed, , " + + "customer=Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+ThrowingMessageException, customer message, " + + "customerWithoutMessage=Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+CustomerException, " + + "outsideCoreLib=System.Xml.XmlException, bad xml"); + result.Errors.Should().BeNullOrEmpty(); + } + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_CustomerExceptionLocalShowsMessageField() + { + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("CustomerExceptionLocal", typeof(ThrowingMessageException), new ThrowingMessageException("customer message"), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""ref"":""CustomerExceptionLocal""}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be("Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+ThrowingMessageException, customer message"); + result.Errors.Should().BeNullOrEmpty(); + } + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_ReflectionTypeLoadExceptionDoesNotReadLoaderExceptionMessages() + { + // On .NET Core, ReflectionTypeLoadException.Message reads each loader exception's Message. + var exception = new ReflectionTypeLoadException(Type.EmptyTypes, [new ThrowingMessageException("loader")], "loader failed"); + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("ExceptionLocal", typeof(ReflectionTypeLoadException), exception, ScopeMemberKind.Local)); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldHolder), new DumpExceptionFieldHolder(exception), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: + [ + new DebuggerExpression(string.Empty, @"{""ref"":""ExceptionLocal""}", null), + new DebuggerExpression(null, null, " | "), + new DebuggerExpression(string.Empty, @"{""getmember"":[{""ref"":""DumpHolderLocal""},""error""]}", null), + new DebuggerExpression(null, null, " | "), + new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null) + ], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + const string expected = "System.Reflection.ReflectionTypeLoadException, loader failed"; + result.Template.Should().Be($"{expected} | {expected} | error={expected}"); + result.Errors.Should().BeNullOrEmpty(); + } + +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_SwitchExpressionExceptionDoesNotFormatUnmatchedValue() + { + // SwitchExpressionException.Message formats the unmatched value, which calls its ToString. + var exception = new SwitchExpressionException(new ThrowingToStringValue()); + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldHolder), new DumpExceptionFieldHolder(exception), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""getmember"":[{""ref"":""DumpHolderLocal""},""error""]}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().StartWith("System.Runtime.CompilerServices.SwitchExpressionException, "); + result.Errors.Should().BeNullOrEmpty(); + } +#endif + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_GetMemberCollectionClassOutsideAllowlistDumpsFields() + { + // Collection isn't on the snapshot allowlist, and no allowlisted type derives from it, so the + // dump is decided when the expression compiles: its fields, not its items. + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(AllowedRuntimeTypesHolder), new AllowedRuntimeTypesHolder(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""getmember"":[{""ref"":""DumpHolderLocal""},""collection""]}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().StartWith("items="); + result.Template.Should().NotContain("[a, b]"); + result.Errors.Should().BeNullOrEmpty(); + } + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_NullCheckDoesNotInvokeEqualityOperator() + { + var scopeMembers = CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("CustomerValueLocal", typeof(ThrowingEqualityOperator), new ThrowingEqualityOperator(), ScopeMemberKind.Local)); + + var evaluator = CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""ref"":""CustomerValueLocal""}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be("_value=1"); + result.Errors.Should().BeNullOrEmpty(); + } + [Fact] public void ProbeExpressionEvaluator_SpanDecorationDump_RedactsSensitiveObjectFields() { @@ -4224,6 +4460,77 @@ public void ProbeExpressionEvaluator_TemplateDump_IDictionaryField_RedactsConfig Redaction.Instance.ResetInstance(); } } + + [Theory] + [InlineData("value", "{redacted}")] + [InlineData("items", "{redacted}")] + [InlineData("other", "_data=visible")] + [InlineData("error", "{redacted}")] + public void ProbeExpressionEvaluator_TemplateDump_GetMemberNonSealedField_RedactsConfiguredRuntimeType(string memberName, string expected) + { + // getmember keeps the declared type (object, IList, Exception), so only the runtime + // type shows that the value is a redacted type. + var scopeMembers = _tests.CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpNonSealedFieldsHolder), new DumpNonSealedFieldsHolder(), ScopeMemberKind.Local)); + + try + { + Redaction.Instance.ResetInstance(); + Redaction.Instance.SetConfig( + new HashSet(), + new HashSet(), + new HashSet { typeof(RedactedRuntimeTypeValue).FullName, typeof(List).FullName, typeof(InvalidOperationException).FullName }); + + var evaluator = _tests.CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().Be(expected); + result.Errors.Should().BeNullOrEmpty(); + } + finally + { + Redaction.Instance.ResetInstance(); + } + } + + [Fact] + public void ProbeExpressionEvaluator_TemplateDump_ExceptionField_RedactsConfiguredRuntimeType() + { + var scopeMembers = _tests.CreateScopeMembers(); + scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpNonSealedFieldsHolder), new DumpNonSealedFieldsHolder(), ScopeMemberKind.Local)); + + try + { + Redaction.Instance.ResetInstance(); + Redaction.Instance.SetConfig( + new HashSet(), + new HashSet(), + new HashSet { typeof(InvalidOperationException).FullName }); + + var evaluator = _tests.CreateEvaluator( + templates: [new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)], + condition: null, + metric: null, + spanDecorations: null, + captureExpressions: null); + + var result = evaluator.Evaluate(scopeMembers); + + result.Template.Should().EndWith(", error={redacted}"); + result.Template.Should().NotContain("TOP_SECRET_VALUE"); + result.Errors.Should().BeNullOrEmpty(); + } + finally + { + Redaction.Instance.ResetInstance(); + } + } } internal class GenericReferenceTypeTarget @@ -4484,6 +4791,150 @@ public DumpRedactionDictionaryHolder(string authorizationSecret, string publicVa } } + // Each field's declared type allows enumeration or ToString, but its runtime type is a customer + // subclass whose members throw if called. + private sealed class CustomerRuntimeTypesHolder + { + private readonly List _items = new ThrowingEnumerationList { CustomerDumpMarker }; + + private readonly IList _itemsInterface = new ThrowingEnumerationList { CustomerDumpMarker }; + + private readonly IReadOnlyList _readOnlyItems = new ThrowingEnumerationList { CustomerDumpMarker }; + + private readonly Dictionary _headers = new ThrowingEnumerationDictionary { { "public", CustomerDumpMarker } }; + + private readonly Uri _uri = new ThrowingToStringUri(); + } + + private sealed class DumpNonSealedFieldsHolder + { + private readonly object value = new RedactedRuntimeTypeValue("TOP_SECRET_VALUE"); + + private readonly IList items = new List { "TOP_SECRET_VALUE" }; + + private readonly object other = new VisibleRuntimeTypeValue("visible"); + + private readonly Exception error = new InvalidOperationException("TOP_SECRET_VALUE"); + } + + private sealed class DumpExceptionFieldsHolder + { + private readonly Exception coreLib = new InvalidOperationException("operation failed"); + + private readonly Exception customer = new ThrowingMessageException("customer message"); + + private readonly Exception customerWithoutMessage = new CustomerException(); + + private readonly Exception outsideCoreLib = new XmlException("bad xml"); + } + + private sealed class ThrowingMessageException : InvalidOperationException + { + public ThrowingMessageException(string message) + : base(message) + { + } + + public override string Message => throw new InvalidOperationException("Customer Message should not be called."); + + public override string StackTrace => throw new InvalidOperationException("Customer StackTrace should not be called."); + } + + private sealed class CustomerException : Exception + { + } + + private sealed class DumpExceptionFieldHolder + { + private readonly Exception error; + + public DumpExceptionFieldHolder(Exception error) + { + this.error = error; + } + } + + private sealed class ThrowingToStringValue + { + public override string ToString() => throw new InvalidOperationException("Customer ToString should not be called."); + } + + private sealed class RedactedRuntimeTypeValue + { + private readonly string _data; + + public RedactedRuntimeTypeValue(string data) + { + _data = data; + } + } + + private sealed class VisibleRuntimeTypeValue + { + private readonly string _data; + + public VisibleRuntimeTypeValue(string data) + { + _data = data; + } + } + + private sealed class AllowedRuntimeTypesHolder + { + private readonly IList items = new List { "a", "b" }; + + private readonly Uri uri = new("http://localhost/"); + + private readonly XmlException xmlError = new("bad xml"); + + private readonly Collection collection = new() { "a", "b" }; + } + + private sealed class ThrowingEnumerationList : List, IEnumerable + { + public int CustomerField { get; } = 1; + + IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called."); + + IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called."); + } + + private sealed class ThrowingEnumerationDictionary : Dictionary, IDictionary, IEnumerable> + { + public int CustomerField { get; } = 1; + + IDictionaryEnumerator IDictionary.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called."); + + IEnumerator> IEnumerable>.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called."); + + IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called."); + } + + private sealed class ThrowingToStringUri : Uri + { + public ThrowingToStringUri() + : base("http://localhost/") + { + } + + public int CustomerField { get; } = 1; + + public override string ToString() => throw new InvalidOperationException("Customer ToString should not be called."); + } + + private sealed class ThrowingEqualityOperator + { + private readonly int _value = 1; + + public static bool operator ==(ThrowingEqualityOperator left, object right) => throw new InvalidOperationException("Customer equality operator should not be called."); + + public static bool operator !=(ThrowingEqualityOperator left, object right) => throw new InvalidOperationException("Customer equality operator should not be called."); + + public override bool Equals(object obj) => ReferenceEquals(this, obj); + + public override int GetHashCode() => _value; + } + private sealed class DumpCustomIDictionaryFieldHolder { private readonly IDictionary customMap; diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt index cc3c7de55910..8b810ec58d83 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt @@ -46,7 +46,7 @@ Expressions: ( var StringArg = (string)scopeMemberArray[18].Value; var CollectionArg = (List)scopeMemberArray[19].Value; var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value; - var $dd_el_result = (this.NullableNullValue == null) + var $dd_el_result = !this.NullableNullValue.HasValue ? "null" : { fieldsArray = ProbeExpressionParser diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt index d87bbc426523..c3f340b0915a 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt @@ -46,7 +46,7 @@ Expressions: ( var StringArg = (string)scopeMemberArray[18].Value; var CollectionArg = (List)scopeMemberArray[19].Value; var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value; - var $dd_el_result = (this.NullableNotNullValue == null) + var $dd_el_result = !this.NullableNotNullValue.HasValue ? "null" : { fieldsArray = ProbeExpressionParser diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt index 380da9118c74..adb5fab1e084 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt @@ -13,8 +13,9 @@ Expressions: ( { string loopItem; IEnumerator enumerator; - int index; - StringBuilder itemValues; + int index, index2; + StringBuilder itemValues, fieldValues; + FieldInfo[] fieldsArray; var this = (scopeMember.Value == null) ? default(DebuggerExpressionLanguageTests.TestStruct) : (DebuggerExpressionLanguageTests.TestStruct)scopeMember.Value; @@ -42,49 +43,96 @@ Expressions: ( var StringArg = (string)scopeMemberArray[18].Value; var CollectionArg = (List)scopeMemberArray[19].Value; var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value; - var $dd_el_result = (this.Collection == null) - ? "null" - : { - enumerator = this.Collection.GetEnumerator(); - index = 0; - itemValues = new StringBuilder(); - itemValues.Append("["); - while (true) - { - if (enumerator.MoveNext()) - { - string loopItem; + var $dd_el_result = + { + var dumpValue = (object)this.Collection; - if (index < 3) - { - if (index > 0) + return (dumpValue == null) + ? "null" + : ProbeExpressionParser.IsRedactedRuntimeValue(dumpValue, typeof(List)) + ? "{redacted}" + : (dumpValue.GetType() == typeof(List)) + ? { + enumerator = ((List)dumpValue).GetEnumerator(); + index = 0; + itemValues = new StringBuilder(); + itemValues.Append("["); + while (true) { - return itemValues.Append(", "); + if (enumerator.MoveNext()) + { + string loopItem; + + if (index < 3) + { + if (index > 0) + { + return itemValues.Append(", "); + } + + loopItem = enumerator.Current; + itemValues.Append(ProbeExpressionParser.DumpObject( + loopItem, + typeof(string), + "", + 1)); + index++; + } + else + { + itemValues.Append(", ..."); + break; + } + } + else + { + break; + } } + itemValues.Append("]"); - loopItem = enumerator.Current; - itemValues.Append(ProbeExpressionParser.DumpObject( - loopItem, - typeof(string), - "", - 1)); - index++; + return itemValues.ToString(); } - else - { - itemValues.Append(", ..."); - break; - } - } - else - { - break; - } - } - itemValues.Append("]"); + : { + fieldsArray = ProbeExpressionParser + .GetSafeFieldsForDump( + dumpValue.GetType().GetFields( + BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + .OrderBy(fieldInfo => fieldInfo.MetadataToken) + .ToArray(); + fieldValues = new StringBuilder(); + index2 = 0; + while (true) + { + if ((index2 < fieldsArray.Length) && (index2 < 5)) + { + fieldValues.Append( + ProbeExpressionParser.DumpObject( + ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index2], (object)dumpValue), + fieldsArray[index2].FieldType, + fieldsArray[index2].Name, + 0)); + index2++; + + if ((index2 < fieldsArray.Length) && (index2 < 5)) + { + return fieldValues.Append(", "); + } + } + else + { + if (index2 < fieldsArray.Length) + { + return fieldValues.Append(", ..."); + } + + break; + } + } - return itemValues.ToString(); - }; + return fieldValues.ToString(); + }; + }; return $dd_el_result; } diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt index d6d446c475ce..eb9fa9e85cad 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt @@ -46,49 +46,56 @@ Expressions: ( var StringArg = (string)scopeMemberArray[18].Value; var CollectionArg = (List)scopeMemberArray[19].Value; var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value; - var $dd_el_result = (((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0]) == null) - ? "null" - : { - fieldsArray = ProbeExpressionParser - .GetSafeFieldsForDump( - ((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0]).GetType().GetFields( - BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - .OrderBy(fieldInfo => fieldInfo.MetadataToken) - .ToArray(); - fieldValues = new StringBuilder(); - index = 0; - while (true) - { - if ((index < fieldsArray.Length) && (index < 5)) - { - fieldValues.Append( - ProbeExpressionParser.DumpObject( - ProbeExpressionParser.GetFieldValueForDump( - fieldsArray[index], - (object)((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0])), - fieldsArray[index].FieldType, - fieldsArray[index].Name, - 0)); - index++; + var $dd_el_result = + { + var dumpValue = (object)((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0]); - if ((index < fieldsArray.Length) && (index < 5)) + return (dumpValue == null) + ? "null" + : ProbeExpressionParser.IsRedactedRuntimeValue( + dumpValue, + typeof(DebuggerExpressionLanguageTests.TestStruct.NestedObject)) + ? "{redacted}" + : { + fieldsArray = ProbeExpressionParser + .GetSafeFieldsForDump( + dumpValue.GetType().GetFields( + BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + .OrderBy(fieldInfo => fieldInfo.MetadataToken) + .ToArray(); + fieldValues = new StringBuilder(); + index = 0; + while (true) { - return fieldValues.Append(", "); - } - } - else - { - if (index < fieldsArray.Length) - { - return fieldValues.Append(", ..."); - } + if ((index < fieldsArray.Length) && (index < 5)) + { + fieldValues.Append( + ProbeExpressionParser.DumpObject( + ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index], (object)dumpValue), + fieldsArray[index].FieldType, + fieldsArray[index].Name, + 0)); + index++; - break; - } - } + if ((index < fieldsArray.Length) && (index < 5)) + { + return fieldValues.Append(", "); + } + } + else + { + if (index < fieldsArray.Length) + { + return fieldValues.Append(", ..."); + } + + break; + } + } - return fieldValues.ToString(); - }; + return fieldValues.ToString(); + }; + }; return $dd_el_result; } diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt index 930e5e3dfea0..3a6c57919197 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt @@ -38,10 +38,17 @@ Expressions: ( var StringArg = (string)scopeMemberArray[18].Value; var CollectionArg = (List)scopeMemberArray[19].Value; var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value; - var $dd_el_result = (@exception == null) - ? "System.Exception" - : "System.Exception" + ", " + @exception.Message + ", " + @exception.StackTrace; + var $dd_el_result = + { + var dumpValue = (object)@exception; + + return (dumpValue == null) + ? "System.Exception" + : ProbeExpressionParser.IsRedactedRuntimeValue(dumpValue, typeof(Exception)) + ? "{redacted}" + : ProbeExpressionParser.DumpException((Exception)dumpValue, null); + }; return $dd_el_result; } -Result: The result of the expression is: System.Exception, Can not cast X to Y, +Result: The result of the expression is: System.InvalidCastException, Can not cast X to Y, diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt index 3a3625bd63cc..efa3e6e2fe67 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt @@ -41,47 +41,56 @@ Expressions: ( var StringArg = (string)scopeMemberArray[18].Value; var CollectionArg = (List)scopeMemberArray[19].Value; var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value; - var $dd_el_result = (this.Nested == null) - ? "null" - : { - fieldsArray = ProbeExpressionParser - .GetSafeFieldsForDump( - this.Nested.GetType().GetFields( - BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - .OrderBy(fieldInfo => fieldInfo.MetadataToken) - .ToArray(); - fieldValues = new StringBuilder(); - index = 0; - while (true) - { - if ((index < fieldsArray.Length) && (index < 5)) - { - fieldValues.Append( - ProbeExpressionParser.DumpObject( - ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index], (object)this.Nested), - fieldsArray[index].FieldType, - fieldsArray[index].Name, - 0)); - index++; + var $dd_el_result = + { + var dumpValue = (object)this.Nested; - if ((index < fieldsArray.Length) && (index < 5)) + return (dumpValue == null) + ? "null" + : ProbeExpressionParser.IsRedactedRuntimeValue( + dumpValue, + typeof(DebuggerExpressionLanguageTests.TestStruct.NestedObject)) + ? "{redacted}" + : { + fieldsArray = ProbeExpressionParser + .GetSafeFieldsForDump( + dumpValue.GetType().GetFields( + BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + .OrderBy(fieldInfo => fieldInfo.MetadataToken) + .ToArray(); + fieldValues = new StringBuilder(); + index = 0; + while (true) { - return fieldValues.Append(", "); - } - } - else - { - if (index < fieldsArray.Length) - { - return fieldValues.Append(", ..."); - } + if ((index < fieldsArray.Length) && (index < 5)) + { + fieldValues.Append( + ProbeExpressionParser.DumpObject( + ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index], (object)dumpValue), + fieldsArray[index].FieldType, + fieldsArray[index].Name, + 0)); + index++; - break; - } - } + if ((index < fieldsArray.Length) && (index < 5)) + { + return fieldValues.Append(", "); + } + } + else + { + if (index < fieldsArray.Length) + { + return fieldValues.Append(", ..."); + } + + break; + } + } - return fieldValues.ToString(); - }; + return fieldValues.ToString(); + }; + }; return $dd_el_result; }