The issue
RVVM preserves a reserved mtvec.MODE value when writing to mtvec.
According to the RISC-V specification, the mtvec MODE field only defines:
0: Direct
1: Vectored
>= 2: Reserved
In my test, I wrote all ones to mtvec, then read it back. RVVM returned:
mtvec = 0xffffffffffffffff
This means:
mtvec.MODE = mtvec[1:0] = 3
Since MODE = 3 is reserved, RVVM appears to expose an architecturally invalid mtvec.MODE value instead of legalizing or rejecting it.
This looks like a specification mismatch in RVVM's WARL handling for mtvec.
Steps to reproduce
Build this bare-metal test case:
li t0, -1
csrw mtvec, t0
csrr t6, mtvec
Build command used:
riscv64-unknown-elf-gcc \
-march=rv64imafdch_zicfiss_zicbom_zicboz_v_zicsr_zca_zimop_zcmop_zbb_zbs_zkne_zbkb_zabha_zacas_zawrs_zkr_smepmp_zcb_zicond_zba_zknd_zbc_zbkc_zfh_zfbfmin_zfhmin_zfa_zifencei_zvfbfmin_zbkx_zvksed_zvksh_zvknha_zvknhb_zvkg_zvfbfwma_zvbc_zvbb_zvkned_zksed_zksh_zknh_zvkb_zicbop_zicfilp_svinval_zve32f \
-mabi=lp64 \
-mcmodel=medany \
-nostdlib \
-nostartfiles \
-T linker.ld \
code.S machine_to_supervisor.S machine_to_user.S \
-o code.elf
Run RVVM:
rvvm code.elf -m 256M -nogui -serial null -gdbstub
Connect with GDB:
riscv64-unknown-elf-gdb code.elf
set pagination off
target remote :1234
Continue execution and inspect the result register used by the test.
Expected architectural result:
Observed result on RVVM:
t6 = 0xffffffffffffffff
mtvec.MODE = t6[1:0] = 3
So RVVM reads back a reserved mtvec.MODE value.
Investigation
I found this while checking whether RVVM legalizes reserved mtvec.MODE values.
The current implementation appears to route mtvec through the generic CSR helper.
Old code:
// RVVM/src/riscv_csr.c
static inline bool riscv_csr_helper(rvvm_hart_t* vm, rvvm_uxlen_t* csr, rvvm_uxlen_t* dest, uint8_t op)
{
if (vm->rv64) {
rvvm_uxlen_t tmp = *csr;
switch (op) {
case CSR_SWAP:
*csr = *dest;
break;
case CSR_SETBITS:
*csr |= *dest;
break;
case CSR_CLEARBITS:
*csr &= ~(*dest);
break;
}
*dest = tmp;
return true;
} else {
return riscv_csr_helper_masked(vm, csr, dest, -1, op);
}
}
case CSR_MTVEC:
return riscv_csr_helper(vm, &vm->csr.tvec[RISCV_PRIV_MACHINE], dest, op);
This path stores the raw written value. Therefore, after writing -1 to mtvec, RVVM preserves the lower bits as 11.
This makes:
even though values greater than or equal to 2 are reserved.
Workarounds
I do not know of a guest-side workaround other than avoiding writes that set mtvec.MODE >= 2.
Guest software can explicitly mask the low bits before writing to mtvec.
For emulator-side testing, modifying RVVM to legalize mtvec.MODE after writes fixes the observed mismatch.
Suggested fix / Expected behavior
RVVM should not preserve reserved mtvec.MODE values as readable architectural state.
A possible fix is to use a dedicated helper for tvec CSRs and legalize the MODE field after CSR writes.
Suggested change:
// RVVM/src/riscv_csr.c
static inline bool riscv_csr_tvec(rvvm_hart_t* vm, rvvm_uxlen_t* csr, rvvm_uxlen_t* dest, uint8_t op)
{
// Apply normal CSR op first. This preserves read-old-value behavior.
riscv_csr_helper(vm, csr, dest, op);
// WARL legalization for tvec.MODE:
// Legal values are 0 (Direct) and 1 (Vectored).
// Any write producing mode >= 2 is mapped to a legal value.
*csr = bit_replace(*csr, 0, 2, bit_cut(*csr, 0, 2) & 0x1);
return true;
}
case CSR_MTVEC:
return riscv_csr_tvec(vm, &vm->csr.tvec[RISCV_PRIV_MACHINE], dest, op);
I think the expected behavior is:
- Writing a value that would produce
mtvec.MODE = 0 should keep Direct mode.
- Writing a value that would produce
mtvec.MODE = 1 should keep Vectored mode if supported.
- Writing a value that would produce
mtvec.MODE >= 2 should not make the reserved value observable on readback.
After writing all ones to mtvec, reading it back should not return:
Additional information
Spec summary:
mtvec.MODE = 0: Direct
mtvec.MODE = 1: Vectored
mtvec.MODE >= 2: Reserved
Real impact:
Medium real impact. This does not necessarily create a direct security issue by itself, but it can cause firmware, OS code, or tests to observe an architecturally invalid mtvec.MODE value.
Software may incorrectly believe that a reserved trap-vector mode is supported. Trap-vector behavior may also diverge from real hardware if RVVM internally uses the invalid mode.
This reduces RVVM's reliability as a RISC-V specification-conformance target.
Host OS / Architecture:
Operating system: Linux
OS/kernel version: Linux DESKTOP-PL0JDQL 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Architecture: x86_64
RVVM version/commit: latest version at the time of testing, exact commit unknown
Verbose logs:
If needed, rerun with verbose logging enabled:
rvvm code.elf -m 256M -nogui -serial null -gdbstub -verbose
The issue
RVVM preserves a reserved
mtvec.MODEvalue when writing tomtvec.According to the RISC-V specification, the
mtvecMODEfield only defines:In my test, I wrote all ones to
mtvec, then read it back. RVVM returned:This means:
Since
MODE = 3is reserved, RVVM appears to expose an architecturally invalidmtvec.MODEvalue instead of legalizing or rejecting it.This looks like a specification mismatch in RVVM's WARL handling for
mtvec.Steps to reproduce
Build this bare-metal test case:
Build command used:
Run RVVM:
Connect with GDB:
riscv64-unknown-elf-gdb code.elf set pagination off target remote :1234Continue execution and inspect the result register used by the test.
Expected architectural result:
Observed result on RVVM:
So RVVM reads back a reserved
mtvec.MODEvalue.Investigation
I found this while checking whether RVVM legalizes reserved
mtvec.MODEvalues.The current implementation appears to route
mtvecthrough the generic CSR helper.Old code:
This path stores the raw written value. Therefore, after writing
-1tomtvec, RVVM preserves the lower bits as11.This makes:
even though values greater than or equal to 2 are reserved.
Workarounds
I do not know of a guest-side workaround other than avoiding writes that set
mtvec.MODE >= 2.Guest software can explicitly mask the low bits before writing to
mtvec.For emulator-side testing, modifying RVVM to legalize
mtvec.MODEafter writes fixes the observed mismatch.Suggested fix / Expected behavior
RVVM should not preserve reserved
mtvec.MODEvalues as readable architectural state.A possible fix is to use a dedicated helper for
tvecCSRs and legalize theMODEfield after CSR writes.Suggested change:
I think the expected behavior is:
mtvec.MODE = 0should keep Direct mode.mtvec.MODE = 1should keep Vectored mode if supported.mtvec.MODE >= 2should not make the reserved value observable on readback.After writing all ones to
mtvec, reading it back should not return:Additional information
Spec summary:
Real impact:
Medium real impact. This does not necessarily create a direct security issue by itself, but it can cause firmware, OS code, or tests to observe an architecturally invalid
mtvec.MODEvalue.Software may incorrectly believe that a reserved trap-vector mode is supported. Trap-vector behavior may also diverge from real hardware if RVVM internally uses the invalid mode.
This reduces RVVM's reliability as a RISC-V specification-conformance target.
Host OS / Architecture:
Verbose logs:
If needed, rerun with verbose logging enabled: