From e8072f7eb4bdf8583566b201313867d6c0c1341b Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Wed, 9 Sep 2026 05:51:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(host):=20=E8=B7=A8=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E4=B8=B2=E8=A1=8C=E5=8C=96=E5=8F=AF=E7=BB=A7?= =?UTF-8?q?=E6=89=BF=E7=AE=A1=E9=81=93=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src-tauri/src/core.rs | 6 +- frontend/src-tauri/src/extension_process.rs | 3 + frontend/src-tauri/src/extension_stdio.rs | 63 ++++++++++++++++++--- frontend/src-tauri/src/lib.rs | 3 + frontend/src-tauri/src/process_creation.rs | 8 +++ 5 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 frontend/src-tauri/src/process_creation.rs diff --git a/frontend/src-tauri/src/core.rs b/frontend/src-tauri/src/core.rs index aadd4e9..9a1805d 100644 --- a/frontend/src-tauri/src/core.rs +++ b/frontend/src-tauri/src/core.rs @@ -325,7 +325,11 @@ impl CoreSupervisor { use std::os::windows::process::CommandExt; command.creation_flags(0x08000000); // CREATE_NO_WINDOW } - let child = command.group_spawn().map_err(|_| "CORE_SPAWN_FAILED")?; + let child = { + #[cfg(windows)] + let _creation = crate::process_creation::lock()?; + command.group_spawn().map_err(|_| "CORE_SPAWN_FAILED")? + }; let mut session = Session { child, lifetime: Arc::new(Mutex::new(None)), diff --git a/frontend/src-tauri/src/extension_process.rs b/frontend/src-tauri/src/extension_process.rs index 5eafa73..749a1c1 100644 --- a/frontend/src-tauri/src/extension_process.rs +++ b/frontend/src-tauri/src/extension_process.rs @@ -148,6 +148,9 @@ impl<'a> Suspended<'a> { startup.lpAttributeList = attributes.buffer.as_mut_ptr().cast(); // Keep both the handle array and the owning pipe ends alive across // CreateProcessW. No arbitrary inheritable Host handle is admitted. + let io = io + .map(crate::extension_stdio::ChildIo::inherit) + .transpose()?; let inherited = io.as_ref().map(|value| value.handles()); if let Some(handles) = &inherited { if unsafe { diff --git a/frontend/src-tauri/src/extension_stdio.rs b/frontend/src-tauri/src/extension_stdio.rs index 5bc5e25..8eff7ac 100644 --- a/frontend/src-tauri/src/extension_stdio.rs +++ b/frontend/src-tauri/src/extension_stdio.rs @@ -46,13 +46,6 @@ impl ChildIo { output, error, }; - for handle in child.handles() { - if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } - == 0 - { - return Err(HostError::new("EXTENSION_PIPE_CREATE_FAILED")); - } - } Ok(( child, HostIo { @@ -62,6 +55,23 @@ impl ChildIo { }, )) } + /// Own both the pipe ends and the launch lock. Field drop order closes all + /// inheritable ends before allowing a competing Host launch to proceed. + pub(crate) fn inherit(self) -> Result { + let lock = crate::process_creation::lock().map_err(HostError::new)?; + let guarded = InheritedIo { + child: self, + _creation: lock, + }; + for handle in guarded.handles() { + if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } + == 0 + { + return Err(HostError::new("EXTENSION_PIPE_CREATE_FAILED")); + } + } + Ok(guarded) + } pub(crate) fn handles(&self) -> [HANDLE; 3] { [ self.input.as_raw_handle(), @@ -71,6 +81,16 @@ impl ChildIo { } } +pub(crate) struct InheritedIo { + child: ChildIo, + _creation: std::sync::MutexGuard<'static, ()>, +} +impl InheritedIo { + pub(crate) fn handles(&self) -> [HANDLE; 3] { + self.child.handles() + } +} + /// NDJSON maximum excludes the line terminator. A protocol/IO error poisons /// the decoder; the runtime must terminate the instance and close its pipes. /// This synchronous decoder needs a separate IO cancellation/deadline owner. @@ -148,7 +168,7 @@ pub fn write_frame(writer: &mut impl std::io::Write, frame: &[u8]) -> Result<()> mod tests { use super::*; #[test] - fn only_child_ends_are_inheritable_and_all_streams_are_distinct() { + fn pipes_are_private_until_the_serialized_creation_window() { let (child, host) = ChildIo::create().unwrap(); let ends = child .handles() @@ -159,12 +179,39 @@ mod tests { host.error.as_raw_handle(), ]) .collect::>(); + for handle in &ends { + let mut flags = 0; + assert_ne!(unsafe { GetHandleInformation(*handle, &mut flags) }, 0); + assert_eq!(flags & HANDLE_FLAG_INHERIT, 0); + } + let child = child.inherit().unwrap(); for (index, handle) in ends.iter().enumerate() { let mut flags = 0; assert_ne!(unsafe { GetHandleInformation(*handle, &mut flags) }, 0); assert_eq!(flags & HANDLE_FLAG_INHERIT != 0, index < 3); assert!(!ends[..index].contains(handle)); } + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (acquired_tx, acquired_rx) = std::sync::mpsc::channel(); + let worker = std::thread::spawn(move || { + started_tx.send(()).unwrap(); + let _creation = crate::process_creation::lock().unwrap(); + acquired_tx.send(()).unwrap(); + }); + started_rx.recv().unwrap(); + assert!(acquired_rx + .recv_timeout(std::time::Duration::from_millis(30)) + .is_err()); + drop(child); + acquired_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + worker.join().unwrap(); + // The last writer was closed before the lock was released; no child + // process was launched in this ownership test, so Host sees EOF. + use std::io::Read; + let mut output = host.output; + assert_eq!(output.read(&mut [0; 1]).unwrap(), 0); } #[test] fn frames_are_bounded_across_fragmentation_and_poison_after_errors() { diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 3f5fb26..fdfe8b3 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -96,3 +96,6 @@ pub mod extension_call_authorization; #[cfg(all(windows, feature = "desktop"))] pub mod extension_instance; + +#[cfg(windows)] +mod process_creation; diff --git a/frontend/src-tauri/src/process_creation.rs b/frontend/src-tauri/src/process_creation.rs new file mode 100644 index 0000000..e716293 --- /dev/null +++ b/frontend/src-tauri/src/process_creation.rs @@ -0,0 +1,8 @@ +//! Coordinate Host-controlled Windows launches while inheritable handles exist. +//! This does not serialize foreign libraries that bypass this Host boundary. +use std::sync::{Mutex, MutexGuard}; +static CREATION: Mutex<()> = Mutex::new(()); + +pub(crate) fn lock() -> Result, &'static str> { + CREATION.lock().map_err(|_| "PROCESS_CREATION_LOCK_FAILED") +}