From 70b07117276adb5d23c350544b1bcde55a1710c3 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 23 Feb 2026 15:43:39 -0600 Subject: [PATCH] Spawn supervised child suspended before job assignment Set CREATE_SUSPENDED for Windows spawn supervision, assign the child to the job object, then resume the process. This removes the post-spawn assignment race for short-lived child processes. If job assignment fails, kill the suspended child before returning an error to avoid leaving a stuck suspended process. --- crates/uv-windows/src/spawn.rs | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/uv-windows/src/spawn.rs b/crates/uv-windows/src/spawn.rs index cf524d56cb7fe..a7c8948f959aa 100644 --- a/crates/uv-windows/src/spawn.rs +++ b/crates/uv-windows/src/spawn.rs @@ -13,12 +13,30 @@ use std::process::{Child, Command}; use windows::Win32::Foundation::{HANDLE, WAIT_OBJECT_0}; use windows::Win32::System::Threading::{ - CREATE_NO_WINDOW, GetExitCodeProcess, INFINITE, WaitForSingleObject, + CREATE_NO_WINDOW, CREATE_SUSPENDED, GetExitCodeProcess, INFINITE, WaitForSingleObject, }; use crate::job::JobError; use crate::{Job, install_ctrl_handler}; +#[allow(unsafe_code)] +#[link(name = "ntdll")] +unsafe extern "system" { + fn NtResumeProcess(processhandle: HANDLE) -> i32; +} + +#[allow(unsafe_code)] +fn resume_process(process: HANDLE) -> std::io::Result<()> { + // SAFETY: `process` is a valid process handle from `std::process::Child`. + let status = unsafe { NtResumeProcess(process) }; + if status < 0 { + return Err(std::io::Error::other(format!( + "NtResumeProcess failed with status {status:#x}" + ))); + } + Ok(()) +} + /// A child process supervised by a [`Job`] object. /// /// The [`BorrowedHandle`] ties the lifetime of the job to the child process, @@ -58,14 +76,28 @@ impl<'a> SupervisedChild<'a> { pub fn spawn_child(cmd: &mut Command, hide_console: bool) -> std::io::Result { cmd.stdin(std::process::Stdio::inherit()); + // Spawn suspended so job assignment happens before child execution. + // This avoids post-spawn assignment races with short-lived children. + // + // We use `NtResumeProcess` later because `std::process::Child` does not + // expose the primary thread handle needed for `ResumeThread`. + let mut creation_flags = CREATE_SUSPENDED; if hide_console { - cmd.creation_flags(CREATE_NO_WINDOW.0); + creation_flags |= CREATE_NO_WINDOW; } + cmd.creation_flags(creation_flags.0); + + let mut child = cmd.spawn()?; - let child = cmd.spawn()?; + let supervised = match SupervisedChild::new(&child) { + Ok(supervised) => supervised, + Err(err) => { + let _ = child.kill(); + return Err(std::io::Error::other(err.to_string())); + } + }; - let supervised = - SupervisedChild::new(&child).map_err(|e| std::io::Error::other(e.to_string()))?; + resume_process(supervised.raw_handle())?; // Ignore control-C/control-Break/logout/etc.; the same event will be delivered // to the child, so we let them decide whether to exit or not.