From f5d24b087791288d8d5d1bd96833c6793d1d2501 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Tue, 17 Feb 2026 10:48:59 +0900 Subject: [PATCH] Use try_lock in py_os_after_fork_child after_forkers_child.lock() can deadlock in the forked child if another thread held the mutex at the time of fork. Use try_lock and skip at-fork callbacks when the lock is unavailable, matching the pattern used in after_fork_child for thread_handles. --- crates/vm/src/stdlib/nt.rs | 19 ++----------------- crates/vm/src/stdlib/os.rs | 30 ++++++++++++++++++++++++++++-- crates/vm/src/stdlib/posix.rs | 27 ++++++++++----------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index 0013aa0f970..5b2cf3b92f5 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -6,7 +6,7 @@ pub use module::raw_set_handle_inheritable; #[pymodule(name = "nt", with(super::os::_os))] pub(crate) mod module { use crate::{ - Py, PyObjectRef, PyResult, TryFromObject, VirtualMachine, + Py, PyResult, TryFromObject, VirtualMachine, builtins::{PyBaseExceptionRef, PyDictRef, PyListRef, PyStrRef, PyTupleRef}, common::{crt_fd, suppress_iph, windows::ToWideString}, convert::ToPyException, @@ -1212,21 +1212,6 @@ pub(crate) mod module { } } - fn envobj_to_dict(env: ArgMapping, vm: &VirtualMachine) -> PyResult { - let obj = env.obj(); - if let Some(dict) = obj.downcast_ref_if_exact::(vm) { - return Ok(dict.to_owned()); - } - let keys = vm.call_method(obj, "keys", ())?; - let dict = vm.ctx.new_dict(); - for key in keys.get_iter(vm)?.into_iter::(vm)? { - let key = key?; - let val = obj.get_item(&*key, vm)?; - dict.set_item(&*key, val, vm)?; - } - Ok(dict) - } - #[cfg(target_env = "msvc")] #[pyfunction] fn execve( @@ -1261,7 +1246,7 @@ pub(crate) mod module { .chain(once(core::ptr::null())) .collect(); - let env = envobj_to_dict(env, vm)?; + let env = crate::stdlib::os::envobj_to_dict(env, vm)?; // Build environment strings as "KEY=VALUE\0" wide strings let mut env_strings: Vec = Vec::new(); for (key, value) in env.into_iter() { diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 8b7d3915278..2fb71d9ec01 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -2,10 +2,10 @@ use crate::{ AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, - builtins::{PyModule, PySet}, + builtins::{PyDictRef, PyModule, PySet}, common::crt_fd, convert::{IntoPyException, ToPyException, ToPyObject}, - function::{ArgumentError, FromArgs, FuncArgs}, + function::{ArgMapping, ArgumentError, FromArgs, FuncArgs}, }; use std::{fs, io, path::Path}; @@ -2038,6 +2038,32 @@ pub fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { Ok(()) } +/// Convert a mapping (e.g. os._Environ) to a plain dict for use by execve/posix_spawn. +/// +/// For `os._Environ`, accesses the internal `_data` dict directly at the Rust level. +/// This avoids Python-level method calls that can deadlock after fork() when +/// parking_lot locks are held by threads that no longer exist. +pub(crate) fn envobj_to_dict(env: ArgMapping, vm: &VirtualMachine) -> PyResult { + let obj = env.obj(); + if let Some(dict) = obj.downcast_ref_if_exact::(vm) { + return Ok(dict.to_owned()); + } + if let Some(inst_dict) = obj.dict() + && let Ok(Some(data)) = inst_dict.get_item_opt("_data", vm) + && let Some(dict) = data.downcast_ref_if_exact::(vm) + { + return Ok(dict.to_owned()); + } + let keys = vm.call_method(obj, "keys", ())?; + let dict = vm.ctx.new_dict(); + for key in keys.get_iter(vm)?.into_iter::(vm)? { + let key = key?; + let val = obj.get_item(&*key, vm)?; + dict.set_item(&*key, val, vm)?; + } + Ok(dict) +} + #[cfg(not(windows))] use super::posix as platform; diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 1c4b502f9e2..ce70412df76 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -716,7 +716,15 @@ pub mod module { vm.signal_handlers .get_or_init(crate::signal::new_signal_handlers); - let after_forkers_child: Vec = vm.state.after_forkers_child.lock().clone(); + let after_forkers_child = match vm.state.after_forkers_child.try_lock() { + Some(guard) => guard.clone(), + None => { + // SAFETY: After fork in child process, only the current thread + // exists. The lock holder no longer exists. + unsafe { vm.state.after_forkers_child.force_unlock() }; + vm.state.after_forkers_child.lock().clone() + } + }; run_at_forkers(after_forkers_child, false, vm); } @@ -1073,21 +1081,6 @@ pub mod module { .map_err(|err| err.into_pyexception(vm)) } - fn envobj_to_dict(env: ArgMapping, vm: &VirtualMachine) -> PyResult { - let obj = env.obj(); - if let Some(dict) = obj.downcast_ref_if_exact::(vm) { - return Ok(dict.to_owned()); - } - let keys = vm.call_method(obj, "keys", ())?; - let dict = vm.ctx.new_dict(); - for key in keys.get_iter(vm)?.into_iter::(vm)? { - let key = key?; - let val = obj.get_item(&*key, vm)?; - dict.set_item(&*key, val, vm)?; - } - Ok(dict) - } - #[pyfunction] fn execve( path: OsPath, @@ -1110,7 +1103,7 @@ pub mod module { return Err(vm.new_value_error("execve() arg 2 first element cannot be empty")); } - let env = envobj_to_dict(env, vm)?; + let env = crate::stdlib::os::envobj_to_dict(env, vm)?; let env = env .into_iter() .map(|(k, v)| -> PyResult<_> {