feat(windows): 管理 AppContainer 配置并验证挂起进程身份
This commit is contained in:
@@ -321,3 +321,12 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
|||||||
- 4 项真实 Windows 进程测试与全目标 Clippy -D warnings 通过,日志 `.build/extension-job-tests.log`。三个 ignored 条目是由父测试实际启动的辅助入口。验证进程配额为 1 时第二进程被拒绝且从未恢复、关闭 Job 后进程退出、受管理后代计数归零、显式终止,以及正常 32 MiB 分配成功/600 MiB 分配失败。
|
- 4 项真实 Windows 进程测试与全目标 Clippy -D warnings 通过,日志 `.build/extension-job-tests.log`。三个 ignored 条目是由父测试实际启动的辅助入口。验证进程配额为 1 时第二进程被拒绝且从未恢复、关闭 Job 后进程退出、受管理后代计数归零、显式终止,以及正常 32 MiB 分配成功/600 MiB 分配失败。
|
||||||
- 测试初次使用固定等待观察进程计数失败,改为最多 5 秒等待计数收敛后通过;不将单次固定延时当作清理证明。生产参数测试读取 OS 返回的内存、进程和 CPU 配置,但尚未完成长时间 CPU 压力、10 秒超限终止及 scratch 配额测试。
|
- 测试初次使用固定等待观察进程计数失败,改为最多 5 秒等待计数收敛后通过;不将单次固定延时当作清理证明。生产参数测试读取 OS 返回的内存、进程和 CPU 配置,但尚未完成长时间 CPU 压力、10 秒超限终止及 scratch 配额测试。
|
||||||
- API 依据 Microsoft Job Objects 与 JOBOBJECT_CPU_RATE_CONTROL_INFORMATION 文档: https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects 、 https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information 。AppContainer、文件/网络 broker、恶意程序矩阵及实际扩展调用仍待完成,不能据此通过 C 项或开启 extensions capability。
|
- API 依据 Microsoft Job Objects 与 JOBOBJECT_CPU_RATE_CONTROL_INFORMATION 文档: https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects 、 https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information 。AppContainer、文件/网络 broker、恶意程序矩阵及实际扩展调用仍待完成,不能据此通过 C 项或开启 extensions capability。
|
||||||
|
|
||||||
|
|
||||||
|
## 增量:AppContainer 配置所有权与原生身份验证
|
||||||
|
|
||||||
|
- 新增 extension_container::Profile,为每个实例创建随机 OpenNexus.sandbox 名称、无声明能力的 AppContainer 配置,校验 SID 并提供 OS 返回的配置目录。同名已存在时拒绝,不接管或删除旧配置;显式 remove 报告清理失败,Drop 尝试最终清理并释放 SID。
|
||||||
|
- 真实 Windows 测试创建两个配置验证 SID 不同、重复名称失败不破坏原配置、显式删除后可重新创建。另用 SECURITY_CAPABILITIES 创建挂起的系统进程,先加入 Job,再读取 TokenIsAppContainer、TokenAppContainerSid 与 TokenCapabilities,确认容器身份、精确 SID 和零能力;没有恢复命令解释器执行。
|
||||||
|
- 空环境及仅 SystemRoot 的启动尝试返回 Windows 203;使用明确的 SystemRoot 和指向容器自身目录的 LOCALAPPDATA/TEMP/TMP 后通过。此处是测试启动环境,不是完整生产环境/参数构造器。
|
||||||
|
- 两项 AppContainer 原生测试与全目标 Clippy -D warnings 通过,日志 `.build/extension-container-tests.log`。API 依据 Microsoft CreateAppContainerProfile / DeleteAppContainerProfile / Launch an AppContainer 文档。所有测试创建的配置通过所属对象清理,未借用既有用户配置。
|
||||||
|
- 生产启动器、包只读 ACL、独立 scratch 配额、原始网络拒绝实测、文件与网络 broker、崩溃后孤立配置清理以及恶意程序矩阵仍待完成。当前不声明完整沙箱,也未开启 extensions capability。
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ cap-fs-ext = "4.0.2"
|
|||||||
jsonschema = { version = "0.55", default-features = false }
|
jsonschema = { version = "0.55", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_System_SystemInformation", "Win32_Storage_FileSystem", "Win32_System_RemoteDesktop", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader"] }
|
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Isolation", "Win32_Security_Authorization", "Win32_System_Com", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_System_SystemInformation", "Win32_Storage_FileSystem", "Win32_System_RemoteDesktop", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", optional = true , features = [] }
|
tauri-build = { version = "2", optional = true , features = [] }
|
||||||
|
|||||||
@@ -0,0 +1,360 @@
|
|||||||
|
//! Per-instance AppContainer profile ownership. No existing profile is adopted.
|
||||||
|
use crate::workspace::{HostError, Result};
|
||||||
|
use windows_sys::Win32::Security::{
|
||||||
|
FreeSid, IsValidSid,
|
||||||
|
Isolation::{CreateAppContainerProfile, DeleteAppContainerProfile},
|
||||||
|
PSID,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Profile {
|
||||||
|
name: Vec<u16>,
|
||||||
|
sid: PSID,
|
||||||
|
exists: bool,
|
||||||
|
}
|
||||||
|
impl Profile {
|
||||||
|
pub fn create() -> Result<Self> {
|
||||||
|
Self::create_named(format!(
|
||||||
|
"OpenNexus.sandbox.{}",
|
||||||
|
uuid::Uuid::new_v4().simple()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn create_named(name: String) -> Result<Self> {
|
||||||
|
let name: Vec<u16> = name.encode_utf16().chain(Some(0)).collect();
|
||||||
|
let mut sid = std::ptr::null_mut();
|
||||||
|
let status = unsafe {
|
||||||
|
CreateAppContainerProfile(
|
||||||
|
name.as_ptr(),
|
||||||
|
name.as_ptr(),
|
||||||
|
name.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
&mut sid,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if status < 0 {
|
||||||
|
// In particular, ERROR_ALREADY_EXISTS must not transfer ownership.
|
||||||
|
if !sid.is_null() {
|
||||||
|
unsafe {
|
||||||
|
FreeSid(sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_CREATE_FAILED"));
|
||||||
|
}
|
||||||
|
let profile = Self {
|
||||||
|
name,
|
||||||
|
sid,
|
||||||
|
exists: true,
|
||||||
|
};
|
||||||
|
if sid.is_null() || unsafe { IsValidSid(sid) } == 0 {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_SID_INVALID"));
|
||||||
|
}
|
||||||
|
Ok(profile)
|
||||||
|
}
|
||||||
|
/// Borrowed SID for SECURITY_CAPABILITIES. Valid only while this owner lives.
|
||||||
|
pub fn sid(&self) -> PSID {
|
||||||
|
self.sid
|
||||||
|
}
|
||||||
|
pub fn folder(&self) -> Result<std::path::PathBuf> {
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
use windows_sys::Win32::{
|
||||||
|
Foundation::LocalFree,
|
||||||
|
Security::{
|
||||||
|
Authorization::ConvertSidToStringSidW, Isolation::GetAppContainerFolderPath,
|
||||||
|
},
|
||||||
|
System::Com::CoTaskMemFree,
|
||||||
|
};
|
||||||
|
let mut string = std::ptr::null_mut();
|
||||||
|
if unsafe { ConvertSidToStringSidW(self.sid, &mut string) } == 0 {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_SID_INVALID"));
|
||||||
|
}
|
||||||
|
let mut folder = std::ptr::null_mut();
|
||||||
|
let status = unsafe { GetAppContainerFolderPath(string, &mut folder) };
|
||||||
|
unsafe {
|
||||||
|
LocalFree(string.cast());
|
||||||
|
}
|
||||||
|
if status < 0 || folder.is_null() {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_FOLDER_FAILED"));
|
||||||
|
}
|
||||||
|
let mut length = 0;
|
||||||
|
while length < 32768 && unsafe { *folder.add(length) } != 0 {
|
||||||
|
length += 1;
|
||||||
|
}
|
||||||
|
let result = if length == 32768 {
|
||||||
|
Err(HostError::new("EXTENSION_CONTAINER_FOLDER_FAILED"))
|
||||||
|
} else {
|
||||||
|
Ok(std::path::PathBuf::from(std::ffi::OsString::from_wide(
|
||||||
|
unsafe { std::slice::from_raw_parts(folder, length) },
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
CoTaskMemFree(folder.cast());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
/// Stop all container processes and close their handles before removal.
|
||||||
|
pub fn remove(mut self) -> Result<()> {
|
||||||
|
self.remove_inner()
|
||||||
|
}
|
||||||
|
fn remove_inner(&mut self) -> Result<()> {
|
||||||
|
if self.exists {
|
||||||
|
if unsafe { DeleteAppContainerProfile(self.name.as_ptr()) } < 0 {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_CLEANUP_FAILED"));
|
||||||
|
}
|
||||||
|
self.exists = false;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Profile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Explicit remove reports failures; drop is a final best-effort retry.
|
||||||
|
let _ = self.remove_inner();
|
||||||
|
if !self.sid.is_null() {
|
||||||
|
unsafe {
|
||||||
|
FreeSid(self.sid);
|
||||||
|
}
|
||||||
|
self.sid = std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::{
|
||||||
|
mem::size_of,
|
||||||
|
os::windows::{
|
||||||
|
ffi::OsStrExt,
|
||||||
|
io::{AsHandle, AsRawHandle, FromRawHandle, OwnedHandle},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::{
|
||||||
|
Security::{
|
||||||
|
EqualSid, GetTokenInformation, TokenIsAppContainer, SECURITY_CAPABILITIES, TOKEN_QUERY,
|
||||||
|
},
|
||||||
|
System::Threading::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn distinct_profiles_and_collision_do_not_take_ownership_of_existing_data() {
|
||||||
|
let one = Profile::create().unwrap();
|
||||||
|
let two = Profile::create().unwrap();
|
||||||
|
assert_eq!(unsafe { EqualSid(one.sid(), two.sid()) }, 0);
|
||||||
|
let name = String::from_utf16(&one.name[..one.name.len() - 1]).unwrap();
|
||||||
|
assert!(Profile::create_named(name.clone()).is_err());
|
||||||
|
// Repeated collision must still fail: the failing owner did not delete it.
|
||||||
|
assert!(Profile::create_named(name.clone()).is_err());
|
||||||
|
one.remove().unwrap();
|
||||||
|
Profile::create_named(name).unwrap().remove().unwrap();
|
||||||
|
two.remove().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Attributes {
|
||||||
|
buffer: Vec<usize>,
|
||||||
|
}
|
||||||
|
impl Attributes {
|
||||||
|
fn new() -> Self {
|
||||||
|
let mut bytes = 0;
|
||||||
|
unsafe {
|
||||||
|
InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut bytes);
|
||||||
|
}
|
||||||
|
assert!(bytes > 0);
|
||||||
|
let mut result = Self {
|
||||||
|
buffer: vec![0; bytes.div_ceil(size_of::<usize>())],
|
||||||
|
};
|
||||||
|
assert_ne!(
|
||||||
|
unsafe {
|
||||||
|
InitializeProcThreadAttributeList(
|
||||||
|
result.buffer.as_mut_ptr().cast(),
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
&mut bytes,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Attributes {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
DeleteProcThreadAttributeList(self.buffer.as_mut_ptr().cast());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Process {
|
||||||
|
process: OwnedHandle,
|
||||||
|
_thread: OwnedHandle,
|
||||||
|
}
|
||||||
|
impl Drop for Process {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
TerminateProcess(self.process.as_raw_handle(), 1);
|
||||||
|
WaitForSingleObject(self.process.as_raw_handle(), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn real_suspended_process_has_appcontainer_token_before_job_resume() {
|
||||||
|
let profile = Profile::create().unwrap();
|
||||||
|
let mut attributes = Attributes::new();
|
||||||
|
let caps = SECURITY_CAPABILITIES {
|
||||||
|
AppContainerSid: profile.sid(),
|
||||||
|
Capabilities: std::ptr::null_mut(),
|
||||||
|
CapabilityCount: 0,
|
||||||
|
Reserved: 0,
|
||||||
|
};
|
||||||
|
assert_ne!(
|
||||||
|
unsafe {
|
||||||
|
UpdateProcThreadAttribute(
|
||||||
|
attributes.buffer.as_mut_ptr().cast(),
|
||||||
|
0,
|
||||||
|
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize,
|
||||||
|
(&caps as *const SECURITY_CAPABILITIES).cast(),
|
||||||
|
size_of::<SECURITY_CAPABILITIES>(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let mut startup = STARTUPINFOEXW::default();
|
||||||
|
startup.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
|
||||||
|
startup.lpAttributeList = attributes.buffer.as_mut_ptr().cast();
|
||||||
|
let executable = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap())
|
||||||
|
.join("System32/cmd.exe");
|
||||||
|
let executable: Vec<u16> = executable
|
||||||
|
.as_os_str()
|
||||||
|
.encode_wide()
|
||||||
|
.chain(Some(0))
|
||||||
|
.collect();
|
||||||
|
let folder = profile.folder().unwrap();
|
||||||
|
let environment: Vec<u16> = format!(
|
||||||
|
"LOCALAPPDATA={}\0SystemRoot={}\0TEMP={}\0TMP={}\0\0",
|
||||||
|
folder.display(),
|
||||||
|
std::env::var("SystemRoot").unwrap(),
|
||||||
|
folder.join("Temp").display(),
|
||||||
|
folder.join("Temp").display()
|
||||||
|
)
|
||||||
|
.encode_utf16()
|
||||||
|
.collect();
|
||||||
|
let mut info = PROCESS_INFORMATION::default();
|
||||||
|
assert_ne!(
|
||||||
|
unsafe {
|
||||||
|
CreateProcessW(
|
||||||
|
executable.as_ptr(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
CREATE_SUSPENDED
|
||||||
|
| CREATE_NO_WINDOW
|
||||||
|
| EXTENDED_STARTUPINFO_PRESENT
|
||||||
|
| CREATE_UNICODE_ENVIRONMENT,
|
||||||
|
environment.as_ptr().cast(),
|
||||||
|
std::ptr::null(),
|
||||||
|
&startup.StartupInfo,
|
||||||
|
&mut info,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
"AppContainer process creation failed: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
|
let process = Process {
|
||||||
|
process: unsafe { OwnedHandle::from_raw_handle(info.hProcess) },
|
||||||
|
_thread: unsafe { OwnedHandle::from_raw_handle(info.hThread) },
|
||||||
|
};
|
||||||
|
let job = crate::extension_job::Job::new().unwrap();
|
||||||
|
unsafe {
|
||||||
|
job.assign_suspended(process.process.as_handle()).unwrap();
|
||||||
|
}
|
||||||
|
let mut token = std::ptr::null_mut();
|
||||||
|
assert_ne!(
|
||||||
|
unsafe { OpenProcessToken(process.process.as_raw_handle(), TOKEN_QUERY, &mut token) },
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let token = unsafe { OwnedHandle::from_raw_handle(token) };
|
||||||
|
let mut contained = 0u32;
|
||||||
|
let mut length = 0;
|
||||||
|
assert_ne!(
|
||||||
|
unsafe {
|
||||||
|
GetTokenInformation(
|
||||||
|
token.as_raw_handle(),
|
||||||
|
TokenIsAppContainer,
|
||||||
|
(&mut contained as *mut u32).cast(),
|
||||||
|
size_of::<u32>() as u32,
|
||||||
|
&mut length,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(contained, 1);
|
||||||
|
use windows_sys::Win32::Security::{
|
||||||
|
TokenAppContainerSid, TokenCapabilities, TOKEN_APPCONTAINER_INFORMATION,
|
||||||
|
};
|
||||||
|
let mut required = 0;
|
||||||
|
unsafe {
|
||||||
|
GetTokenInformation(
|
||||||
|
token.as_raw_handle(),
|
||||||
|
TokenAppContainerSid,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
&mut required,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(required > 0 && required < 4096);
|
||||||
|
let mut data = vec![0usize; (required as usize).div_ceil(size_of::<usize>())];
|
||||||
|
assert_ne!(
|
||||||
|
unsafe {
|
||||||
|
GetTokenInformation(
|
||||||
|
token.as_raw_handle(),
|
||||||
|
TokenAppContainerSid,
|
||||||
|
data.as_mut_ptr().cast(),
|
||||||
|
required,
|
||||||
|
&mut required,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let identity = unsafe { &*data.as_ptr().cast::<TOKEN_APPCONTAINER_INFORMATION>() };
|
||||||
|
assert_ne!(
|
||||||
|
unsafe { EqualSid(identity.TokenAppContainer, profile.sid()) },
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let mut required = 0;
|
||||||
|
unsafe {
|
||||||
|
GetTokenInformation(
|
||||||
|
token.as_raw_handle(),
|
||||||
|
TokenCapabilities,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
&mut required,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!((4..4096).contains(&required));
|
||||||
|
let mut capabilities = vec![0usize; (required as usize).div_ceil(size_of::<usize>())];
|
||||||
|
assert_ne!(
|
||||||
|
unsafe {
|
||||||
|
GetTokenInformation(
|
||||||
|
token.as_raw_handle(),
|
||||||
|
TokenCapabilities,
|
||||||
|
capabilities.as_mut_ptr().cast(),
|
||||||
|
required,
|
||||||
|
&mut required,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(unsafe { *capabilities.as_ptr().cast::<u32>() }, 0);
|
||||||
|
|
||||||
|
// No command interpreter instruction was resumed by this identity test.
|
||||||
|
job.terminate().unwrap();
|
||||||
|
drop(token);
|
||||||
|
drop(process);
|
||||||
|
drop(job);
|
||||||
|
drop(attributes);
|
||||||
|
profile.remove().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,3 +54,6 @@ pub mod extension_trust;
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod extension_job;
|
pub mod extension_job;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub mod extension_container;
|
||||||
|
|||||||
Reference in New Issue
Block a user