[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.)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.