Subject
async-lock 3.4.2 is the synchronization-primitive crate of the smol-rs
ecosystem. It exports Mutex, RwLock, Semaphore, Barrier, and OnceCell
in async-aware form, plus matching _blocking methods so the same type can be
used from both async and sync code paths. The waiting and wakeup mechanism is
delegated to event-listener and event-listener-strategy; the locking
logic, fairness policy, state machines, and unsafe arithmetic are
implemented here. The crate is no_std-capable behind a default std feature
and supports wasm32. MSRV is 1.85, edition 2021.
Methodology
Tools: openvet 0.6.0, ripgrep, diff, wc, manual reading. No tests were
executed.
The audit covered every .rs file under contents/src/ end-to-end (lib.rs,
mutex.rs, once_cell.rs, rwlock.rs, rwlock/raw.rs, rwlock/futures.rs,
semaphore.rs, barrier.rs; about 4900 LOC). All five integration test
files and tests/common/mod.rs were read in full to assess coverage, as was
.github/workflows/ci.yml to see what the upstream pipeline actually runs.
Metadata (Cargo.toml, Cargo.toml.orig, README.md, CHANGELOG.md) was
read in full.
diff -rq contents vcs found only the expected cargo normalisation
differences (Cargo.toml, Cargo.lock, .cargo_vcs_info.json, the
Cargo.toml.orig file, and the publish-excluded .git, .github,
.gitignore). Two source-text differences exist: a single bullet listing
OnceCell is present in the VCS README.md and src/lib.rs doc preamble
but removed in the published crate. The OnceCell type is still in the
public API. These are documentation drift, not a supply-chain signal.
Results
The crate has no FFI, no network, filesystem, process, or environment
surface, and no cryptography (uses-network=false, uses-filesystem=false,
uses-exec=false, uses-environment=false, uses-crypto=false,
uses-jit=false, uses-interpreter=false). It has no build script or
proc macro and ships no binaries (has-build-exec=false,
has-install-exec=false, has-binaries=false). It exists to implement
concurrency primitives (impl-concurrency=true, uses-concurrency=true)
and one data structure (impl-datastructure=true for OnceCell); it does
not implement a parser, protocol, interpreter, JIT, algorithm, or crypto
primitive (impl-parser=false, impl-protocol=false,
impl-interpreter=false, impl-jit=false, impl-algorithm=false,
impl-crypto=false). No malicious code was observed; is-benign=true.
The unsafe surface (uses-unsafe=true) lives in src/mutex.rs,
src/once_cell.rs, src/rwlock.rs, src/rwlock/raw.rs, and
src/rwlock/futures.rs. Each block carries a SAFETY: comment or, for
unsafe fn, a # Safety doc clause. The blocks fall into a small set of
patterns: unsafe impl Send/Sync on the public types with bounds that match
the std mutex pattern (justified type-by-type in the annotations);
UnsafeCell dereferences inside guard Deref/DerefMut where the guard's
existence is the proof of access rights; MaybeUninit reads in OnceCell
gated on a state-machine transition to Initialized via Release/Acquire;
arithmetic on a packed (reader_count, WRITER_BIT) word in RawRwLock
with explicit overflow guards (crate::abort() on reader counts above
isize::MAX, line 73 of raw.rs); and the 'static lifetime fiction in
UpgradeArc where the reference points into the heap allocation of an
Arc co-stored in the same struct so movement of the outer struct does not
invalidate it. The Send/Sync bounds were checked against what each guard
exposes; MutexGuardArc::source is correctly restricted to T: Send to
prevent the inner Arc from being cloned out and dropped on a thread that
does not own T. All PinnedDrop impls correctly distinguish the
cancelled-mid-acquire case (where they must release lock state they set)
from the completed case (where the caller now owns a guard that will release
on drop). I reviewed every unsafe block and consider them sound and
minimal (unsafe-safe=true, unsafe-documented=true,
unsafe-minimal=true).
Concurrency correctness was assessed by reading the state machines for
each primitive and tracing the relevant memory orderings. Mutex packs a
lock bit and a 30/62-bit starvation counter into one AtomicUsize; the
"eventually fair" fallback path under starvation uses fetch_or against the
packed state to claim the lock without resetting the starvation counter
until take_mutex runs (and take_mutex decrements only when starved is
set, balancing the fetch_add(2) at line 588). RwLock is built on a
RawRwLock that uses the writer mutex from this crate to serialize writers
and an (WRITER_BIT, reader_count) packed state for reader/writer
arbitration; write-preference is enforced by readers refusing to acquire
when WRITER_BIT is set (line 67 of raw.rs and the equivalent check in
RawRead::poll). The orderings on the lock state are Acquire on loads
that gate use of the protected data, Release on stores that publish
release of the lock, and SeqCst for the writer-bit toggles in
upgrade/downgrade_*/write_unlock/read_unlock paths and for the
checks in RawWrite::poll that confirm there are no remaining readers.
Semaphore is a straightforward CAS loop on a counter. Barrier uses a
generation counter under the local Mutex to detect spurious wakeups
across barrier generations. The thread-safety contract for every public
type is documented (concurrency-safe=true, concurrency-documented=true,
concurrency-impl-safe=true, concurrency-impl-documented=true).
OnceCell is the data structure (impl-datastructure=true,
datastructure-impl-safe=true, datastructure-impl-bounds=true; get and set
are O(1) with one waker queue traversal under contention). Its state-machine Guard correctly resets
the cell to Uninitialized and notifies the next waiter if the initializer
panics or returns Err, so a failed initialization does not poison the
cell. Storage of T is UnsafeCell<MaybeUninit<T>> and every read is
gated on observing Initialized via Acquire. The state-machine invariants
hold (datastructure-impl-correct=true); however, the
no-reentrant-initializer precondition that the state machine relies on is
not documented, see finding 1.
Three findings, all low severity. Finding 1 (quality): OnceCell::get_or_init
and friends deadlock if called reentrantly from the initializer closure,
and that hazard is not mentioned in the public docs. Finding 2 (correctness):
the Mutex "eventual fairness" guarantee is gated behind
cfg(all(feature = "std", not(target_family = "wasm"))) and silently does
not hold on no_std or wasm targets, despite the doc comment on Mutex
not noting this limitation. This justifies concurrency-impl-correct=false.
Finding 3 (quality): the only loom test in the repository is
tests/loom.rs::barrier_smoke, which carries #[ignore]. The CI invocation
does not pass --ignored, so loom coverage is effectively zero, and
Mutex, RwLock, Semaphore, and OnceCell have no loom tests at all;
this justifies concurrency-impl-tested=false and
datastructure-impl-tested=false. Miri runs in CI against the integration
tests, which gave me enough coverage of the unsafe blocks under realistic
single-thread paths to assert unsafe-tested=true.
has-integration-tests=true (43 #[test] functions in tests/ plus
doctests); has-unit-tests=false (no #[cfg(test)] mod tests in src/);
has-fuzz-tests=false and has-property-tests=false (neither contents/fuzz/
nor any proptest!/quickcheck! invocation exists in the source tree).
Conclusion
async-lock 3.4.2 is sound. The unsafe is concentrated, well-commented,
and correct on the code paths I traced. The Send/Sync bounds match what each
guard actually exposes, the atomic orderings are appropriate for the
synchronization role they play, and the future state machines correctly
balance the locking state they set against the cancellation paths that must
release it. The findings are documentation and test-coverage gaps rather
than soundness bugs: a reentrant-initializer hazard on OnceCell that
deserves a doc note, a fairness guarantee that quietly does not hold on
no_std or wasm targets, and an effectively dormant loom test suite.