-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
.NET 6 C++/CLI runtime generates InvalidProgramException when large stack-allocated variables are used #60852
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @JulieLeeMSFT Issue DetailsDescriptionI 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 StepsA 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 behaviorThe program should not crash as long as we don't exceed the thread stack size. Actual behaviorThe 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 WorkaroundsIn my case, as this is a scratch buffer, I could use Configuration.NET 6 RC2 Haven't tried any other configurations. Other informationNo response
|
I'll take a look. |
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. |
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. |
@lstrudwick can you double-check that the above was working on an earlier release, and if so, which one? |
.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. |
All generated types have the following structure in the broken case.
|
@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. ; Assembly listing for method ClassLibrary1.Class1:BrokenFunction():System.String
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; debuggable code
; rbp based frame
; fully interruptible
; Final local variable assignments
;
; V00 loc0 [V00 ] ( 1, 1 ) ref -> [rbp-0x10018] must-init class-hnd exact ptr
; V01 loc1 [V01 ] ( 1, 1 ) struct (65544) [rbp-0x10010] do-not-enreg[XSB] addr-exposed ld-addr-op unsafe-buffer
; V02 tmp0 [V02 ] ( 1, 1 ) int -> [rbp-0x1001C] do-not-enreg[X] addr-exposed "GSCookie dummy"
; V03 OutArgs [V03 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] "OutgoingArgSpace"
; V04 GsCookie [V04 ] ( 1, 1 ) long -> [rbp-0x08] do-not-enreg[X] addr-exposed "GSSecurityCookie"
;
; Lcl frame size = 65600
G_M20969_IG01:
55 push rbp
4C8D9C24C0FFFEFF lea r11, [rsp-10040H]
E852B5A25F call CORINFO_HELP_STACK_PROBE
498BE3 mov rsp, r11
488DAC2440000100 lea rbp, [rsp+10040H]
33C0 xor rax, rax
488985E8FFFEFF mov qword ptr [rbp-10018H], rax
48B878563412F0DEBC9A mov rax, 0x9ABCDEF012345678
488945F8 mov qword ptr [rbp-08H], rax
;; bbWeight=1 PerfScore 5.75
G_M20969_IG02:
48B8B893730CFA7F0000 mov rax, 0x7FFA0C7393B8
833800 cmp dword ptr [rax], 0
7405 je SHORT G_M20969_IG04
;; bbWeight=1 PerfScore 3.25
G_M20969_IG03:
E8CC2A745F call CORINFO_HELP_DBG_IS_JUST_MY_CODE
;; bbWeight=0.50 PerfScore 0.50
G_M20969_IG04:
33D2 xor edx, edx
488D8DF0FFFEFF lea rcx, bword ptr [rbp-10010H]
41B801000100 mov r8d, 0x10001
4D63C0 movsxd r8, r8d
E8C5B0A25F call CORINFO_HELP_MEMSET
48B80855D67665020000 mov rax, 0x26576D65508
488B00 mov rax, gword ptr [rax]
488985E8FFFEFF mov gword ptr [rbp-10018H], rax
488B85E8FFFEFF mov rax, gword ptr [rbp-10018H]
48B978563412F0DEBC9A mov rcx, 0x9ABCDEF012345678
48394DF8 cmp qword ptr [rbp-08H], rcx
7405 je SHORT G_M20969_IG05
E8D5F5735F call CORINFO_HELP_FAIL_FAST
;; bbWeight=1 PerfScore 9.75
G_M20969_IG05:
90 nop
;; bbWeight=1 PerfScore 0.25
G_M20969_IG06:
488D6500 lea rsp, [rbp]
5D pop rbp
C3 ret
;; bbWeight=1 PerfScore 2.00
; Total bytes of code 146, prolog size 48, PerfScore 36.10, (MethodHash=ff99ae16) for method ClassLibrary1.Class1:BrokenFunction():System.String
|
@lstrudwick as a workaround you can pass C# repro: using System;
using System.Runtime.CompilerServices;
public unsafe class Program
{
[SkipLocalsInit]
public static void Main()
{
Test t;
Console.WriteLine(t.Bytes[4]);
}
private struct Test
{
public fixed byte Bytes[0x10008];
}
} 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 |
Alternatively we can increase the size of |
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...] |
For very large structs (> 64K in size) poisoning could end up generating instructions requiring larger local var offsets than we can handle. This hits IMPL_LIMIT that throws InvalidProgramException. Turn off poisoning for larger structs that require more than 16 movs to also avoid the significant code bloat by the singular movs. This is a less risky version of dotnet#61521 for backporting to .NET 6. Fix dotnet#60852
For very large structs (> 64K in size) poisoning could end up generating instructions requiring larger local var offsets than we can handle. This hits IMPL_LIMIT that throws InvalidProgramException. Turn off poisoning for larger structs that require more than 16 movs to also avoid the significant code bloat by the singular movs. This is a less risky version of #61521 for backporting to .NET 6. Fix #60852
For very large structs (> 64K in size) poisoning could end up generating instructions requiring larger local var offsets than we can handle. This hits IMPL_LIMIT that throws InvalidProgramException. Turn off poisoning for larger structs that require more than 16 movs to also avoid the significant code bloat by the singular movs. This is a less risky version of #61521 for backporting to .NET 6. Fix #60852
For very large structs poisoning could end up generating instructions requiring larger local var offsets than we can handle which would hit an IMPL_LIMIT that throws InvalidProgramException. Switch to using rep stosd (x86/x64)/memset helper (other platforms) when a local needs more than a certain number of mov instructions to poison. Also includes a register allocator change to mark killed registers as modified in addRefsForPhysRegMask instead of by the (usually) calling function, since this function is used directly in the change. Fix dotnet#60852
* Disable poisoning for large structs For very large structs (> 64K in size) poisoning could end up generating instructions requiring larger local var offsets than we can handle. This hits IMPL_LIMIT that throws InvalidProgramException. Turn off poisoning for larger structs that require more than 16 movs to also avoid the significant code bloat by the singular movs. This is a less risky version of #61521 for backporting to .NET 6. Fix #60852 * Run jit-format * Add regression test * Update src/coreclr/jit/codegencommon.cpp Co-authored-by: Andy Ayers <andya@microsoft.com> * Don't check poisoning for large struct in test Since it's disabled, there is nothing to check. Co-authored-by: Jakob Botsch Nielsen <jakob.botsch.nielsen@gmail.com> Co-authored-by: Andy Ayers <andya@microsoft.com>
Description
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.Configuration
.NET 6 RC2
Windows 10, x64
VS2022
Haven't tried any other configurations.
Other information
No response
The text was updated successfully, but these errors were encountered: