Repository navigation
Conversation
A few paths in `concurrent.rs` move a live fiber out of one store-owned location and then run a fallible step before it reaches the next one. If that step fails, the unfinished fiber is dropped and its `Drop` impl panics, turning the original error into a process abort. Keep such fibers owned by the store until they reach their destination: hold them in a small `DisposeFiber` guard in `resume_fiber` and `run_on_worker`, do the fallible lookups before taking the fiber in `resume_fiber` and `Waitable::mark_ready`, and put the item back into the store before the existing `bail_bug!`s in `set_switch_item`, `handle_work_item`, and `subtask_cancel`. Fixes bytecodealliance#14241.
7286354 to
9e3ae02
Compare
|
Thanks, @Byte-Naut!
FWIW, my solution in #14382 was to store the item in |
| self.0.resume_fiber(fiber).await?; | ||
| } | ||
| other => { | ||
| *state = other; |
There was a problem hiding this comment.
A comment here to remind the reader why we're putting the object back in the store would be helpful.
| }, | ||
| Some(WaitMode::Caller { .. }) => { | ||
| Some(mode @ WaitMode::Caller { .. }) => { | ||
| waiting.insert(thread, mode); |
There was a problem hiding this comment.
Again, a comment here would be helpful to remind the reader why we're putting this back in the store.
|
Personally I'm a bit wary of a strategy like this because everywhere a fiber might be used we still have to proactively reach for the |
|
Thanks both. Happy to take the "fibers stay in the table" approach instead. My rough plan: keep each fiber in its GuestThread state and have queued work items and waiters refer to the thread, taking the fiber out only while it's being resumed. That shouldn't add allocations or state on the normal path beyond a table lookup at resume time; if something does, I'll call it out. @alexcrichton does that match what you had in mind, and is there anything from your local attempt I should follow? I can do this on this PR or as a new one, whichever you prefer. |
|
I've put the table-based approach up as #14418, following @alexcrichton's suggestion above: guest fibers now stay in their thread's state (or the worker slot), and are only taken out while Thanks both for the quick and clear feedback, and @dicej for the pointer on keeping items in the table. |
Fixes #14241.
Motivation
While reviewing #14146, @alexcrichton pointed out that many places in
concurrent.rshave a live fiber in scope across a?. These only fail when an internal invariant is already broken, but if one did, the unfinished fiber would be dropped andStoreFiber'sDropwould panic, turning a trap orbail_bug!into a process abort. @dicej filed #14241 to track this and described the two safe options: keep the fiber owned by the store, or hold it in an RAII guard that disposes of it through the store.This PR applies those two options to the fiber handoffs in
concurrent.rs.Changes
resume_fiberandrun_on_worker: hold the fiber in a small privateDisposeFiberguard until it reaches its destination, so an early return disposes of it through the existingStoreFiber::dispose. In theYieldingandExplicitlySuspendingarms the thread lookup now happens before the fiber is taken; previously the right-hand side of the assignment took the fiber first. The two waiter arms useEntryso the fiber only moves into a vacant slot.Waitable::mark_ready: do the fallible lookups while the waiter is still in its set, and only remove it once its destination has been checked.set_switch_item: if the slot is already occupied, push the incoming item onto the high-priority queue before the existingbail_bug!, so the store still owns its fiber.handle_work_item(ResumeThread) andsubtask_cancel: put the fiber-owning state back before the existingbail_bug!s.resume_thread: reuse the already-borrowedConcurrentStateinstead of looking it up again after taking the fiber.Not changed: happy-path scheduling, error values and messages, the panic for duplicate waiters, and the assertion in
StoreFiber::dropfor fibers that were never disposed. There are no public API changes. About 200 of the added lines are tests.Design
DisposeFiberfollows the same pattern as the existingDisposeguard inpoll_untiland theDropimpl ofFiberFuture: it callsStoreFiber::disposeonly if the fiber has not been handed off yet. Elsewhere the fix is a reordering so that the fiber is taken last, which avoids adding a guard where the lookup can simply happen first.One thing worth calling out for review: when
set_switch_itemfinds the slot occupied, the incoming item goes tohigh_priorityonly so that it is disposed of with the store. Thebail_bug!poisons the store, so the item never runs. Happy to keep it somewhere else if that reads better.Testing
These paths are only reachable once an invariant is already broken, so the new unit tests in
concurrent.rs(fiber_disposal_tests) set up the broken state directly: an invalid waitable set, thread, or caller when a fiber suspends, an invalid waiting thread inmark_ready, and an occupied switch slot. Each test checks that the original error is returned and that the fiber is either disposed of or still owned by the store. All six abort the test process on currentmainand pass with this change, with and without debug assertions.Locally: fmt, clippy, the full
wasmtimelib test suite, and the async component-model.wasttests on Cranelift and Winch pass.Developed with assistance from Claude (Anthropic). Per the Bytecode Alliance AI Tool Use Policy, I'm the author and accountable for this change.