Skip to content

sched: take the lock before checking the wait queue in condvar wake_one/wake_all - #1480

Open
gburd wants to merge 1 commit into
cloudius-systems:masterfrom
gburd:pr/condvar-wake-lock
Open

sched: take the lock before checking the wait queue in condvar wake_one/wake_all#1480
gburd wants to merge 1 commit into
cloudius-systems:masterfrom
gburd:pr/condvar-wake-lock

Conversation

@gburd

@gburd gburd commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Problem

condvar::wake_one() and condvar::wake_all() begin with an early-return on an
unlocked read of _waiters_fifo.oldest, before taking the condvar's
internal mutex _m:

void condvar::wake_all() {
    if (!_waiters_fifo.oldest) {   // unlocked read
        return;
    }
    _m.lock();
    ...

A waiter (condvar::wait) links its wait_record into _waiters_fifo under
_m, then releases _m, then sleeps. The waker does not take _m for the
pre-check, so there is no acquire to pair with the waiter's release: the
just-linked wait_record can be read as a stale nullptr, and the waker
returns without waking anyone (a dropped wakeup).

wake_one() re-read the head under the lock afterwards, so only its unlocked
early-return was unsound; wake_all() had no post-lock recheck at all once the
early-return was taken.

The window is wider under CONF_fork: a forked child's wait_record is
allocated for cross-address-space coherence, and a broadcast may come from a
different address space than the waiter, with no _m synchronizing the two.

Fix

Take _m first, then read and act on _waiters_fifo, so the emptiness check is
synchronized with the waiter's link under the same lock. The prior unlocked
read was only an optimization for the no-waiter case.

Note

Found while investigating an I/O-completion hang; this is submitted on its own
as a self-contained correctness fix for the condvar primitive.

condvar::wake_one() and wake_all() early-returned on an unlocked read of
_waiters_fifo.oldest before taking the internal mutex _m. A waiter links its
wait_record into the FIFO under _m and then releases _m, but the waker did not
take _m for that pre-check, so there is no acquire to pair with the waiter's
release: the just-linked wait_record can be read as a stale nullptr and the
wakeup silently dropped. The window is wider under CONF_fork, where a forked
child's wait_record lives in another address space and the signaller may run in
a different one.

Take _m first, then read and act on _waiters_fifo, so the emptiness check is
synchronized with the waiter's link. The prior unlocked read was only an
optimization for the no-waiter case; correctness comes first. (wake_one already
re-read the head under the lock, so only its early-return was unsound; wake_all
had no post-lock recheck at all.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant