[PATCH] lib: sbi: Order the publication of IPI device nodes
Nickolai Zeldovich
nickolai at csail.mit.edu
Tue Oct 6 12:36:27 PDT 2026
From: Nickolai Zeldovich <nickolai.zeldovich at gmail.com>
sbi_ipi_add_device() links the new node into ipi_dev_node_list with
plain stores while warm harts already walk the list with plain loads
in sbi_ipi_raw_clear(true). A reader can see the node pointer before
the node's fields and the device's per-hart data are visible.
Publish the node after smp_wmb() and pair it with smp_rmb() after each
node load in the all-devices walk.
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:
sbi_ipi_add_device() links a freshly allocated node into
ipi_dev_node_list with plain stores, and warm harts walk that list with
plain loads in sbi_ipi_raw_clear(true) while the cold hart is still
inserting. A reader can see the new node pointer and then read the
node's fields, or the device's per-hart data, before the writer's stores
to them are visible. A release before the publishing stores and an
acquire after the reader's load of each node close it.
An execution that triggers it:
Setup: eight harts booting; the cold hart runs sbi_init's cold path; the
other seven were released at wake_coldboot_harts() and run
init_warmboot. RVWMO hardware.
1. Cold hart: sbi_ipi_init(true) → fdt_ipi_init → aclint_mswi_cold_init
→ sbi_ipi_add_device(&aclint_mswi). It calls sbi_zalloc for the node
(entry), then stores entry->dev = &aclint_mswi and the node's prev
and next links, then stores ipi_dev_node_list.prev->next =
&entry->head (the publish). No fence between the two groups.
2. Warm hart 3, in parallel: init_warmboot → wait_for_coldboot (returns:
coldboot_done was released earlier) → sbi_ipi_raw_clear(true) →
sbi_list_for_each_entry(entry, &ipi_dev_node_list, head): loads
ipi_dev_node_list.next.
3. Under RVWMO the cold hart's stores may become visible to hart 3 in
any order. Hart 3's load returns the published pointer &entry->head
(store from step 1's last group), but the stores to entry->dev and
entry->head.next are not yet visible to hart 3.
4. Hart 3 loads entry->dev: it reads the sbi_zalloc'd zero, or garbage
if the allocation reused memory. It then evaluates
entry->dev->ipi_clear: a load through a NULL or wild pointer. Load
through NULL faults in M-mode (no PMP region at 0 on virt): hart 3
traps through _trap_handler into sbi_trap_error, prints a register
dump and hangs. The system comes up with 7 harts.
5. A second variant with the same window: hart 3 reads entry->head.next
stale (zero) instead of &ipi_dev_node_list, so the walk never reaches
the list head and dereferences address 0 on the next iteration.
6. A third variant needs no NULL: hart 3 reads dev->ipi_clear correctly
but mswi_ipi_clear then reads the hart's own aclint_mswi_data pointer
from its scratch, written by the cold hart in aclint_mswi_cold_init
*before* the add. Without an acquire the dependent read of the
scratch word is unordered too, and a stale NULL there makes
mswi_ipi_clear return early without clearing a pending IPI, leaving
the hart's msip latched: its first wfi in sbi_hsm_hart_wait returns
immediately and it spins instead of sleeping, and a later HART_START
IPI is indistinguishable from the stale one.
The walk is designed to tolerate an empty or partially populated list;
it is not safe against a node whose contents are not yet visible. The
all-devices walk in sbi_ipi_raw_clear is the only reader that runs
concurrently with sbi_ipi_add_device; sbi_ipi_raw_send walks the list
only after cold boot.
Verification:
Proved: with the fences, every warm hart's sbi_ipi_raw_clear(true)
satisfies its contract against the Sail RISC-V model under an RVWMO-
faithful memory model, in both interleavings (empty list, node present).
Without them the reader has no ordering on the node's fields and the
contract is unprovable.
lib/sbi/sbi_ipi.c | 25 +++++++++++++++++++++++-- 1 file changed, 23
insertions(+), 2 deletions(-)
FYI, the same change is also filed as GitHub pull request #430:
https://github.com/riscv-software-src/opensbi/pull/430
diff --git a/lib/sbi/sbi_ipi.c b/lib/sbi/sbi_ipi.c
index b04a5877..e933ccd7 100644
--- a/lib/sbi/sbi_ipi.c
+++ b/lib/sbi/sbi_ipi.c
@@ -314,6 +314,14 @@ void sbi_ipi_raw_clear(bool all_devices)
if (all_devices) {
sbi_list_for_each_entry(entry, &ipi_dev_node_list, head) {
+ /*
+ * Pair with the smp_wmb() in sbi_ipi_add_device():
+ * a warm hart may reach here while the cold hart is
+ * still adding devices. Order the load of the node
+ * pointer before the loads of the node's contents
+ * and of the per-hart data that ipi_clear() reads.
+ */
+ smp_rmb();
if (entry->dev->ipi_clear)
entry->dev->ipi_clear();
}
@@ -349,9 +357,22 @@ void sbi_ipi_add_device(const struct sbi_ipi_device *dev)
entry = sbi_zalloc(sizeof(*entry));
if (!entry)
return;
- SBI_INIT_LIST_HEAD(&entry->head);
entry->dev = dev;
- sbi_list_add_tail(&entry->head, &ipi_dev_node_list);
+ entry->head.prev = ipi_dev_node_list.prev;
+ entry->head.next = &ipi_dev_node_list;
+
+ /*
+ * Warm harts walk ipi_dev_node_list in sbi_ipi_raw_clear(true)
+ * from wait_for_coldboot() onwards, concurrently with the cold
+ * hart's cold-boot initialization. Make the node's contents
+ * (and the device's per-hart data set up by its cold_init)
+ * visible before the node becomes reachable from the list head.
+ * The reader pairs with this through the smp_rmb() in
+ * sbi_ipi_raw_clear().
+ */
+ smp_wmb();
+ ipi_dev_node_list.prev->next = &entry->head;
+ ipi_dev_node_list.prev = &entry->head;
if (!ipi_dev || ipi_dev->rating < dev->rating)
ipi_dev = dev;
--
2.55.0
More information about the opensbi
mailing list