Skip to content

Fix #3799 and three related stackalloc initializer decompilation defects#3802

Merged
siegfriedpammer merged 4 commits into
masterfrom
fix-3799-stackalloc-elementtype
Jun 23, 2026
Merged

Fix #3799 and three related stackalloc initializer decompilation defects#3802
siegfriedpammer merged 4 commits into
masterfrom
fix-3799-stackalloc-elementtype

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jun 22, 2026

Copy link
Copy Markdown
Member

Fixes #3799, and bundles three independent stackalloc initializer decompilation defects found alongside it.

Only the first change below is the reported crash and the actual #3799 fix. The other three were uncovered by an exploratory testing sweep over the surrounding code (varying element types, element positions, allocation shapes, and use contexts, validating each result by recompiling it); they are unrelated improvements and do not reference the issue.

1. Element type lost when the type hint is not a pointer (the reported crash, fixes #3799)

A constant-size stackalloc initializer whose buffer is passed to a static call without being dereferenced threw given Block is invalid! (reproduced on the reporter's JetBrains.HabitatDetector.dll). The element type is recovered from the surrounding type hint, but for this shape the folded constant size erases the sizeof and optimized codegen keeps the buffer as a native int, so the hint is not a pointer and the element type fell back to byte. Derive it from the type actually being stored.

2. float/double constants decoded as raw integer bits

A mixed float/double initializer stores its constant prefix through a data blob; ReadElement decoded each element by width, so 1f was read as Int32 and emitted as 1.0653532E+09f (and the wrong stack type tripped StObj.CheckInvariant). Dispatch on the type code so Single/Double are read as floating-point, matching the heap-array decoder.

3. Partially written reinterpreted buffer reconstructed as an initializer

byte* p = stackalloc byte[16] written through three of its four int slots decompiled to stackalloc int[4] { 1, v, 3 } — an initializer with fewer elements than the length, which does not compile. Only form the initializer when every element is written; otherwise keep individual stores.

4. Pointer-typed stackalloc inlined into an expression

A single-use pointer stackalloc was inlined into its use, producing K.V(stackalloc int[3] { 1, 2, v }) or *stackalloc ...; in expression position the stackalloc is typed Span<T>, which does not convert to a pointer, so the output did not compile. Keep it a local; inlining into a local store (its declaration) and into the Span<T>/ReadOnlySpan<T> constructor stay allowed.

Tests

Each fix adds a regression case to CS73_StackAllocInitializers (red without the fix in the relevant configurations, green with it). No regressions across the stackalloc/span/pointer-touching suites: CS73_StackAllocInitializers, InitializerTests, UnsafeCode, InlineArrayTests, RefFields, IndexRangeTest, ParamsCollections, PointerArithmetic, CompoundAssignmentTest, ExpressionTrees (Pretty), and UnsafeCode/InitializerTests (Correctness). The minimal repro and the real JetBrains.HabitatDetector method decompile correctly.


This PR description was written by an AI agent (Claude, claude-opus-4-8, via Claude Code).

@siegfriedpammer
siegfriedpammer force-pushed the fix-3799-stackalloc-elementtype branch from a1684dd to 15b74c2 Compare June 22, 2026 19:32
@siegfriedpammer siegfriedpammer changed the title Fix #3799: keep stackalloc initializer element type when the hint is not a pointer Fix #3799: stackalloc initializer element-type hint + float/double constant decoding Jun 22, 2026
@siegfriedpammer siegfriedpammer changed the title Fix #3799: stackalloc initializer element-type hint + float/double constant decoding Fix #3799: stackalloc initializer decompilation (4 related fixes) Jun 22, 2026
@siegfriedpammer

Copy link
Copy Markdown
Member Author

Reviewed the full diff. The four fixes are each narrowly scoped and individually test-covered:

  1. ExpressionBuilder — falling back to the stored type t whenever the hint is not a usable PointerType (not only when incompatible) is correct; a non-pointer hint previously left the element type mis-derived.
  2. ReadElement float/double — required: the GetSize() switch would otherwise decode a float via ReadInt32/LdcI4, producing both the wrong bit pattern and the wrong stack type (violating StObj.CheckInvariant).
  3. ILInlining — the StackType.I + LocAlloc/StackAllocInitializer + non-StLoc-parent gate is suitably narrow, and IsStackAllocSpanConstructorArgument correctly limits the one permitted inline position to the first (pointer) argument of a Span<T>/ReadOnlySpan<T> constructor.
  4. HandleSequentialLocAllocInitializerArray.IndexOf(values, null) >= 0 correctly rejects partially-written buffers that cannot be represented as an initializer.

No correctness concerns. (GitHub does not allow approving one's own PR, so this is a comment rather than a formal approval.)

(This review was written by an AI agent on behalf of Siegfried Pammer.)

…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
A constant-size stackalloc initializer stores its constant elements through a
data blob (cpblk from a <PrivateImplementationDetails> field). ReadElement
decoded every element by width, so a 4-byte float was read as Int32 and an
8-byte double as Int64. The resulting constant carried the raw bit pattern
(1f decoded as 1.0653532E+09f) and its stack type no longer matched the store,
tripping StObj.CheckInvariant. Dispatch on the element's type code so Single and
Double are read as floating-point, matching the heap-array decoder.

Found while exploring stackalloc-initializer coverage around the element-type
hint fix; floating-point element types had no test case.

Assisted-by: Claude:claude-opus-4-8:Claude Code
HandleSequentialLocAllocInitializer formed a stackalloc initializer whenever the
explicit stores were contiguous from offset 0, even if they did not cover the
whole buffer. A 'stackalloc byte[16]' reinterpreted as int and written through
three of its four elements decompiled to 'stackalloc int[4] { 1, v, 3 }', whose
initializer has fewer elements than the declared length and does not compile.
Require every element to be written (from the constant data blob or an explicit
store) before forming the initializer; otherwise the buffer stays a plain
stackalloc with individual stores.

Found while exploring stackalloc-initializer coverage.

Assisted-by: Claude:claude-opus-4-8:Claude Code
A stackalloc whose result is a pointer is only valid C# as the initializer of a
pointer-typed local. The inliner moved a single-use pointer stackalloc into its
use, producing e.g. 'K.V(stackalloc int[3] { 1, 2, v })' or '*stackalloc ...';
in an expression position the stackalloc is typed as Span<T>, which does not
convert to a pointer, so the output did not compile. Keep such a stackalloc as a
separate local. Moving it into a local store (its declaration) and into the
Span<T>/ReadOnlySpan<T> constructor stay allowed, since those are the positions
where the pointer or span form is exactly what is wanted.

Found while exploring stackalloc-initializer coverage.

Assisted-by: Claude:claude-opus-4-8:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the fix-3799-stackalloc-elementtype branch 2 times, most recently from f579b5e to 94fd452 Compare June 23, 2026 06:02
@siegfriedpammer siegfriedpammer changed the title Fix #3799: stackalloc initializer decompilation (4 related fixes) Fix #3799 and three related stackalloc initializer decompilation defects Jun 23, 2026
@siegfriedpammer
siegfriedpammer merged commit d4922cf into master Jun 23, 2026
13 checks passed
@christophwille
christophwille deleted the fix-3799-stackalloc-elementtype branch June 23, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System.ArgumentException: given Block is invalid!

1 participant