Rust LD_PRELOAD拦截posix_spawnp启用Tokio Tracing时挂死问题
Let's break down why your LD_PRELOAD library is hanging when intercepting posix_spawnp with tracing enabled, and how to fix it.
Root Cause Analysis
The core issue here is fork/posix_spawn safety with Tokio Tracing's global state. When posix_spawnp runs, it creates a new process by copying the entire address space of the parent. Tokio Tracing's default setup relies on:
- Global singleton subscribers (protected by locks like
once_cellorMutex) - Background threads for processing/logging (if using async backends)
If the parent process has any of these locks held (even by another thread) when posix_spawnp is called, the child process inherits the locked state—but the thread that held the lock doesn't exist in the child. This leads to a permanent futex wait (the hang you see in strace). Even initializing tracing without emitting logs can trigger this, since initialization sets up those global locks/threads.
Fixes to Try
1. Avoid Tracing in the posix_spawnp Interceptor
The simplest workaround is to skip tracing calls entirely in the posix_spawnp intercept function, and use a lower-level logging method that doesn't rely on global state. For example:
use libc::{c_char, c_int, posix_spawn_file_actions_t, posix_spawnattr_t}; // Original function pointer static mut POSIX_SPAWNP: Option<unsafe extern "C" fn( pid: *mut c_int, path: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int> = None; #[no_mangle] unsafe extern "C" fn posix_spawnp( pid: *mut c_int, path: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int { // Use direct write to stderr instead of tracing macros let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("unknown"); let log_msg = format!("[TRACE] posix_spawnp called: {}\n", path_str); libc::write(libc::STDERR_FILENO, log_msg.as_ptr() as *const _, log_msg.len()); // Call the original function POSIX_SPAWNP.unwrap()(pid, path, file_actions, attrp, argv, envp) }
This bypasses the global tracing state entirely, so there's no lock to get stuck on during process spawning.
2. Use a Fork-Safe Tracing Setup
If you still want to use Tokio Tracing, configure it to avoid fork-unsafe features:
- Disable background threads in the tracing subscriber
- Use a synchronous, non-threaded backend
Update your initialization code (e.g., in a #[ctor] function):
use tracing_subscriber::fmt; #[ctor] fn init_library() { // Initialize tracing without background threads fmt() .with_threads(false) // Disable thread-aware logging to avoid background threads .with_writer(std::io::stderr) // Use synchronous stderr writing .init(); }
This removes the background processing threads that cause issues when their state is copied into the spawned process.
3. Defer Tracing Until After Spawning (If Possible)
If you need to trace the spawned process's activity, don't handle it in the parent's posix_spawnp interceptor. Instead:
- Pass an environment variable to the child process indicating it should enable tracing on startup
- Have your LD_PRELOAD library check for this variable when the child process loads, and initialize tracing then
This way, tracing is initialized in the child process after it's fully spawned, avoiding the parent's locked state.
Key Takeaway
LD_PRELOAD + process spawning is a tricky combination for any library that uses global state or threads. Tokio Tracing's default setup isn't fork-safe out of the box, so you need to either work around it in your interceptor or adjust the tracing configuration to avoid unsafe state.
内容的提问来源于stack exchange,提问作者Sarvi Shanmugham

