JIT: (bug) object stack allocation has no per-method frame budget - mutually exclusive allocations sum into the frame (stack overflow)
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
`ObjectAllocator::CanAllocateLclVarOnStack` bounds each candidate against `JitObjectStackAllocationSize` (528 bytes) but nothing charges accepted sites against a method-level budget. N mutually exclusive allocations each get their own stack home, growing the frame by N × size; with recursion this turns a working program into a stack overflow.
### Minimal Repro
```csharp
using System;
using System.Runtime.CompilerServices;
class Program
{
static int s_sink;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static int Test(int sel, int idx, int depth)
{
int r = 0;
switch (sel)
{
case 0: { int[] a = new int[120]; a[idx] = idx + 0; r = a[idx] + a[0]; break; }
case 1: { int[] a = new int[120]; a[idx] = idx + 1; r = a[idx] + a[0]; break; }
// ... 40 cases total, identical shape, case N uses "idx + N"
case 39: { int[] a = new int[120]; a[idx] = idx + 39; r = a[idx] + a[0]; break; }
}
if (depth > 0) r += Test((sel + 7) % 40, idx, depth - 1);
return r;
}
static int Main(string[] args)
{
s_sink = Test(3, 5, args.Length > 0 ? int.Parse(args[0]) : 0);
Console.WriteLine("ok " + s_sink);
return 0;
}
}
```
(The 40 `case` bodies are identical apart from the `idx + N` constant; expand them literally.) Run with argument `300`.
### Expected
```
ok 7338
```
Only one 496-byte array is live per call, so depth 300 needs roughly 10 KB of stack. This is what `DOTNET_JitObjectStackAllocation=0` prints.
### Actual
```
Stack overflow.
Repeated 78 times:
--------------------------------
at Program.Test(Int32, Int32, Int32)
--------------------------------
at Program.Main(System.String[])
```
Exit code `0xC00000FD`. Only ~78 recursion levels fit in the default 1 MB stack.
### Notes
- `Lcl frame size` is **19872** by default vs **32** with `DOTNET_JitObjectStackAllocation=0`; the dump shows 40 separate `struct (496) ... "stack allocated array temp"` entries — a ~620x frame growth.
- Also reproduces on shipped .NET 10.0.12 Release with the same 78 repeated frames.
- A per-method cumulative byte budget would avoid this; stack-slot sharing between provably non-overlapping allocation sites would be a stronger fix.
- The untaken paths' homes also get zero-initialized in the prologue and increase stack probing cost.
Contributor guide
Research direction
Start at ObjectAllocator::CanAllocateLclVarOnStack and reproduce the issue with the provided 40-case C# program using argument 300. Inspect how accepted stack allocations contribute to the method frame budget. Done means the repro prints the expected result without stack overflow and avoids the excessive frame growth described in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- compilers, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100