[PATCH] firmware: fw_base: add an acquire fence after the _boot_status wait loop
Nickolai Zeldovich
nickolai at csail.mit.edu
Tue Oct 6 12:36:08 PDT 2026
From: Nickolai Zeldovich <nickolai.zeldovich at gmail.com>
Non-boot harts exit _wait_for_boot_hart on a plain load of _boot_status
and go on to read platform.hart_index2id and other data the boot hart
relocated before publishing. Nothing orders those loads after the
status load, so under RVWMO a hart can read the unrelocated pointer and
fault through it before mtvec is set.
Pair the boot hart's release fence with a "fence r, rw" after the loop.
Signed-off-by: Nickolai Zeldovich <nickolai.zeldovich at gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
---
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:
Non-boot harts leave _wait_for_boot_hart on a plain load of _boot_status
and immediately read data the boot hart produced before publishing.
Nothing on the waiting side orders that load before the later loads.
Under RVWMO a waiter can observe the "done" status and still read pre-
relocation values, including the unrelocated platform.hart_index2id
pointer, and fault through it before mtvec is set. One fence r, rw after
the loop closes it.
An execution that triggers it:
Setup: fw_dynamic loaded at an address different from its link address,
so the boot hart runs the self-relocation pass; eight harts; RVWMO
hardware (or any model that lets a plain load be satisfied early, e.g.
the Sail/MachCSL model used to find this).
1. Hart 1 reaches _wait_for_boot_hart while hart 0 is still relocating.
Hart 1's loop body is ld t1, _boot_status; div; div; div; bne. Its
load of _boot_status returns the pre-done value, it loops.
2. Hart 1's core speculatively issues the *next* iteration's loads
early. RVWMO allows a later load to be performed before an earlier
load to a different address when nothing orders them. In particular
the loads that follow the loop exit, lwu platform.hart_count, ld
platform.hart_index2id, may be satisfied now, from memory that hart 0
has not yet rewritten: platform.hart_index2id still holds its link-
time value 0x449f8.
3. Hart 0 finishes: it fixes up platform.hart_index2id to 0x800449f8 via
the R_RISCV_RELATIVE pass, executes fence rw, rw, stores _boot_status
= BOOT_STATUS_BOOT_HART_DONE.
4. Hart 1's loop load now returns DONE, the bne falls through. The
already-performed load of platform.hart_index2id is not re-executed:
s9 = 0x449f8.
5. Hart 1 executes lwu a5, 0(s9): a load from 0x449f8, which is outside
every PMA region on the virt platform. The access faults.
6. Hart 1 has not yet executed _start_warm's csrw mtvec; mtvec holds its
reset value. The trap vectors to an undefined address and the hart is
lost. The system boots with 7 harts, or hangs in smp_init waiting for
it.
The release on the boot hart (fence rw, rw before the status store) is
correct; the bug is the missing acquire on the waiting side. The fence
added here sits *after* the loop and runs once per hart. A 2019 attempt
placed a fence rw, rw inside the loop before the load (05602e2bf481) and
was reverted (69d794cbcbab) after a warm-reset regression on the HiFive
Unleashed whose root cause was never found; that fence did not provide
the needed ordering anyway.
Verification:
Proved: with the fence, the non-boot harts' path from _start to
_start_warm satisfies its contract against the Sail RISC-V model under
an RVWMO-faithful memory model. Without the fence the stale read is an
allowed execution and the contract is unprovable.
firmware/fw_base.S | 11 +++++++++++ 1 file changed, 11 insertions(+)
FYI, the same change is also filed as GitHub pull request #429:
https://github.com/riscv-software-src/opensbi/pull/429
diff --git a/firmware/fw_base.S b/firmware/fw_base.S
index a76ddc1b..294787a2 100644
--- a/firmware/fw_base.S
+++ b/firmware/fw_base.S
@@ -407,6 +407,17 @@ _wait_for_boot_hart:
div t2, t2, zero
div t2, t2, zero
bne t0, t1, _wait_for_boot_hart
+ /*
+ * Acquire: order the _boot_status load above before every later
+ * load (platform.hart_count, platform.hart_index2id, the scratch
+ * space, ...). The boot hart publishes with "fence rw, rw" before
+ * its store to _boot_status, but without a matching acquire here
+ * RVWMO lets a waiting hart that saw BOOT_STATUS_BOOT_HART_DONE
+ * still read pre-relocation values, e.g. the unrelocated
+ * platform.hart_index2id pointer, and fault through it before
+ * mtvec is set.
+ */
+ fence r, rw
_start_warm:
/* Reset all registers except ra, a0, a1, a2, a3 and a4 for non-boot HART */
--
2.55.0
More information about the opensbi
mailing list