Skip to content

Commit 2893430

Browse files
committed
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 f00fcd2 commit 2893430

File tree

3 files changed

+53
-0
lines changed

3 files changed

+53
-0
lines changed

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

0 commit comments

Comments
 (0)