Browse Source

Fix #3799: keep stackalloc initializer element type when the hint is not a pointer

A constant-size stackalloc initializer whose buffer is only passed to a static
call (never dereferenced as a typed pointer) threw "given Block is invalid!".
TranslateStackAllocInitializer recovers the element type from the surrounding
type hint, but that hint is unreliable for this shape: the constant allocation
size is folded to a byte count, so it can no longer be read off a
'count * sizeof(T)' expression, and the buffer is kept on the stack as a native
int instead of a T* local, leaving the hint a non-pointer. The guard only
repaired an incompatible pointer hint, so a non-pointer hint fell through to a
'byte' element type that is incompatible with the actual stores, and the
per-element check threw. Derive the element type from the type being stored
whenever the hint is not already a compatible pointer.

The regression is driven by the code shape, not the compiler version: in
optimized builds the buffer is kept on the stack as a native int whenever it is
passed to a static call without being dereferenced. This reproduces across
Roslyn versions, including the pinned test compiler, so the added fixture is red
without this fix in the optimized configurations.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3813/head
Siegfried Pammer 2 weeks ago committed by Siegfried Pammer
parent
commit
0c23009b3e
  1. 16
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/CS73_StackAllocInitializers.cs
  2. 8
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

16
ICSharpCode.Decompiler.Tests/TestCases/Pretty/CS73_StackAllocInitializers.cs

@ -219,6 +219,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -219,6 +219,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return UsePointer((byte*)ptr);
}
// The buffer is only passed to a static call, never dereferenced as a typed pointer, so
// some compilers keep it on the stack as a native int rather than in an int* local.
// Combined with the constant allocation size being folded to a byte count, that previously
// made the decompiler lose the element type and throw "given Block is invalid!".
public unsafe string Issue3799(int pid)
{
int* ptr = stackalloc int[4] { 1, 14, 1, pid };
long num = 0L;
return UseTwoPointers(ptr, &num);
}
public unsafe string NotAnInitializer(int a, int b, int c)
{
int* ptr = stackalloc int[6];
@ -255,6 +266,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -255,6 +266,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return ptr->ToString();
}
public unsafe static string UseTwoPointers(int* ptr, long* length)
{
return ((byte*)ptr)->ToString();
}
public string GetSpan()
{
Span<int> span = stackalloc int[GetSize()];

8
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -3713,7 +3713,13 @@ namespace ICSharpCode.Decompiler.CSharp @@ -3713,7 +3713,13 @@ namespace ICSharpCode.Decompiler.CSharp
IType elementType;
if (block.Instructions.Count < 2 || !block.Instructions[1].MatchStObj(out _, out _, out var t))
throw new ArgumentException("given Block is invalid!");
if (typeHint is PointerType pt && !TypeUtils.IsCompatibleTypeForMemoryAccess(t, pt.ElementType))
// Derive the element type from the type actually being stored when the type hint does
// not pin it down. The hint is unreliable here: when the allocation size is a folded
// constant (e.g. 'localloc 16' for 'stackalloc int[4]') the element type can no longer
// be read off a 'count * sizeof(T)' expression, and the surrounding context may type the
// buffer as a plain native int rather than a T*. In both cases falling back to 't' keeps
// the element type consistent with the stores in the block.
if (!(typeHint is PointerType pt) || !TypeUtils.IsCompatibleTypeForMemoryAccess(t, pt.ElementType))
{
typeHint = new PointerType(t);
}

Loading…
Cancel
Save