feat(sandbox): 按句柄授予包访问权限并验证容器隔离
This commit is contained in:
@@ -54,6 +54,100 @@ impl Profile {
|
|||||||
pub fn sid(&self) -> PSID {
|
pub fn sid(&self) -> PSID {
|
||||||
self.sid
|
self.sid
|
||||||
}
|
}
|
||||||
|
/// Grant this instance read/execute access to one Host-owned package object.
|
||||||
|
/// The caller must open it without following reparse points and retain the
|
||||||
|
/// verified package handles for the entire launch. No recursive inheritance
|
||||||
|
/// is used: every directory and file must be checked and granted separately.
|
||||||
|
/// This adds an ACE; it does not sanitize pre-existing permissions.
|
||||||
|
pub fn grant_package_read_execute(&self, object: &std::fs::File) -> Result<()> {
|
||||||
|
use std::os::windows::{fs::MetadataExt, io::AsRawHandle};
|
||||||
|
use windows_sys::Win32::{
|
||||||
|
Foundation::LocalFree,
|
||||||
|
Security::{Authorization::*, DACL_SECURITY_INFORMATION},
|
||||||
|
Storage::FileSystem::{
|
||||||
|
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
|
||||||
|
FILE_ATTRIBUTE_REPARSE_POINT, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
struct LocalAllocation(*mut core::ffi::c_void);
|
||||||
|
impl Drop for LocalAllocation {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.0.is_null() {
|
||||||
|
unsafe {
|
||||||
|
LocalFree(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let metadata = object
|
||||||
|
.metadata()
|
||||||
|
.map_err(|_| HostError::new("EXTENSION_CONTAINER_ACL_FAILED"))?;
|
||||||
|
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
|
||||||
|
|| !(metadata.is_file() || metadata.is_dir())
|
||||||
|
{
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_ACL_OBJECT_INVALID"));
|
||||||
|
}
|
||||||
|
if metadata.is_file() {
|
||||||
|
let mut info = BY_HANDLE_FILE_INFORMATION::default();
|
||||||
|
if unsafe { GetFileInformationByHandle(object.as_raw_handle(), &mut info) } == 0
|
||||||
|
|| info.nNumberOfLinks != 1
|
||||||
|
{
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_ACL_OBJECT_INVALID"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut old_acl = std::ptr::null_mut();
|
||||||
|
let mut descriptor = std::ptr::null_mut();
|
||||||
|
let status = unsafe {
|
||||||
|
GetSecurityInfo(
|
||||||
|
object.as_raw_handle(),
|
||||||
|
SE_FILE_OBJECT,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut old_acl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut descriptor,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let _descriptor = LocalAllocation(descriptor);
|
||||||
|
// A null DACL grants everyone full access, so fail closed rather than
|
||||||
|
// silently treating it as a suitably isolated package object.
|
||||||
|
if status != 0 || old_acl.is_null() {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_ACL_FAILED"));
|
||||||
|
}
|
||||||
|
let entry = EXPLICIT_ACCESS_W {
|
||||||
|
grfAccessPermissions: FILE_GENERIC_READ | FILE_GENERIC_EXECUTE,
|
||||||
|
grfAccessMode: GRANT_ACCESS,
|
||||||
|
grfInheritance: 0,
|
||||||
|
Trustee: TRUSTEE_W {
|
||||||
|
TrusteeForm: TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||||
|
ptstrName: self.sid.cast(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut acl = std::ptr::null_mut();
|
||||||
|
let status = unsafe { SetEntriesInAclW(1, &entry, old_acl, &mut acl) };
|
||||||
|
let _acl = LocalAllocation(acl.cast());
|
||||||
|
if status != 0 || acl.is_null() {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_ACL_FAILED"));
|
||||||
|
}
|
||||||
|
let status = unsafe {
|
||||||
|
SetSecurityInfo(
|
||||||
|
object.as_raw_handle(),
|
||||||
|
SE_FILE_OBJECT,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
acl,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if status != 0 {
|
||||||
|
return Err(HostError::new("EXTENSION_CONTAINER_ACL_FAILED"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn folder(&self) -> Result<std::path::PathBuf> {
|
pub fn folder(&self) -> Result<std::path::PathBuf> {
|
||||||
use std::os::windows::ffi::OsStringExt;
|
use std::os::windows::ffi::OsStringExt;
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::{
|
||||||
@@ -73,6 +167,11 @@ impl Profile {
|
|||||||
LocalFree(string.cast());
|
LocalFree(string.cast());
|
||||||
}
|
}
|
||||||
if status < 0 || folder.is_null() {
|
if status < 0 || folder.is_null() {
|
||||||
|
if !folder.is_null() {
|
||||||
|
unsafe {
|
||||||
|
CoTaskMemFree(folder.cast());
|
||||||
|
}
|
||||||
|
}
|
||||||
return Err(HostError::new("EXTENSION_CONTAINER_FOLDER_FAILED"));
|
return Err(HostError::new("EXTENSION_CONTAINER_FOLDER_FAILED"));
|
||||||
}
|
}
|
||||||
let mut length = 0;
|
let mut length = 0;
|
||||||
@@ -198,6 +297,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn real_suspended_process_has_appcontainer_token_before_job_resume() {
|
fn real_suspended_process_has_appcontainer_token_before_job_resume() {
|
||||||
let profile = Profile::create().unwrap();
|
let profile = Profile::create().unwrap();
|
||||||
|
checked_process(&profile, None);
|
||||||
|
profile.remove().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only tests use cmd.exe, with fixed commands and controlled temporary paths.
|
||||||
|
// A production extension launcher must use a verified entry, never a shell.
|
||||||
|
fn checked_process(profile: &Profile, command: Option<&str>) -> Option<u32> {
|
||||||
let mut attributes = Attributes::new();
|
let mut attributes = Attributes::new();
|
||||||
let caps = SECURITY_CAPABILITIES {
|
let caps = SECURITY_CAPABILITIES {
|
||||||
AppContainerSid: profile.sid(),
|
AppContainerSid: profile.sid(),
|
||||||
@@ -239,12 +345,24 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.encode_utf16()
|
.encode_utf16()
|
||||||
.collect();
|
.collect();
|
||||||
|
let mut command_line: Vec<u16> = command
|
||||||
|
.map(|command| {
|
||||||
|
format!("cmd.exe /d /c {command}")
|
||||||
|
.encode_utf16()
|
||||||
|
.chain(Some(0))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
let mut info = PROCESS_INFORMATION::default();
|
let mut info = PROCESS_INFORMATION::default();
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
unsafe {
|
unsafe {
|
||||||
CreateProcessW(
|
CreateProcessW(
|
||||||
executable.as_ptr(),
|
executable.as_ptr(),
|
||||||
std::ptr::null_mut(),
|
if command_line.is_empty() {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
} else {
|
||||||
|
command_line.as_mut_ptr()
|
||||||
|
},
|
||||||
std::ptr::null(),
|
std::ptr::null(),
|
||||||
std::ptr::null(),
|
std::ptr::null(),
|
||||||
0,
|
0,
|
||||||
@@ -349,12 +467,118 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(unsafe { *capabilities.as_ptr().cast::<u32>() }, 0);
|
assert_eq!(unsafe { *capabilities.as_ptr().cast::<u32>() }, 0);
|
||||||
|
|
||||||
// No command interpreter instruction was resumed by this identity test.
|
let exit = command.map(|_| {
|
||||||
|
assert_ne!(
|
||||||
|
unsafe { ResumeThread(process._thread.as_raw_handle()) },
|
||||||
|
u32::MAX
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { WaitForSingleObject(process.process.as_raw_handle(), 10_000) },
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let mut exit = 0;
|
||||||
|
assert_ne!(
|
||||||
|
unsafe { GetExitCodeProcess(process.process.as_raw_handle(), &mut exit) },
|
||||||
|
0
|
||||||
|
);
|
||||||
|
exit
|
||||||
|
});
|
||||||
job.terminate().unwrap();
|
job.terminate().unwrap();
|
||||||
drop(token);
|
drop(token);
|
||||||
drop(process);
|
drop(process);
|
||||||
drop(job);
|
drop(job);
|
||||||
drop(attributes);
|
drop(attributes);
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_container_can_read_only_explicitly_granted_package_objects() {
|
||||||
|
use std::os::windows::fs::OpenOptionsExt;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::*;
|
||||||
|
let profile = Profile::create().unwrap();
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let payload = directory.path().join("payload.txt");
|
||||||
|
let hidden = directory.path().join("ungranted.txt");
|
||||||
|
std::fs::write(&payload, b"verified package content").unwrap();
|
||||||
|
std::fs::write(&hidden, b"private sibling").unwrap();
|
||||||
|
let open = |path: &std::path::Path| {
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.access_mode(READ_CONTROL | WRITE_DAC)
|
||||||
|
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
|
||||||
|
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||||
|
.open(path)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let root_handle = open(directory.path());
|
||||||
|
let file_handle = open(&payload);
|
||||||
|
let read = format!("set /p value=<\"{}\"", payload.display());
|
||||||
|
assert_ne!(checked_process(&profile, Some(&read)), Some(0));
|
||||||
|
profile.grant_package_read_execute(&root_handle).unwrap();
|
||||||
|
// The directory ACE does not propagate to existing children.
|
||||||
|
assert_ne!(checked_process(&profile, Some(&read)), Some(0));
|
||||||
|
profile.grant_package_read_execute(&file_handle).unwrap();
|
||||||
|
assert_eq!(checked_process(&profile, Some(&read)), Some(0));
|
||||||
|
let other = Profile::create().unwrap();
|
||||||
|
assert_ne!(checked_process(&other, Some(&read)), Some(0));
|
||||||
|
other.remove().unwrap();
|
||||||
|
let created = directory.path().join("new.txt");
|
||||||
|
assert_ne!(
|
||||||
|
checked_process(
|
||||||
|
&profile,
|
||||||
|
Some(&format!("echo changed>\"{}\"", created.display()))
|
||||||
|
),
|
||||||
|
Some(0)
|
||||||
|
);
|
||||||
|
assert!(!created.exists());
|
||||||
|
assert_ne!(
|
||||||
|
checked_process(
|
||||||
|
&profile,
|
||||||
|
Some(&format!("set /p value=<\"{}\"", hidden.display()))
|
||||||
|
),
|
||||||
|
Some(0)
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
checked_process(
|
||||||
|
&profile,
|
||||||
|
Some(&format!("echo changed>\"{}\"", payload.display()))
|
||||||
|
),
|
||||||
|
Some(0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(&payload).unwrap(),
|
||||||
|
b"verified package content"
|
||||||
|
);
|
||||||
|
drop(file_handle);
|
||||||
|
drop(root_handle);
|
||||||
|
profile.remove().unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn package_grant_rejects_hardlinks_and_handles_without_acl_write_access() {
|
||||||
|
use std::os::windows::fs::OpenOptionsExt;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::*;
|
||||||
|
let profile = Profile::create().unwrap();
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let payload = directory.path().join("payload.txt");
|
||||||
|
let alias = directory.path().join("alias.txt");
|
||||||
|
std::fs::write(&payload, b"unchanged").unwrap();
|
||||||
|
let read_only = std::fs::File::open(&payload).unwrap();
|
||||||
|
assert!(profile.grant_package_read_execute(&read_only).is_err());
|
||||||
|
drop(read_only);
|
||||||
|
std::fs::hard_link(&payload, &alias).unwrap();
|
||||||
|
let handle = std::fs::OpenOptions::new()
|
||||||
|
.access_mode(READ_CONTROL | WRITE_DAC)
|
||||||
|
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
|
||||||
|
.open(&payload)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
profile
|
||||||
|
.grant_package_read_execute(&handle)
|
||||||
|
.unwrap_err()
|
||||||
|
.code,
|
||||||
|
"EXTENSION_CONTAINER_ACL_OBJECT_INVALID"
|
||||||
|
);
|
||||||
|
assert_eq!(std::fs::read(&alias).unwrap(), b"unchanged");
|
||||||
|
drop(handle);
|
||||||
profile.remove().unwrap();
|
profile.remove().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user