From ae9d01121377cdbef32b9d5e8559843cce9f927e Mon Sep 17 00:00:00 2001 From: Jens Holdgaard Pedersen Date: Mon, 20 Jul 2026 19:04:08 +0200 Subject: [PATCH] signal: restore MSRV by removing OnceLock::wait from the Windows handler (#8300) OnceLock::wait was stabilized in Rust 1.86, so its use in the Windows console ctrl handler broke tokio's declared rust-version of 1.71 on windows targets in 1.53.0 (any cargo check with a 1.71..1.86 toolchain fails with E0599). The wait existed only because SetConsoleCtrlHandler was called inside REGISTRY's get_or_init closure, i.e. before the OnceLock was actually initialized, leaving a window where an invoked handler could observe an uninitialized REGISTRY. Initialize the registry first and register the OS handler afterwards (exactly once, through a second OnceLock that also caches a registration failure so every subsequent call reports the same error, matching the previous behavior). The handler can then rely on plain get(): registration happens-after initialization, so an invoked handler always finds the registry. Verified with cargo +1.71 check -p tokio --features full --target x86_64-pc-windows-msvc (fails with the reported E0599 before this change, clean after) and --all-targets on stable for the same target. Fixes #8299 --- tokio/src/signal/windows/sys.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tokio/src/signal/windows/sys.rs b/tokio/src/signal/windows/sys.rs index 1968f5304..0a472d5a4 100644 --- a/tokio/src/signal/windows/sys.rs +++ b/tokio/src/signal/windows/sys.rs @@ -57,15 +57,20 @@ pub(super) fn ctrl_shutdown() -> io::Result { } fn new(signal: SignalKind) -> io::Result { - let registry = REGISTRY + // Initialize the registry BEFORE registering the OS handler: the + // handler thread can then always observe an initialized `REGISTRY` + // (`SetConsoleCtrlHandler` happens-after the initialization below), + // so it needs no blocking wait. + let registry = REGISTRY.get_or_init(Registry::default); + + HANDLER_RESULT .get_or_init( || match unsafe { console::SetConsoleCtrlHandler(Some(handler), 1) } { 0 => Err(Error::last_os_error().raw_os_error().expect("unreachable")), - _ => Ok(Registry::default()), + _ => Ok(()), }, ) - .as_ref() - .map_err(|&code| Error::from_raw_os_error(code))?; + .map_err(Error::from_raw_os_error)?; let rx = registry[signal].subscribe(); Ok(RxFuture::new(rx)) @@ -94,7 +99,11 @@ impl Index for Registry { } } -static REGISTRY: OnceLock> = OnceLock::new(); +static REGISTRY: OnceLock = OnceLock::new(); + +/// Whether `SetConsoleCtrlHandler` succeeded, initialized (once) only +/// after `REGISTRY` — see `new` for the ordering argument. +static HANDLER_RESULT: OnceLock> = OnceLock::new(); unsafe extern "system" fn handler(ty: u32) -> BOOL { let signal = match ty { @@ -107,11 +116,13 @@ unsafe extern "system" fn handler(ty: u32) -> BOOL { _ => return 0, }; - // Note that `OnceLock::get` does not handle the small window between calling - // `SetConsoleCtrlHandler` and `REGISTRY` being initialized. - let Ok(registry) = REGISTRY.wait().as_ref() else { - // Technically unreachable since `handler` is only called if - // `SetConsoleCtrlHandler` succeeded. + // `new` initializes `REGISTRY` before it registers this handler with + // the OS, so an invoked handler always finds it initialized — + // `get()` suffices and no blocking wait is needed (using `get` + // also keeps the crate's MSRV: `OnceLock::wait` needs Rust 1.86). + let Some(registry) = REGISTRY.get() else { + // Unreachable by the ordering above; kept as a defensive + // fallback that lets the OS run the next handler. return 0; };