#stackalloc
Decompiling C# 12 inline arrays and stackalloc: fixed buffers without the unsafe
C# 12 added inline arrays, and they are easy to misread even in source: a fixed-size buffer that lives _inside_ its containing struct, value-type all the way down, no heap allocation, no array object. It is the managed, safe answer to the C programmer's `int buf[8]` — the thing you previously could only get with an `unsafe` fixed-size buffer. Paired with `stackalloc`, which has been quietly modernised to hand you a `Span<T>` instead of a raw pointer, you can now write buffer-heavy, allocation-free code without a single `unsafe` keyword. That safety is a source-level illusion built by the compiler, and the illusion is exactly what a decompiler has to reconstruct. Underneath, an inline array is not an array — it is a one-field struct with an attribute that tells the _runtime_ to lay out N copies of the field. Element access is not `ldelem` — it is a call to a runtime intrinsic that returns a managed reference. And `stackalloc` is not safe at all underneath — it is a `localloc`, the rawest stack allocation the CLR has, wrapped in a `Span` constructor so the pointer never reaches your eyes. A decompiler that lowers to the metal and stops shows you ref arithmetic, helper calls with names you cannot type, and `unsafe` blocks that were never in the source. The code it emits often will not even recompile. This post walks the IL for both features and shows what a decompiler must recognise to put the source back together. ## An inline array is a struct wearing an attribute Here is the declaration and a trivial use: [System.Runtime.CompilerServices.InlineArray(8)] public struct Buffer8 { private int _element0; } public static int Sum(Buffer8 buf) { int total = 0; for (int i = 0; i < 8; i++) total += buf[i]; return total; } `Buffer8` has one field, `_element0`, and one attribute, `[InlineArray(8)]`. That is the whole trick: the attribute instructs the runtime to allocate the field eight times contiguously, so a `Buffer8` is 32 bytes and `buf[3]` means "the fourth `int` in that block." There is no array object and no length word — the length is baked into the attribute, known at compile time, and never stored at runtime. In metadata the type is a plain `sealed value type` with a single field. Nothing in the type definition says "array"; the only signal is the custom attribute: .class public sequential ansi sealed beforefieldinit Buffer8 extends [System.Runtime]System.ValueType { .custom instance void [System.Runtime]System.Runtime.CompilerServices.InlineArrayAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) // InlineArray(8) .field private int32 _element0 } A decompiler that classifies types by their shape sees a one-field struct and, unless it reads that attribute, prints it as a one-field struct — losing the entire meaning. So the first recognition rule is: **a value type carrying`[InlineArray(N)]` is an inline array of its single field's type, length N**, and it must be rendered with the attribute and the single backing field exactly as the compiler emitted them. ## Indexing lowers to a runtime intrinsic, not ldelem Now the access. `buf[i]` compiles not to an array-load but to a call that produces a managed reference to element `i`. The compiler routes it through a generated helper in `<PrivateImplementationDetails>` that wraps `RuntimeHelpers.InlineArrayElementRef` (or builds a `Span<T>` over the buffer with `InlineArrayAsSpan` and indexes that, depending on context). The read inside the loop lowers roughly to: // total += buf[i]; ldarga.s buf ldloc i call !!1& [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::InlineArrayElementRef<valuetype Buffer8, int32>(!!0&, int32) ldind.i4 add The `InlineArrayElementRef` call takes a reference to the buffer and an index, and returns a _byref_ to the element; `ldind.i4` then dereferences it to load the `int`. For a write you would see `stind.i4` against the same kind of ref. There is no `ldelem`, no bounds-check opcode pattern, nothing that looks like an array — just an intrinsic call returning a `ref`. A naive decompiler renders that literally: a call to `RuntimeHelpers.InlineArrayElementRef<Buffer8, int>(ref buf, i)` followed by a dereference, or worse, a reference to a `<PrivateImplementationDetails>` helper whose name contains characters you cannot write in C#. Either way the output does not say `buf[i]` and does not recompile. The recognition rule is: **a call to the inline-array element-ref intrinsic (or the generated helper around it) against an`[InlineArray]` type, followed by a load or store through the returned ref, is an indexer access — render it as `buf[i]`.** The same applies to the `InlineArrayAsSpan` form, which a decompiler should fold back either into element access or into a `Span<T>` view of the buffer, depending on how the result is used. ## stackalloc is a localloc wearing a Span Now the second feature. Modern `stackalloc` assigned to a `Span<T>` looks completely safe in source: public static int SumFirstFour(ReadOnlySpan<int> input) { Span<int> scratch = stackalloc int[4]; for (int i = 0; i < 4; i++) scratch[i] = input[i] * input[i]; int total = 0; foreach (int v in scratch) total += v; return total; } No `unsafe`, no pointer. But `stackalloc` has exactly one lowering: the `localloc` instruction, which allocates a block on the current method's stack frame and pushes a native pointer to it. The compiler sizes the block (`4 * sizeof(int)`), allocates, then constructs a `Span<int>` over the raw pointer and the element count using the `Span<T>(void*, int)` constructor: // Span<int> scratch = stackalloc int[4]; ldc.i4.4 conv.u ldc.i4.4 mul.ovf.un // 4 elements * 4 bytes localloc // -> native int (pointer to the stack block) ldc.i4.4 newobj instance void valuetype [System.Runtime]System.Span`1<int32>::.ctor(void*, int32) stloc.0 // scratch That is the signature: an element-count `ldc`, a size multiply (`conv.u` / `mul.ovf.un`), a `localloc`, then a `newobj` on `Span<T>::.ctor(void*, int32)`. A decompiler that renders those instructions literally produces an `unsafe` method with a `void*`, a multiplication, a `localloc` intrinsic (which C# cannot even express directly), and a `Span` constructor taking a pointer. None of that is in the source, and the `void*` forces an `unsafe` context the author deliberately avoided. The recognition rule folds the whole shape into one expression: **a`localloc` whose size is `count * sizeof(T)`, consumed by `Span<T>` or `ReadOnlySpan<T>`'s `(void*, int)` constructor, is `stackalloc T[count]`.** When the element type's size is a compile-time constant and the count is too, Glass reconstructs `stackalloc int[4]`; when the count is a variable, it reconstructs `stackalloc int[n]`. The raw pointer, the multiply, and the constructor all disappear back into the single `stackalloc` the author typed, and the method stays safe — no `unsafe` keyword, because the source had none. There is a subtlety worth naming: not every `stackalloc` becomes a `Span`. The older form, `int* p = stackalloc int[4];`, assigns the pointer directly and _is_ genuinely unsafe — there the `void*` and `unsafe` belong in the output, because they were in the source. The decompiler distinguishes the two by what consumes the `localloc`: a `Span`/`ReadOnlySpan` constructor means the safe form, a direct pointer assignment means the unsafe form. Getting that distinction right is the difference between faithfully reproducing a safe method and falsely accusing it of being unsafe. ## Why faithful recovery matters here specifically For most language features, a decompiler that gets the details slightly wrong produces code that is ugly but still builds. These two are different, because both features exist precisely to give you low-level performance _while staying in safe, high-level C#_ — and the incorrect decompilation destroys exactly that property. An inline array rendered as `InlineArrayElementRef` calls references helpers in `<PrivateImplementationDetails>`, a compiler-internal type whose members have names containing `<` and `>` that are illegal in C# source. That output cannot be pasted back and compiled; it is a description of the IL, not a reconstruction of the program. A `stackalloc` rendered as `localloc` plus a `void*` turns a safe method into one that requires `unsafe`, misrepresenting the method's safety contract and, again, often failing to compile without edits the author never made. Glass reads both patterns at the level the author worked at. It classifies `[InlineArray(N)]` structs as inline arrays and renders their element access as indexers; it recognises the `localloc`-plus-`Span`-constructor shape as `stackalloc` and keeps the method safe, while still distinguishing the genuinely unsafe pointer form. The output reads like the source — `buf[i]` and `stackalloc int[4]` — and, just as importantly, recompiles like the source, which is the real test of whether a decompiler understood the program or merely transcribed its bytes. ## Summary Inline arrays and modern `stackalloc` are two of the clearest cases where the IL is dramatically lower-level than the C#. An inline array is a one-field struct whose `[InlineArray(N)]` attribute is the only evidence it is a buffer, indexed through runtime intrinsics that return byrefs rather than any array opcode. A `Span`-typed `stackalloc` is a raw `localloc` dressed in a `Span<T>(void*, int)` constructor so the pointer never shows. A decompiler earns its keep by recovering the author's intent: the inline-array declaration and its indexers, the single `stackalloc` expression, and the safe, `unsafe`-free method body that made these features worth using in the first place.
dev.to
October 11, 2026 at 8:09 AM
[Testing] stackalloc arrays contains gcrefs (110925) https://github.com/dotnet/runtime/pull/110925
[Testing] stackalloc arrays contain gcrefs by hez2010 · Pull Request #110925 · dotnet/runtime
github.com
December 24, 2024 at 6:57 AM
Span is a stack-only view, not a free speedup. It wins on stackalloc, no-copy slicing, and bounds-check elision. Nowhere else.
https://startdebugging.net/2026/06/what-is-span-and-when-does-it-make-my-code-faster/?utm_source=bluesky&utm_medium=social&utm_campaign=auto
June 20, 2026 at 11:22 AM
IMO older C# - eg like 4.x era - was much more limited and people just do not realize what it's capable of now. Both in terms of perf (stackalloc, InlineArray, ref, etc) and in terms of dynamic/untyped code (eg 'dynamic'/etc). Even Python interop...so good now.
November 13, 2025 at 9:09 PM
Forgot to recompile before measuring, Baseline is closer to 114ms.

Making the native arrays uninitialized lowered it to ~112ms, and stack allocating it with [SkipLocalsInit] actually increased it to ~115ms.

It only happens a few thousand times a frame at my measuring point, so not a big impact.
December 15, 2024 at 1:37 PM
Fields are the most direct form of memory storage. There’s zero indirection. Properties are methods.

C# lets you do low-level operations with fields that are hard or impossible with properties: e.g., ref locals and ref returns, stackalloc scenarios, custom memory layout (StructLayout), pinning, etc
May 5, 2025 at 1:29 PM
Stackalloc: an old-school .NET Feature -- it's been around since .NET Framework 1.0! #dotnetdc
November 17, 2024 at 9:53 PM
Happening in an hour -- the @dotnetdc Meetup with @vcsjones. Come say hi!

https://www.meetup.com/dotnetdc/events/284638318/

#dotnet #meetup #stackalloc
Login to Meetup | Meetup
Not a Meetup member yet? Log in and find groups that host online or in person events and meet people in your local community who share your interests.
www.meetup.com
November 17, 2024 at 9:53 PM
Happening tomorrow -- @dotnetdc Meetup brings you @vcsjones with a great talk on stackalloc!

https://www.meetup.com/dotnetdc/events/284638318/

#dotnet #meetup
Login to Meetup | Meetup
Not a Meetup member yet? Log in and find groups that host online or in person events and meet people in your local community who share your interests.
www.meetup.com
November 17, 2024 at 9:52 PM
How do you stackalloc a class or arrays of classes in C#? The worst thing about C# for me is that I need to decide whether or not a given class/struct is a reference type or a value type when I define it instead of when I use it, even though I almost always want to do both.
November 13, 2025 at 5:37 PM
Wow, I had no idea stackalloc was a thing (I've been out of the C# loop since 2011); cool!!

Sorry if this is a stupid question, but: can you alloc multiple points in a single contiguous block (ie like an array of structs in C)? This is something that's missing from Haxe and it's pretty annoying. :/
November 13, 2025 at 9:21 PM
Inside C#: Stack & Heap, Value Types, Boxing, stackalloc + More

#csharp #dotnet
Inside C#: Stack & Heap, Value Types, Boxing, stackalloc + More
Found a really good video on the subject, and thought it deserved some more attention: [https://youtu.be/cCsVY0Ixx04](https://youtu.be/cCsVY0Ixx04)
old.reddit.com
January 21, 2025 at 9:23 AM
How is stackalloc implemented ?

#csharp #dotnet
How is stackalloc implemented ?
I became aware of creating variable sized arrays in c sharp through stackalloc keyword recently. How are variable sized arrays possible to be...
old.reddit.com
January 6, 2025 at 12:43 PM
C#:バイナリ操作でよく使うAPIたち

C# でバイナリ操作を行う際によく使う API をまとめています。 確保/解放 スタック領域を確保 stackalloc でスタック領域にバッファを確保します。この確保・解放コストは非常に低く抑えられます。 Span buffer = stackalloc byte; スタック領域の大きさは実行環境や実装によりますが、例えば Windows ではスレッドあたり1MBとかになります。stackalloc での確保量が大きいと、メソッド呼び出しの深さなどによってはスタックオーバーフローを起こす可能性があるので注意が必要です。…
C#:バイナリ操作でよく使うAPIたち
C# でバイナリ操作を行う際によく使う API をまとめています。 確保/解放 スタック領域を確保 stackalloc でスタック領域にバッファを確保します。この確保・解放コストは非常に低く抑えられます。 Span&lt;byte&gt; buffer = stackalloc byte; スタック領域の大きさは実行環境や実装によりますが、例えば Windows ではスレッドあたり1MBとかになります。stackalloc での確保量が大きいと、メソッド呼び出しの深さなどによってはスタックオーバーフローを起こす可能性があるので注意が必要です。 配列プールからの確... Source link
inmobilexion.com
May 14, 2025 at 1:03 PM
CVE-2026-44375 - Nerdbank.MessagePack: Attacker-controlled stackalloc in DateTime decoding causes process-terminating StackOverflowException
CVE ID : CVE-2026-44375

Published : May 14, 2026, 3:16 p.m. | 1 hour, 14 minutes ago

Description : Nerdbank.MessagePack is a Nativ...
CVE-2026-44375 - Nerdbank.MessagePack: Attacker-controlled stackalloc in DateTime decoding causes process-terminating StackOverflowException
Nerdbank.MessagePack is a NativeAOT-compatible MessagePack serialization library. Prior to 1.1.62, Nerdbank.MessagePack contains an uncontrolled stack allocation vulnerability in DateTime decoding. A malicious MessagePack payload can declare an oversized timestamp extension length, causing the reader to allocate an attacker-controlled number of bytes on the stack. This can trigger a StackOverflowException, which …
cvefeed.io
May 14, 2026 at 5:06 PM
Even from the byte[] you get back from the reader into the Memory?? Is the memory backing like byte[] from stackalloc or some shit or is it just new or like rented from a pool
April 8, 2025 at 10:37 PM
Microsoft outlines C#/.NET changes to make memory bugs rarer: tighter rules around stackalloc, refs and Span, plus better diagnostics to catch unsafe patterns earlier. Safer defaults for everyday code. #CSharp #dotnet #MemorySafety

devblogs.microsoft.com/dotnet/impro...
June 22, 2026 at 2:04 PM
Szymon Kulec on using unsafe code in .NET: "Give me the chainsaw so that I can cut down the tree very quickly, but also so that if I slip, you're going to call me Lefty from now on."

Great episode on knowing when you need the chainsaw and when you don't 🎯

🔗
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code.
dotnetcore.show
May 16, 2026 at 9:00 AM
𝗣𝗼𝘀𝘁 𝟭:

🎧 New episode of The Modern .NET Show!

Szymon Kulec joins me for a deep dive into systems programming in .NET: profiling, benchmarking, and the rare cases where unsafe code is justified.

🔗
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code.
dotnetcore.show
May 13, 2026 at 11:31 AM
𝗣𝗼𝘀𝘁 𝟭:

Szymon Kulec joins me for a deep dive into systems programming in .NET: profiling, benchmarking, and the rare cases where unsafe code is justified.

🔗 dotnetcore.show/season-8/measure-twice-cut-once-benchmarking-hot-paths-and-the-chainsaw-of-unsafe-code-with-szymon-kulec/
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code.
dotnetcore.show
May 7, 2026 at 4:00 PM