Conversation
… the wait queue lock semaphore::try_resume() walks the wait queue under q.lock when the head demands more than the count in hand, looking for a smaller waiter behind it. Resuming that waiter goes through prelocked_thread_interrupt() -> thread::dequeue_ready_atomic(), which locks the very same q.lock to erase the thread from the queue. photon::spinlock is not recursive, so the walk deadlocks against itself -- while still holding the semaphore's splock, which takes down every vCPU that touches the semaphore afterwards. The same loop also advanced with th = th->next() after the resume, but a resumed thread has already been erased from the queue, and erase() leaves its links pointing at itself, so the walk could never have moved on. dequeue_ready_atomic() and prelocked_thread_interrupt() now take a waitq_locked flag, following the SCOPED_LOCK(x, cond * 2) idiom already used by prepare_usleep(), and the out-of-order walk remembers the successor before resuming. The flag defaults to false, so the other three call sites keep their behaviour. The path is only reachable with a semaphore constructed as semaphore(count, /*in_order_resume=*/false), which nothing in the tree does -- the default is in-order, and the in-order branch resumes through ScopedLockHead, holding only th->lock. That is why this has gone unnoticed: even throttle, whose waiters ask for wildly different counts, never enters it. The new test hangs deterministically without this fix, on a single vCPU: ooo_bypass queues a waiter for 10, then one for 1, and signals 1. The remaining cases hunt for waiters that never wake up under multi-vCPU traffic, in-order and out-of-order alike. A regression here is a hang rather than a wrong value, so the suite carries a plain OS-thread watchdog: under a spinlock convoy the photon scheduler may never run the main thread again, which makes any in-coroutine deadline useless.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
photon::semaphoredeadlocks against itself in the out-of-order resume path, taking down every vCPU that touches the semaphore afterwards. The path is only reachable for a semaphore constructed assemaphore(count, /*in_order_resume=*/false), which nothing in the tree currently does, so no in-tree user is affected today — but the mode is public API and the failure is deterministic, not probabilistic.Changes
semaphore::try_resume(): the out-of-order walk now tells the resume path that the wait queue lock is already held, and remembers the successor of the node it is about to resume.thread::dequeue_ready_atomic()andprelocked_thread_interrupt(): newwaitq_lockedparameter, defaulting tofalse, using theSCOPED_LOCK(x, cond * 2)idiom already used byprepare_usleep(). The other three call sites keep their behaviour byte for byte.thread/test/test-semaphore-lost-wakeup.cpp(9 cases), registered inthread/test/CMakeLists.txt.Motivation
Two defects sit in the same loop:
Self-deadlock. When the queue head demands more than the count in hand,
try_resume()walks the queue underq.locklooking for a smaller waiter behind it. Resuming that waiter goes throughprelocked_thread_interrupt()→thread::dequeue_ready_atomic(), which locks the very sameq.lockto erase the thread from the queue.photon::spinlockis not recursive, so the walk spins on a lock it already owns — while still holding the semaphore'ssplock, so every other vCPU enteringsignal()orwait()piles up behind it.The walk could never advance. It stepped with
th = th->next()after the resume, but a resumed thread has already been erased from the queue, anderase()→remove_from_list()leaves the node's links pointing at itself.Why this has gone unnoticed: the branch is guarded by
m_ooo_resume, and the default is in-order. The in-order branch resumes throughScopedLockHead, which holds onlyth->lock, so it is safe. Evenphoton::throttle, whose waiters ask for wildly different counts (bytes) and therefore do leave the head out of reach, uses the default constructor and never enters the out-of-order branch.Verification
Built and tested on aarch64 Linux,
MinSizeRel:mainSemaphoreLostWakeup.ooo_bypassSemaphoreLostWakeup.mixed_counts_oootest-threadtest-multi-vcpu-locking,test-std-compat,test-go-channel,test-workpool-fanout,test-sleepq-stale-idx,test-poolooo_bypassis deterministic and single-vCPU — it queues a waiter for 10, then one for 1, and signals 1 — so it also proves the out-of-order branch is actually entered, rather than relying on a race being hit by chance.A regression here is a hang, not a wrong value, so the suite carries a watchdog on a plain OS thread rather than an in-coroutine deadline: under a spinlock convoy the photon scheduler may never run the main photon thread again, which was observed while diagnosing this bug and makes any coroutine-side deadline useless.
References
semaphore::try_resume()inthread/thread.cppsemaphore(uint64_t count, bool in_order_resume)inthread/thread.hphoton::spinlock(non-recursive TTAS) inthread/thread.h