mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-10-16 15:34:08 +08:00
fix(sandbox): grant sandbox accounts NUL access
This commit is contained in:
@@ -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. 阻塞与避让
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
|
||||
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<T>(
|
||||
desired_access: u32,
|
||||
operation: impl FnOnce(
|
||||
windows_sys::Win32::Foundation::HANDLE,
|
||||
*mut windows_sys::Win32::Security::ACL,
|
||||
) -> io::Result<T>,
|
||||
) -> io::Result<T> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user