Windows TLS - Only register the atexit hook when cleanup can be unloaded#157645
Conversation
|
|
|
I like avoiding @bors try jobs=msvc,mingw |
This comment has been minimized.
This comment has been minimized.
…maybe-deadlock-on-shutdown, r=<try> Windows TLS - when `cleanup` can be unloaded, leak instead of running dtors in `atexit` try-job: *msvc* try-job: *mingw*
|
Hm, looking at the current docs for LocalKey, it says:
This suggests to me we should favour attempting to run dtors but document the caveats. A deadlock is not a memory safety issue and many types of dtors (e.g. freeing memory) likely won't deadlock. Unloading a DLL (outside of program termination) is a niche use case but not so niche that I think we can justify not running dtors at all if the API claims to bias towards running them. |
Actually, seems like I was wrong about the cause of the deadlock: the issue is specifically calling I'll need to investigate this a bit more and I'll update the impl once I have a better idea if the fix is legit. |
282e4fe to
6bf8d21
Compare
cleanup can be unloaded, leak instead of running dtors in atexitcleanup can be unloaded, do not register the atexit hook
6bf8d21 to
6830dac
Compare
cleanup can be unloaded, do not register the atexit hookatexit hook when cleanup can be unloaded
6830dac to
b545eb8
Compare
|
@ChrisDenton changed to not-leak when DLL unload, kept the no-atexit-in-main-exec because that solves the FlsFree lock. I don't think the docs need any changes now because if you unload the DLL we say that the dtor will run so they must run in the loader lock. |
b545eb8 to
f5e5709
Compare
|
@bors r+ |
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
Rollup of 23 pull requests Successful merges: - #157280 (traits: Allow escaping self types in ExistentialTraitRef::with_self_ty) - #157282 (Fix post-monomorphization error note race in the parallel frontend) - #157352 (Make the retained dep graph deterministic under the parallel frontend) - #157601 (Emit error for unused target expression in glob and list delegations) - #157611 (Update `browser-ui-test` version to `0.24.0`) - #157620 (Add a strategy FnMut to inject behavior into the flush cycle) - #157645 (Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded) - #157647 (Start using comptime for reflection intrinsics and their wrapper functions) - #157719 (resolve: Partially revert "Remove a special case for dummy imports") - #156497 (fix-155516: Don't suggest wrong unwrap expect) - #156583 (Support defaults for static EIIs) - #157013 (Ensure inferred let pattern types are well-formed) - #157230 (borrowck: avoid ICE describing fields on generic params) - #157288 (platform support: add SNaN erratum to MIPS targets) - #157350 (compiletest: ignore SVG `y` offset in by-lines comparison) - #157355 (Add `or_try_*` variants for `HashMap` and `BTreeMap` Entry APIs) - #157384 (Add `#[rustc_dump_generics]` attribute) - #157577 (Fix parser error recovery treating 'dyn' as a strict keyword in Rust 2015 when used in `dyn + dyn`) - #157670 (Rename `errors.rs` file to `diagnostics.rs` (4/N)) - #157691 (Move symbol hiding code to a new file) - #157697 (Add more tests that exercise the well-formedness checking of lazy type aliases) - #157700 (Rename `errors.rs` file to `diagnostics.rs` (5/N)) - #157703 (Fix doc link to Instant sub in saturating caveat) Failed merges: - #157699 (Arg splat experiment - hir FnDecl impl)
Rollup of 23 pull requests Successful merges: - #157280 (traits: Allow escaping self types in ExistentialTraitRef::with_self_ty) - #157282 (Fix post-monomorphization error note race in the parallel frontend) - #157352 (Make the retained dep graph deterministic under the parallel frontend) - #157601 (Emit error for unused target expression in glob and list delegations) - #157611 (Update `browser-ui-test` version to `0.24.0`) - #157620 (Add a strategy FnMut to inject behavior into the flush cycle) - #157645 (Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded) - #157647 (Start using comptime for reflection intrinsics and their wrapper functions) - #157719 (resolve: Partially revert "Remove a special case for dummy imports") - #156497 (fix-155516: Don't suggest wrong unwrap expect) - #156583 (Support defaults for static EIIs) - #157013 (Ensure inferred let pattern types are well-formed) - #157230 (borrowck: avoid ICE describing fields on generic params) - #157288 (platform support: add SNaN erratum to MIPS targets) - #157350 (compiletest: ignore SVG `y` offset in by-lines comparison) - #157355 (Add `or_try_*` variants for `HashMap` and `BTreeMap` Entry APIs) - #157384 (Add `#[rustc_dump_generics]` attribute) - #157577 (Fix parser error recovery treating 'dyn' as a strict keyword in Rust 2015 when used in `dyn + dyn`) - #157670 (Rename `errors.rs` file to `diagnostics.rs` (4/N)) - #157691 (Move symbol hiding code to a new file) - #157697 (Add more tests that exercise the well-formedness checking of lazy type aliases) - #157700 (Rename `errors.rs` file to `diagnostics.rs` (5/N)) - #157703 (Fix doc link to Instant sub in saturating caveat) Failed merges: - #157699 (Arg splat experiment - hir FnDecl impl)
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
…instead-of-maybe-deadlock-on-shutdown, r=ChrisDenton Windows TLS - Only register the `atexit` hook when `cleanup` can be unloaded Followup to rust-lang#148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the [docs](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors)). However, it is still _possible_ to deadlock on Windows with a destructor that uses a `join`, but **only** during an application shutdown. (Edited. The original PR leaked the dtors when a DLL was unloaded.) This is because calling `FlsFree` from the `atexit` hook _from the main executable_ can deadlock if other threads are also running FLS destructors. This PR tweaks the implementation to only register the hook if `cleanup` can be unloaded, meaning we are loaded as a DLL. This has an added benefit of not triggering `FlsFree` at all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock). Checking if the `cleanup` hook is unload-able is done by checking if it is in the same module as the main exe `GetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null())`. <details> <summary>Join-on-Drop on Process Exit Deadlock Example</summary> ```rust use std::thread::JoinHandle; use std::time::Duration; struct JoinOnDrop(Option<JoinHandle<()>>); impl Drop for JoinOnDrop { fn drop(&mut self) { println!("Joining thread... ({:?})", std::thread::current()); self.0.take().unwrap().join().unwrap(); println!("Thread joined"); } } thread_local! { static HANDLE: JoinOnDrop = { let thread = std::thread::spawn(|| { println!("Starting..."); std::thread::sleep(Duration::from_secs(3)); println!("Done ({:?})", std::thread::current()); }); JoinOnDrop(Some(thread)) }; } fn main() { let thread = std::thread::spawn(|| { HANDLE.with(|_| { println!("Some other thread ({:?})", std::thread::current()); }) }); // ***Here***, because we do not join beforehand, the drops are called from atexit which is done while in loader-lock, so the the binary will deadlock. // thread.join().unwrap(); std::thread::sleep(Duration::from_secs(1)); println!("Done ({:?})", std::thread::current()); } ``` </details> r? @ChrisDenton
Followup to #148799 - One of the motivations was to remove the "Synchronization in thread-local destructors" limitation (see the docs). However, it is still possible to deadlock on Windows with a destructor that uses a
join, but only during an application shutdown.(Edited. The original PR leaked the dtors when a DLL was unloaded.)
This is because calling
FlsFreefrom theatexithook from the main executable can deadlock if other threads are also running FLS destructors.This PR tweaks the implementation to only register the hook if
cleanupcan be unloaded, meaning we are loaded as a DLL.This has an added benefit of not triggering
FlsFreeat all during normal application shutdown, which is the most common case and is now handled more naturally by the Windows runtime (which calls the FLS hooks without holding the load-lock).Checking if the
cleanuphook is unload-able is done by checking if it is in the same module as the main exeGetModuleHandleExW(FLAG_FROM_ADDRESS, cleanup) == GetModuleHandleW(ptr::null()).Join-on-Drop on Process Exit Deadlock Example
r? @ChrisDenton