feat(safety): phase 5 — loom model checking + invariant audit/assertion sweep

State machine extracted to src/slot_state.rs as a standalone unit (StateWord:
publish_queued / try_claim / yield_return / park_return / unpark / set_done /
reclaim, plus the Status view for cold paths). runtime.rs keeps the protocol
rationale and consumes the mechanism; every transition self-asserts its
precondition per the assert-the-invariants rule.

src/sync_shim.rs: std vs loom indirection (atomics + UnsafeCell with the
with/with_mut access API) for the two loom-modeled modules. Loom models run
the PRODUCTION code, not a replica:
- slot_state: no-lost-wakeup (park vs unpark), two-unparkers-one-enqueue,
  stale-unpark-never-hits-reused-slot (the ABA theorem), unpark-vs-claim.
- run_queue: mpmc exactly-once through a lap wraparound, push/pop race,
  striped two-producer drain.
RUSTFLAGS="--cfg loom" cargo test --lib --release — 7 models, all pass.
RawMutex deliberately not loom-modeled (futexes can't be; textbook mutex3
with stress + unwind coverage).

Assertion sweep (invariants now checked at the point of reliance):
- enqueue debug-asserts the word reads EXACTLY (gen, Queued) — the
  at-most-once-enqueued invariant the ring capacity proof leans on.
- RawMutex enforces the leaf rule mechanically: debug-build thread-local
  held-count, panics at the acquisition that violates it.
- live_actors underflow (double finalize) asserted.
- StateWord transition preconditions asserted (yield/park/done/reclaim/
  publish/claim).

Audit: with_runtime, RawMutex guards, run-queue ops, and trace::record all
gate preemption (and thereby the stop sentinel) for their span; trace was
already self-gating. Sole remaining exception is the channel std MutexGuard
— the documented first post-v0.5 fast-follow.

Review fixes to the branched-in work:
- slot_state::status_for mapped (matching gen, Vacant) to Stale instead of
  unreachable!: Pid::new is public, so a forged/never-issued pid (e.g.
  monitor(Pid::new(5,0)) on a fresh slab) could reach it — "no such actor"
  is the correct total answer; issued pids still can't get there.
- Tightened enqueue's assert from Status::Live to the exact (gen, Queued)
  word its own comment argues for.

Validated: 22 suites in debug (all asserts + leaf counter live) for
rq-mutex/rq-mpmc/rq-striped, release for rq-mutex, smarm-trace build +
stress, loom 7/7.
This commit is contained in:
Claude
2026-06-09 21:27:45 +00:00
parent 6d9f3698d4
commit 039703dbeb
9 changed files with 651 additions and 156 deletions
+31
View File
@@ -45,6 +45,35 @@ const CONTENDED: u32 = 2;
/// sender), so a short spin almost always avoids the syscall.
const SPIN_LIMIT: u32 = 64;
// The leaf rule, mechanically enforced (debug builds): slot cold locks, the
// free list, and the stack pool — i.e. every RawMutex — are mutual leaves.
// Holding two at once is a deadlock waiting for the right interleaving, so
// fail at the acquisition that violates it, not in the eventual hang.
#[cfg(debug_assertions)]
thread_local! {
static RAW_MUTEXES_HELD: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
#[inline]
fn leaf_check_acquire() {
#[cfg(debug_assertions)]
RAW_MUTEXES_HELD.with(|c| {
debug_assert_eq!(
c.get(),
0,
"leaf rule violated: acquiring a RawMutex while already holding one \
(cold locks / free list / stack pool are mutual leaves)"
);
c.set(c.get() + 1);
});
}
#[inline]
fn leaf_check_release() {
#[cfg(debug_assertions)]
RAW_MUTEXES_HELD.with(|c| c.set(c.get() - 1));
}
pub(crate) struct RawMutex<T> {
state: AtomicU32,
data: UnsafeCell<T>,
@@ -69,6 +98,7 @@ impl<T> RawMutex<T> {
// Enter NoPreempt *before* acquiring, so a preemption can't fire
// between acquisition and guard construction.
let prev_preempt = crate::preempt::PREEMPTION_ENABLED.with(|c| c.replace(false));
leaf_check_acquire();
if self
.state
.compare_exchange(UNLOCKED, LOCKED, Ordering::Acquire, Ordering::Relaxed)
@@ -142,6 +172,7 @@ impl<T> Drop for RawMutexGuard<'_, T> {
#[inline]
fn drop(&mut self) {
self.m.unlock();
leaf_check_release();
// Restore preemption only after the lock is released.
crate::preempt::PREEMPTION_ENABLED.with(|c| c.set(self.prev_preempt));
}