https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126244
Bug ID: 126244
Summary: [RISC-V] wrong-code: dead vsetvli writes VLMAX into
a0, clobbering memset destination argument at -O3 with
RVV
Product: gcc
Version: 15.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c
Assignee: unassigned at gcc dot gnu.org
Reporter: christian.herber at oss dot nxp.com
Target Milestone: ---
Overview
--------
At -O3 with RVV enabled and -mrvv-vector-bits=zvl, GCC miscompiles a simple
scalar zero-initialization
loop. The loop is lowered to a sequence of memset() calls (loop-distribute-
patterns). Immediately before the first memset call, GCC emits
vsetvli a0,zero,e32,m8,ta,ma
whose VL result is written into a0 - the exact register that must hold the
memset destination pointer per the calling convention. The VL value in a0 is
never used (dead), but a0 is simultaneously live: it carries the first
memset's destination pointer. The result is a call to memset(0x40, 0, 1028)
(0x40 = VLMAX for e32,m8 at VLEN=256), which segfaults.
The C source is valid and standard; it is correct on scalar RISC-V, on Arm,
and under -ffast-math/-fsanitize=address (scheduling changes move the vsetvli
off a0).
Build / version
---------------
$ gcc -v
gcc version 15.2.0 (Ubuntu 15.2.0-14ubuntu1~24~ppa1)
Compiler runs natively on a SpaceMiT K1 (RISC-V, RVV 1.0, VLEN=256).
Flags
-----
-O3 -march=rv64gcv_zba_zbb_zbc_zbs_zvl256b -mrvv-vector-bits=zvl
Source
------
lib/speexdsp/libspeexdsp/mdf.c, function speex_echo_cancellation:
/* So we can use power_spectrum_accum */
for (i=0;i<=st->frame_size;i++)
st->Rf[i] = st->Yf[i] = st->Xf[i] = 0;
With frame_size == 256, each of Rf/Yf/Xf is (frame_size+1)=257 int32/float
elements = 1028 bytes. GCC lowers the three assignments into three
memset(ptr, 0, 1028) calls. This translation unit is compiled with RVV
intrinsic code active in the same function, so the function body contains RVV
code adjacent to the memset-lowered loop.
Wrong code (objdump of speex_echo_cancellation)
-----------------------------------------------
Pointers were loaded earlier: a0 <- st->Xf, s6 <- st->Yf, s4 <- st->Rf;
s8 holds the length (1028).
6fba: beqz a4, ... # end of overlap checks
6fbe: vsetvli a0,zero,e32,m8,ta,ma # a0 = VLMAX = 8*256/32 = 64 = 0x40
# -> CLOBBERS a0 (held st->Xf); VL
never used
6fc2: mv a2,s8 # len = 1028
6fc4: li a1,0 # fill = 0
6fc6: sd a6,0(sp)
6fc8: jal memset@plt # memset(a0=0x40, 0, 1028) ->
SIGSEGV
6fcc: mv a2,s8
6fce: li a1,0
6fd0: mv a0,s6 # 2nd memset dst = st->Yf (correct)
6fd2: jal memset@plt
6fd6: mv a2,s8
6fd8: mv a0,s4 # 3rd memset dst = st->Rf (correct)
6fda: li a1,0
6fdc: frrm s7
6fe0: jal memset@plt
The 2nd and 3rd calls reload their destination into a0 (mv a0,s6 / mv a0,s4)
and are correct. Only the first call relies on a0 still holding st->Xf, but a0
was overwritten by the vsetvli at 6fbe.
Runtime (gdb)
-------------
Program received signal SIGSEGV, Segmentation fault.
__GI_memset (dstpp=0x40, c=0, len@entry=1028) at ./string/memset.c:56
#0 __GI_memset (dstpp=0x40, c=0, len@entry=1028)
#1 speex_echo_cancellation (...) at .../mdf.c:913
dstpp = 0x40 = VLMAX for e32,m8 at VLEN=256, exactly the value the vsetvli
writes into a0. The struct and all three destination pointers are valid and
non-overlapping at the crash - this is not memory corruption, only a0 is wrong.
Analysis
--------
The vsetvli GPR destination (a0) holds a dead VL value, but the register
allocator / post-RA scheduler placed it on top of a0, which is live as the
memset destination argument to the immediately following call. The vsetvli
destination should have been a scratch GPR (or x0), or a0's memset-pointer
value should have been kept live across the vsetvli. This is a dead-def placed
over a live ABI call argument.
Expected: a0 holds st->Xf at the first memset call.
Actual: a0 holds VLMAX (0x40); memset(0x40,...) segfaults.
Steps to reproduce
------------------
The bug surfaced while developing a RISC-V version of the Audiomark benchmark.
It can be reproduced on this commit:
https://github.com/sohail103/audiomark/commit/b044b6f13a8ca205bf63a5f99cf09a79f625c0cd
The defect was observed building AudioMark's speexdsp echo-canceller test
(test_aec_f32), compiled and run natively on the SpaceMiT K1:
FLAGS="-O3 -march=rv64gcv_zba_zbb_zbc_zbs_zvl256b -mrvv-vector-bits=zvl"
cmake .. -DPORT_DIR=ports/riscv/v -DCMAKE_C_FLAGS="$FLAGS"
make test_aec_f32
./test_aec_f32 # SIGSEGV in memset (dst=0x40) via mdf.c:913
The ports/riscv/v port force-includes RVV intrinsic kernels into mdf.c, which
places RVV code next to the scalar memset-lowered loop; this is what exposes
the a0 clobber. The scalar RISC-V (port/riscv) and Arm (port/arm) ports emit no
vsetvli and do not
fault.
Bug hunted down manually, report written mostly by Claude Opus 4.8