Skip to content

snrt_barrier potential livelock #26

Description

@jpf-h

@Aquaticfuller @DiyouS

Found this while programming a kernel, but I'm not sure if it is a real issue. At least I did not see it causing problems yet, but Claude Code flagged it as a potential issue. Can you check this?

The reset barr->barrier = 0 is a plain volatile store and the release __atomic_add_fetch(&barrier_iteration, 1, __ATOMIC_RELAXED) is a relaxed RMW. C11 gives no happens-before edge between them as observed from another thread — a waiter that observes the iteration bump has no guarantee of seeing the counter reset. volatile orders nothing across threads; relaxed orders nothing, period.

By the ISA: under RVWMO the two writes are to different addresses with no fence between them and no .aq/.rl — unordered, so a conforming RISC-V implementation may make the AMO visible first. And on this platform specifically, even upgrading the RMW to __ATOMIC_RELEASE would not fix it, because Snitch ignores the .rl bit on AMOs (the platform's own known erratum) — only an explicit fence works.

The failure when it fires: a waiter released by the early-visible bump sprints to the next barrier round and amoadds the stale counter; the late-arriving reset clobbers that increment; the new round's count can then never reach n, and every participant spins forever.

Plausibility on this hardware: the two writes demonstrably travel different physical paths — the plain store through the cache datapath, the AMO through the insitu AMO shim at the bank. The window is narrow (a few dozen cycles inside the last arriver's call) and needs tight barrier reuse to exploit — which is why it hasn't bitten anyone — but tight reuse is exactly what back-to-back barrier calls do, and our interval loop crosses the same object four times per interval with nothing in between.

All three counting barriers in barrier.c share the defect — snrt_barrier, snrt_cluster_sw_barrier, and snrt_global_barrier are the same reset-then-bump shape. snrt_cluster_hw_barrier is unaffected.

The fix is one fence per function: between the counter reset and the iteration increment, asm volatile("fence rw, rw" ::: "memory") (a release thread-fence also works and lowers to a fence; a .rl on the RMW does not, per the Snitch erratum).

A secondary observation, separable: these barriers provide no memory ordering for user data at all (everything is relaxed, no fences) — they are pure rendezvous. Most callers assume "barrier" implies "my writes before it are visible after it," and on this platform that assumption silently fails. Either document it loudly or give the barrier acquire/release semantics

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions