| Issue |
208092
|
| Summary |
[MemCpyOpt][x86] stack-move alloca merge produces a memcpy whose align exceeds its destination alloca's alignment
|
| Labels |
|
| Assignees |
|
| Reporter |
darioush
|
Hello. I was able to reproduce a segfault on x86-64 with the following dependency-free rust program (1.93.0) (Emulation such as qemu-user or Rosetta does not fault): https://gist.github.com/darioush/6d1ee4e63aa209479d963aa012c95ff3
```
git clone https://gist.github.com/darioush/6d1ee4e63aa209479d963aa012c95ff3.git && cd 6d1ee4e63aa209479d963aa012c95ff3
sh run.sh # => Segmentation fault (exit 139)
```
I have verified the reproduction and include the following AI generated analysis in case it is useful for maintainers. It seems to be a miscompile in the MemCpyOpt pass. AI was used in search for a minimal reproduction. Other rustc versions may not emit the same frame layout, however the underlying MemCpyOpt pass issue seems present on trunk.
## Summary
After the MemCpyOpt pass, a `llvm.memcpy` annotated `align 16` writes into a stack `alloca` whose
own alignment is only `align 8`. The x86-64 backend trusts the `align 16` attribute and lowers the
24-byte copy to a 128-bit aligned `movaps`/`vmovaps` store into an 8-aligned stack slot, which
raises `#GP` and crashes at runtime (SIGSEGV).
The IR *after* MemCpyOpt is internally inconsistent (memcpy dst alignment > destination alloca
alignment), so the backend is not at fault. The malformed IR also passes `opt -passes=verify`
cleanly, so nothing flags it before the backend. `opt -passes=memcpyopt` on its own introduces the
defect (isolated `.ll` below), so it is the pass, not something upstream.
Suspect: the single-BB stack-move optimization in `MemCpyOptimizer.cpp` (introduced in D153453 /
commit 569769b, reverted and reapplied several times).
## The bug, in IR
(from the reproducer program, rustc 1.93.0, `-C opt-level=3 -C lto=no -C codegen-units=1`)
Same `memcpy` (identical source `%val.sroa.5.0.result.sroa_idx.i`), before vs. after MemCpyOpt.
Only the destination changes — from an `align 16` alloca to an `align 8` alloca — while the
`memcpy`'s `align 16` pointer attribute is left unchanged:
```llvm
; BEFORE memcpyopt (in repro::query) -- destination alloca is align 16: consistent
%_20.sroa.14 = alloca [24 x i8], align 16
...
call void @llvm.memcpy.p0.p0.i64(
ptr noundef nonnull align 16 dereferenceable(24) %_20.sroa.14,
ptr noundef nonnull align 16 dereferenceable(24) %val.sroa.5.0.result.sroa_idx.i,
i64 24, i1 false)
```
```llvm
; AFTER memcpyopt -- same source, but destination is now an align 8 alloca: INCONSISTENT
%_5.sroa.6.sroa.7.sroa.7 = alloca [24 x i8], align 8
...
call void @llvm.memcpy.p0.p0.i64(
ptr noundef nonnull align 16 dereferenceable(24) %_5.sroa.6.sroa.7.sroa.7,
ptr noundef nonnull align 16 dereferenceable(24) %val.sroa.5.0.result.sroa_idx.i,
i64 24, i1 false)
```
MemCpyOpt forwarded the copy of the `align 16` source directly into `%_5.sroa.6.sroa.7.sroa.7`
(`align 8`), eliminating the intermediate buffers, but did not lower the `memcpy`'s `align 16`
attribute to match the smaller-aligned destination (nor raise the destination alloca to `align 16`).
Faulting store the backend then emits:
```
movaps %xmm0,0xe8(%rsp) ; rsp is 16-aligned, 0xe8 = 232 ≡ 8 (mod 16) -> misaligned 16-byte store
```
## MemCpyOpt in isolation
(pure `.ll`, no Rust) — reproduces opt 18.1 .. trunk
`repro.ll` in the gist is a 20-line module that `opt -passes=memcpyopt` alone miscompiles, with no
Rust and no backend. It merges the five allocas; the merged destination `%e` comes out `align 8`
while the `align 16`, size-24 `memcpy` is redirected to write into it:
**Compiler Explorer (opt + llc, LLVM trunk): https://godbolt.org/z/baPYEc4Y7**
```llvm
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
define fastcc void @f() {
start:
%a = alloca [24 x i8], align 8
%b = alloca [24 x i8], align 1
%c = alloca [24 x i8], align 1
%d = alloca [24 x i8], align 16
%e = alloca [24 x i8], align 1
br i1 false, label %bb19, label %bb5
bb5:
call void @llvm.memcpy.p0.p0.i64(ptr align 16 dereferenceable(24) %d, ptr null, i64 24, i1 false)
call void @llvm.memcpy.p0.p0.i64(ptr %c, ptr %d, i64 24, i1 false)
call void @llvm.memcpy.p0.p0.i64(ptr %e, ptr %c, i64 24, i1 false)
ret void
bb19:
call void null(ptr %a, ptr null, ptr null)
call void @llvm.memcpy.p0.p0.i64(ptr %b, ptr %a, i64 24, i1 false)
call void @llvm.memcpy.p0.p0.i64(ptr %e, ptr %b, i64 24, i1 false)
ret void
}
declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)
```
```
# 1. malformed IR: align-8 alloca %e is the dst of an align-16 memcpy
$ opt -passes=memcpyopt -S repro.ll | grep -E '%e = alloca|dereferenceable\(24\) %e'
%e = alloca [24 x i8], align 8
call void @llvm.memcpy.p0.p0.i64(ptr align 16 dereferenceable(24) %e, ptr null, i64 24, i1 false)
# 2. it still passes the verifier
$ opt -passes=memcpyopt -S repro.ll | opt -passes=verify -S >/dev/null; echo $?
0
# 3. backend materializes the misaligned store
$ opt -passes=memcpyopt -S repro.ll | llc -mtriple=x86_64-unknown-linux-gnu -o - | grep movaps
movaps %xmm0, -120(%rsp) ; -120 ≡ 8 (mod 16): 16-byte store to an 8-aligned slot
```
Without the pass, `llc` on the original module stores only to 16-aligned offsets (-64/-96/-128).
The `opt -passes=memcpyopt` transform produces the same malformed IR on **opt 18.1.0, 19.1.0,
20.1.0, 21.1.0, 22.1.0, and trunk** (checked on Compiler Explorer; the assertions-trunk build
fires no assertion).
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs