[PATCH] lib: sbi: Order the publication of IPI device nodes
Anup Patel
anup at brainfault.org
Tue Nov 3 22:21:22 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_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();
The smp_rmp() upon every loop iteration is a big hammer in the path.
We need to think of a better solution.
> 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;
This looks unmaintainable.
Why not introduce read-write spinlock and use that to
protect ipi_dev_node_list ? Basically, sbi_ipi_add_device()
will take write-lock whereas sbi_ipi_raw_clear() and
sbi_ipi_raw_send() will take read-lock.
>
> if (!ipi_dev || ipi_dev->rating < dev->rating)
> ipi_dev = dev;
> --
> 2.55.0
>
>
> --
> opensbi mailing list
> opensbi at lists.infradead.org
> http://lists.infradead.org/mailman/listinfo/opensbi
Regards,
Anup
More information about the opensbi
mailing list