[PATCH v3] KVM: selftests: riscv: Add lazy V extension enablement for guests

From: JinRui

Date: Tue Jul 21 2026 - 07:30:08 EST


From: jinrui <jinrui@xxxxxxxxxxx>

When the cross-compiler defaults to an -march that includes the V
(vector) extension, -O2 auto-vectorization generates vector
instructions (e.g. vsetvli, vadd.vv) in Guest binary code. If the
Guest executes a vector instruction while sstatus.VS is Off, an
EXC_INST_ILLEGAL (scause=2) is raised. KVM's hedeleg delegates this
exception to the Guest, but the selftest has no way to handle it as a
bare-metal program, causing all Guest tests to fail.

In contrast, a real OS kernel handles this via riscv_v_first_use_handler(),
which detects the vector instruction, sets sstatus.VS to Initial, and
srets to re-execute.

Fix this with four changes in processor.c:

1. Delete the now-unused guest_unexp_trap() handler, which is replaced
by the full exception vector table.

2. In vm_arch_vcpu_add(), notify KVM that the Guest is allowed to use
the V extension via __vcpu_set_reg(V, 1) (best-effort, silently
ignores errors on hardware without V). Also replace the raw stvec
handler with the full exception vector table, which provides
save_context/restore_context for safe lazy enablement.

3. In route_exception(), add a lazy V enablement check that runs
before any test-registered handler. When the cause is a non-IRQ
EXC_INST_ILLEGAL and sstatus.VS is Off, set VS to Initial and
return so the faulting instruction is re-executed via sret.

4. Make vm_init_vector_tables() idempotent by checking vm->handlers
before allocation, so tests that manually call it (ebreak_test,
arch_timer, sbi_pmu_test) do not leak memory.

The check runs before test-registered EXC_INST_ILLEGAL handlers
(e.g. sbi_pmu_test) to ensure V enablement takes priority.

Tested on a riscv64 host with KVM enabled: all 12 KVM selftest
binaries pass.

Signed-off-by: jinrui <jinrui@xxxxxxxxxxx>
---
Changes in v3:
- Move v_available from a host-side static variable into struct
handlers (Guest memory), fixing synchronization issue between
host (writer) and Guest (reader).
Changes in v2:
- Add a v_available flag to prevent infinite exception loops on
hosts without the V extension.
- Use regs->status instead of reading the live sstatus CSR.

.../selftests/kvm/lib/riscv/processor.c | 68 ++++++++++++++++---
1 file changed, 58 insertions(+), 10 deletions(-)

diff --git a/tools/testing/selftests/kvm/lib/riscv/processor.c b/tools/testing/selftests/kvm/lib/riscv/processor.c
index ded5429f3448..841474acfabd 100644
--- a/tools/testing/selftests/kvm/lib/riscv/processor.c
+++ b/tools/testing/selftests/kvm/lib/riscv/processor.c
@@ -297,14 +297,6 @@ void vcpu_arch_dump(FILE *stream, struct kvm_vcpu *vcpu, u8 indent)
" T3: 0x%016lx T4: 0x%016lx T5: 0x%016lx T6: 0x%016lx\n",
core.regs.t3, core.regs.t4, core.regs.t5, core.regs.t6);
}
-
-static void __aligned(16) guest_unexp_trap(void)
-{
- sbi_ecall(KVM_RISCV_SELFTESTS_SBI_EXT,
- KVM_RISCV_SELFTESTS_SBI_UNEXP,
- 0, 0, 0, 0, 0, 0);
-}
-
void vcpu_arch_set_entry_point(struct kvm_vcpu *vcpu, void *guest_code)
{
vcpu_set_reg(vcpu, RISCV_CORE_REG(regs.pc), (unsigned long)guest_code);
@@ -348,8 +340,33 @@ struct kvm_vcpu *vm_arch_vcpu_add(struct kvm_vm *vm, u32 vcpu_id)
/* Setup sscratch for guest_get_vcpuid() */
vcpu_set_reg(vcpu, RISCV_GENERAL_CSR_REG(sscratch), vcpu_id);

- /* Setup default exception vector of guest */
- vcpu_set_reg(vcpu, RISCV_GENERAL_CSR_REG(stvec), (unsigned long)guest_unexp_trap);
+ /*
+ * Enable the V (vector) extension in KVM so that the compiler can
+ * safely generate vector instructions (e.g. via -O2 auto-
+ * vectorization). Silently ignore errors; the test will still work
+ * without V.
+ */
+ __vcpu_set_reg(vcpu, RISCV_ISA_EXT_REG(KVM_RISCV_ISA_EXT_V), 1);
+
+ /*
+ * Use the full exception vector table (which provides lazy V
+ * extension enablement for EXC_INST_ILLEGAL in route_exception)
+ * as the default exception handler. vm_init_vector_tables() is
+ * idempotent; tests that call it again will get a no-op.
+ */
+ vm_init_vector_tables(vm);
+ vcpu_init_vector_tables(vcpu);
+
+ /*
+ * Record V extension availability in the handlers struct so that
+ * route_exception() (called from Guest context) can check it
+ * without relying on a host-side global variable.
+ */
+ {
+ struct handlers *h = addr_gva2hva(vm, vm->handlers);
+
+ h->v_available = __vcpu_has_isa_ext(vcpu, KVM_RISCV_ISA_EXT_V);
+ }

return vcpu;
}
@@ -415,6 +432,7 @@ void assert_on_unhandled_exception(struct kvm_vcpu *vcpu)

struct handlers {
exception_handler_fn exception_handlers[NR_VECTORS][NR_EXCEPTIONS];
+ bool v_available;
};

void route_exception(struct pt_regs *regs)
@@ -432,6 +450,33 @@ void route_exception(struct pt_regs *regs)
ec = 0;
}

+ /*
+ * Handle V (vector) extension lazy enablement before any
+ * registered handler. The compiler's default march may include
+ * V, and auto-vectorization generates vector instructions that
+ * trigger EXC_INST_ILLEGAL when VS (Vector Status) in sstatus
+ * is Off. Enable VS to Initial and re-execute the faulting
+ * instruction, mimicking what a real OS kernel does.
+ *
+ * This check runs before any test-registered handler, so tests
+ * that install their own EXC_INST_ILLEGAL handler (e.g.
+ * sbi_pmu_test) are not affected.
+ */
+ if (!(regs->cause & CAUSE_IRQ_FLAG) && ec == EXC_INST_ILLEGAL) {
+ /*
+ * If KVM supports the V extension for this Guest and VS
+ * (Vector Status) is Off in the saved sstatus, set it to
+ * Initial and sret to re-execute the faulting instruction.
+ * Use regs->status (saved at exception entry) rather than
+ * reading the live CSR to avoid a TOCTOU race with nested
+ * exceptions.
+ */
+ if (handlers && handlers->v_available && !(regs->status & SR_VS)) {
+ regs->status |= SR_VS_INITIAL;
+ return;
+ }
+ }
+
if (handlers && handlers->exception_handlers[vector][ec])
return handlers->exception_handlers[vector][ec](regs);

@@ -448,6 +493,9 @@ void vcpu_init_vector_tables(struct kvm_vcpu *vcpu)

void vm_init_vector_tables(struct kvm_vm *vm)
{
+ if (vm->handlers)
+ return;
+
vm->handlers = __vm_alloc(vm, sizeof(struct handlers), vm->page_size,
MEM_REGION_DATA);

--
2.43.0