//! Regression: request_stop racing an alloc-under-lock must not poison any //! runtime mutex. Before the fix, check_cancelled() fired regardless of //! PREEMPTION_ENABLED, so a sentinel unwind could trigger inside with_shared //! (which allocates: run_queue.push_back, waiters.push, etc), poisoning the //! shared mutex and cascading lock().unwrap() panics. use smarm::runtime::{init, Config}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; #[test] fn stop_storm_does_not_poison_runtime() { // alloc_interval(1): every allocation is an observation point, so pre-fix // the sentinel fires inside with_shared's alloc sites (run_queue.push_back, // waiters.push, ...) with near-certainty rather than 1-in-128. let rt = init(Config::exact(4).alloc_interval(1)); let completed = Arc::new(AtomicUsize::new(0)); let c = completed.clone(); rt.run(move || { // Spawn many short actors that allocate + yield heavily (hammering // with_shared), and request_stop each one mid-flight from siblings. let mut handles = Vec::new(); for _ in 0..200 { let h = smarm::spawn(|| { for _ in 0..50 { let _v: Vec = Vec::with_capacity(64); // alloc → maybe_preempt smarm::yield_now(); } }); handles.push(h); } // Cancel half of them; the others run to completion. If any lock got // poisoned the runtime would panic on a subsequent lock().unwrap(). for (i, h) in handles.iter().enumerate() { if i % 2 == 0 { smarm::request_stop(h.pid()); } } for h in handles { let _ = h.join(); } c.fetch_add(1, Ordering::SeqCst); }); assert_eq!(completed.load(Ordering::SeqCst), 1, "root completed cleanly"); } /// The sharper repro: a stop-flagged actor whose *next allocation* is the /// `Box::new(closure)` inside `spawn`'s `with_shared` critical section. /// Pre-fix, the ungated `check_cancelled` in `maybe_preempt` raises the /// sentinel right there, unwinding while the global shared mutex is held → /// poisoned → every later `with_shared` panics on every scheduler thread and /// the whole runtime collapses. Post-fix, observation is deferred to the next /// lock-free point and each actor dies a clean `Outcome::Stopped`. /// /// `request_stop(self_pid())` sets the flag without parking (we're running), /// and the capturing closure makes `Box::new(f)` a real allocation. With /// `alloc_interval(1)` the check fires every other allocation, so each actor /// has ~50% odds of the check landing on the under-lock alloc; 64 actors make /// a pre-fix escape astronomically unlikely. #[test] fn self_stop_during_spawn_does_not_poison_shared_mutex() { let rt = init(Config::exact(4).alloc_interval(1)); let completed = Arc::new(AtomicUsize::new(0)); let c = completed.clone(); rt.run(move || { let mut handles = Vec::new(); for i in 0..64usize { let h = smarm::spawn(move || { // Vary pre-stop allocation count to cover both phases of the // every-other-allocation check cadence. for _ in 0..(i % 2) { let _phase: Vec = Vec::with_capacity(8); } let payload = vec![0u8; 64]; // captured → Box::new(f) allocates smarm::request_stop(smarm::self_pid()); let grandchild = smarm::spawn(move || drop(payload)); let _ = grandchild.join(); }); handles.push(h); } for h in handles { // Stopped reports Ok from join; the point is that join itself // (with_shared) doesn't panic on a poisoned mutex. let _ = h.join(); } c.fetch_add(1, Ordering::SeqCst); }); assert_eq!(completed.load(Ordering::SeqCst), 1, "root completed cleanly"); }