Skip to content

mtvec accepts reserved MODE value and reads it back as 3 #211

@whensun

Description

@whensun

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:

t6[1:0] != 3

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:

mtvec.MODE = 3

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:

mtvec.MODE = 3

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions