fix(scheduler): entry-side check_cancelled in park_current — stop against a QUEUED actor was lossy

request_stop sets the flag and issues a wildcard unpark, but unpark
no-ops on a Queued actor (the pending run "is" the wake). If the
actor's first action on resume is a blocking park — no allocation, no
check!() on the way — the wake-side-only check in park_current is
unreachable: the actor parks with the stop flag already set and no
wake is ever coming. The runtime then idles forever.

Fix: check_cancelled at the ENTRY of park_current, before the switch.
The remaining window (flag lands after the entry check, before the
park) is covered by the existing protocol: the stop's unpark finds
Running / the prep-to-park window, sets Notified, and the park-return
re-queues into the wake-side check. Unwinding from the entry is the
same unwind path as the wake side; leftover wait registrations are
stale-epoch / dead-generation and self-clean at their wakers' failed
CAS.

This is bug #1 of the urus chunk-2 session (bug #2, the terminal
wake, is the preceding commit). Repro: tests/cancel.rs
stop_flagged_while_queued_lands_at_first_park — deadlocks without the
fix (watchdog-bounded), passes in <50ms with it.
This commit is contained in:
Claude
2026-06-11 21:15:03 +00:00
parent eddf3fe929
commit 7bab4d23ea
2 changed files with 70 additions and 0 deletions
+12
View File
@@ -212,6 +212,18 @@ pub fn yield_now() {
}
pub fn park_current() {
// Entry-side observation point: a stop flagged while we were QUEUED is
// otherwise lost — the stop's wildcard unpark no-ops on a Queued actor
// (the pending run "is" the wake), so if our first action on resume is
// this park, no wake is ever coming and the wake-side check below is
// unreachable. Checking here closes that hole; a flag that lands after
// this check is covered by the existing protocol (the stop's unpark
// finds Running / the prep-to-park window, sets Notified, and the
// park-return re-queues us into the wake-side check). Unwinding from
// here is the same unwind path as the wake side: leftover wait
// registrations are stale-epoch / dead-generation and self-clean at
// their wakers' failed CAS.
crate::preempt::check_cancelled();
runtime::set_yield_intent(YieldIntent::Park);
unsafe { crate::context::switch_to_scheduler() };
// Observation point on the wakeup side of every blocking primitive