Repository navigation
Conversation
dicej
left a comment
There was a problem hiding this comment.
Thanks, @Byte-Naut; looks good overall; just a few inline suggestions.
| async fn run_on_worker(self, item: WorkerItem) -> Result<()> { | ||
| let worker = if let Some(fiber) = self.0.concurrent_state_mut()?.worker.take() { | ||
| let state = self.0.concurrent_state_mut()?; | ||
| assert!(state.worker_item.is_none()); |
There was a problem hiding this comment.
While we're here, might as well turn this into a bail_bug! instead of panicking.
| Entry::Vacant(entry) => { | ||
| entry.insert(WaitMode::Fiber(resuming.fiber.take().unwrap())); | ||
| } | ||
| Entry::Occupied(_) => panic!("assertion failed: old.is_none()"), |
There was a problem hiding this comment.
Let's bail_bug! instead of panic!ing here. Also, please update the error message to be something like "entry unexpectedly already exists for {thread}".
| } | ||
| } | ||
|
|
||
| if matches!(thread.state, GuestThreadState::Ready { .. }) { |
There was a problem hiding this comment.
Nit: looks like these two if statements could be combined into a single match statement.
| let wake_on_cancel = state.get_mut(thread.thread)?.wake_on_cancel.take(); | ||
| if let Some((&thread, _)) = set_state.waiting.first_key_value() { | ||
| let wake_on_cancel = state.get_mut(thread.thread)?.wake_on_cancel; | ||
| assert!(wake_on_cancel.is_none() || wake_on_cancel == WakeOnCancel::Waiting(set)); |
There was a problem hiding this comment.
Let's make this a bail_bug! while we're here.
An unfinished StoreFiber dropped mid-transfer triggers a StoreFiber::drop assertion and may double-panic on unwind. Several scheduler paths moved a live fiber out of one store-owned location with a fallible step before it reached the next, so a lookup error or bail_bug! could turn a recoverable error into a host panic or process abort. This commit fixes those windows without changing the scheduler representation. GuestThreadState, WaitMode::Fiber, and WorkItem::ResumeFiber keep their existing shapes; waiting threads and scheduled fibers continue to appear Running to the thread intrinsics. | Location | Before | After | |---|---|---| | resume_fiber | async fn; fiber disposed only in Err branch | plain fn returning impl Future; ResumingFiber guard built before returning | | SuspendReason arms | write destination slot then move fiber | check_no_fiber on slot; move fiber only after check passes | | set_thread_running | overwrites state unconditionally | returns bail_bug! for unexpected Suspended/Ready | | wake_waiter (from mark_ready) | moves fiber out of wait set before checks | all fallible lookups complete before any transfer | | worker take | worker.take() then fallible state access | assert worker_item empty; take in one non-fallible step | GuestThreadState::check_no_fiber centralises the pre-handoff check for Suspended and Ready. No public API changes, no new scheduler states, no new ConcurrentState fields, no per-suspension allocations.
Two related gaps in work-item handling left fibers temporarily unguarded or at risk of being lost on a predicate panic. | Location | Before | After | |---|---|---| | handle_work_item | async fn; ResumeFiber arm entered async block before guard was built | plain fn returning impl Future; ResumeFiber guard built before returning, matching resume_fiber | | Other work-item variants | fibers stay in store until first poll | unchanged | | promote | drained queues into a local iterator; predicate panic dropped remaining items | scans high_priority and low_priority in place; predicate panic leaves unexamined items in the queues | No behaviour change on the happy path.
Add a comment noting that scanning from the back of the deque considers the oldest items first, matching the previous drain-and-rebuild ordering. No behaviour change.
- Replace assert! with bail_bug! in run_on_worker, wake_waiter, and the
SuspendReason::Waiting occupied-entry branch.
- Update the occupied-entry error message to name the thread:
"entry unexpectedly already exists for {thread:?}".
- Combine the two if-statements in resume_thread into one match.
- Update the duplicate_waiter_keeps_old_fiber and
waiter_assertion_keeps_fiber_in_set tests to use the bug() helper so
they work with both debug and release builds.
Follows review suggestions from @dicej on bytecodealliance#14620.
7920c91 to
c95864e
Compare
|
@dicej Thanks for the review! I've addressed all four suggestions: replaced the |
Fixes #14241.
This supersedes #14418. Based on @alexcrichton's feedback there, this takes the smaller ownership-repair path:
GuestThreadState,WaitMode, andWorkItemkeep their existing shapes —WaitMode::FiberandWorkItem::ResumeFiberstill carry the fiber directly, and waiting or scheduled threads continue to appearRunningto the thread intrinsics — and the fix concentrates on guarding the intervals where a fiber is outside the store's scheduler state.Motivation
An unfinished
StoreFiberthat is dropped directly triggers an assertion inStoreFiber::drop. Several paths in the scheduler moved a live fiber out of one store-owned location and into another with a fallible step in between. When that step fails — a table lookup error,bail_bug!, or a panic — the fiber is dropped mid-transfer, turning an ordinary error into a host panic or process abort.Changes
This rebases on top of #14624, #14635, #14638, and #14610, which landed between the first push and now.
Commit 1 — protect fiber handoffs without adding thread states
resume_fiberis converted fromasync fnto a plain function returningimpl Future. AResumingFiberguard is constructed before returning the future, so a caller that drops the future before its first poll still disposes of the fiber through the store. The fiber enters the guard after being taken from aWaitMode::Fiberentry, aWorkItem::ResumeFiberitem, or aGuestThreadState::Readyslot — the same locations as before — and the guard holds it until it reaches its next store-owned location.For every
SuspendReasonarm that hands the fiber to a destination, the destination slot is checked for an existing fiber before the fiber is moved out of the guard. If the check fails, the fiber stays in the guard and is disposed on return.GuestThreadState::check_no_fibercentralises that check forSuspendedandReady.set_thread_runningandmark_readyare restructured so all fallible lookups complete before any ownership transfer begins. The worker-fiber take is reordered soworker_itemis asserted empty and the take happens in one non-fallible step.Commit 2 — guard pending work and preserve queues on promotion panic
handle_work_itemis converted fromasync fnto a plain function returningimpl Future<Output = Result<()>>. TheResumeFiberarm is handled in the non-async prefix so the guard is established before the future is returned, matching the treatment inresume_fiber.promoteis restructured to scan the queues in place rather than draining and rebuilding them; a predicate panic no longer drops the remaining items.Commit 3 — clarify the oldest-first promotion scan
One-line comment clarification; no behaviour change.
Design notes
The fix does not add scheduler states, new fields to
ConcurrentState, or per-suspension allocations. The existingStoreFiber::dropassertion remains the final defence. Two properties worth checking in review: everySuspendReasonarm checks its destination before taking the fiber out of the guard (check_no_fiberfor thread slots, the entry API for wait sets, andnext_switch_itemfor subtask yields); and theResumingFiberguard together with theFiberFutureinsidefiber::resolve_or_releasecover the full resume interval including pre-first-poll cancellation.Testing
Results: 221 unit tests (including 27 fiber ownership tests), 128 async WAST trials on Cranelift and Winch, 258 component model integration tests, fmt and clippy clean, non-async and no_std builds pass. The fiber ownership tests also pass with debug assertions disabled.
Five paths are covered by fault-injection tests in
concurrent/fiber_ownership_tests.rs, verified against unpatched builds that exit SIGABRT:unpolled_resume_disposes_fiber:resume_fiberfuture dropped before first pollyielding_invalid_thread_returns_table_error:SuspendReason::Yieldingwith an invalid destination threadfull_table_during_switch_save_keeps_fiber: resource table full when savingnext_switch_itemunpolled_work_item_disposes_fiber:handle_work_itemResumeFiberfuture dropped before first pollpromotion_panic_keeps_queued_fibers: predicate panic duringpromote_work_item_matchingthread-wait-resume.wast(carried forward from #14418) checks that a thread blocked inwaitable-set.waitstill producescannot resume thread which is not suspendedforthread.resume-laterandthread.suspend-then-resume.Reviewing
Three commits, each self-contained:
ResumingFiber,check_no_fiber, and the restructured handoff paths inresume_fiber,mark_ready, andset_thread_running. Start here.handle_work_itempre-first-poll guard and the in-place promotion scan. "Hide whitespace" helps on thehandle_work_itemdiff.The representation is unchanged from
main:GuestThreadStatestill hasSuspendedandReadycarrying the fiber directly,WaitMode::FiberandWorkItem::ResumeFiberstill carry the fiber. Developed with assistance from Claude (Anthropic). Per the Bytecode Alliance AI Tool Use Policy, I'm the author and accountable for this change.