[Bug target/126741] New: x86_64-w64-min gw32: 512-bit by-value arguments and return v alues are stored to caller stack temporaries with aligned vmovapd, but the temporaries are only 16-byte aligned — runtime crash (0xC0 000005)
gdwolfman at icloud dot com via Gcc-bugs <[email protected]>
| Newsgroups | gmane.comp.gcc.bugs |
|---|---|
| Message-ID | <[email protected]/bugzilla/> |
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126741
Bug ID: 126741
Summary: x86_64-w64-mingw32: 512-bit by-value arguments and
return values are stored to caller stack temporaries
with aligned vmovapd, but the temporaries are only
16-byte aligned — runtime crash (0xC0000005)
Product: gcc
Version: 16.1.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: target
Assignee: unassigned at gcc dot gnu.org
Reporter: gdwolfman at icloud dot com
Target Milestone: ---
Created attachment 65279
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65279&action=edit
Minimal reproducer
Component: target
Version: 16.1.0 (also expected on earlier branches; not yet bisected)
Target: x86_64-w64-mingw32
Host/Build: x86_64-w64-mingw32 (WinLibs UCRT+POSIX+SEH build by Brecht
Sanders, r2, via winget)
Keywords: wrong-code, ABI
See also: PR 110273 (the i686-w64-mingw32 sibling: "-mavx512f generates
AVX instructions without stack alignment"), PR 49001 (the 2011 32-byte/ymm
ancestor on win64)
On x86_64-w64-mingw32, passing a `__m512d` (or any 64-byte-aligned
aggregate, e.g. a struct wrapping one) **by value** to a function that is
not inlined makes the caller materialize the argument in a stack temporary
and pass its address (the ms_abi invisible-reference convention). GCC
stores into that temporary — and reads the invisible-reference **return**
slot — with the alignment-checking `vmovapd`, but allocates the slots at
fixed offsets from `rsp` without any dynamic realignment, in a function
whose prologue is a plain `sub`. The Windows x64 ABI guarantees only
16-byte stack alignment at a call site, so whether the slots happen to be
64-byte aligned depends on the caller's accidental `rsp` (mod 64): exactly
one of the four ABI-legal residues works, the other three fault with an
access violation (0xC0000005).
The bug reproduces at **every optimization level, including -O0**, so it is
not an optimizer path. Named over-aligned locals and by-value **return**
slots in isolation are handled correctly (GCC over-allocates and aligns a
pointer through a scratch register — the same function may realign one
local through `r10` while leaving its argument temporaries misaligned), and
plain register-pressure zmm spills are correctly emitted as unaligned
`vmovupd`. It is specifically the invisible-reference argument/return
temporaries that both (a) assume 64-byte alignment in the access
instruction and (b) never receive it.
`-mstackrealign` and `-mpreferred-stack-boundary=6` do not change the
emitted pattern at all (verified by disassembly diff on a large real-world
TU: identical aligned-store counts, no added realignment).
Clang (clang-cl 22.x, MSVC ABI) compiles the identical source correctly —
it either aligns the temporaries or uses unaligned moves; the repro runs
clean under it at all four stack residues. MSVC likewise.
Reproducer (repro.cpp, attached; 60 lines, freestanding):
Compile and run:
g++ -O2 -mavx512f -o repro.exe repro.cpp # any -O level reproduces
./repro.exe
Output on AVX-512 hardware (Ryzen 7 7445HS, Zen 4, Windows 11):
stack shift 0... <access violation 0xC0000005 here or within
the first two shifts; exit -1073741819>
Expected: all four stack shifts print "ok (120)" and the program prints
"no fault" (this is what the clang-cl build of the same file does).
`main` walks the caller's stack pointer through all four 16-byte residues
(mod 64) using `alloca` in a noinline wrapper; all four are ABI-legal
states, so a correct build must survive all of them.
A variant passing a Highway-style wrapper `struct V { __m512d raw; }`
instead of the bare `__m512d` behaves identically (repro-struct.cpp).
Disassembly evidence (g++ -O2 -mavx512f, GCC 16.1.0):
0000000140001780 <_Z6driverPKd>:
140001780: sub $0xe8,%rsp ; prologue: no realignment
140001787: mov %rcx,%rax
14000178a: lea 0x60(%rsp),%rdx ; &arg-temp a -> param 2
14000178f: lea 0x20(%rsp),%r8 ; &arg-temp x -> param 3
140001794: vmovupd (%rax),%zmm0
14000179a: lea 0xa0(%rsp),%rcx ; &return slot -> param 1
1400017a2: vmovapd %zmm0,0x60(%rsp) ; ALIGNED store, slot only
1400017ad: vmovupd 0x40(%rax),%zmm0 ; 16-byte aligned
1400017b4: vmovapd %zmm0,0x20(%rsp) ; ALIGNED store, ditto
1400017bf: vzeroupper
1400017c2: call *0x8848(%rip) ; core_ptr
1400017c8: vmovapd 0xa0(%rsp),%zmm0 ; ALIGNED read of ret slot
At function entry `rsp ≡ 8 (mod 16)` is all the ABI promises. The three
slots (`rsp+0x20`, `rsp+0x60`, `rsp+0xa0`) are 64-byte aligned only when
entry `rsp ≡ 8 (mod 64)` — one residue in four. There is no `and
$-64,%rsp`, no frame-pointer realignment, and no aligned-scratch-pointer
indirection for these slots anywhere in the function.
How this was found (real-world impact):
Found in corvus (github.com/OldCrow/corvus), a Google-Highway-based SIMD
special-function library: refactoring hot kernels into noinline helpers
taking Highway vector wrappers by value made a test binary segfault at
AVX-512 while a smaller test of the same kernel ran clean — the difference
was purely each binary's accidental `rsp` residue at the crash site. In a
production build of that library, 120 aligned `vmovapd` zmm accesses of
this kind coexist with 620 correctly-unaligned `vmovupd` register spills.
Any mingw-built AVX-512 code that passes 512-bit values by value across a
non-inlined call boundary is exposed, and the failure is
binary-layout-dependent, so test suites can pass while shipped binaries
crash.
Toolchain detail:
gcc version 16.1.0 (MinGW-W64 x86_64-ucrt-posix-seh,
built by Brecht Sanders, r2)
Target: x86_64-w64-mingw32
Thread model: posix
binutils/ld from the same WinLibs distribution
(Full `g++ -v` output available on request; nothing exotic — release
checking, SEH, UCRT.)