From e85d2ba00eb2c0b257f577c4738e2bf13ed5a40f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 25 Aug 2026 23:08:17 +0200 Subject: [PATCH] Detect local functions renamed by obfuscators Obfuscators strip the CompilerGeneratedAttribute and rewrite the "g__name|x_y" name, which is all IsLocalFunctionMethod had to go on. The method then stays an ordinary static method, its display struct escapes by ref into a plain call, and TransformDisplayClassUsage correctly refuses to scalar-replace it -- so the closure fields leak into the output as "<>c__DisplayClass29_0_.iid" (issue #3202). The one marker an obfuscator cannot remove is the signature: Roslyn emits struct closures exclusively for local functions, and no hand-written C# can name a "<>c__DisplayClass" type, so a by-ref parameter of one identifies the method regardless of what it is called. #3202 Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../IL/Transforms/LocalFunctionDecompiler.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs index 763d7e649..daec256c8 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs @@ -781,36 +781,46 @@ namespace ICSharpCode.Decompiler.IL.Transforms var method = metadata.GetMethodDefinition(methodHandle); var declaringType = method.GetDeclaringType(); - if ((method.Attributes & MethodAttributes.Assembly) == 0 || !(method.IsCompilerGenerated(metadata) || declaringType.IsCompilerGenerated(metadata))) + if ((method.Attributes & MethodAttributes.Assembly) == 0) return false; - if (!ParseLocalFunctionName(metadata.GetString(method.Name), out _, out _)) - return false; + if ((method.IsCompilerGenerated(metadata) || declaringType.IsCompilerGenerated(metadata)) + && ParseLocalFunctionName(metadata.GetString(method.Name), out _, out _)) + { + return true; + } - return true; + // Obfuscators strip the CompilerGeneratedAttribute and rewrite the + // "g__name|x_y" name, but they cannot remove the by-ref display-struct + // parameter: a compiler-generated struct closure is only ever passed by reference + // to the local functions that capture it. + return HasDisplayStructParameter(module, methodHandle); } - public static bool LocalFunctionNeedsAccessibilityChange(MetadataFile module, MethodDefinitionHandle methodHandle) + /// + /// True if any parameter is a by-ref compiler-generated closure struct of this module. + /// + static bool HasDisplayStructParameter(MetadataFile module, MethodDefinitionHandle methodHandle) { - if (!IsLocalFunctionMethod(module, methodHandle)) - return false; - var metadata = module.Metadata; var method = metadata.GetMethodDefinition(methodHandle); - FindRefStructParameters visitor = new FindRefStructParameters(); method.DecodeSignature(visitor, default); - foreach (var h in visitor.RefStructTypes) { var td = metadata.GetTypeDefinition(h); - if (td.IsCompilerGenerated(metadata) && td.IsValueType(metadata)) + if (td.IsCompilerGenerated(metadata) && td.IsValueType(metadata) && td.HasGeneratedName(metadata)) return true; } - return false; } + public static bool LocalFunctionNeedsAccessibilityChange(MetadataFile module, MethodDefinitionHandle methodHandle) + { + return IsLocalFunctionMethod(module, methodHandle) + && HasDisplayStructParameter(module, methodHandle); + } + public static bool IsLocalFunctionDisplayClass(MetadataFile module, TypeDefinitionHandle typeHandle, ILTransformContext context = null) { if (context != null && context.PEFile != module)