tcg/riscv64/tcg-target.c.inc | 4 ++++ 1 file changed, 4 insertions(+)
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
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~
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~
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~
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.
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
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~
© 2016 - 2026 Red Hat, Inc.