Skip to content

emscripten_futex_wait can return 0 spuriously due to _emscripten_thread_notify race #27759

Description

@sbc100

Note: This bug report was generated by AI under my guidance while investigating
#27755.

In #26659 and #26735, side-channel notifications (_emscripten_thread_notify)
were introduced to interrupt threads blocked in emscripten_futex_wait. The
intent in #26735 was that such interruptions return -EINTR rather than 0
so that POSIX/musl internals handle them as interrupted waits.

However, emscripten_futex_wait can still return 0 (ATOMICS_WAIT_OK)
spuriously before the application's wait condition is met. Because callers often
assume 0 means the address was explicitly signaled for completion (and PR
#26849 optimized wait loops to only retry on r == EINTR), this leads to race
conditions where waiting threads wake up prematurely and assume success.


Root Cause 1: Multi-wake race in _emscripten_thread_notify

In system/lib/pthread/emscripten_futex_wake.c, _emscripten_thread_notify
loops until the target clears wait_addr:

void _emscripten_thread_notify(pthread_t target) {
  uintptr_t wait_addr = atomic_fetch_or(&target->wait_addr, NOTIFY_BIT);
  if (wait_addr == 0 || (wait_addr & NOTIFY_BIT)) {
    return;
  }
  while (target->wait_addr == (wait_addr | NOTIFY_BIT)) {
    emscripten_futex_wake((void*)wait_addr, INT_MAX);
    sched_yield();
  }
}

This creates a race window when the waiter re-enters wait:

  1. Thread A is waiting on addr with self->wait_addr = addr.
  2. Thread B notifies Thread A: sets target->wait_addr = addr | NOTIFY_BIT
    and calls emscripten_futex_wake(addr, INT_MAX).
  3. Thread A wakes up, runs:
    bool notified = atomic_exchange(&self->wait_addr, 0) & NOTIFY_BIT;
    notified is true, so Thread A returns -EINTR.
  4. The caller of emscripten_futex_wait observes -EINTR and immediately
    re-enters emscripten_futex_wait(addr, ...) to continue waiting.
  5. Thread A sets self->wait_addr = addr (without NOTIFY_BIT) and calls
    __builtin_wasm_memory_atomic_wait32.
  6. Meanwhile, Thread B was already in the process of executing a second
    emscripten_futex_wake iteration (e.g. before observing that wait_addr
    changed, or due to scheduling/pipelining).
  7. Thread B's second futex_wake wakes Thread A from its new wait.
  8. Thread A wakes up, inspects self->wait_addr & NOTIFY_BIT (which is now
    false), and returns 0 (ATOMICS_WAIT_OK), even though the
    underlying condition was never satisfied!

Root Cause 2: Collateral wakeups on shared futex addresses

_emscripten_thread_notify(target) calls:

emscripten_futex_wake((void*)wait_addr, INT_MAX);

If multiple threads are waiting on the same wait_addr (e.g. a condition
variable or barrier):

  • Only target has NOTIFY_BIT set on its struct pthread.
  • All other threads waiting on wait_addr are woken up by INT_MAX.
  • Because NOTIFY_BIT is not set on those other threads, they all return 0
    spuriously.

Root Cause 3: Async wait cancellations

emscripten_atomic_cancel_wait_async calls Atomics.notify on the wait
address to facilitate JavaScript garbage collection (WebAssembly/threads#176).
Any synchronous thread waiting on that address is woken with return value 0.


Recommended Solutions

  1. Update API Documentation (threading_primitives.h):
    Document explicitly in system/include/emscripten/threading_primitives.h
    that emscripten_futex_wait may experience spurious wakeups returning 0,
    matching the Linux futex(2) contract. Callers must always invoke it
    in a loop verifying their application condition rather than assuming 0
    guarantees state change.

  2. Audit internal callers:
    Ensure all internal loops (like proxying_legacy.c, library_pthread.c,
    etc.) verify the underlying condition rather than relying solely on r == EINTR.

  3. Explore tightening _emscripten_thread_notify:
    Investigate whether the loop in _emscripten_thread_notify can avoid
    sending redundant INT_MAX wakes once the target thread has observed the
    notification.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions