Skip to content

Fix nsync_waiter_wipe_ zeroing mutexes held by surviving thread after fork#1487

Open
igravious wants to merge 1 commit into
jart:masterfrom
igravious:fix/nsync-waiter-wipe-held-mutex
Open

Fix nsync_waiter_wipe_ zeroing mutexes held by surviving thread after fork#1487
igravious wants to merge 1 commit into
jart:masterfrom
igravious:fix/nsync-waiter-wipe-held-mutex

Conversation

@igravious

Copy link
Copy Markdown

Summary

nsync_waiter_wipe_() (introduced in 98c5847) unconditionally zeroes every mutex pointed to by wipe_mu after fork(). This is incorrect when the forking (surviving) thread holds the mutex — the child inherits the lock but nsync_mu_unlock() sees word=0 and panics:

nsync panic: attempt to nsync_mu_unlock() an nsync_mu not held in write mode

Root cause

  1. Thread A acquires mutex M
  2. Thread B blocks waiting on M → creates waiter with wipe_mu = M
  3. Thread A calls fork()
  4. In child, nsync_waiter_wipe_() zeroes M's word (from Thread B's waiter)
  5. Thread A tries to unlock M → (0 - MU_WLOCK) underflows → panic

Fix

Before zeroing a mutex in nsync_waiter_wipe_(), check if it's currently held (MU_WLOCK or MU_RLOCK_FIELD bits set). After fork, only the forking thread survives, so any held mutex must be held by it. For held mutexes: preserve lock state but strip stale waiter metadata (MU_WAITING, MU_WRITER_WAITING, etc.) since those waiters are dead. Same logic applied to wipe_cv.

Test plan

  • New test test/libc/thread/nsync_fork_wipe_test.c — reproduces the exact crash without the fix, passes with it
  • Verified fix resolves 2 test failures in Ruby 4.0.0 bootstrap suite (test_ractor.rb Initial Crypto (RSA/ECDSA) support #1414, Ip6 getifaddrs #1415) which hit this bug via Ractor threads + fork() inside RB_VM_LOCKING()

… fork

nsync_waiter_wipe_() (introduced in 98c5847) unconditionally zeroes
every mutex pointed to by wipe_mu after fork(). This is incorrect when
the forking (surviving) thread holds the mutex: the child inherits the
lock but nsync_mu_unlock() sees word=0 and panics with "attempt to
nsync_mu_unlock() an nsync_mu not held in write mode".

After fork(), only the forking thread survives, so any held mutex must
be held by the survivor. Check MU_WLOCK and MU_RLOCK_FIELD before
zeroing: if set, preserve lock state but strip stale waiter metadata
(MU_WAITING, MU_WRITER_WAITING, etc.) since those waiters are dead.
Apply the same logic to wipe_cv.

Reproducer: hold a pthread_mutex, have another thread block on it
(creating a wipe_mu waiter), fork(), then unlock in the child.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Kimi <noreply@moonshot.cn>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant