From b3b4feb2db165bc947e2ec2c4a3411eaee533324 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 --- .../OpenNexus生产化实施进度-2026-09-08.md | 10 +++ 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 +++ 6 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 frontend/src-tauri/src/process_creation.rs diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index 6df8731..402b475 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -574,3 +574,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 原生 MCP fixture 新增 mcp_memory(尝试预留 600 MiB)与 mcp_processes(连续启动同包受管后代)模式。原 AppContainer 和后台注册表显式测试扩展为 CPU/内存/进程三类:核对调用错误、失败会话、资源状态、Job 清空、Host 保存;后台额外核对旧 Endpoint、工具目录清理、包 ACL 基线和每次失败后同包新实例实际调用成功。内存/进程回执限定 10 秒,CPU 保留既有 20 秒观察上限且仍不作为连续 10 秒计时证明。 - 初次后台三类测试通过,CPU/内存/进程调用到错误分别约 12.339 秒、2.509 毫秒、395.929 毫秒,整项 16.34 秒;AppContainer 协议组三类也通过,整项 30.59 秒。日志 `.build/extension-resource-registry.log`、`.build/extension-resource-container.log`。后续以收紧后的内存/进程 10 秒测试边界重新执行最终版本,结果另记。 - 最终版本两项显式原生/后台资源集成测试同时执行通过,合计 26.42 秒,日志 `.build/extension-resource-explicit-final.log`;内存错误约 1.0–1.3 毫秒,进程数错误约 378–445 毫秒。默认扩展回归 69 通过、10 ignored,日志 `.build/extension-resource-regression.log`;desktop 全目标 Clippy -D warnings 通过,日志 `.build/extension-resource-clippy.log`。本轮没有重跑全部前端、后端或远端部署测试,也未声称完整生产化完成。 + + +## 增量:Host 进程创建期间的句柄继承窗口 + +- 扫描 frontend/src-tauri/src 下进程创建调用,当前自有生产路径包括 Core 的 command.group_spawn 与扩展的 CreateProcessW;其余直接创建进程位置位于测试模块。扩展此前在 ChildIo.create 即设置三个子端的继承标志,直到创建函数退出才关闭,Core 启动没有与该窗口协调。 +- 新增 Windows process_creation 共享互斥。ChildIo.create 现在保留六个端点默认不可继承;进入扩展原生创建区间时,InheritedIo 先持有共享锁,再设置三个子端继承标志,继续只将这三个句柄放入 PROC_THREAD_ATTRIBUTE_HANDLE_LIST。InheritedIo 字段析构顺序保证先关闭子端,再释放锁;部分设置失败也由同一所有者清理。锁中毒导致后续启动失败,不继续开放继承。 +- Core.group_spawn 的同步创建区间使用同一共享锁,锁在返回子进程后即释放,不覆盖 Core 握手、请求或整个生命周期。扩展不带 stdio 的创建仍使用 bInheritHandles=false。新所有权测试核对所有管道在区间外不可继承、区间内仅三个子端可继承、竞争启动线程须等待锁释放、释放后 Host 输出端到达 EOF。 +- [CreateProcessW](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw) 文档说明通用继承在多线程并发创建时可能带入其他可继承句柄;[UpdateProcThreadAttribute](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute) 仍要求 HANDLE_LIST 中的句柄具有继承标志。本轮因此同时保留显式列表并约束自有创建窗口。 +- 该锁只约束已接入的自有 Host 路径,不约束绕开该入口的第三方库或系统组件。尚未完成全部依赖/平台进程创建审计,以及真实并发启动的完整跨进程句柄攻击矩阵,不能据此宣称整个 Host 继承问题已经完全验收。第三方扩展仍未启用。 +- desktop 全目标测试累计 129 通过、12 ignored:库 114、Host 8、其余集成 7;覆盖原生扩展协议、后台实例、Core 生命周期/Workspace、凭据所有权及 Sync 故障恢复集成。本轮未显式重跑 ignored 长时资源/工具期限验收,辅助进程由其父测试驱动。日志 `.build/process-creation-full.log`。desktop 全目标 Clippy -D warnings 通过,日志 `.build/process-creation-clippy.log`。 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") +}