[PATCH] lib: sbi: Install the next-stage entry as a direct-mode xtvec
Anup Patel
anup at brainfault.org
Tue Nov 3 19:31:45 PST 2026
On Wed, Oct 7, 2026 at 1:06 AM Nickolai Zeldovich
<nickolai at csail.mit.edu> wrote:
>
> From: Nickolai Zeldovich <nickolai.zeldovich at gmail.com>
>
> sbi_hart_switch_mode() writes next_addr unmasked into stvec (vstvec,
> utvec). Bits [1:0] of xtvec are the MODE field, so a 2-byte aligned
> entry, legal with the C extension, selects a reserved mode: QEMU drops
> the write, the Sail model keeps the old mode, and the next stage starts
> with a trap vector that is not its entry.
>
> Mask the mode bits and install the entry as a direct-mode vector; mepc
> still receives the exact address.
>
> Signed-off-by: Nickolai Zeldovich <nickolai.zeldovich at gmail.com>
> Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
Prefer using Assisted-by for LLMs
> ---
> This bug was found in the process of formally verifying the correctness
> of OpenSBI on top of the Sail RISC-V semantics using Lean.
>
> Summary:
>
> sbi_hart_switch_mode() writes the next stage's entry address to mepc
> and, for an S-mode target, to stvec (vstvec for a virtualised target,
> utvec for U-mode with N). mepc tolerates any instruction address: with
> the C extension only bit 0 is cleared. stvec does not: bits [1:0] are
> the MODE field, and the values 2 and 3 are reserved. An entry address
> that is 2-byte but not 4-byte aligned, legal and common with the C
> extension, has low bits 10 and asks for a reserved mode. The outcome is
> implementation-defined, and in no implementation is it "stvec = entry".
> Mask the mode bits so the entry is installed as a direct-mode vector.
>
> An execution that triggers it:
>
> Setup: a kernel built with the C extension whose secondary entry is
> compressed-aligned. Current Linux is one: secondary_start_sbi is at
> 0xffffffff800000a6, physical 0x802000a6, so
> __pa_symbol(secondary_start_sbi) ends in …a6, low bits 10.
>
> 1. The kernel's __cpu_up for CPU 3 calls sbi_hsm_hart_start(hartid = 3,
> saddr = 0x802000a6, opaque) (cpu_ops_sbi.c: sbi_cpu_start).
> 2. OpenSBI's sbi_hsm_hart_start() records next_addr = 0x802000a6,
> next_mode = PRV_S in hart 3's scratch and rings it. Hart 3 leaves
> sbi_hsm_hart_wait(), runs the warm init, and
> sbi_hsm_hart_start_finish() calls sbi_hart_switch_mode(hartid,
> opaque, 0x802000a6, PRV_S, false).
> 3. csr_write(CSR_MEPC, 0x802000a6): legal, mepc = 0x802000a6.
> 4. csr_write(CSR_STVEC, 0x802000a6): the MODE field is 2, reserved. What
> the hart does depends on the implementation:
> - QEMU (target/riscv/tcg/csr.c: write_stvec) ignores the write and logs
> "CSR_STVEC: reserved mode not supported" under -d unimp. stvec
> keeps its previous value: whatever this hart had at reset or from a
> previous life. On virt after a cold reset that is 0.
> - The Sail reference model (legalize_tvec with
> xtvec_mode_reserved_behavior = Xtvec_Ignore) keeps the previous
> MODE and takes the new base: stvec = 0x802000a4 | old_mode.
> - Hardware may do either, or treat the value as direct mode; the spec
> leaves it open.
> 5. mret. Hart 3 is in S-mode at 0x802000a6 with sstatus.SIE = 0 and a
> trap vector that is not the entry: 0 (QEMU) or 0x802000a4 | mode
> (Sail).
> 6. Nothing bad happens *on this kernel*, because the first five
> instructions of secondary_start_sbi (csrw sie, 0; csrw sip, 0; …)
> cannot trap and the sixth is csrw stvec, .Lsecondary_park, which
> installs the kernel's own vector. The window is six instructions
> wide. What would go wrong, and where the bug bites, is any of:
> - a next stage whose first instructions can fault (a load from a not-
> yet-mapped address, an illegal instruction from a misdetected
> extension, a misaligned access on a strict core) before it installs
> its own stvec. OpenSBI's intent, visible in the code, is that such
> a trap re-enters the next stage at its entry. Instead it vectors to
> 0 (QEMU: sepc and scause are set, execution jumps to address 0,
> which is unmapped on virt, so a second fault happens with stvec
> still 0: an infinite trap loop, the hart is lost), or to entry - 2
> (Sail: into the middle of the preceding function's last compressed
> instruction);
> - the Sail-model variant with the previous mode being *vectored* (mode
> 1, left there by a previous OS or firmware stage): an interrupt in
> the window dispatches to entry - 2 + 4 * cause, into the kernel's
> code at an arbitrary offset;
> - a previous stvec whose own MODE field is reserved (a value a prior
> stage wrote and that the hardware accepted literally): the Sail
> model flags the write as an internal error, i.e. the behaviour is
> undefined.
>
> The fix writes next_addr & ~3UL to the xtvec register. For a 4-byte
> aligned entry nothing changes. For a compressed-aligned entry the vector
> is the 4-byte aligned address just below the entry, two bytes into the
> preceding instruction stream; that is the best a direct-mode vector can
> do, is well-defined on every implementation, and mepc, written unmasked,
> still delivers control to the real entry. A next stage that relies on
> the firmware-installed vector must have a 4-byte aligned entry; the
> kernel does not, and installs its own.
>
> Verification:
>
> Proved: with the mask, the hand-off post-condition stvec = next_addr &
> ~3 holds against the Sail RISC-V model for any entry address. Without
> the mask the post-condition stvec = next_addr is false for a 2-byte
> aligned entry, and the outcome of the write depends on the previous
> stvec, which nothing on the hart-start path constrains.
Missing "---" over here.
>
> lib/sbi/sbi_hart.c | 18 +++++++++++++++--- 1 file changed, 15
> insertions(+), 3 deletions(-)
>
> FYI, the same change is also filed as GitHub pull request #431:
> https://github.com/riscv-software-src/opensbi/pull/431
>
> diff --git a/lib/sbi/sbi_hart.c b/lib/sbi/sbi_hart.c
> index bee88557..c0d64c83 100644
> --- a/lib/sbi/sbi_hart.c
> +++ b/lib/sbi/sbi_hart.c
> @@ -751,6 +751,7 @@ sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
> #else
> unsigned long val;
> #endif
> + unsigned long tvec;
>
> switch (next_mode) {
> case PRV_M:
> @@ -783,21 +784,32 @@ sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
> csr_write(CSR_MSTATUS, val);
> csr_write(CSR_MEPC, next_addr);
>
> + /*
> + * The xtvec registers encode the trap mode in bits [1:0]: 0 is
> + * direct, 1 is vectored, 2 and 3 are reserved. next_addr is an
> + * instruction address and may be only 2-byte aligned when the C
> + * extension is in use, in which case writing it unmasked asks
> + * for a reserved mode and the result is implementation-defined
> + * (the write is ignored, or the old mode is kept). Install the
> + * entry as a direct-mode vector instead.
> + */
> + tvec = next_addr & ~0x3UL;
Use MTVEC_MODE instead of magic value 0x3UL.
> +
> if (next_mode == PRV_S) {
> if (next_virt) {
> - csr_write(CSR_VSTVEC, next_addr);
> + csr_write(CSR_VSTVEC, tvec);
> csr_write(CSR_VSSCRATCH, 0);
> csr_write(CSR_VSIE, 0);
> csr_write(CSR_VSATP, 0);
> } else {
> - csr_write(CSR_STVEC, next_addr);
> + csr_write(CSR_STVEC, tvec);
> csr_write(CSR_SSCRATCH, 0);
> csr_write(CSR_SIE, 0);
> csr_write(CSR_SATP, 0);
> }
> } else if (next_mode == PRV_U) {
> if (misa_extension('N')) {
> - csr_write(CSR_UTVEC, next_addr);
> + csr_write(CSR_UTVEC, tvec);
> csr_write(CSR_USCRATCH, 0);
> csr_write(CSR_UIE, 0);
> }
> --
> 2.55.0
>
>
> --
> opensbi mailing list
> opensbi at lists.infradead.org
> http://lists.infradead.org/mailman/listinfo/opensbi
I have taken care of the above comments at the time of merging.
Reviewed-by: Anup Patel <anup at brainfault.org>
Applied this patch to the riscv/opensbi repo.
Thanks,
Anup
More information about the opensbi
mailing list