You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
.NET 6 C++/CLI runtime generates InvalidProgramException when large stack-allocated variables are used #60852
I am porting some code to .NET 6 and encountered some issues running C++/CLI code. After narrowing it down, it seems that using large stack-allocated arrays (larger than 65536 bytes) will generate a System.InvalidProgramException when the method with the offending variable is called for the first time.
Reproduction Steps
A minimal repro is to put the following code into a CLR Class Library and call it from a C# program:
As can be seen, it looks like there's a fundamental limit in the compiler-generated types which back the arrays. Note that prior to .NET 6, this appeared to work without issue.
Expected behavior
The program should not crash as long as we don't exceed the thread stack size.
Actual behavior
The program throws an exception as soon as a method with a large local array is called.
Regression?
Yes, this appears to be a .NET 6 specific issue.
Known Workarounds
In my case, as this is a scratch buffer, I could use alloca instead.
Just guessing based on the function name - those generated classes never had local variables with offsets above that size (I’m fairly sure they didn’t have any member fields, just an explicit layout/size). I assume that whatever has changed is now trying to compute local offsets for something placed at the end of the type.
.NET 5 and .NET Framework 4.7.2 can both run this without issue. I've previously run this on .NET Core 3.1 as well, but wasn't in a position to build for that right this moment.
@lstrudwick sorry for the delay -- I can repro this now.
In 6.0 we add "stack poisoning" for uninitialized locals (see #54685). Unfortunately in this case the poisoning happens via a sequence of discrete 8 byte stores.
Eventually the offsets in these stores get so large that the JIT hits an internal limitation; this surfaces as an invalid program exception.
@lstrudwick as a workaround you can pass /clr:initlocals to the C++ compiler. It will mean the poisoning will not happen and that the struct will be zero initialized instead.
As for a fix I'm not sure if we should just give up on poisoning for structs this large or try to support it (e.g. using rep stosd, not sure what to use on ARM architectures). Thoughts @AndyAyersMS?
Alternatively we can increase the size of emitLclVarAddr to allow encoding larger offsets. That would increase the memory usage slightly on 32-bit platforms, but not on 64-bit platforms as it is part of a union with pointer members already.
I'm not sure if we should just give up on poisoning for structs this large or try to support it
Not sure either -- I haven't looked at where the poisoning code ends up, but if it's at the end of the prolog or the first thing after the prolog then using loops shouldn't be too hard.
Activity
AndyAyersMS commentedon Oct 26, 2021
I'll take a look.
AndyAyersMS commentedon Oct 26, 2021
C++/CLI compiler is creating a value class that is larger than 64K in size, and the resulting IL is hitting this implementation limitation in the jit:
runtime/src/coreclr/jit/emit.cpp
Lines 541 to 544 in ae6df09
This limitation is not new so I'm not sure how this worked in prior releases.
eatplayhate commentedon Oct 26, 2021
Just guessing based on the function name - those generated classes never had local variables with offsets above that size (I’m fairly sure they didn’t have any member fields, just an explicit layout/size). I assume that whatever has changed is now trying to compute local offsets for something placed at the end of the type.
AndyAyersMS commentedon Oct 26, 2021
@lstrudwick can you double-check that the above was working on an earlier release, and if so, which one?
lstrudwick commentedon Oct 26, 2021
.NET 5 and .NET Framework 4.7.2 can both run this without issue. I've previously run this on .NET Core 3.1 as well, but wasn't in a position to build for that right this moment.
lstrudwick commentedon Oct 26, 2021
All generated types have the following structure in the broken case.
AndyAyersMS commentedon Nov 12, 2021
@lstrudwick sorry for the delay -- I can repro this now.
In 6.0 we add "stack poisoning" for uninitialized locals (see #54685). Unfortunately in this case the poisoning happens via a sequence of discrete 8 byte stores.
Eventually the offsets in these stores get so large that the JIT hits an internal limitation; this surfaces as an invalid program exception.
cc @jakobbotsch
For 5.0 and earlier the JIT won't do this poisoning.
2 remaining items
jakobbotsch commentedon Nov 12, 2021
@lstrudwick as a workaround you can pass
/clr:initlocalsto the C++ compiler. It will mean the poisoning will not happen and that the struct will be zero initialized instead.C# repro:
As for a fix I'm not sure if we should just give up on poisoning for structs this large or try to support it (e.g. using
rep stosd, not sure what to use on ARM architectures). Thoughts @AndyAyersMS?jakobbotsch commentedon Nov 12, 2021
Alternatively we can increase the size of
emitLclVarAddrto allow encoding larger offsets. That would increase the memory usage slightly on 32-bit platforms, but not on 64-bit platforms as it is part of a union with pointer members already.AndyAyersMS commentedon Nov 12, 2021
Not sure either -- I haven't looked at where the poisoning code ends up, but if it's at the end of the prolog or the first thing after the prolog then using loops shouldn't be too hard.
[Edit, looks like indeed it wasn't...]
Disable poisoning for large structs (#61589)
Disable poisoning for large structs
Fix poisoning for very large structs
[release/6.0] Disable poisoning for large structs (#61601)