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

Reply via email to