feat(channel): migrate Inner to RawMutex; two-class lock order (Leaf -> Channel)

Phase 2 surfaced the hole this closes: channel guards were held with
preemption enabled, so a timeslice switch inside a channel critical
section could release the pthread mutex from a different OS thread (UB).
RawMutex disables preemption for the guard span and is
cross-thread-release sound by construction; poison goes away with it.

The strict single-class leaf rule cannot survive the migration: finalize
clones the supervisor/trap senders and monitor() clones the Down sender,
all under a cold lock, and those senders live in the slot - the nesting
is structural. The leaf check is therefore generalized to two classes:
Leaf (cold locks, free list, stack pool) and Channel. Order is
Leaf -> Channel, at most one of each; both directions of violation are
debug-asserted at the acquisition site. Channel critical sections call
only the lock-free unpark protocol, so the order is acyclic.

Tests: class-ordering unit tests in raw_mutex (allowed nesting + all
three rejected shapes), plus a multi-scheduler integration test driving
channels through monitor churn and actor death so any ordering
regression trips the debug assert instead of deadlocking.
This commit is contained in:
smarm
2026-06-09 22:51:37 +00:00
parent d789d301e0
commit 8ff6cf4afd
4 changed files with 206 additions and 42 deletions
+5 -4
View File
@@ -109,10 +109,11 @@ pub struct Monitor {
pub fn monitor(target: Pid) -> Monitor {
let (tx, rx) = channel::<Down>();
// Register under the target's cold lock. `tx.clone()` only touches the
// channel's own mutex, never any runtime lock, so it is safe here — but
// we must not *send* under the lock, as `Sender::send` can unpark a
// parked receiver, and there's no reason to nest that.
// Register under the target's cold lock. `tx.clone()` takes the channel's
// own lock — a Channel-class RawMutex, explicitly permitted *under* a Leaf
// (cold) lock by the lock order (see raw_mutex.rs). We must still not
// *send* under the lock, as `Sender::send` can unpark a parked receiver,
// and there's no reason to nest that.
let (id, registered) = with_runtime(|inner| {
let id = inner.alloc_monitor_id();
let registered = match inner.slot_at(target) {