std::thread::spawn aborts when thread::current() is called during DLL_THREAD_ATTACH on Windows
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
std::thread::spawn aborts after thread::current() is called during DLL_THREAD_ATTACH on Windows — is this intentional?
I'm not sure if this is considered a bug or intentional behavior. The crash is rtabort! in set_current(), which means the Rust runtime explicitly chose to abort in this case. However, the trigger is extremely easy to hit accidentally (even a println! during DLL_THREAD_ATTACH), and there's no documentation warning about this, so I wanted to bring it to your attention.
I tried this code
// cdylib loaded via LoadLibrary on Windows
use std::sync::OnceLock;
use windows_sys::Win32::System::SystemServices::{DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH};
static INIT: OnceLock<()> = OnceLock::new();
#[unsafe(no_mangle)]
extern "system" fn DllMain(
_hinst: *mut core::ffi::c_void,
reason: u32,
_reserved: *mut core::ffi::c_void,
) -> i32 {
match reason {
DLL_PROCESS_ATTACH => {
INIT.set(()).ok();
std::thread::spawn(|| {
println!("worker thread running");
});
}
DLL_THREAD_ATTACH => {
// even println! here calls thread::current() and pollutes TLS
println!(
"DLL_THREAD_ATTACH on thread {:?}",
std::thread::current().id()
);
}
_ => {}
}
1
}
I expected to see this happen
The worker thread spawned during DLL_PROCESS_ATTACH should run successfully regardless of what happens during DLL_THREAD_ATTACH.
Instead, this happened
The process aborts with:
fatal runtime error: current thread handle already set during thread spawn, aborting
Exit code: 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN)
(Possible) Root cause analysis
The call chain is:
DLL_THREAD_ATTACH:
println!("...")
→ std::io::stdout()
→ ReentrantLock::try_lock()
→ std::thread::current()
→ init_current() // src/std/thread/current.rs
→ CURRENT = Some(handle) // ← TLS POLLUTED
... later, on the main thread ...
std::thread::spawn(|| { ... })
→ CreateThread → thread_start
→ init.init()
→ set_current(self.handle)
→ if CURRENT.get().is_some() { // ← DETECTS POLLUTION
rtabort!(
"current thread handle already set during thread spawn, aborting"
)
}
The issue is in std::sys::thread::current() (specifically init_current()) — it unconditionally sets CURRENT on the calling thread's TLS, regardless of whether that thread is owned by the Rust runtime. When DLL_THREAD_ATTACH fires on a foreign thread (e.g., Vulkan Loader's internal thread), Rust's std pollutes CURRENT on that thread's TLS. Later, when std::thread::spawn creates a new thread, set_current() in the new thread (not the foreign thread) detects CURRENT is already Some and aborts.
Key observation: println!, format!, eprintln!, dbg!, and tracing all eventually call std::io::stdout() / stderr() → ReentrantLock::try_lock() → std::thread::current(). So even seemingly harmless debug output triggers this crash.
Real-world trigger
This reliably occurs when a Rust cdylib is loaded as a Vulkan Layer, because the Vulkan Loader creates internal threads and sends DLL_THREAD_ATTACH notifications to all loaded layers.
Minimal reproduction repository
Complete MRE with 7 test scenarios:
https://github.com/pengjiaxusz/rust-dll-thread-attach-tls-pollution
git clone https://github.com/pengjiaxusz/rust-dll-thread-attach-tls-pollution.git
cd rust-dll-thread-attach-tls-pollution
.\run-all-tests.ps1
The repo includes isolated child-process testing (crashes don't affect the runner) and covers these scenarios:
| # | Scenario | Result |
|---|---|---|
| 1 | println! in DLL_THREAD_ATTACH |
crash |
| 2 | format! + println! |
crash |
| 3 | eprintln! + dbg! |
crash |
| 4 | OutputDebugStringW (no Rust std) |
safe |
| 5 | CreateThread bypasses check |
safe |
| 6 | Early return guard (zero Rust code) | safe |
| 7 | tracing::non_blocking internal spawn |
crash |
Possible approaches
- Make
init_current()detect foreign threads — check if the current thread is already known to the Rust runtime before settingCURRENT. If it's a foreign thread, don't pollute TLS. - Document the hazard prominently — add a warning to Windows-specific platform docs and the
DllMaindocumentation that any Rust std code duringDLL_THREAD_ATTACHcauses undefined behavior.
Workaround
Add an early return guard at the very top of DllMain:
if reason == DLL_THREAD_ATTACH {
return 1; // no Rust code executes on this path
}
Meta
rustc --version --verbose:
rustc 1.97.0-nightly (365c0e1d7 2026-05-06)
binary: rustc
commit-hash: 365c0e1d7a614ca94cb48431dcd2bc6d3b645db1
commit-date: 2026-05-06
host: x86_64-pc-windows-msvc
release: 1.97.0-nightly
LLVM version: 22.1.4
Tested on Windows 11 only. This is a Windows-specific issue since DllMain / DLL_THREAD_ATTACH is a Windows concept.
Note to reviewers: This issue was drafted with the help of an AI assistant. While I've done my best to verify the core problem myself (the crash is reproducible and the test cases in the linked repo all produce consistent results), the root cause analysis and technical explanations in this post may contain inaccuracies or oversights that I'm not qualified to catch. Please take the diagnosis with a grain of salt, and I'd greatly appreciate any corrections where my understanding falls short.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the linked reproduction repository with .\run-all-tests.ps1, then read src/std/thread/current.rs, especially init_current and set_current. Trace the DLL_THREAD_ATTACH and thread::spawn paths against the seven reported scenarios. Done means the intended Windows behavior is resolved and the crash is either prevented with coverage or clearly documented as a constraint.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100