Skip to content

Commit 9a12a8d

Browse files
authored
Fix _at_fork_reinit to write INIT directly instead of calling unlock() (#7312)
* Fix _at_fork_reinit to write INIT directly instead of calling unlock() unlock() goes through unlock_slow() which accesses parking_lot's global hash table to unpark waiters. After fork(), this hash table contains stale entries from dead parent threads, making unlock_slow() unsafe. Writing INIT directly bypasses parking_lot internals entirely. * Add import lock (IMP_LOCK) reinit after fork The import lock is a ReentrantMutex that was never reinit'd after fork(). If a parent thread held it during fork, the child would deadlock on any import. Only reset if the owner is a dead thread; if the surviving thread held it, normal unlock still works.
1 parent c71f4ad commit 9a12a8d

4 files changed

Lines changed: 59 additions & 14 deletions

File tree

crates/common/src/lock.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,24 @@ pub type PyRwLockWriteGuard<'a, T> = RwLockWriteGuard<'a, RawRwLock, T>;
5757
pub type PyMappedRwLockWriteGuard<'a, T> = MappedRwLockWriteGuard<'a, RawRwLock, T>;
5858

5959
// can add fn const_{mutex,rw_lock}() if necessary, but we probably won't need to
60+
61+
/// Reset a `PyMutex` to its initial (unlocked) state after `fork()`.
62+
///
63+
/// After `fork()`, locks held by dead parent threads would deadlock in the
64+
/// child. This zeroes the raw lock bytes directly, bypassing the normal unlock
65+
/// path which may interact with parking_lot's internal waiter queues.
66+
///
67+
/// # Safety
68+
///
69+
/// Must only be called from the single-threaded child process immediately
70+
/// after `fork()`, before any other thread is created.
71+
#[cfg(unix)]
72+
pub unsafe fn reinit_mutex_after_fork<T: ?Sized>(mutex: &PyMutex<T>) {
73+
// lock_api::Mutex<R, T> layout: raw R at offset 0, then UnsafeCell<T>.
74+
// Zeroing R resets to unlocked for both parking_lot::RawMutex (AtomicU8)
75+
// and RawCellMutex (Cell<bool>).
76+
unsafe {
77+
let ptr = mutex as *const PyMutex<T> as *mut u8;
78+
core::ptr::write_bytes(ptr, 0, core::mem::size_of::<RawMutex>());
79+
}
80+
}

crates/vm/src/stdlib/imp.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,34 @@ mod lock {
3333
fn lock_held(_vm: &VirtualMachine) -> bool {
3434
IMP_LOCK.is_locked()
3535
}
36+
37+
/// Reset import lock after fork() — only if held by a dead thread.
38+
///
39+
/// `IMP_LOCK` is a reentrant mutex. If the *current* (surviving) thread
40+
/// held it at fork time, the child must be able to release it normally.
41+
/// Only reset if a now-dead thread was the owner.
42+
///
43+
/// # Safety
44+
///
45+
/// Must only be called from single-threaded child after fork().
46+
#[cfg(unix)]
47+
pub(crate) unsafe fn reinit_after_fork() {
48+
if IMP_LOCK.is_locked() && !IMP_LOCK.is_owned_by_current_thread() {
49+
// Held by a dead thread — reset to unlocked.
50+
// Same pattern as RLock::_at_fork_reinit in thread.rs.
51+
unsafe {
52+
let old: &crossbeam_utils::atomic::AtomicCell<RawRMutex> =
53+
core::mem::transmute(&IMP_LOCK);
54+
old.swap(RawRMutex::INIT);
55+
}
56+
}
57+
}
58+
}
59+
60+
/// Re-export for fork safety code in posix.rs
61+
#[cfg(all(unix, feature = "threading"))]
62+
pub(crate) unsafe fn reinit_imp_lock_after_fork() {
63+
unsafe { lock::reinit_after_fork() }
3664
}
3765

3866
#[cfg(not(feature = "threading"))]

crates/vm/src/stdlib/posix.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,10 @@ pub mod module {
737737
force_unlock_mutex_after_fork(&vm.state.global_trace_func);
738738
force_unlock_mutex_after_fork(&vm.state.global_profile_func);
739739
crate::gc_state::gc_state().force_unlock_after_fork();
740+
741+
// Import lock (ReentrantMutex) — was previously not reinit'd
742+
#[cfg(feature = "threading")]
743+
crate::stdlib::imp::reinit_imp_lock_after_fork();
740744
}
741745

742746
// Mark all other threads as done before running Python callbacks

crates/vm/src/stdlib/thread.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,9 @@ pub(crate) mod _thread {
152152

153153
#[pymethod]
154154
fn _at_fork_reinit(&self, _vm: &VirtualMachine) -> PyResult<()> {
155-
if self.mu.is_locked() {
156-
unsafe {
157-
self.mu.unlock();
158-
};
159-
}
160-
// Casting to AtomicCell is as unsafe as CPython code.
161-
// Using AtomicCell will prevent compiler optimizer move it to somewhere later unsafe place.
162-
// It will be not under the cell anymore after init call.
163-
155+
// Reset the mutex to unlocked by directly writing the INIT value.
156+
// Do NOT call unlock() here — after fork(), unlock_slow() would
157+
// try to unpark stale waiters from dead parent threads.
164158
let new_mut = RawMutex::INIT;
165159
unsafe {
166160
let old_mutex: &AtomicCell<RawMutex> = core::mem::transmute(&self.mu);
@@ -252,11 +246,9 @@ pub(crate) mod _thread {
252246

253247
#[pymethod]
254248
fn _at_fork_reinit(&self, _vm: &VirtualMachine) -> PyResult<()> {
255-
if self.mu.is_locked() {
256-
unsafe {
257-
self.mu.unlock();
258-
};
259-
}
249+
// Reset the reentrant mutex to unlocked by directly writing INIT.
250+
// Do NOT call unlock() — after fork(), the slow path would try
251+
// to unpark stale waiters from dead parent threads.
260252
self.count.store(0, core::sync::atomic::Ordering::Relaxed);
261253
let new_mut = RawRMutex::INIT;
262254
unsafe {

0 commit comments

Comments
 (0)