This is a data race, and UB. Furthermore, constructing &mut T during fetch_add while another thread performs an unsynchronized read violates reference exclusivity invariants. Even though the optimistic read discards its value if stamp validation fails, the data race and aliasing violation occur instantaneously upon execution.
Full Gemini Unsafe Code Audit Report
Unsafe Rust Review: crossbeam_utils (v0_8)
Overall Safety Assessment
The crossbeam_utils crate version v0_8 contains several crucial thread-safety utilities (AtomicCell, ShardedLock, Parker, etc.) which rely heavily on unsafe Rust and direct atomic manipulation. While the implementation of scoped threads and parking is generally sound and elegant (matching standard patterns), we have identified critical correctness and soundness issues in AtomicCell's fallback path (for types that are not lock-free). In addition, there is a total lack of safety comments (the style is either generic comments or completely missing), which does not satisfy the strict proof obligations of the safety review standard.
Critical Findings
1. AtomicCell Data Race Undefined Behavior in Fallback Path π΄ π€¦
- Severity: π΄ High
- Threat Vector: π€¦ Accidental Misuse
- Bug Type: Data Race
In src/atomic/atomic_cell.rs (lines 1048-1084), when executing atomic_load for types that are not lock-free (which fall back to global locks):
// Try doing an optimistic read first.
if let Some(stamp) = lock.optimistic_read() {
// ...
let val = ptr::read_volatile(src.cast::<MaybeUninit<T>>());
if lock.validate_read(stamp) {
return val.assume_init();
}
}
The optimistic read performs a volatile read (ptr::read_volatile) from src without acquiring the fallback lock. Concurrently, another thread can write to the same cell using atomic_store or atomic_swap, which calls ptr::write or ptr::replace under the fallback lock (i.e. without using atomics or volatile writes).
Under the Rust Memory Model, a concurrent unsynchronized read and write on the same memory location where at least one is not an atomic operation is a data race, which is Undefined Behavior. Even though the read value is discarded if the stamp validation fails, the data race itself has already occurred and can be exploited by compiler optimizations.
2. AtomicCell Exclusive Borrow (&mut T) Aliasing Violation in Fallback Path π΄ π€¦
- Severity: π΄ High
- Threat Vector: π€¦ Accidental Misuse
- Bug Type: Aliasing Violation
In src/atomic/atomic_cell.rs (specifically in methods like fetch_add, fetch_sub, etc. on lines 371, 394, 934), the fallback implementation creates a mutable reference &mut T to the underlying value under the lock:
let _guard = lock(self.as_ptr() as usize).write();
let value = unsafe { &mut *(self.as_ptr()) };
Because atomic_load performs an optimistic read using ptr::read_volatile on the raw pointer without holding the lock, a reader thread can access the memory concurrently while the writer thread holds &mut T. This violates the absolute exclusivity requirement of &mut T (no other references or accesses to the memory may exist for its duration), leading to Undefined Behavior.
Fishy Findings
1. ShardedLock Self-Referential Design Relying on Heap Allocation Stability π‘ π€¦
- Severity: π‘ Low
- Threat Vector: π€¦ Accidental Misuse
- Bug Type: Missing Invariant Documentation
In src/sync/sharded_lock.rs, the ShardedLock type contains a slice of shards: shards: Box<[CachePadded<Shard>]>. The Shard struct contains a write_guard field of type UnsafeCell<Option<RwLockWriteGuard<'static, ()>>>.
During write locking, the lock write guards (which borrow Shard::lock) are transmuted to 'static and stored in write_guard. This is a self-referential struct design.
This self-referential pattern is sound only because shards is stored inside a Box, meaning it is heap-allocated and its memory address remains stable even if ShardedLock is moved.
However, there is no documentation or safety comment explaining this crucial invariant. If a future maintainer changes Box<[CachePadded<Shard>]> to an inline array (e.g. [CachePadded<Shard>; NUM_SHARDS]) to avoid heap allocation, it would lead to silent memory corruption and use-after-free when ShardedLock is moved.
Missing Safety Comments
1. src/thread.rs π΄
unsafe impl Sync for Scope<'_> {} (line 224)
Proposed safety comment:
// SAFETY: `Scope` only exposes thread-safe operations. The list of join
// handles is wrapped in `Arc<Mutex<Vec<...>>>` and `wait_group` is a thread-safe
// `WaitGroup`. Therefore, it is safe to share `Scope` across threads and spawn
// threads concurrently.
unsafe impl<T> Send/Sync for ScopedJoinHandle<'_, T> {} (lines
489-490)
Proposed safety comment:
// SAFETY: `ScopedJoinHandle` behaves like a standard `JoinHandle` but operates on
// scoped data. The thread handle and result fields are thread-safe (`Arc<Mutex<...>>`
// and `thread::Thread` are Send/Sync). Although `T` may not be Sync, the result
// is protected by a Mutex, ensuring safe concurrent access. Since `spawn` requires
// `T: Send`, it is safe to send the join handle itself across threads.
- Erasing
'env bound on closure (lines 463-466)
Proposed safety comment:
// SAFETY: We transmute the closure's lifetime to `'static` to allow it to be passed
// to `std::thread::spawn`. This is safe because:
// 1. The parent scope blocks on `wg.wait()` until all spawned threads (which hold
// clones of the scope's `wait_group`) finish execution.
// 2. Any remaining threads are joined inside the `scope` function before it returns.
// This guarantees that no thread spawned in this scope can outlive the `'env` lifetime,
// preventing any use-after-free of references borrowed by the closure.
2. src/sync/parker.rs π΄
unsafe impl Send for Parker and unsafe impl Send/Sync for Unparker
(lines 58, 221-222)
Proposed safety comment:
// SAFETY: `Parker` and `Unparker` synchronize thread parking using `Arc<Inner>`,
// which contains `AtomicUsize`, `Mutex`, and `Condvar`. All of these types are
// Send and Sync, making the raw thread parking primitives thread-safe.
Unparker::from_raw / Parker::from_raw (lines 202, 289)
Proposed safety comment:
// SAFETY: The caller guarantees that `ptr` was obtained from `into_raw`.
// `into_raw` constructs `ptr` by calling `Arc::into_raw` on the `Unparker`'s inner `Arc`.
// Reconstructing the `Arc` using `Arc::from_raw` is safe because it recovers exactly
// one reference count from the original `Arc` instance, which was consumed by `into_raw`.
3. src/sync/sharded_lock.rs π΄
unsafe impl Send/Sync for ShardedLock and guards (lines 86-87,
492, 523)
Proposed safety comment:
// SAFETY: `ShardedLock` is thread-safe because access to the value is guarded
// by a set of reader-writer locks (`RwLock`). If `T` is `Send`, the lock can be
// transferred to another thread. If `T` is `Send + Sync`, concurrent reads are
// safe since multiple threads can access `&T` simultaneously.
get_mut dereference (line 193)
Proposed safety comment:
// SAFETY: The method takes `&mut self`, which guarantees exclusive access to the
// `ShardedLock`. No other thread can hold a read or write lock, making it safe to
// mutably dereference the underlying `UnsafeCell`.
transmute of RwLockWriteGuard in write/try_write (lines
350, 426)
Proposed safety comment:
// SAFETY: We transmute the guard's lifetime to `'static` because it must be stored
// inside the `Shard` itself (self-referential structure). This is safe because:
// 1. The shards are heap-allocated inside a `Box`, meaning they will not move and
// their memory addresses are stable.
// 2. The `ShardedLockWriteGuard` borrows `self` for its lifetime, preventing
// `ShardedLock` from being dropped or moved while the guards are active.
// 3. The `ShardedLockWriteGuard` destructor drops these guards in reverse order,
// releasing the locks before the borrow ends.
- Guard
Deref / DerefMut implementations (lines 498, 556, 562)
Proposed safety comment:
// SAFETY: The guard's existence proves that the thread has successfully acquired
// the appropriate (read or write) lock on the shards. For read guards, no writers
// can run concurrently. For write guards, exclusive access is guaranteed. Thus,
// dereferencing the `UnsafeCell` is safe.
4. src/cache_padded.rs π΄
unsafe impl Send/Sync for CachePadded (lines 152-153)
Proposed safety comment:
// SAFETY: `CachePadded<T>` is a transparent wrapper that only adds alignment and
// padding to `T`. It has no other fields and does not affect the thread-safety
// characteristics of `T`. Therefore, it is `Send`/`Sync` if and only if `T` is.
5. src/sync/once_lock.rs π΄
unsafe impl Send/Sync for OnceLock (lines 16-17)
Proposed safety comment:
// SAFETY: `OnceLock` synchronizes access to the `UnsafeCell` using `std::sync::Once`.
// Once initialized, the value is immutable. `Sync` requires `T: Sync + Send` because
// the value can be initialized on one thread and dropped on another. `Send` requires
// `T: Send` for the same reason.
slot.write in initialize (line 68)
Proposed safety comment:
// SAFETY: `call_once` guarantees that this closure is executed exactly once
// across all threads. No other thread can access the slot concurrently. Since
// the cell starts as uninitialized, we can safely overwrite it without dropping
// an old value.
get_unchecked dereference (line 77)
Proposed safety comment:
// SAFETY: The caller guarantees that the value is initialized. `MaybeUninit<T>`
// has the same layout and alignment as `T`. Dereferencing the cast pointer is
// safe since the value is valid.
assume_init_drop in Drop (line 85)
Proposed safety comment:
// SAFETY: `is_completed` is true, which guarantees the inner value is initialized.
// Since we are in `drop(&mut self)`, we have exclusive access to `self`, making it
// safe to drop the initialized value in place.
Note
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
In
crossbeam_utilsv0_8,AtomicCellprovides atomic operations on typesT. For types that are not lock-free,AtomicCellfalls back to acquiring a global sharded lock.However, in
atomic_loadat crossbeam-utils/src/atomic/atomic_cell.rs#L1048-L1084, the method attempts an optimistic read first:This performs an unsynchronized volatile read from
srcwithout holding the fallback lock. Concurrently, another thread executingatomic_store,atomic_swap, orfetch_*acquires the lock and performs unsynchronized writes (ptr::write) or constructs an exclusive reference (&mut T).This is a data race, and UB. Furthermore, constructing
&mut Tduringfetch_addwhile another thread performs an unsynchronized read violates reference exclusivity invariants. Even though the optimistic read discards its value if stamp validation fails, the data race and aliasing violation occur instantaneously upon execution.Minimal Reproduction (Miri)
Suggested Fix
Optimistic reads using
ptr::read_volatilecannot be safely used as a fallback mechanism alongside non-atomic writes or&mut Treferences under global locks. For non-lock-free fallback types,atomic_loadshould directly acquire a read lock (lock.read()) before copying the value.Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue.
Full Gemini Unsafe Code Audit Report
Unsafe Rust Review:
crossbeam_utils(v0_8)Overall Safety Assessment
The
crossbeam_utilscrate versionv0_8contains several crucial thread-safety utilities (AtomicCell,ShardedLock,Parker, etc.) which rely heavily on unsafe Rust and direct atomic manipulation. While the implementation of scoped threads and parking is generally sound and elegant (matching standard patterns), we have identified critical correctness and soundness issues inAtomicCell's fallback path (for types that are not lock-free). In addition, there is a total lack of safety comments (the style is either generic comments or completely missing), which does not satisfy the strict proof obligations of the safety review standard.Critical Findings
1.
AtomicCellData Race Undefined Behavior in Fallback Path π΄ π€¦In
src/atomic/atomic_cell.rs(lines 1048-1084), when executingatomic_loadfor types that are not lock-free (which fall back to global locks):The optimistic read performs a volatile read (
ptr::read_volatile) fromsrcwithout acquiring the fallback lock. Concurrently, another thread can write to the same cell usingatomic_storeoratomic_swap, which callsptr::writeorptr::replaceunder the fallback lock (i.e. without using atomics or volatile writes).Under the Rust Memory Model, a concurrent unsynchronized read and write on the same memory location where at least one is not an atomic operation is a data race, which is Undefined Behavior. Even though the read value is discarded if the stamp validation fails, the data race itself has already occurred and can be exploited by compiler optimizations.
2.
AtomicCellExclusive Borrow (&mut T) Aliasing Violation in Fallback Path π΄ π€¦In
src/atomic/atomic_cell.rs(specifically in methods likefetch_add,fetch_sub, etc. on lines 371, 394, 934), the fallback implementation creates a mutable reference&mut Tto the underlying value under the lock:Because
atomic_loadperforms an optimistic read usingptr::read_volatileon the raw pointer without holding the lock, a reader thread can access the memory concurrently while the writer thread holds&mut T. This violates the absolute exclusivity requirement of&mut T(no other references or accesses to the memory may exist for its duration), leading to Undefined Behavior.Fishy Findings
1.
ShardedLockSelf-Referential Design Relying on Heap Allocation Stability π‘ π€¦In
src/sync/sharded_lock.rs, theShardedLocktype contains a slice of shards:shards: Box<[CachePadded<Shard>]>. TheShardstruct contains awrite_guardfield of typeUnsafeCell<Option<RwLockWriteGuard<'static, ()>>>.During write locking, the lock write guards (which borrow
Shard::lock) are transmuted to'staticand stored inwrite_guard. This is a self-referential struct design.This self-referential pattern is sound only because
shardsis stored inside aBox, meaning it is heap-allocated and its memory address remains stable even ifShardedLockis moved.However, there is no documentation or safety comment explaining this crucial invariant. If a future maintainer changes
Box<[CachePadded<Shard>]>to an inline array (e.g.[CachePadded<Shard>; NUM_SHARDS]) to avoid heap allocation, it would lead to silent memory corruption and use-after-free whenShardedLockis moved.Missing Safety Comments
1.
src/thread.rsπ΄unsafe impl Sync for Scope<'_> {}(line 224)Proposed safety comment:
unsafe impl<T> Send/Sync for ScopedJoinHandle<'_, T> {}(lines489-490)
Proposed safety comment:
'envbound on closure (lines 463-466)Proposed safety comment:
2.
src/sync/parker.rsπ΄unsafe impl Send for Parkerandunsafe impl Send/Sync for Unparker(lines 58, 221-222)
Proposed safety comment:
Unparker::from_raw/Parker::from_raw(lines 202, 289)Proposed safety comment:
3.
src/sync/sharded_lock.rsπ΄unsafe impl Send/Sync for ShardedLockand guards (lines 86-87,492, 523)
Proposed safety comment:
get_mutdereference (line 193)Proposed safety comment:
transmuteofRwLockWriteGuardinwrite/try_write(lines350, 426)
Proposed safety comment:
Deref/DerefMutimplementations (lines 498, 556, 562)Proposed safety comment:
4.
src/cache_padded.rsπ΄unsafe impl Send/Sync for CachePadded(lines 152-153)Proposed safety comment:
5.
src/sync/once_lock.rsπ΄unsafe impl Send/Sync for OnceLock(lines 16-17)Proposed safety comment:
slot.writeininitialize(line 68)Proposed safety comment:
get_uncheckeddereference (line 77)Proposed safety comment:
assume_init_dropinDrop(line 85)Proposed safety comment: