[PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves

Max Chou posted 1 patch 1 week, 2 days ago
tcg/riscv64/tcg-target.c.inc | 4 ++++
1 file changed, 4 insertions(+)
[PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Max Chou 1 week, 2 days ago
According to the RISC-V unpriviledge spec,
  "vset{i}vl{i} and whole register loads and stores do not depend upon
   vtype."
The whole-register vector move instructions reuqire legal vtype that
trap when VILL is set.

The tcg_out_mov in tcg/riscv64 missed the vtype setup before emitting the
OPC_VMVNR_V.

Fixes: d4be6ee1111 ("tcg/riscv: Implement vector mov/dup{m/i}")
Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>
Reviewed-by: Joel Stanley <joel@jms.id.au>
Signed-off-by: Max Chou <max.chou@sifive.com>
---
 tcg/riscv64/tcg-target.c.inc | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/tcg/riscv64/tcg-target.c.inc b/tcg/riscv64/tcg-target.c.inc
index 2ce9d47a633..e3cfedb579d 100644
--- a/tcg/riscv64/tcg-target.c.inc
+++ b/tcg/riscv64/tcg-target.c.inc
@@ -778,6 +778,10 @@ static bool tcg_out_mov(TCGContext *s, TCGType type, TCGReg ret, TCGReg arg)
         {
             int lmul = type - riscv_lg2_vlenb;
             int nf = 1 << MAX(lmul, 0);
+
+            if (s->riscv_cur_type == TCG_TYPE_COUNT) {
+                set_vtype_len(s, type);
+            }
             tcg_out_opc_vi(s, OPC_VMVNR_V, ret, arg, nf - 1);
         }
         break;
-- 
2.43.0
Re: [PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Richard Henderson 1 week, 1 day ago
On 9/17/26 03:37, Max Chou wrote:
> According to the RISC-V unpriviledge spec,
>    "vset{i}vl{i} and whole register loads and stores do not depend upon
>     vtype."
> The whole-register vector move instructions reuqire legal vtype that
> trap when VILL is set.
> 
> The tcg_out_mov in tcg/riscv64 missed the vtype setup before emitting the
> OPC_VMVNR_V.
> 
> Fixes: d4be6ee1111 ("tcg/riscv: Implement vector mov/dup{m/i}")
> Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>
> Reviewed-by: Joel Stanley <joel@jms.id.au>
> Signed-off-by: Max Chou <max.chou@sifive.com>
> ---
>   tcg/riscv64/tcg-target.c.inc | 4 ++++
>   1 file changed, 4 insertions(+)
> 
> diff --git a/tcg/riscv64/tcg-target.c.inc b/tcg/riscv64/tcg-target.c.inc
> index 2ce9d47a633..e3cfedb579d 100644
> --- a/tcg/riscv64/tcg-target.c.inc
> +++ b/tcg/riscv64/tcg-target.c.inc
> @@ -778,6 +778,10 @@ static bool tcg_out_mov(TCGContext *s, TCGType type, TCGReg ret, TCGReg arg)
>           {
>               int lmul = type - riscv_lg2_vlenb;
>               int nf = 1 << MAX(lmul, 0);
> +
> +            if (s->riscv_cur_type == TCG_TYPE_COUNT) {
> +                set_vtype_len(s, type);
> +            }
>               tcg_out_opc_vi(s, OPC_VMVNR_V, ret, arg, nf - 1);
>           }
>           break;

Do you have a reproducer for this?  I ask because I still wonder how 
this can occur.

Given that

(1) all vector registers are call-clobbered, there should be none live 
across tcg_out_call_int (which clears the remembered vtype),

(2) the first vector action with no vector registers live must be a load 
or movi (which will initialize vtype).

Performing a move between vector registers when all such registers must 
be dead is... wrong.  It indicates that there's a bug elsewhere.

I'll throw an assert in here locally and see what happens...


r~
Re: [PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Palmer Dabbelt 1 week, 1 day ago
On Thu, 17 Sep 2026 13:51:10 PDT (-0700), Richard Henderson wrote:
> On 9/17/26 03:37, Max Chou wrote:
>> According to the RISC-V unpriviledge spec,
>>    "vset{i}vl{i} and whole register loads and stores do not depend upon
>>     vtype."
>> The whole-register vector move instructions reuqire legal vtype that
>> trap when VILL is set.
>>
>> The tcg_out_mov in tcg/riscv64 missed the vtype setup before emitting the
>> OPC_VMVNR_V.
>>
>> Fixes: d4be6ee1111 ("tcg/riscv: Implement vector mov/dup{m/i}")
>> Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>
>> Reviewed-by: Joel Stanley <joel@jms.id.au>
>> Signed-off-by: Max Chou <max.chou@sifive.com>
>> ---
>>   tcg/riscv64/tcg-target.c.inc | 4 ++++
>>   1 file changed, 4 insertions(+)
>>
>> diff --git a/tcg/riscv64/tcg-target.c.inc b/tcg/riscv64/tcg-target.c.inc
>> index 2ce9d47a633..e3cfedb579d 100644
>> --- a/tcg/riscv64/tcg-target.c.inc
>> +++ b/tcg/riscv64/tcg-target.c.inc
>> @@ -778,6 +778,10 @@ static bool tcg_out_mov(TCGContext *s, TCGType type, TCGReg ret, TCGReg arg)
>>           {
>>               int lmul = type - riscv_lg2_vlenb;
>>               int nf = 1 << MAX(lmul, 0);
>> +
>> +            if (s->riscv_cur_type == TCG_TYPE_COUNT) {
>> +                set_vtype_len(s, type);
>> +            }
>>               tcg_out_opc_vi(s, OPC_VMVNR_V, ret, arg, nf - 1);
>>           }
>>           break;
>
> Do you have a reproducer for this?  I ask because I still wonder how
> this can occur.
>
> Given that
>
> (1) all vector registers are call-clobbered, there should be none live
> across tcg_out_call_int (which clears the remembered vtype),
>
> (2) the first vector action with no vector registers live must be a load
> or movi (which will initialize vtype).
>
> Performing a move between vector registers when all such registers must
> be dead is... wrong.  It indicates that there's a bug elsewhere.

I'm not quite sure the commit message is right here, but this whole area 
is a bit of a landmine: one of the post-ratification changes to V was 
either this (or very close to this), so we've been through it for every 
project.

Here's the LLVM thread, which I think has links to everything else: 
https://github.com/llvm/llvm-project/issues/114518 (there's a GCC change 
as well).  IIRC hardware is a bit all over the place on this one, for 
the compilers we decided to just emit the code that's safe either way -- 
that's not really an option for QEMU, though, so we'll have to pick one 
side or the other.

> I'll throw an assert in here locally and see what happens...
>
>
> r~
Re: [PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Richard Henderson 1 week, 1 day ago
On 9/17/26 10:59, Palmer Dabbelt wrote:
> On Thu, 17 Sep 2026 13:51:10 PDT (-0700), Richard Henderson wrote:
>> On 9/17/26 03:37, Max Chou wrote:
>>> According to the RISC-V unpriviledge spec,
>>>    "vset{i}vl{i} and whole register loads and stores do not depend upon
>>>     vtype."
>>> The whole-register vector move instructions reuqire legal vtype that
>>> trap when VILL is set.
>>>
>>> The tcg_out_mov in tcg/riscv64 missed the vtype setup before emitting 
>>> the
>>> OPC_VMVNR_V.
>>>
>>> Fixes: d4be6ee1111 ("tcg/riscv: Implement vector mov/dup{m/i}")
>>> Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>
>>> Reviewed-by: Joel Stanley <joel@jms.id.au>
>>> Signed-off-by: Max Chou <max.chou@sifive.com>
>>> ---
>>>   tcg/riscv64/tcg-target.c.inc | 4 ++++
>>>   1 file changed, 4 insertions(+)
>>>
>>> diff --git a/tcg/riscv64/tcg-target.c.inc b/tcg/riscv64/tcg-target.c.inc
>>> index 2ce9d47a633..e3cfedb579d 100644
>>> --- a/tcg/riscv64/tcg-target.c.inc
>>> +++ b/tcg/riscv64/tcg-target.c.inc
>>> @@ -778,6 +778,10 @@ static bool tcg_out_mov(TCGContext *s, TCGType 
>>> type, TCGReg ret, TCGReg arg)
>>>           {
>>>               int lmul = type - riscv_lg2_vlenb;
>>>               int nf = 1 << MAX(lmul, 0);
>>> +
>>> +            if (s->riscv_cur_type == TCG_TYPE_COUNT) {
>>> +                set_vtype_len(s, type);
>>> +            }
>>>               tcg_out_opc_vi(s, OPC_VMVNR_V, ret, arg, nf - 1);
>>>           }
>>>           break;
>>
>> Do you have a reproducer for this?  I ask because I still wonder how
>> this can occur.
>>
>> Given that
>>
>> (1) all vector registers are call-clobbered, there should be none live
>> across tcg_out_call_int (which clears the remembered vtype),
>>
>> (2) the first vector action with no vector registers live must be a load
>> or movi (which will initialize vtype).
>>
>> Performing a move between vector registers when all such registers must
>> be dead is... wrong.  It indicates that there's a bug elsewhere.
> 
> I'm not quite sure the commit message is right here, but this whole area 
> is a bit of a landmine: one of the post-ratification changes to V was 
> either this (or very close to this), so we've been through it for every 
> project.
> 
> Here's the LLVM thread, which I think has links to everything else: 
> https://github.com/llvm/llvm-project/issues/114518 (there's a GCC change 
> as well).  IIRC hardware is a bit all over the place on this one, for 
> the compilers we decided to just emit the code that's safe either way -- 
> that's not really an option for QEMU, though, so we'll have to pick one 
> side or the other.
Yes, I get that.  But my point stands that after a function call (which 
can set VILL), all vector registers should be dead.  A move of a dead 
register is itself a bug.

There should have been *something* that brought the source vector 
register back to life: a load or a movi.  Both of which reinitialize vtype.

That's why I'd like to see a reproducer, or if there isn't one,

     tcg_debug_assert(s->riscv_cur_type != TCG_TYPE_COUNT);

I'm currently running the assert through the testsuite to make sure I've 
not gone loopy.


r~

Re: [PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Palmer Dabbelt 1 week, 1 day ago
On Thu, 17 Sep 2026 14:49:07 PDT (-0700), Richard Henderson wrote:
> On 9/17/26 10:59, Palmer Dabbelt wrote:
>> On Thu, 17 Sep 2026 13:51:10 PDT (-0700), Richard Henderson wrote:
>>> On 9/17/26 03:37, Max Chou wrote:
>>>> According to the RISC-V unpriviledge spec,
>>>>    "vset{i}vl{i} and whole register loads and stores do not depend upon
>>>>     vtype."
>>>> The whole-register vector move instructions reuqire legal vtype that
>>>> trap when VILL is set.
>>>>
>>>> The tcg_out_mov in tcg/riscv64 missed the vtype setup before emitting
>>>> the
>>>> OPC_VMVNR_V.
>>>>
>>>> Fixes: d4be6ee1111 ("tcg/riscv: Implement vector mov/dup{m/i}")
>>>> Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>
>>>> Reviewed-by: Joel Stanley <joel@jms.id.au>
>>>> Signed-off-by: Max Chou <max.chou@sifive.com>
>>>> ---
>>>>   tcg/riscv64/tcg-target.c.inc | 4 ++++
>>>>   1 file changed, 4 insertions(+)
>>>>
>>>> diff --git a/tcg/riscv64/tcg-target.c.inc b/tcg/riscv64/tcg-target.c.inc
>>>> index 2ce9d47a633..e3cfedb579d 100644
>>>> --- a/tcg/riscv64/tcg-target.c.inc
>>>> +++ b/tcg/riscv64/tcg-target.c.inc
>>>> @@ -778,6 +778,10 @@ static bool tcg_out_mov(TCGContext *s, TCGType
>>>> type, TCGReg ret, TCGReg arg)
>>>>           {
>>>>               int lmul = type - riscv_lg2_vlenb;
>>>>               int nf = 1 << MAX(lmul, 0);
>>>> +
>>>> +            if (s->riscv_cur_type == TCG_TYPE_COUNT) {
>>>> +                set_vtype_len(s, type);
>>>> +            }
>>>>               tcg_out_opc_vi(s, OPC_VMVNR_V, ret, arg, nf - 1);
>>>>           }
>>>>           break;
>>>
>>> Do you have a reproducer for this?  I ask because I still wonder how
>>> this can occur.
>>>
>>> Given that
>>>
>>> (1) all vector registers are call-clobbered, there should be none live
>>> across tcg_out_call_int (which clears the remembered vtype),
>>>
>>> (2) the first vector action with no vector registers live must be a load
>>> or movi (which will initialize vtype).
>>>
>>> Performing a move between vector registers when all such registers must
>>> be dead is... wrong.  It indicates that there's a bug elsewhere.
>>
>> I'm not quite sure the commit message is right here, but this whole area
>> is a bit of a landmine: one of the post-ratification changes to V was
>> either this (or very close to this), so we've been through it for every
>> project.
>>
>> Here's the LLVM thread, which I think has links to everything else:
>> https://github.com/llvm/llvm-project/issues/114518 (there's a GCC change
>> as well).  IIRC hardware is a bit all over the place on this one, for
>> the compilers we decided to just emit the code that's safe either way --
>> that's not really an option for QEMU, though, so we'll have to pick one
>> side or the other.
> Yes, I get that.  But my point stands that after a function call (which
> can set VILL), all vector registers should be dead.  A move of a dead
> register is itself a bug.

That's true for standard ABI functions, but there's a variant for 
vector-ABI functions that allows some registers to be saved and that 
variant doesn't preserve vtype.  That's basically how we ended up in the 
mess with whole-register moves: pre-change vector-1.0 versions didn't 
trap whole-vector moves on vtype=VILL, so variant functions could use 
whole-register moves to save/restore registers and it was legal.  It was 
only when Linux started clobbering with VILL on implementations that 
respected the post-change ISA that we ended up with the traps.

That said, in GCC we just had a proper bug in there where we treated 
vytpe as saved over calls, so it was pretty easy to roll it up into that 
fix.  Here's the GCC bug, if you're interested: 
<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=117544>.  IIUC it was a 
little different in LLVM because they didn't have that bug and were thus 
actually correct for the pre-change ISA, but they also decided to just 
emit the code that's safe for both cases.

> There should have been *something* that brought the source vector
> register back to life: a load or a movi.  Both of which reinitialize vtype.
>
> That's why I'd like to see a reproducer, or if there isn't one,
>
>      tcg_debug_assert(s->riscv_cur_type != TCG_TYPE_COUNT);
>
> I'm currently running the assert through the testsuite to make sure I've
> not gone loopy.

Ya, seems reasonable.  It's certainly pretty likely this is masking some 
other bug as it was in GCC.

Re: [PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Max Chou 1 week, 1 day ago
On 2026-09-17 17:21, Palmer Dabbelt wrote:
> On Thu, 17 Sep 2026 14:49:07 PDT (-0700), Richard Henderson wrote:
> > On 9/17/26 10:59, Palmer Dabbelt wrote:
> > > On Thu, 17 Sep 2026 13:51:10 PDT (-0700), Richard Henderson wrote:
> > > > On 9/17/26 03:37, Max Chou wrote:
> > > > > According to the RISC-V unpriviledge spec,
> > > > >    "vset{i}vl{i} and whole register loads and stores do not depend upon
> > > > >     vtype."
> > > > > The whole-register vector move instructions reuqire legal vtype that
> > > > > trap when VILL is set.
> > > > > 
> > > > > The tcg_out_mov in tcg/riscv64 missed the vtype setup before emitting
> > > > > the
> > > > > OPC_VMVNR_V.
> > > > > 
> > > > > Fixes: d4be6ee1111 ("tcg/riscv: Implement vector mov/dup{m/i}")
> > > > > Reviewed-by: Daniel Henrique Barboza <daniel.barboza@oss.qualcomm.com>
> > > > > Reviewed-by: Joel Stanley <joel@jms.id.au>
> > > > > Signed-off-by: Max Chou <max.chou@sifive.com>
> > > > > ---
> > > > >   tcg/riscv64/tcg-target.c.inc | 4 ++++
> > > > >   1 file changed, 4 insertions(+)
> > > > > 
> > > > > diff --git a/tcg/riscv64/tcg-target.c.inc b/tcg/riscv64/tcg-target.c.inc
> > > > > index 2ce9d47a633..e3cfedb579d 100644
> > > > > --- a/tcg/riscv64/tcg-target.c.inc
> > > > > +++ b/tcg/riscv64/tcg-target.c.inc
> > > > > @@ -778,6 +778,10 @@ static bool tcg_out_mov(TCGContext *s, TCGType
> > > > > type, TCGReg ret, TCGReg arg)
> > > > >           {
> > > > >               int lmul = type - riscv_lg2_vlenb;
> > > > >               int nf = 1 << MAX(lmul, 0);
> > > > > +
> > > > > +            if (s->riscv_cur_type == TCG_TYPE_COUNT) {
> > > > > +                set_vtype_len(s, type);
> > > > > +            }
> > > > >               tcg_out_opc_vi(s, OPC_VMVNR_V, ret, arg, nf - 1);
> > > > >           }
> > > > >           break;
> > > > 
> > > > Do you have a reproducer for this?  I ask because I still wonder how
> > > > this can occur.
> > > > 
> > > > Given that
> > > > 
> > > > (1) all vector registers are call-clobbered, there should be none live
> > > > across tcg_out_call_int (which clears the remembered vtype),
> > > > 
> > > > (2) the first vector action with no vector registers live must be a load
> > > > or movi (which will initialize vtype).
> > > > 
> > > > Performing a move between vector registers when all such registers must
> > > > be dead is... wrong.  It indicates that there's a bug elsewhere.
> > > 
> > > I'm not quite sure the commit message is right here, but this whole area
> > > is a bit of a landmine: one of the post-ratification changes to V was
> > > either this (or very close to this), so we've been through it for every
> > > project.
> > > 
> > > Here's the LLVM thread, which I think has links to everything else:
> > > https://github.com/llvm/llvm-project/issues/114518 (there's a GCC change
> > > as well).  IIRC hardware is a bit all over the place on this one, for
> > > the compilers we decided to just emit the code that's safe either way --
> > > that's not really an option for QEMU, though, so we'll have to pick one
> > > side or the other.
> > Yes, I get that.  But my point stands that after a function call (which
> > can set VILL), all vector registers should be dead.  A move of a dead
> > register is itself a bug.
> 
> That's true for standard ABI functions, but there's a variant for vector-ABI
> functions that allows some registers to be saved and that variant doesn't
> preserve vtype.  That's basically how we ended up in the mess with
> whole-register moves: pre-change vector-1.0 versions didn't trap
> whole-vector moves on vtype=VILL, so variant functions could use
> whole-register moves to save/restore registers and it was legal.  It was
> only when Linux started clobbering with VILL on implementations that
> respected the post-change ISA that we ended up with the traps.
> 
> That said, in GCC we just had a proper bug in there where we treated vytpe
> as saved over calls, so it was pretty easy to roll it up into that fix.
> Here's the GCC bug, if you're interested:
> <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=117544>.  IIUC it was a little
> different in LLVM because they didn't have that bug and were thus actually
> correct for the pre-change ISA, but they also decided to just emit the code
> that's safe for both cases.
> 
> > There should have been *something* that brought the source vector
> > register back to life: a load or a movi.  Both of which reinitialize vtype.
> > 
> > That's why I'd like to see a reproducer, or if there isn't one,
> > 
> >      tcg_debug_assert(s->riscv_cur_type != TCG_TYPE_COUNT);
> > 
> > I'm currently running the assert through the testsuite to make sure I've
> > not gone loopy.
> 
> Ya, seems reasonable.  It's certainly pretty likely this is masking some
> other bug as it was in GCC.

Hi Palmer and Richard,

Thanks Palmer for providing the toolchain discussion information and
links.

Richard, I have a reproducer for this issue listed below:

—
The reproducer follows this flow:

x86_64 `ls` executable -> qemu_x86-64 -> qemu-system-riscv64

1. Get x86_64 sysroot and `ls` executable from x86_64 host
```
$ podman create --name x86_64_container ubuntu:24.04
$ podman cp x86_64_container:/ {x86_64_sysroot}
```
2. Cross build qemu-x86_64
3. Obtain the riscv64 images (I built it using buildroot with
`qemu_riscv64_virt_defconfig` config).
4. Boot up riscv64 Linux on qemu-system-riscv64 and mount the
{x86_64_sysroot} (and the qemu-x86_64) directory.
5. Execute the x86_64 ls executable on qemu-x86_64 in
qemu-system-riscv64
```
# qemu-x86_64 -cpu max -L {x86_64_sysroot} {x86_64_sysroot}/usr/bin/ls
qemu-x86_64: QEMU internal SIGILL {code=ILLOPC, addr=0x7fff8c076450}
Illegal instruction
```
—

I understand that this reproducer could be simplified by building a
standalone executable without copying from the x86_64 container. I’ll
try to make it simpler.

rnax

Re: [PATCH v2] tcg/riscv64: Set vtype before whole-register vector moves
Posted by Richard Henderson 1 week, 1 day ago
On 9/18/26 08:10, Max Chou wrote:
> Richard, I have a reproducer for this issue listed below:
> 
> —
> The reproducer follows this flow:
> 
> x86_64 `ls` executable -> qemu_x86-64 -> qemu-system-riscv64
> 
> 1. Get x86_64 sysroot and `ls` executable from x86_64 host
> ```
> $ podman create --name x86_64_container ubuntu:24.04
> $ podman cp x86_64_container:/ {x86_64_sysroot}
> ```
> 2. Cross build qemu-x86_64
> 3. Obtain the riscv64 images (I built it using buildroot with
> `qemu_riscv64_virt_defconfig` config).
> 4. Boot up riscv64 Linux on qemu-system-riscv64 and mount the
> {x86_64_sysroot} (and the qemu-x86_64) directory.
> 5. Execute the x86_64 ls executable on qemu-x86_64 in
> qemu-system-riscv64
> ```
> # qemu-x86_64 -cpu max -L {x86_64_sysroot} {x86_64_sysroot}/usr/bin/ls
> qemu-x86_64: QEMU internal SIGILL {code=ILLOPC, addr=0x7fff8c076450}
> Illegal instruction
> ```
Whee!  Thanks.  An ubuntu 26.04 x86_64 ls triggers the aforementioned 
assert on a riscv64 host.  So, thankfully, no double containers required.

I'll report back...


r~