Repository navigation
Conversation
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@gmail.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This bug was found in the process of formally verifying the correctness of OpenSBI on top of the Sail RISC-V semantics using Lean.
sbi_hart_switch_mode()writes the next stage's entry address tomepcand, for an S-mode target, tostvec(vstvecfor a virtualised target,utvecfor U-mode with N).mepctolerates any instruction address: with the C extension only bit 0 is cleared.stvecdoes 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 bits10and 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_sbiis at0xffffffff800000a6, physical0x802000a6, so__pa_symbol(secondary_start_sbi)ends in…a6, low bits10.__cpu_upfor CPU 3 callssbi_hsm_hart_start(hartid = 3, saddr = 0x802000a6, opaque)(cpu_ops_sbi.c: sbi_cpu_start).sbi_hsm_hart_start()recordsnext_addr = 0x802000a6,next_mode = PRV_Sin hart 3's scratch and rings it. Hart 3 leavessbi_hsm_hart_wait(), runs the warm init, andsbi_hsm_hart_start_finish()callssbi_hart_switch_mode(hartid, opaque, 0x802000a6, PRV_S, false).csr_write(CSR_MEPC, 0x802000a6): legal,mepc = 0x802000a6.csr_write(CSR_STVEC, 0x802000a6): the MODE field is 2, reserved. What the hart does depends on the implementation:target/riscv/tcg/csr.c: write_stvec) ignores the write and logs "CSR_STVEC: reserved mode not supported" under-d unimp.stveckeeps its previous value: whatever this hart had at reset or from a previous life. Onvirtafter a cold reset that is0.legalize_tvecwithxtvec_mode_reserved_behavior = Xtvec_Ignore) keeps the previous MODE and takes the new base:stvec = 0x802000a4 | old_mode.mret. Hart 3 is in S-mode at0x802000a6withsstatus.SIE = 0and a trap vector that is not the entry:0(QEMU) or0x802000a4 | mode(Sail).secondary_start_sbi(csrw sie, 0; csrw sip, 0; …) cannot trap and the sixth iscsrw 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:stvec. OpenSBI's intent, visible in the code, is that such a trap re-enters the next stage at its entry. Instead it vectors to0(QEMU:sepcandscauseare set, execution jumps to address 0, which is unmapped onvirt, so a second fault happens withstvecstill 0: an infinite trap loop, the hart is lost), or toentry - 2(Sail: into the middle of the preceding function's last compressed instruction);entry - 2 + 4 * cause, into the kernel's code at an arbitrary offset;stvecwhose 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 & ~3ULto 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, andmepc, 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 & ~3holds against the Sail RISC-V model for any entry address. Without the mask the post-conditionstvec = next_addris false for a 2-byte aligned entry, and the outcome of the write depends on the previousstvec, which nothing on the hart-start path constrains.