compare_exchange_weak with a release ordering never succeeds on 32-bit PowerPC (e500v2): release fence is emitted inside the lwarx/stwcx. reservation window
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
On powerpc-unknown-linux-musl running on an e500v2 core, compare_exchange_weak
with any release-side ordering never succeeds, on an uncontended cell, in a
single-threaded program. Because AtomicUsize::fetch_update is a
compare_exchange_weak retry loop, every fetch_update on this target spins
forever.
This makes async Rust unusable on the platform: tokio's reactor calls
fetch_update on the first event it receives and never returns.
Code
use std::sync::atomic::{AtomicUsize, Ordering::{AcqRel, Acquire}};
fn main() {
let cell = AtomicUsize::new(0);
for i in 1..=1_000_000u32 {
if cell.compare_exchange_weak(0, 1, AcqRel, Acquire).is_ok() {
println!("succeeded on try {i}");
return;
}
}
println!("1000000 consecutive failures, cell is still {}", cell.load(Acquire));
}
Current output
1000000 consecutive failures, cell is still 0
Each failure returns Err(0) — the value it compared against — so the
comparison matches and the store-conditional is what fails. The strong
compare_exchange on the same cell succeeds, as do fetch_or, fetch_add,
load and store.
The 1,000,000 iterations take 2.29 s, or 2.3 µs per turn. That is several
orders of magnitude slower than a userspace reservation loop and about right
for a kernel trap, which suggests the reservation is being lost to
trap-and-emulate of an instruction the core does not implement. I have not
confirmed that part; the codegen below stands on its own.
Expected output
succeeded on try 1
Cause
rustc -O --target powerpc-unknown-linux-musl for
compare_exchange_weak(0, 1, AcqRel, Acquire):
lwarx 4, 0, 3 ; reserve
cmplwi 4, 0
bne .LBB1_2
li 4, 1
lwsync ; <-- release barrier INSIDE the reservation window
stwcx. 4, 0, 3 ; always fails
beq .LBB1_5
.LBB1_2: ; -> Err, and the weak form has no retry
The release barrier is emitted between the lwarx and the stwcx.. On this
core that clears the reservation, so the store-conditional can never succeed,
and the weak form has no retry to recover with.
The strong form is generated with a retry that re-reserves after the barrier,
which is why it works:
lwarx 4, 0, 3
cmplwi 4, 0
bne .LBB0_4
li 4, 1
lwsync ; same barrier, same place
.LBB0_2:
stwcx. 4, 0, 3 ; also fails the first time
beq .LBB0_7
lwarx 5, 0, 3 ; re-reserves, no barrier after it
cmplwi 5, 0
beq .LBB0_2 ; second attempt succeeds
fetch_or and fetch_add are unaffected because atomicrmw lowers to a tight
reservation loop with the barriers outside it.
GCC 13.3.0 for the same target and the same operation puts the barrier where it
belongs:
sync ; release barrier BEFORE the reservation
lwarx 9, 0, 3
stwcx. 10, 0, 3 ; clean reservation window
isync ; acquire barrier after
Note also that GCC emits sync rather than lwsync: it knows the core. LLVM's
PowerPC backend has no e500 CPU model at all — -C target-cpu=e500, =8548 and
=e500mc are all rejected — so there is no way to tell it either.
Where the decision is made
This is not accidental. llvm/lib/CodeGen/AtomicExpandPass.cpp (21.x, line 1369)
sinks the release barrier into the reservation window deliberately, and it
singles out the weak form to do it:
// There's no overhead for sinking the release barrier in a weak cmpxchg, so
// do it even on minsize.
bool UseUnconditionalReleaseBarrier = F->hasMinSize() && !CI->isWeak();
!CI->isWeak() makes this false for every weak cmpxchg, so the fence is
always emitted in the conditional cmpxchg.fencedstore block, between the
load-linked and the store-conditional:
if (ShouldInsertFencesForAtomic && UseUnconditionalReleaseBarrier)
TLI->emitLeadingFence(Builder, CI, SuccessOrder); // before the loop
...
Builder.SetInsertPoint(ReleasingStoreBB);
if (ShouldInsertFencesForAtomic && !UseUnconditionalReleaseBarrier)
TLI->emitLeadingFence(Builder, CI, SuccessOrder); // inside the window
The reasoning in the surrounding comments is entirely about code size: sinking
the barrier avoids duplicating the load-linked block, and for the weak form
there is no duplicate block to pay for. What it does not account for is that on
an LL/SC target a barrier inside the reservation window may clear the
reservation. The strong form hides this behind its retry, which re-reserves
after the barrier. The weak form has no retry, so the operation cannot succeed
at all — the "no overhead" case is the one that cannot recover.
Making the placement unconditional for weak cmpxchg on targets that use fences
for atomics would fix it and would match what GCC already emits. I have read
this in the 21.x sources and it matches the generated code exactly, but I have
not built a patched LLVM to confirm the fix.
Which cases are affected
Success ordering decides it; the failure ordering and the width do not matter.
| success ordering | reservation window | result |
|---|---|---|
Relaxed |
clean | works |
Acquire |
clean | works |
Release |
barrier inside | never succeeds |
AcqRel |
barrier inside | never succeeds |
SeqCst |
barrier inside (sync) |
never succeeds |
AtomicU8, AtomicU16, AtomicU32 and AtomicUsize all behave the same.
fetch_update takes AcqRel/Acquire at nearly every call site in the
ecosystem, so in practice it is always the broken case.
Meta
rustc 1.93.1 (01f6ddf75 2026-02-11)
binary: rustc
commit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf
commit-date: 2026-02-11
host: aarch64-apple-darwin
release: 1.93.1
LLVM version: 21.1.8
Target: powerpc-unknown-linux-musl, e500v2 (Freescale P2020), 32-bit
big-endian, soft-float — a Turris 1.x router running OpenWrt. Reproduced with
LTO off at -C opt-level=1 as well as at -O.
I could not check a newer rustc directly: rustup target add powerpc-unknown-linux-musl reports no prebuilt artifacts, so the toolchain
above is OpenWrt's. The barrier placement is in the LLVM PowerPC backend, so I
would expect it to be unchanged unless someone has touched it since 21.1.8.
Found while porting a network utility to Rust specifically because this
architecture has no Go support: tokio-rs/tokio#8352.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in llvm/lib/CodeGen/AtomicExpandPass.cpp around line 1369 and trace the weak and strong cmpxchg fence-placement paths. Compare the generated PowerPC code with the e500v2 reproduction, then validate that the release barrier is outside the reservation window and that weak compare-exchange succeeds; add or update regression coverage if the relevant test location is identified.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100