[wasm] Don't emit R2R function types over the wasm parameter limit - #132865
[wasm] Don't emit R2R function types over the wasm parameter limit#132865lewing wants to merge 5 commits into
Conversation
crossgen2 targeting wasm could emit a function type with more than 1000 parameters. Every engine rejects such a module, and the runtime responds by silently interpreting the whole assembly, so the only trace was a "Ready to Run header not found" line in the ReadyToRun log. Decline to compile a method whose lowered signature exceeds the limit, and decline its callers, since a narrow caller can still contain a call site needing the same oversized type. This is the crossgen2 counterpart of the Mono fix in dotnet#80243. Guarding only the compiled method was not sufficient. ObjectNode.GetStaticDependencies marked a WasmTypeNode for every method code node unconditionally, so a declined method - whose node is skipped at emission because its code is empty - still contributed its function type to the type section with no function referencing it. An unreferenced over-limit type makes the module just as unloadable as one in use. The checks are managed-side rather than NYI_WASM because JitWasmNyiToR2RUnsupported defaults to 0, so a JIT-side guard would fall back to NYIRAW and hard-fail under any invocation that does not pass it. Fixes dotnet#132855 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use ' - ' rather than ' -- ' for parenthetical asides, matching
CorInfoImpl.ReadyToRun.cs ('Should be unreachable - couldn't find a
TypeSpec'); repo-wide ' -- ' is predominantly a definition separator.
Fold the <param> rationale into <summary>, since that file documents
private members with <summary> only and has no <param> tags.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The added comments ran at a 0.60 comment-to-code ratio against 0.05-0.28 in the files being modified. Trimmed to 0.36, cutting restatement and narrative while keeping the non-obvious invariants: that types[0] is the return type, that the import thunk is emitted only when marked, and that an unreferenced over-limit type is enough to make the module unloadable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
recordCallSite is never reached on wasm: emitRecordCallSite's body is entirely inside #if defined(DEBUG) and is called only from the xarch emitter, so the guard there was unreachable. Removed it and moved the Import.OnMarked note to recordWasmManagedCallSig, which is the live path. Managed calls reach recordWasmManagedCallSig before getWasmTypeSymbol, so the end-to-end test was exercising only the former. Verified the latter in isolation by disabling the former: the test still passes, which also shows a RequiresRuntimeJitException thrown from a JIT-EE callback unwinds safely mid-codegen. Added unit coverage for unmanaged lowering, which omits the shadow stack pointer and portable entrypoint and so trips the limit at a different managed parameter count. Also softened the WasmLimits wording: 1000 is a widely adopted implementation limit, not a conformance requirement of the core spec. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR hardens the wasm ReadyToRun (crossgen2) pipeline against emitting WebAssembly function types that exceed common engine/tool implementation limits (notably the 1000-parameter cap), by declining compilation of offending methods/call-sites and preventing orphaned invalid type-section entries from being emitted.
Changes:
- Introduces a shared
WasmLimitshelper and uses it to decline R2R compilation for methods and call sites whose lowered wasm signature exceeds the parameter/result limits. - Prevents
ObjectNode.GetStaticDependenciesfrom marking wasm type-signature nodes for methods that will be skipped at emission (avoiding invalid orphaned types). - Adds unit and end-to-end tests that validate boundary behavior and ensure no over-limit function types appear in the emitted wasm type section.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/tools/Common/Compiler/DependencyAnalysis/WasmTypeNode.cs | Fails the build if an over-limit wasm function type still reaches emission. |
| src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs | Adds WasmLimits constants/utility for shared limit checks. |
| src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectNode.cs | Skips adding wasm type-signature dependencies for nodes that won’t be emitted. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs | Declines compilation for over-limit lowered signatures and for over-limit call-site function types. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs | Adds boundary tests asserting when lowering crosses the wasm parameter limit. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs | Adds type-section scanning helper to detect over-limit orphaned functypes. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWideSignatureModule.cs | New test input module with an over-limit signature and a narrow caller that invokes it. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs | Adds an end-to-end test suite validating the wide-signature scenario and type-section constraints. |
getWasmTypeSymbol allocated the CorInfoWasmType[] and narrowed typesSize to int before validating the arity. Check typesSize first so an over-limit signature neither allocates nor reaches the narrowing cast. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
I had a fix for this in the JIT a while back but backed it out, see #129555 (comment). |
Do we want to be able to compile and run json tests? right now the module is emitted and will fail to validate, the only reason it doesn't break more things harder is that we currently reject the r2r module if fails to validate and the whole assembly falls back to the interpreter. My change lets nativeaot continue to fail to compile these cases. We can ifdef out the tests but I think that is strictly less valuable. As for the number of limits https://github.com/v8/v8/blob/main/src/wasm/wasm-limits.h are the main ones we are likely to hit |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There is no
I view these kinds of tests as similar to what we have with "allocate a 4 GB array" tests (I think we have more of those that 1000 parameter methods). They are excluded on 32 bit platforms, and that's ok. WASM is quirky, some things don't work on WASM as well as on other platforms, and that's also ok. I agree it is concerning that we have an "all or nothing" plus silent fallback behavior which is a performance trap. It can be a justification for trying to make 1000 parameters work with R2R. At the same time, this is about methods with one thousand parameters. It is almost by definition test code only scenario. |
|
Should we just pack all params after 999 into a struct to handle this? Matching ABI shouldn't be a concern since native can't define it. |
Agreed, but since the interpreter can run this code my preference is that we treat this as not AOT able rather than generate code most engines will reject.
The test was written because of json source generators creating huge parameter counts, and that is where the real danger for this sort of thing lives in practive however unlikely (I'm sure Jared would be happy to tell stories). I agree that we don't need to go to extremes to support these cases as a rule but experience says making it work when we can leads to fewer issues getting filed.
I fixed the fallback for the tests in #132870 so at least this sort of problem will surface correctly while testing. I'm ok with ifdefing the test for all coreclr wasm if that is preferred but I think for the r2r case rejecting is genuinely better regardless. |
I will leave that up to you then. For me the "bright line" between the decision of "making it work" and "not trying to make it work" is the purpose. We have a test that tests 1000 parameters. It happens to trip the limit. We could have a test with 800 parameters. Or the limit could have been 2000. We wouldn't have noticed then. Would anyone else? It is possible. It is possible they'd also notice their 1000 parameter method is now 50x slower because it is interpreted. It is again possible they wouldn't care. As you note there are other limits. We will likely never trip most of them because we don't have tests that would trip them. A limit we actually hit in a "real world" scenario in NAOT-LLVM is 50k locals-per-method. In other words, adding product code to fix test code I don't see as valuable. Adding product code to fix problems that we think users may actually encounter and be unable to work around easily (by splitting their methods / classes) is a different matter. |
crossgen2 targeting wasm could emit a function type declaring more than 1000 parameters. Every engine rejects such a module (V8, wasmtime, wasm-tools), and CoreCLR responds by catching the module-construction failure and silently interpreting the entire assembly. The app runs, tests pass, and the only trace is a
Ready to Run header not foundline inDOTNET_ReadyToRunLogFileoutput, so an assembly can lose all of its R2R coverage with no visible error.This is the crossgen2 recurrence of a Mono-side bug fixed by #80243 ("Disable llvm for methods/calls with more than 1000 parameters"), and it is triggered by the same test type named in #76719:
ClassWithManyConstructorParametersin the System.Text.Json test assets, whose.ctor,Create, andDeconstructeach take 1000 parameters and lower to 1003 wasm parameters.Fixes #132855
Approach
crossgen2 now declines to compile a method whose lowered wasm signature would exceed the limit, and declines its callers too, since a narrow caller can still contain a call site needing the same oversized type. Declining uses
RequiresRuntimeJitException, which is already handled byReadyToRunCodegenCompilationand leaves the method to the interpreter.One of these guards sits in
getWasmTypeSymbol, a JIT-EE callback reached from RyuJIT codegen rather than from importation, so declining there unwinds a C++ exception through codegen frames. That is safe and was checked rather than assumed: with the earlierrecordWasmManagedCallSigguard temporarily disabled so that this one fires alone, crossgen2 still declines the caller and still emits a valid image with the wide methods absent.The guards are managed-side rather than the existing
NYI_WASMmechanism, becauseJitWasmNyiToR2RUnsupporteddefaults to 0 and is set only bycrossgen-corelib.projand the browser CoreCLR targets. A JIT-side guard would fall back toNYIRAWand hard-fail under any invocation that does not pass the flag.A shared
WasmLimitsholds the constants.WasmTypeNode.GetDatathrows if an over-limit type still reaches emission, turning a silently unloadable image into a build failure.The part worth reviewing carefully
Guarding the compiled method was not sufficient, and the second defect is the less obvious one.
ObjectNode.GetStaticDependenciesmarked aWasmTypeNodefor every method code node unconditionally. A declined method publishes empty code and its node is skipped at emission byMethodWithGCInfo.ShouldSkipEmittingObjectNode, but the type dependency had already been marked, so the 1003-parameter function type still landed in the type section with no function referencing it. An unreferenced over-limit type makes the module just as unloadable as one in use.ObjectNode.csis shared with NativeAOT. The new condition is a no-op there: NativeAOT'sMethodCodeNodedoes not overrideShouldSkipEmittingObjectNodeso it inherits=> false, and the twoDelayLoad*MethodImportimplementors ofIMethodCodeNodeWithTypeSignaturederive fromEmbeddedObjectNode, which is not anObjectNodeand never reaches this path. Only crossgen2'sMethodWithGCInfois affected.A consequence worth noting for anyone auditing the adjacent wasm limits: a check that walks emitted functions cannot find an orphaned invalid type. It has to decode the type section.
Verification
Baseline captured on main before any code change, then re-measured after.
Type section of
System.Text.Json.Tests.wasm, read directly from the image with no runtime involved:Filtered
ConstructorTests_AsyncStreamrun:The single failure is identical in both runs (
BindingBetweenRefProps, a pre-existing[ActiveIssue]), so there is no test regression. All 128 per-assembly R2R images scan clean, and composite mode was verified separately (max 68, zero over-limit). crossgen2-vconfirms the three methods are not compiled rather than miscompiled.Tests
WasmArgumentLayoutTestsgets boundary theories over the lowered parameter count. Managed and unmanaged lowering trip the limit at different managed parameter counts, because unmanaged signatures get neither the shadow stack pointer nor the portable entrypoint.R2RTestSuites.WasmWideSignatureModuleis end to end: it compiles an assembly containing an over-limit method, a narrow method that calls it, and ordinary methods, then asserts that no function type in the type section exceeds the limit, that both wide methods are absent from R2R, and that the ordinary methods are still compiled. It was validated as a negative control: with the guards reverted it fails withAssert.InRange.Follow-up
Not addressed here, and worth its own issue: the adjacent limits with the same silent-failure shape, namely 1000 results, 50,000 locals, and 65,520
br_tableentries.br_tableis the most likely to be hit, since wasm R2R already emits it forTypeCodestyle dispatch.Note
This pull request description was drafted with GitHub Copilot (AI-generated) and reviewed by the author.