[mono][amd64] Fix calling-convention mismatch for small straddling vtypes in shared generics#129702
[mono][amd64] Fix calling-convention mismatch for small straddling vtypes in shared generics#129702pavelsavara wants to merge 9 commits into
Conversation
…ackend On amd64 a <=16 byte all-integer value type whose nested field crosses the 8-byte SysV eightbyte boundary (e.g. Nullable<T> where T is an 8-byte struct, giving value@4 size 8) is forced onto the stack (ArgOnStack -> LLVMArgVtypeByVal) because the JIT cannot place a straddling field in a register pair. The concrete, fully-flattened instantiation of the same layout has no straddling field and is passed in two integer registers (ArgValuetypeInReg). These two classifications must agree because partially-shared generic code calls the concrete method: the partially-shared caller sees the value type as an opaque type parameter (one straddling field -> byval) while the concrete callee sees the flattened layout (-> registers). Older LLVM passed the small byval struct in registers so they happened to match; newer LLVM lowers byval onto the stack, so the caller writes the value to the stack while the callee reads it from registers, corrupting it (e.g. box/unbox of Nullable<struct-with-nullable-field> in gshared code returns the wrong value). Mark such all-integer <=16 byte straddling vtypes so the LLVM backend passes them in two integer registers, matching the flattened classification. The JIT path is left unchanged. Only amd64 is affected: arm64 classifies by size (no straddle rule) and uses a register-array representation, and wasm/llvmonly use a different, already-correct nullable box path.
|
Tagging subscribers to this area: @steveisok, @vitek-karas |
With the amd64 LLVM backend now passing small straddling vtypes in registers, these tests pass under Mono full-AOT. Remove the ActiveIssue annotation so CI exercises the fix.
Re-link the disabled Mono full-AOT tests from the #129508 tracking issue to the individual PRs that fix them (nullabletypes -> #129702, call05_large/small -> #129708, WPF_3226 -> #129710, b143840 -> #129713, UnitTest_GVM_TypeLoadException -> #129715). The tests stay disabled; Runtime_105619 keeps the #129508 link.
… changes mini-amd64.* (and other arch backends), mini-exceptions.*, exceptions-*.* and mini-runtime.* feed Mono LLVM full-AOT codegen but were not in the runtime-llvm PR path filter, so PRs touching only those files (e.g. this one) skipped the LLVMFULLAOT validation leg.
The straddle-in-register optimization exists to make a managed call's convention match a fully-flattened instantiation (gshared/partially-shared generics). P/Invoke must follow the native ABI via the byval/stack path, so don't set llvm_inreg_straddle for pinvoke signatures; they keep the prior ArgOnStack -> LLVMArgVtypeByVal lowering. Addresses review feedback.
There was a problem hiding this comment.
I don't believe this is technically the right approach. We have inconsistency in the cconv with the JIT/non-llvm aot so this could still lead to problems, so the proper fix would be also make the non-llvm path pass these in regs as well.
As a low effort fix I believe this is fine, but I would double check that we don't regress any other tests.
@BrzVlad This helped me to realize that it would also create GC hole, thanks. |
|
/azp run runtime-extra-platforms |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Summary
On amd64, a
<= 16-byte value type whose nested field crosses the 8-byte SysV eightbyte boundary was passed on the stack as abyvalstruct, while a fully-flattened instantiation of the same layout is passed in two integer registers. These two classifications must agree, because partially-shared ("gshared") generic code calls the concrete method. Recent LLVM lowers smallbyvalstructs onto the stack instead of into registers, so the value is corrupted across the call. In practice this shows up only under Mono LLVM full-AOT on x64, e.g. boxing/unboxing aNullable<struct-with-a-nullable-field>in shared generic code returns the wrong value.Minimal repro
100.666— the boxed/unboxed value is corrupted.Root cause
add_valuetype(mini-amd64.c) forces a value type onto the stack (ArgOnStack→LLVMArgVtypeByVal) when a nested field straddles the 8-byte eightbyte boundary. ForNullable<GenQ<int>>the value field is at offset 4, size 8, so it crosses byte 8.The concrete, fully-flattened instantiation has no straddling field and is passed in two integer registers (
ArgValuetypeInReg).Partially-shared generic code calls the concrete method, so the conventions must match: the partially-shared caller sees the value type as an opaque type parameter (one straddling field,
byval) while the concreteNullable<T>:Boxcallee sees the flattened layout (registers). Older LLVM passed the smallbyvalstruct in registers so they matched; recent LLVM lowersbyvalonto the stack, so the caller writes the value to the stack while the callee reads it from registers → corruption.Fix
For managed (non-P/Invoke) calls of a
<= 16-byte value type, no longer force a straddling vtype onto the stack: let it fall through to the existing!sig->pinvokeclassification, which splits the value into (at most) two integer eightbytes purely by total size. That split is layout-independent, so the opaque (gshared) view and the concrete (flattened) view produce the sameArgValuetypeInRegclassification — and both the JIT and the LLVM backend honor it, so they agree regardless of how LLVM lowersbyval.P/Invoke, returns, and structs larger than 16 bytes keep the existing stack/
byvalABI.This replaces the earlier LLVM-only
llvm_inreg_straddleapproach (which fixed only the LLVM backend and left the JIT passing the value on the stack). TheArgInfo.llvm_inreg_straddlebitfield and itsget_llvm_call_infospecial-case are removed, making the change a net simplification.GC safety
Register-passing these values is safe for the GC: object references in a
<= 16-byte managed value type are always 8-byte aligned (the type loader rejects misaligned or overlapped references), so an eightbyte never splits a managed pointer and conservative stack/register scanning still finds every reference. (A "pass reference-containing straddlers by-ref" variant was rejected — the concrete side passes<= 16-byte vtypes in registers, not by-ref, so by-ref would re-introduce the very mismatch this fixes.)Scope — x64 only
LLVMArgAsIArgs), so caller and callee already agree.Validation
Built Mono + LLVM full-AOT on x64 and ran, in both JIT and full-AOT:
JIT/Directed/nullabletypestest sources (castclassvaluetype, covering every nullable type —int?,Guid?, the generic-struct cases,float?/double?/decimal?,GCHandle?, interface/explicit-layout structs): all pass;100in both modes);JIT/Directed/StructABI/StraddlingVtypeAbiregression test (see below).Negative check: with the pre-fix behavior restored (all straddlers on the stack), the new test and the repro fail under full-AOT (wrong unboxed value); with the fix they return
100. The JIT path now also passes these in registers, so LLVM↔non-LLVM calls agree.New regression test
src/tests/JIT/Directed/StructABI/StraddlingVtypeAbi.cscovers the combinations the fix distinguishes: the gsharedNullable<GenQ<int>>box/unbox (the bug), an all-integer straddler, a float-field straddler, a direct by-value pass of a straddlingNullablethrough shared generic code, and a reference-containing<= 16-byte struct passed in registers through shared generic code across aGC.Collect(register GC-safety).Alternatives considered
<= 16-byte INTEGERbyvalin registers (restore the old lowering). Uniform, but lives indotnet/llvm-projectand re-introduces reliance on non-standardbyvalregister passing.cfg->gsharedvt == 0), whose call passes the valuebyval(not by-ref), so the out-wrapper expects the wrong convention and faults.llvm_inreg_straddle(the previous revision of this PR) — fixed the LLVM backend but left the JIT passing the value on the stack, so an LLVM↔JIT call could still disagree.byvallowering.Note
This pull request was prepared with the assistance of GitHub Copilot.
Part of the MonoAOT LLVM 23 full-AOT regression set tracked by #129508. Built and tested together with the Emscripten 5.0.6 / LLVM 23 bump on #129396, where the re-enabled
nullabletypestests (boxunboxvaluetype / castclassvaluetype) pass on theruntime-llvmAllSubsets_Mono_LLVMFULLAOT_RuntimeTestsleg.Contributes to #129508.