cargo : atomic-waker @ 1.1.2
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

concurrency-impl-correctconcurrency-impl-documentedconcurrency-impl-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

atomic-waker 1.1.2 is a small no_std cargo crate providing the AtomicWaker synchronization primitive (extracted from futures::task). The crate is benign with sound, minimal, and Miri-tested unsafe code gated by a well-documented two-bit atomic lock. Two low-severity quality findings concern doctests that exercise the upstream futures crate rather than this one, and a thin test suite for a concurrency primitive.

Report

Subject

atomic-waker is a no_std-compatible Rust crate exposing a single public type, AtomicWaker: a multi-consumer / single-producer cell that stores a core::task::Waker and lets one thread register a waker while other threads race to consume and wake it. The implementation is the extraction of futures::task::AtomicWaker into a standalone crate, used by smol-rs and the wider async ecosystem as a building block for cross-task notification.

Methodology

The published crate was downloaded from crates.io and unpacked into contents/; the upstream Git repository at the commit recorded in .cargo_vcs_info.json was checked out into vcs/ for cross-comparison. The published tree was compared against the upstream tree with diff -r, and the single source file (src/lib.rs, ~440 lines), the integration test (tests/basic.rs), the criterion benchmark (benches/waker.rs), and the CI configuration in .github/workflows/ were read in full. The lock protocol used to gate UnsafeCell access was walked through against the inline commentary, and the memory orderings on every compare_exchange, swap, fetch_or, and fetch_and were checked against the documented acquire/release contract.

Results

The published crate matches the upstream Git tree byte-for-byte across all source, test, and bench files; the only differences are cargo's standard normalisation of Cargo.toml and the auto-generated .cargo_vcs_info.json. The original manifest is preserved as Cargo.toml.orig.

The crate ships no binary artefacts (justifying has-binaries), no build.rs, and is not a proc-macro crate (justifying has-build-exec and has-install-exec). It is #![no_std] and pulls in no IO, networking, process, or cryptographic surface (justifying uses-network, uses-filesystem, uses-environment, uses-exec, uses-jit, uses-interpreter, uses-crypto). The crate implements no parser, interpreter, JIT, protocol, data structure, algorithm, or cryptographic primitive (justifying impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, and impl-crypto). It also does not itself spawn threads or drive an executor (justifying uses-concurrency); it is the implemented primitive that callers use to coordinate cross-thread wakeups.

The single optional runtime dependency, portable-atomic (a polyfill for targets without native atomics), is gated behind a feature and not default-on. dev-dependencies (criterion, futures, rayon) are not shipped to consumers.

The crate's central concern is the soundness of two unsafe blocks (justifying uses-unsafe) accessing an UnsafeCell<Option<Waker>> (src/lib.rs:292-350 and 403-412) plus the unsafe impl Send / unsafe impl Sync at src/lib.rs:442-443. Soundness rests on a two-bit lock encoded in an AtomicUsize: REGISTERING (0b01) is taken by the writer in register, WAKING (0b10) is taken by readers in take / wake. The transitions and their acquire/release orderings are documented in the long block comment at src/lib.rs:118-211 and were validated against the implementation. The race window where wake arrives while register holds the lock is closed by the REGISTERING|WAKING fallback branch in register (src/lib.rs:323-348), which swaps the state back to WAITING under AcqRel and drives the wake itself, so no notification is lost. CI runs cargo miri test with -Zmiri-strict-provenance and randomised layout on every push, which exercises the existing test under Miri's data-race detector. Together this supports unsafe-safe, unsafe-documented, unsafe-minimal, unsafe-tested, concurrency-impl-safe, concurrency-impl-correct, and concurrency-impl-documented.

The package ships one integration test (tests/basic.rs) but no unit tests inside src/lib.rs, no fuzz/ directory, and no property-based tests — justifying has-unit-tests, has-fuzz-tests, and has-property-tests. Two low-severity quality findings were recorded. FINDING-1 notes that the two rustdoc examples on AtomicWaker import AtomicWaker from futures::task rather than from this crate, so cargo test --doc does not exercise the audited code — the examples remain semantically valid since this crate is the extraction of that very type. FINDING-2 notes that tests/basic.rs is a single-scenario smoke test with no concurrent-register coverage, no property tests, no fuzz tests, and no loom model-checking; this is why the concurrency-impl-tested claim was not asserted. Miri coverage of the one existing test partially mitigates the gap but does not exercise the REGISTERING|WAKING fallback path. Nothing in the audit indicated malicious intent, supporting is-benign.

Conclusion

atomic-waker is a small, focused, no_std synchronization primitive with a well-documented lock protocol, minimal and contained unsafe usage, and Miri-checked CI. The implementation is sound under review. The only findings are low-severity quality observations about documentation examples and the breadth of the test suite for a concurrency primitive; neither affects real-world safety of the crate.

Findings(2)

FINDING-1 quality low

Doctests exercise futures::task::AtomicWaker, not this crate

The two rustdoc examples on AtomicWaker (src/lib.rs:62-112, 257-284) import AtomicWaker from futures::task rather than from this crate. As a result, cargo test --doc validates the upstream futures crate's implementation, not the local one. Since this crate is the extracted source of futures::task::AtomicWaker the examples remain semantically correct, but they do not exercise the code being audited.

FINDING-2 quality low

Test suite is minimal for a concurrency primitive

tests/basic.rs contains a single integration test that exercises one register/wake interaction across two threads. There are no concurrent-register tests, no stress tests, no property-based tests, and no loom model-checking. The race window between register and wake (handled by the REGISTERING|WAKING fallback path in src/lib.rs:323-348) is therefore not directly exercised by the test suite. Mitigated by CI running cargo miri test with strict-provenance, which catches data races and undefined behaviour on the existing test, but coverage of the fallback branch remains weak. The concurrency-impl-tested claim was not asserted because of this gap.

Annotations(4)

Cargo.toml

Cargo.toml, line 42-50

[dependencies.portable-atomic]
version = "1"
optional = true
default-features = false

[dev-dependencies.criterion]
version = "0.4.0"
features = ["cargo_bench_support"]
default-features = false

Single optional runtime dependency: portable-atomic (no_std atomic polyfill), gated behind the portable-atomic feature with default-features=false. dev-dependencies (criterion, futures, rayon) are not shipped to consumers.

benches/waker.rs

Criterion benchmarks (store and wake, store and take, wake without store, take without store) under low and high contention. Not a correctness test, but the high-contention rayon-driven invocation across 100_000 iterations would surface flagrant data races in practice.

src/lib.rs

src/lib.rs, line 13-13

#![no_std]

The crate is #![no_std], so it inherently cannot touch the filesystem, network, environment variables, or spawn processes via std. Supports uses-network, uses-filesystem, uses-environment, uses-exec, and uses-interpreter (none are imported).

src/lib.rs, line 113-116

pub struct AtomicWaker {
    state: AtomicUsize,
    waker: UnsafeCell<Option<Waker>>,
}

Public type AtomicWaker stores a core::task::Waker behind an UnsafeCell guarded by an AtomicUsize lock state. This is the implemented synchronization primitive, justifying impl-concurrency.

src/lib.rs, line 118-211

// `AtomicWaker` is a multi-consumer, single-producer transfer cell. The cell
// stores a `Waker` value produced by calls to `register` and many threads can
// race to take the waker (to wake it) by calling `wake`.
//
// If a new `Waker` instance is produced by calling `register` before an
// existing one is consumed, then the existing one is overwritten.
//
// While `AtomicWaker` is single-producer, the implementation ensures memory
// safety. In the event of concurrent calls to `register`, there will be a
// single winner whose waker will get stored in the cell. The losers will not
// have their tasks woken. As such, callers should ensure to add synchronization
// to calls to `register`.
//
// The implementation uses a single `AtomicUsize` value to coordinate access to
// the `Waker` cell. There are two bits that are operated on independently.
// These are represented by `REGISTERING` and `WAKING`.
//
// The `REGISTERING` bit is set when a producer enters the critical section. The
// `WAKING` bit is set when a consumer enters the critical section. Neither bit
// being set is represented by `WAITING`.
//
// A thread obtains an exclusive lock on the waker cell by transitioning the
// state from `WAITING` to `REGISTERING` or `WAKING`, depending on the operation
// the thread wishes to perform. When this transition is made, it is guaranteed
// that no other thread will access the waker cell.
//
// # Registering
//
// On a call to `register`, an attempt to transition the state from WAITING to
// REGISTERING is made. On success, the caller obtains a lock on the waker cell.
//
// If the lock is obtained, then the thread sets the waker cell to the waker
// provided as an argument. Then it attempts to transition the state back from
// `REGISTERING` -> `WAITING`.
//
// If this transition is successful, then the registering process is complete
// and the next call to `wake` will observe the waker.
//
// If the transition fails, then there was a concurrent call to `wake` that was
// unable to access the waker cell (due to the registering thread holding the
// lock). To handle this, the registering thread removes the waker it just set
// from the cell and calls `wake` on it. This call to wake represents the
// attempt to wake by the other thread (that set the `WAKING` bit). The state is
// then transitioned from `REGISTERING | WAKING` back to `WAITING`.  This
// transition must succeed because, at this point, the state cannot be
// transitioned by another thread.
//
// # Waking
//
// On a call to `wake`, an attempt to transition the state from `WAITING` to
// `WAKING` is made. On success, the caller obtains a lock on the waker cell.
//
// If the lock is obtained, then the thread takes ownership of the current value
// in the waker cell, and calls `wake` on it. The state is then transitioned
// back to `WAITING`. This transition must succeed as, at this point, the state
// cannot be transitioned by another thread.
//
// If the thread is unable to obtain the lock, the `WAKING` bit is still.  This
// is because it has either been set by the current thread but the previous
// value included the `REGISTERING` bit **or** a concurrent thread is in the
// `WAKING` critical section. Either way, no action must be taken.
//
// If the current thread is the only concurrent call to `wake` and another
// thread is in the `register` critical section, when the other thread **exits**
// the `register` critical section, it will observe the `WAKING` bit and handle
// the wake itself.
//
// If another thread is in the `wake` critical section, then it will handle
// waking the task.
//
// # A potential race (is safely handled).
//
// Imagine the following situation:
//
// * Thread A obtains the `wake` lock and wakes a task.
//
// * Before thread A releases the `wake` lock, the woken task is scheduled.
//
// * Thread B attempts to wake the task. In theory this should result in the
//   task being woken, but it cannot because thread A still holds the wake lock.
//
// This case is handled by requiring users of `AtomicWaker` to call `register`
// **before** attempting to observe the application state change that resulted
// in the task being awoken. The wakers also change the application state before
// calling wake.
//
// Because of this, the waker will do one of two things.
//
// 1) Observe the application state change that Thread B is woken for. In this
//    case, it is OK for Thread B's wake to be lost.
//
// 2) Call register before attempting to observe the application state. Since
//    Thread A still holds the `wake` lock, the call to `register` will result
//    in the task waking itself and get scheduled again.

The module-level commentary (above the impl block) documents the lock protocol: REGISTERING and WAKING bits in the state field gate mutually exclusive access to the waker cell, including the race window where wake and register interleave. The reasoning establishes why every UnsafeCell access happens under the lock invariant, justifying concurrency-impl-safe, concurrency-impl-correct, concurrency-impl-documented and unsafe-documented.

src/lib.rs, line 292-350

                unsafe {
                    // Locked acquired, update the waker cell

                    // Avoid cloning the waker if the old waker will awaken the same task.
                    match &*self.waker.get() {
                        Some(old_waker) if old_waker.will_wake(waker) => (),
                        _ => *self.waker.get() = Some(waker.clone()),
                    }

                    // Release the lock. If the state transitioned to include
                    // the `WAKING` bit, this means that at least one wake has
                    // been called concurrently.
                    //
                    // Start by assuming that the state is `REGISTERING` as this
                    // is what we just set it to. If this holds, we know that no
                    // other writes were performed in the meantime, so there is
                    // nothing to acquire, only release. In case of concurrent
                    // wakers, we need to acquire their releases, so success needs
                    // to do both.
                    let res = self
                        .state
                        .compare_exchange(REGISTERING, WAITING, AcqRel, Acquire);

                    match res {
                        Ok(_) => {
                            // memory ordering: acquired self.state during CAS
                            // - if previous wakes went through it syncs with
                            //   their final release (`fetch_and`)
                            // - if there was no previous wake the next wake
                            //   will wake us, no sync needed.
                        }
                        Err(actual) => {
                            // This branch can only be reached if at least one
                            // concurrent thread called `wake`. In this
                            // case, `actual` **must** be `REGISTERING |
                            // `WAKING`.
                            debug_assert_eq!(actual, REGISTERING | WAKING);

                            // Take the waker to wake once the atomic operation has
                            // completed.
                            let waker = (*self.waker.get()).take().unwrap();

                            // We need to return to WAITING state (clear our lock and
                            // concurrent WAKING flag). This needs to acquire all
                            // WAKING fetch_or releases and it needs to release our
                            // update to self.waker, so we need a `swap` operation.
                            self.state.swap(WAITING, AcqRel);

                            // memory ordering: we acquired the state for all
                            // concurrent wakes, but future wakes might still
                            // need to wake us in case we can't make progress
                            // from the pending wakes.
                            //
                            // So we simply schedule to come back later (we could
                            // also simply leave the registration in place above).
                            waker.wake();
                        }
                    }
                }

The unsafe block in register is entered only after a successful compare_exchange(WAITING, REGISTERING, Acquire, Acquire), which establishes exclusive access to the waker cell. The fallback branch (concurrent wake observed) consumes the waker via swap-to-WAITING under AcqRel, draining any concurrent WAKING bits before invoking waker.wake(), preventing both a lost wakeup and a dangling cell access. Justifies unsafe-safe.

src/lib.rs, line 442-443

unsafe impl Send for AtomicWaker {}
unsafe impl Sync for AtomicWaker {}

unsafe impl Send and unsafe impl Sync are sound because the state-bit lock serialises all UnsafeCell access and Waker itself is Send + Sync (asserted via the AssertSync trait in new).

src/lib.rs, line 403-412

        match self.state.fetch_or(WAKING, AcqRel) {
            WAITING => {
                // The waking lock has been acquired.
                let waker = unsafe { (*self.waker.get()).take() };

                // Release the lock
                self.state.fetch_and(!WAKING, Release);

                waker
            }

take uses fetch_or(WAKING, AcqRel) to acquire the waking lock. The unsafe access to self.waker runs only when the previous state was WAITING (no other access in flight); concurrent REGISTERING returns None and lets the registering thread observe the WAKING bit and wake itself. The lock is released by clearing the WAKING bit via fetch_and under Release ordering after the take. Justifies unsafe-safe and unsafe-minimal.

tests/basic.rs

Single integration test covering one register-then-wake interaction across two threads. See FINDING-2 for coverage limitations. Justifies has-integration-tests.