From 5eebd985f88597419ca92f61fbbe685bb579ce6a Mon Sep 17 00:00:00 2001 From: coso Date: Wed, 26 Aug 2026 17:47:20 +0800 Subject: [PATCH] fix(sandbox): grant sandbox accounts NUL access --- .../codex-desktop-complete-alignment-plan.md | 1 + .../crates/tool-runtime/src/windows_setup.rs | 3 + .../src/windows_setup/accounts.rs | 212 ++++++++++++++++++ 3 files changed, 216 insertions(+) diff --git a/internal/exec-plans/codex-desktop-complete-alignment-plan.md b/internal/exec-plans/codex-desktop-complete-alignment-plan.md index 1a3d82d19..1cffde7f3 100644 --- a/internal/exec-plans/codex-desktop-complete-alignment-plan.md +++ b/internal/exec-plans/codex-desktop-complete-alignment-plan.md @@ -389,6 +389,7 @@ Renderer typed gateway - 2026-08-26:继续 C-3,删除已由真机证据判定无效的 `CreateProcessWithTokenW` fallback、suspended/resume 与后置 Job 分配,改为 `CreateProcessWithLogonW -> sandbox-account runner -> current token 派生 restricted token -> CreateProcessAsUserW + STARTUPINFOEX` 唯一 current 链;有界双 named-pipe framing 承接 spawn/stdin/close/resize/terminate/output/exit/error,pipe ACL 只允许目标 account SID且连接后校验 runner PID,pipe 与 ConPTY 均通过 job-list attribute 原子入 Job,宿主 reaper 持有控制 pipe 与 ACL lease 到 runner 退出。`windows-sandbox-runner.exe` 已进入 dev/Quality 成组构建、Electron Windows resource、release manifest `windowsSandboxRunnerSha256` 和 packaged verifier;Everyone-write case 的 `icacls` 输出改为进程内捕获,避免污染 evidence parser 的稳定 test 行。本地 `cargo fmt --check`、Prettier、`git diff --check`、packaging/sidecar/evidence 定向 Vitest `66/66`、Rust related 反向依赖链、`tool-runtime 373/373`、`app-server-daemon 38/38`、contracts `299 checks`、scripts/release/docs/legacy governance 全部通过,legacy 扫描仍为 2063 files、0/0/0。当前主机未安装 Windows target,因此这些结果不替代 MSVC 编译和 schema v3 七项真机矩阵;计划继续保持 `active`、`44/46 (95.7%)` 与 `Planned/enforced=false`,下一刀是推送该窄提交并监控 Windows Quality evidence。 - 2026-08-26:Windows Quality run `32950744293`(SHA `3ca895af0`)已取得 schema v3 artifact `9600137436`,但在 setup binary 的 MSVC 编译阶段失败,七项尚未执行。失败收敛为单一 Windows-only import:`windows_runner_child.rs` 从 `Win32::Foundation` 导入 `EqualSid`,而 `windows-sys 0.52` 的 owner 是 `Win32::Security`;同时报告本轮 `windows.rs` 的两个 unused import。已按实际模块归属修正并准备最小重跑;该 run 不改变 `44/46 (95.7%)` 或 `Planned/enforced=false`。 - 2026-08-26:Windows Quality run `32951556136`(SHA `dfc05e1e1`)已通过 setup 并完成 MSVC 编译,schema v3 artifact `9600453484` 首次执行完整七项矩阵:Everyone-write audit 通过,其余六项在 runner transport 建立阶段共同失败,错误均为 `SetNamedPipeHandleState(runner pipe): ERROR_ACCESS_DENIED`。根因是 Lime 为保留 15 秒有界连接而以 `PIPE_NOWAIT` 创建 pipe,连接后切换 `PIPE_WAIT`;output server pipe 使用 `PIPE_ACCESS_INBOUND` 时只有 `GENERIC_READ`,不满足 Windows 对 `SetNamedPipeHandleState` 的 `GENERIC_WRITE` 或 `GENERIC_READ + FILE_WRITE_ATTRIBUTES` 合同。已将 output server handle 收敛为 duplex access,runner 仍只请求 write access,account-only DACL、随机 pipe name 与 client PID 校验不变;该 run 为 `1/7` 失败证据,继续保持 `44/46 (95.7%)` 与 `Planned/enforced=false`,待下一轮真机矩阵验证。 +- 2026-08-26:Windows Quality run `32952578959`(SHA `315c1cd79`)确认 runner transport 已越过 pipe state 切换,setup/MSVC 编译继续通过;schema v3 artifact 的七项矩阵仍为 `1/7`,六个 restricted child 均已 spawn 但在产生输出前以 code `1` 退出。对照 Codex current command runner 后确认 Lime 漏掉 restricted PowerShell/stdio 初始化所需的 NUL device ACL;Codex 对 capability SID 授权,而 Lime capability 是每回合随机 UUID,直接照搬会在全局设备 DACL 永久堆积短生命周期 ACE。当前修复改由 elevated setup 一次性向两个稳定 sandbox account SID 授予同等 NUL read/write/execute 权限,并在 readiness 对 effective rights 做 read-back;account SID 仍只作为 restricting identity,不进入 `TokenDefaultDacl`,workspace capability 边界不放宽。该 run 不改变 `44/46 (95.7%)` 或 `Planned/enforced=false`,待真机重跑验证此根因假设。 ## 7. 阻塞与避让 diff --git a/lime-rs/crates/tool-runtime/src/windows_setup.rs b/lime-rs/crates/tool-runtime/src/windows_setup.rs index aeea7ffcf..047f647a8 100644 --- a/lime-rs/crates/tool-runtime/src/windows_setup.rs +++ b/lime-rs/crates/tool-runtime/src/windows_setup.rs @@ -13,6 +13,7 @@ mod accounts; #[cfg(windows)] use accounts::{ ensure_local_account, ensure_local_group, ensure_local_group_member, + ensure_sandbox_accounts_null_device_access, validate_sandbox_accounts_null_device_access, validate_windows_sandbox_group_membership, }; #[cfg(windows)] @@ -143,6 +144,7 @@ pub fn run_windows_sandbox_setup( WINDOWS_SANDBOX_OFFLINE_USERNAME, )?; ensure_local_group_member(WINDOWS_SANDBOX_USERS_GROUP, WINDOWS_SANDBOX_ONLINE_USERNAME)?; + ensure_sandbox_accounts_null_device_access()?; let offline_blob = dpapi_protect(offline_password.as_bytes())?; let online_blob = dpapi_protect(online_password.as_bytes())?; @@ -490,6 +492,7 @@ fn validate_protected_password(label: &str, value: &str) -> Result<(), String> { fn validate_windows_runtime_artifacts(artifacts: &ValidatedSetupArtifacts) -> Result<(), String> { validate_windows_sandbox_group_membership(WINDOWS_SANDBOX_OFFLINE_USERNAME)?; validate_windows_sandbox_group_membership(WINDOWS_SANDBOX_ONLINE_USERNAME)?; + validate_sandbox_accounts_null_device_access()?; validate_windows_sandbox_user("offline", &artifacts.users.offline)?; validate_windows_sandbox_user("online", &artifacts.users.online)?; crate::windows_firewall::verify_offline_rules(WINDOWS_SANDBOX_OFFLINE_USERNAME) diff --git a/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs b/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs index 83254fad6..a97838915 100644 --- a/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs +++ b/lime-rs/crates/tool-runtime/src/windows_setup/accounts.rs @@ -4,6 +4,218 @@ use super::{ }; use std::io; +const READ_CONTROL: u32 = 0x0002_0000; +const WRITE_DAC: u32 = 0x0004_0000; + +pub(super) fn ensure_sandbox_accounts_null_device_access() -> io::Result<()> { + use windows_sys::Win32::Foundation::{LocalFree, ERROR_SUCCESS, HLOCAL}; + use windows_sys::Win32::Security::Authorization::{ + SetEntriesInAclW, SetSecurityInfo, EXPLICIT_ACCESS_W, SET_ACCESS, SE_KERNEL_OBJECT, + TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN, TRUSTEE_W, + }; + use windows_sys::Win32::Storage::FileSystem::{ + FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, + }; + + let mut account_sids = [ + resolve_windows_account_sid(WINDOWS_SANDBOX_OFFLINE_USERNAME).map_err(io::Error::other)?, + resolve_windows_account_sid(WINDOWS_SANDBOX_ONLINE_USERNAME).map_err(io::Error::other)?, + ]; + let entries = account_sids + .iter_mut() + .map(|sid| EXPLICIT_ACCESS_W { + grfAccessPermissions: FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE, + grfAccessMode: SET_ACCESS, + grfInheritance: 0, + Trustee: TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: 0, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_UNKNOWN, + ptstrName: sid.as_mut_ptr() as *mut u16, + }, + }) + .collect::>(); + + with_null_device_dacl(READ_CONTROL | WRITE_DAC, |handle, dacl| { + if dacl.is_null() { + return Err(io::Error::other( + "Windows sandbox NUL device has no editable DACL", + )); + } + let mut updated_dacl = std::ptr::null_mut(); + let update = unsafe { + SetEntriesInAclW( + entries.len() as u32, + entries.as_ptr(), + dacl, + &mut updated_dacl, + ) + }; + if update != ERROR_SUCCESS { + return Err(win32_result_error( + "SetEntriesInAclW(Windows sandbox NUL)", + update, + )); + } + if updated_dacl.is_null() { + return Err(io::Error::other( + "SetEntriesInAclW(Windows sandbox NUL) returned a null DACL", + )); + } + let applied = unsafe { + SetSecurityInfo( + handle, + SE_KERNEL_OBJECT, + windows_sys::Win32::Security::DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + updated_dacl, + std::ptr::null_mut(), + ) + }; + unsafe { + LocalFree(updated_dacl as HLOCAL); + } + if applied == ERROR_SUCCESS { + Ok(()) + } else { + Err(win32_result_error( + "SetSecurityInfo(Windows sandbox NUL)", + applied, + )) + } + }) +} + +pub(super) fn validate_sandbox_accounts_null_device_access() -> Result<(), String> { + use windows_sys::Win32::Foundation::ERROR_SUCCESS; + use windows_sys::Win32::Security::Authorization::{ + GetEffectiveRightsFromAclW, TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN, TRUSTEE_W, + }; + use windows_sys::Win32::Storage::FileSystem::{ + FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, + }; + + let required = FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE; + let account_sids = [ + ( + WINDOWS_SANDBOX_OFFLINE_USERNAME, + resolve_windows_account_sid(WINDOWS_SANDBOX_OFFLINE_USERNAME)?, + ), + ( + WINDOWS_SANDBOX_ONLINE_USERNAME, + resolve_windows_account_sid(WINDOWS_SANDBOX_ONLINE_USERNAME)?, + ), + ]; + with_null_device_dacl(READ_CONTROL, |_handle, dacl| { + if dacl.is_null() { + return Err(io::Error::other( + "Windows sandbox NUL device has no readable DACL", + )); + } + for (account, sid) in &account_sids { + let trustee = TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: 0, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_UNKNOWN, + ptstrName: sid.as_ptr() as *mut u16, + }; + let mut effective = 0; + let result = unsafe { GetEffectiveRightsFromAclW(dacl, &trustee, &mut effective) }; + if result != ERROR_SUCCESS { + return Err(win32_result_error( + "GetEffectiveRightsFromAclW(Windows sandbox NUL)", + result, + )); + } + if effective & required != required { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!("Windows sandbox account {account} lacks required NUL device access"), + )); + } + } + Ok(()) + }) + .map_err(|error| error.to_string()) +} + +fn with_null_device_dacl( + desired_access: u32, + operation: impl FnOnce( + windows_sys::Win32::Foundation::HANDLE, + *mut windows_sys::Win32::Security::ACL, + ) -> io::Result, +) -> io::Result { + use windows_sys::Win32::Foundation::{ + CloseHandle, LocalFree, ERROR_SUCCESS, HLOCAL, INVALID_HANDLE_VALUE, + }; + use windows_sys::Win32::Security::Authorization::{GetSecurityInfo, SE_KERNEL_OBJECT}; + use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, + }; + + let path = to_wide(r"\\.\NUL"); + let handle = unsafe { + CreateFileW( + path.as_ptr(), + desired_access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null_mut(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + 0, + ) + }; + if handle == 0 || handle == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + let mut descriptor = std::ptr::null_mut(); + let mut dacl = std::ptr::null_mut(); + let security = unsafe { + GetSecurityInfo( + handle, + SE_KERNEL_OBJECT, + windows_sys::Win32::Security::DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut dacl, + std::ptr::null_mut(), + &mut descriptor, + ) + }; + if security != ERROR_SUCCESS { + unsafe { + CloseHandle(handle); + } + return Err(win32_result_error( + "GetSecurityInfo(Windows sandbox NUL)", + security, + )); + } + + let result = operation(handle, dacl); + unsafe { + if !descriptor.is_null() { + LocalFree(descriptor as HLOCAL); + } + CloseHandle(handle); + } + result +} + +fn win32_result_error(context: &str, code: u32) -> io::Error { + io::Error::new( + io::ErrorKind::Other, + format!( + "{context} failed: {}", + io::Error::from_raw_os_error(code as i32) + ), + ) +} + pub(super) fn ensure_local_account(username: &str, password: &str) -> io::Result<()> { use windows_sys::Win32::NetworkManagement::NetManagement::{ NERR_UserExists, NetUserAdd, NetUserSetInfo, UF_DONT_EXPIRE_PASSWD, UF_SCRIPT, USER_INFO_1,