From ca5eec3cc9943c37a23013a5b11a340c4331de92 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 27 Jun 2026 15:12:23 +0200 Subject: [PATCH] Don't assert decompiled local types match the signature DebugInfoGenerator asserted that every local variable's decompiled type is equivalent to the metadata local-signature type at its slot. That holds at IL-read time but not afterwards: variable splitting gives one slot several typed variables, pinned-region locals are modeled as pointers (int* vs int& pinned), and generic-context type-parameter identity differs. The assertion therefore aborted PDB generation (Debug builds) for ordinary inputs such as any method with a fixed statement. The type is never written to the PDB - only the slot index and name are - so the mismatch cannot affect the debugging experience. The slot index, the only emitted value, is correct by construction: it is the IL ldloc/stloc operand, sourced from the signature slot when the variable is created and copied verbatim by SplitVariables; it is never reassigned. Keep only the index-bound check and document why the type is not verified. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../ICSharpCode.Decompiler.Tests.csproj | 1 + .../PdbGenerationTestRunner.cs | 23 +++++++++++++ .../TestCases/PdbGen/PinnedLocalSlot.il | 34 +++++++++++++++++++ .../DebugInfo/DebugInfoGenerator.cs | 9 ++++- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/PdbGen/PinnedLocalSlot.il diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 2d40851ac..d38245d46 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -164,6 +164,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs index 904c769d8..0f2b5cf1f 100644 --- a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs @@ -336,6 +336,29 @@ namespace ICSharpCode.Decompiler.Tests Assert.DoesNotThrow(() => decompiler.CreateSequencePoints(syntaxTree)); } + [Test] + public async Task PinnedLocalSlot() + { + // The decompiler models a pinned-region local as a pointer (int*), which never matches the + // metadata local signature type (int& pinned). The slot index is still faithful to the IL, + // and the type is never written to the PDB, so generating debug info must not assert on the + // type difference. + string ilFile = Path.Combine(TestCasePath, nameof(PinnedLocalSlot) + ".il"); + string peFileName = await Tester.AssembleIL(ilFile, AssemblerOptions.Library); + + var module = new PEFile(peFileName); + var resolver = new UniversalAssemblyResolver(peFileName, false, + module.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage); + var decompiler = new CSharpDecompiler(module, resolver, new DecompilerSettings()); + + var syntaxTree = decompiler.DecompileType(new Decompiler.TypeSystem.FullTypeName("C")); + + Assert.DoesNotThrow(() => { + var debugInfo = new Decompiler.DebugInfo.DebugInfoGenerator(decompiler.TypeSystem); + syntaxTree.AcceptVisitor(debugInfo); + }); + } + private class TestProgressReporter : IProgress { private Action reportFunc; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/PinnedLocalSlot.il b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/PinnedLocalSlot.il new file mode 100644 index 000000000..88e3f4f22 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/PinnedLocalSlot.il @@ -0,0 +1,34 @@ +// A simple pinned pointer (fixed statement). The metadata local signature types the slot as +// `int32& pinned`, but the decompiler models a pinned-region local as a pointer (`int*`). The two +// never match, yet the slot index is faithful to the IL ldloc/stloc operand, so PDB generation +// must not assert on the type difference. Regression fixture for that assertion. +.assembly extern System.Runtime { } +.assembly PinnedLocalSlot { } +.module PinnedLocalSlot.dll + +.class public auto ansi beforefieldinit C extends [System.Runtime]System.Object +{ + .method public hidebysig instance int32 M(int32[] a) cil managed + { + .maxstack 2 + .locals ( [0] int32& pinned p ) + ldarg.1 + ldc.i4.0 + ldelema [System.Runtime]System.Int32 + stloc.0 + ldloc.0 + ldind.i4 + ldc.i4.0 + conv.u + stloc.0 + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 1 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } +} diff --git a/ICSharpCode.Decompiler/DebugInfo/DebugInfoGenerator.cs b/ICSharpCode.Decompiler/DebugInfo/DebugInfoGenerator.cs index 5ae963094..a5c8f5782 100644 --- a/ICSharpCode.Decompiler/DebugInfo/DebugInfoGenerator.cs +++ b/ICSharpCode.Decompiler/DebugInfo/DebugInfoGenerator.cs @@ -253,7 +253,14 @@ namespace ICSharpCode.Decompiler.DebugInfo if (v.Index != null && v.Kind.IsLocal()) { #if DEBUG - Debug.Assert(v.Index < types.Length && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(v.Type, types[v.Index.Value])); + // The index is the IL ldloc/stloc operand, which indexes the local signature + // directly: ILReader creates one variable per signature slot, and SplitVariables + // copies that index. It is therefore correct by construction. The variable type + // is intentionally not checked against the signature - it is never written to the + // PDB (only the slot index and name are), and it legitimately diverges after + // variable splitting (one slot, several typed variables), for pinned-region locals + // modeled as pointers, and through generic-context type-parameter identity. + Debug.Assert(v.Index < types.Length); #endif localVariables.Add(v); }