Windows Sandbox - Alpha version (#4905)
- Added the new codex-windows-sandbox crate that builds both a library
entry point (run_windows_sandbox_capture) and a CLI executable to launch
commands inside a Windows restricted-token sandbox, including ACL
management, capability SID provisioning, network lockdown, and output
capture
(windows-sandbox-rs/src/lib.rs:167, windows-sandbox-rs/src/main.rs:54).
- Introduced the experimental WindowsSandbox feature flag and wiring so
Windows builds can opt into the sandbox:
SandboxType::WindowsRestrictedToken, the in-process execution path, and
platform sandbox selection now honor the flag (core/src/features.rs:47,
core/src/config.rs:1224, core/src/safety.rs:19,
core/src/sandboxing/mod.rs:69, core/src/exec.rs:79,
core/src/exec.rs:172).
- Updated workspace metadata to include the new crate and its
Windows-specific dependencies so the core crate can link against it
(codex-rs/
Cargo.toml:91, core/Cargo.toml:86).
- Added a PowerShell bootstrap script that installs the Windows
toolchain, required CLI utilities, and builds the workspace to ease
development
on the platform (scripts/setup-windows.ps1:1).
- Landed a Python smoke-test suite that exercises
read-only/workspace-write policies, ACL behavior, and network denial for
the Windows sandbox
binary (windows-sandbox-rs/sandbox_smoketests.py:1).
This commit is contained in:
286
codex-rs/windows-sandbox-rs/src/acl.rs
Normal file
286
codex-rs/windows-sandbox-rs/src/acl.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::Security::AclSizeInformation;
|
||||
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::Authorization::GetSecurityInfo;
|
||||
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
||||
use windows_sys::Win32::Security::Authorization::SetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::Authorization::SetSecurityInfo;
|
||||
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||
use windows_sys::Win32::Security::EqualSid;
|
||||
use windows_sys::Win32::Security::GetAce;
|
||||
use windows_sys::Win32::Security::GetAclInformation;
|
||||
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
|
||||
use windows_sys::Win32::Security::ACE_HEADER;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
|
||||
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
|
||||
const SE_KERNEL_OBJECT: u32 = 6;
|
||||
const INHERIT_ONLY_ACE: u8 = 0x08;
|
||||
|
||||
pub unsafe fn dacl_has_write_allow_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -> bool {
|
||||
if p_dacl.is_null() {
|
||||
return false;
|
||||
}
|
||||
let mut info: ACL_SIZE_INFORMATION = std::mem::zeroed();
|
||||
let ok = GetAclInformation(
|
||||
p_dacl as *const ACL,
|
||||
&mut info as *mut _ as *mut c_void,
|
||||
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
|
||||
AclSizeInformation,
|
||||
);
|
||||
if ok == 0 {
|
||||
return false;
|
||||
}
|
||||
let count = info.AceCount as usize;
|
||||
for i in 0..count {
|
||||
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||
if GetAce(p_dacl as *const ACL, i as u32, &mut p_ace) == 0 {
|
||||
continue;
|
||||
}
|
||||
let hdr = &*(p_ace as *const ACE_HEADER);
|
||||
if hdr.AceType != 0 {
|
||||
continue; // ACCESS_ALLOWED_ACE_TYPE
|
||||
}
|
||||
// Ignore ACEs that are inherit-only (do not apply to the current object)
|
||||
if (hdr.AceFlags & INHERIT_ONLY_ACE) != 0 {
|
||||
continue;
|
||||
}
|
||||
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
|
||||
let mask = ace.Mask;
|
||||
let base = p_ace as usize;
|
||||
let sid_ptr =
|
||||
(base + std::mem::size_of::<ACE_HEADER>() + std::mem::size_of::<u32>()) as *mut c_void;
|
||||
let eq = EqualSid(sid_ptr, psid);
|
||||
if eq != 0 && (mask & FILE_GENERIC_WRITE) != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Compute effective rights for a trustee SID against a DACL and decide if write is effectively allowed.
|
||||
// This accounts for deny ACEs and ordering; falls back to a conservative per-ACE scan if the API fails.
|
||||
#[allow(dead_code)]
|
||||
pub unsafe fn dacl_effective_allows_write(p_dacl: *mut ACL, psid: *mut c_void) -> bool {
|
||||
if p_dacl.is_null() {
|
||||
return false;
|
||||
}
|
||||
use windows_sys::Win32::Security::Authorization::GetEffectiveRightsFromAclW;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||
|
||||
let trustee = TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||
ptstrName: psid as *mut u16,
|
||||
};
|
||||
let mut access: u32 = 0;
|
||||
let ok = GetEffectiveRightsFromAclW(p_dacl, &trustee, &mut access);
|
||||
if ok != 0 {
|
||||
// Check for generic or specific write bits
|
||||
let write_bits = FILE_GENERIC_WRITE
|
||||
| windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA
|
||||
| windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA
|
||||
| windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA
|
||||
| windows_sys::Win32::Storage::FileSystem::FILE_WRITE_ATTRIBUTES;
|
||||
return (access & write_bits) != 0;
|
||||
}
|
||||
// Fallback: simple allow ACE scan (already ignores inherit-only)
|
||||
dacl_has_write_allow_for_sid(p_dacl, psid)
|
||||
}
|
||||
pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code = GetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr(),
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
);
|
||||
if code != ERROR_SUCCESS {
|
||||
return Err(anyhow!("GetNamedSecurityInfoW failed: {}", code));
|
||||
}
|
||||
let mut added = false;
|
||||
if !dacl_has_write_allow_for_sid(p_dacl, psid) {
|
||||
let trustee = TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||
ptstrName: psid as *mut u16,
|
||||
};
|
||||
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||
explicit.grfAccessPermissions =
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
|
||||
explicit.grfAccessMode = 2; // SET_ACCESS
|
||||
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||
explicit.Trustee = trustee;
|
||||
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
|
||||
if code2 == ERROR_SUCCESS {
|
||||
let code3 = SetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr() as *mut u16,
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
p_new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if code3 == ERROR_SUCCESS {
|
||||
added = true;
|
||||
}
|
||||
if !p_new_dacl.is_null() {
|
||||
LocalFree(p_new_dacl as HLOCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !p_sd.is_null() {
|
||||
LocalFree(p_sd as HLOCAL);
|
||||
}
|
||||
Ok(added)
|
||||
}
|
||||
|
||||
pub unsafe fn revoke_ace(path: &Path, psid: *mut c_void) {
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code = GetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr(),
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
);
|
||||
if code != ERROR_SUCCESS {
|
||||
if !p_sd.is_null() {
|
||||
LocalFree(p_sd as HLOCAL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let trustee = TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||
ptstrName: psid as *mut u16,
|
||||
};
|
||||
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||
explicit.grfAccessPermissions = 0;
|
||||
explicit.grfAccessMode = 4; // REVOKE_ACCESS
|
||||
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||
explicit.Trustee = trustee;
|
||||
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
|
||||
if code2 == ERROR_SUCCESS {
|
||||
let _ = SetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr() as *mut u16,
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
p_new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if !p_new_dacl.is_null() {
|
||||
LocalFree(p_new_dacl as HLOCAL);
|
||||
}
|
||||
}
|
||||
if !p_sd.is_null() {
|
||||
LocalFree(p_sd as HLOCAL);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn allow_null_device(psid: *mut c_void) {
|
||||
let desired = 0x00020000 | 0x00040000; // READ_CONTROL | WRITE_DAC
|
||||
let h = CreateFileW(
|
||||
to_wide(r"\\\\.\\NUL").as_ptr(),
|
||||
desired,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
);
|
||||
if h == 0 || h == INVALID_HANDLE_VALUE {
|
||||
return;
|
||||
}
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code = GetSecurityInfo(
|
||||
h,
|
||||
SE_KERNEL_OBJECT as i32,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
);
|
||||
if code == ERROR_SUCCESS {
|
||||
let trustee = TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||
ptstrName: psid as *mut u16,
|
||||
};
|
||||
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||
explicit.grfAccessPermissions =
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
|
||||
explicit.grfAccessMode = 2; // SET_ACCESS
|
||||
explicit.grfInheritance = 0;
|
||||
explicit.Trustee = trustee;
|
||||
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
|
||||
if code2 == ERROR_SUCCESS {
|
||||
let _ = SetSecurityInfo(
|
||||
h,
|
||||
SE_KERNEL_OBJECT as i32,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
p_new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if !p_new_dacl.is_null() {
|
||||
LocalFree(p_new_dacl as HLOCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !p_sd.is_null() {
|
||||
LocalFree(p_sd as HLOCAL);
|
||||
}
|
||||
CloseHandle(h);
|
||||
}
|
||||
const CONTAINER_INHERIT_ACE: u32 = 0x2;
|
||||
const OBJECT_INHERIT_ACE: u32 = 0x1;
|
||||
37
codex-rs/windows-sandbox-rs/src/allow.rs
Normal file
37
codex-rs/windows-sandbox-rs/src/allow.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::policy::SandboxMode;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn compute_allow_paths(
|
||||
policy: &SandboxPolicy,
|
||||
_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut allow: Vec<PathBuf> = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
if matches!(policy.0, SandboxMode::WorkspaceWrite) {
|
||||
let abs = command_cwd.to_path_buf();
|
||||
if seen.insert(abs.to_string_lossy().to_string()) && abs.exists() {
|
||||
allow.push(abs);
|
||||
}
|
||||
}
|
||||
if !matches!(policy.0, SandboxMode::ReadOnly) {
|
||||
for key in ["TEMP", "TMP"] {
|
||||
if let Some(v) = env_map.get(key) {
|
||||
let abs = PathBuf::from(v);
|
||||
if seen.insert(abs.to_string_lossy().to_string()) && abs.exists() {
|
||||
allow.push(abs);
|
||||
}
|
||||
} else if let Ok(v) = std::env::var(key) {
|
||||
let abs = PathBuf::from(v);
|
||||
if seen.insert(abs.to_string_lossy().to_string()) && abs.exists() {
|
||||
allow.push(abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allow
|
||||
}
|
||||
147
codex-rs/windows-sandbox-rs/src/audit.rs
Normal file
147
codex-rs/windows-sandbox-rs/src/audit.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use crate::acl::dacl_effective_allows_write;
|
||||
use crate::token::world_sid;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
|
||||
fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf) {
|
||||
if let Ok(abs) = p.canonicalize() {
|
||||
if set.insert(abs.clone()) {
|
||||
out.push(abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gather_candidates(cwd: &Path, env: &std::collections::HashMap<String, String>) -> Vec<PathBuf> {
|
||||
let mut set: HashSet<PathBuf> = HashSet::new();
|
||||
let mut out: Vec<PathBuf> = Vec::new();
|
||||
// Core roots
|
||||
for p in [
|
||||
PathBuf::from("C:/"),
|
||||
PathBuf::from("C:/Windows"),
|
||||
PathBuf::from("C:/ProgramData"),
|
||||
] {
|
||||
unique_push(&mut set, &mut out, p);
|
||||
}
|
||||
// User roots
|
||||
if let Some(up) = std::env::var_os("USERPROFILE") {
|
||||
unique_push(&mut set, &mut out, PathBuf::from(up));
|
||||
}
|
||||
if let Some(pubp) = std::env::var_os("PUBLIC") {
|
||||
unique_push(&mut set, &mut out, PathBuf::from(pubp));
|
||||
}
|
||||
// CWD
|
||||
unique_push(&mut set, &mut out, cwd.to_path_buf());
|
||||
// TEMP/TMP
|
||||
for k in ["TEMP", "TMP"] {
|
||||
if let Some(v) = env.get(k).cloned().or_else(|| std::env::var(k).ok()) {
|
||||
unique_push(&mut set, &mut out, PathBuf::from(v));
|
||||
}
|
||||
}
|
||||
// PATH entries
|
||||
if let Some(path) = env
|
||||
.get("PATH")
|
||||
.cloned()
|
||||
.or_else(|| std::env::var("PATH").ok())
|
||||
{
|
||||
for part in path.split(std::path::MAIN_SEPARATOR) {
|
||||
if !part.is_empty() {
|
||||
unique_push(&mut set, &mut out, PathBuf::from(part));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code = GetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr(),
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
);
|
||||
if code != ERROR_SUCCESS {
|
||||
if !p_sd.is_null() {
|
||||
LocalFree(p_sd as HLOCAL);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
let mut world = world_sid()?;
|
||||
let psid_world = world.as_mut_ptr() as *mut c_void;
|
||||
let has = dacl_effective_allows_write(p_dacl, psid_world);
|
||||
if !p_sd.is_null() {
|
||||
LocalFree(p_sd as HLOCAL);
|
||||
}
|
||||
Ok(has)
|
||||
}
|
||||
|
||||
pub fn audit_everyone_writable(
|
||||
cwd: &Path,
|
||||
env: &std::collections::HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let mut flagged: Vec<PathBuf> = Vec::new();
|
||||
let mut checked = 0usize;
|
||||
let candidates = gather_candidates(cwd, env);
|
||||
for root in candidates {
|
||||
if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
|
||||
break;
|
||||
}
|
||||
checked += 1;
|
||||
if unsafe { path_has_world_write_allow(&root)? } {
|
||||
flagged.push(root.clone());
|
||||
}
|
||||
// one level down best-effort
|
||||
if let Ok(read) = std::fs::read_dir(&root) {
|
||||
for ent in read.flatten().take(50) {
|
||||
let p = ent.path();
|
||||
if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
|
||||
break;
|
||||
}
|
||||
// Skip reparse points (symlinks/junctions) to avoid auditing link ACLs
|
||||
let ft = match ent.file_type() {
|
||||
Ok(ft) => ft,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ft.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
if ft.is_dir() {
|
||||
checked += 1;
|
||||
if unsafe { path_has_world_write_allow(&p)? } {
|
||||
flagged.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !flagged.is_empty() {
|
||||
let mut list = String::new();
|
||||
for p in flagged {
|
||||
list.push_str(&format!("\n - {}", p.display()));
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"Refusing to run: found directories writable by Everyone: {}",
|
||||
list
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
50
codex-rs/windows-sandbox-rs/src/cap.rs
Normal file
50
codex-rs/windows-sandbox-rs/src/cap.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct CapSids {
|
||||
pub workspace: String,
|
||||
pub readonly: String,
|
||||
}
|
||||
|
||||
pub fn cap_sid_file(policy_cwd: &Path) -> PathBuf {
|
||||
policy_cwd.join(".codex").join("cap_sid")
|
||||
}
|
||||
|
||||
fn make_random_cap_sid_string() -> String {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let a = rng.next_u32();
|
||||
let b = rng.next_u32();
|
||||
let c = rng.next_u32();
|
||||
let d = rng.next_u32();
|
||||
format!("S-1-5-21-{}-{}-{}-{}", a, b, c, d)
|
||||
}
|
||||
|
||||
pub fn load_or_create_cap_sids(policy_cwd: &Path) -> CapSids {
|
||||
let path = cap_sid_file(policy_cwd);
|
||||
if path.exists() {
|
||||
if let Ok(txt) = fs::read_to_string(&path) {
|
||||
let t = txt.trim();
|
||||
if t.starts_with('{') && t.ends_with('}') {
|
||||
if let Ok(obj) = serde_json::from_str::<CapSids>(t) {
|
||||
return obj;
|
||||
}
|
||||
} else if !t.is_empty() {
|
||||
return CapSids {
|
||||
workspace: t.to_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
CapSids {
|
||||
workspace: make_random_cap_sid_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
}
|
||||
}
|
||||
165
codex-rs/windows-sandbox-rs/src/env.rs
Normal file
165
codex-rs/windows-sandbox-rs/src/env.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::fs::{self};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn normalize_null_device_env(env_map: &mut HashMap<String, String>) {
|
||||
let keys: Vec<String> = env_map.keys().cloned().collect();
|
||||
for k in keys {
|
||||
if let Some(v) = env_map.get(&k).cloned() {
|
||||
let t = v.trim().to_ascii_lowercase();
|
||||
if t == "/dev/null" || t == "\\\\\\\\dev\\\\\\\\null" {
|
||||
env_map.insert(k, "NUL".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_non_interactive_pager(env_map: &mut HashMap<String, String>) {
|
||||
env_map
|
||||
.entry("GIT_PAGER".into())
|
||||
.or_insert_with(|| "more.com".into());
|
||||
env_map
|
||||
.entry("PAGER".into())
|
||||
.or_insert_with(|| "more.com".into());
|
||||
env_map.entry("LESS".into()).or_insert_with(|| "".into());
|
||||
}
|
||||
|
||||
fn prepend_path(env_map: &mut HashMap<String, String>, prefix: &str) {
|
||||
let existing = env_map
|
||||
.get("PATH")
|
||||
.cloned()
|
||||
.or_else(|| env::var("PATH").ok())
|
||||
.unwrap_or_default();
|
||||
let parts: Vec<String> = existing.split(';').map(|s| s.to_string()).collect();
|
||||
if parts
|
||||
.first()
|
||||
.map(|p| p.eq_ignore_ascii_case(prefix))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let mut new_path = String::new();
|
||||
new_path.push_str(prefix);
|
||||
if !existing.is_empty() {
|
||||
new_path.push(';');
|
||||
new_path.push_str(&existing);
|
||||
}
|
||||
env_map.insert("PATH".into(), new_path);
|
||||
}
|
||||
|
||||
fn reorder_pathext_for_stubs(env_map: &mut HashMap<String, String>) {
|
||||
let default = env_map
|
||||
.get("PATHEXT")
|
||||
.cloned()
|
||||
.or_else(|| env::var("PATHEXT").ok())
|
||||
.unwrap_or(".COM;.EXE;.BAT;.CMD".to_string());
|
||||
let exts: Vec<String> = default
|
||||
.split(';')
|
||||
.filter(|e| !e.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let exts_norm: Vec<String> = exts.iter().map(|e| e.to_ascii_uppercase()).collect();
|
||||
let want = [".BAT", ".CMD"]; // move to front if present
|
||||
let mut front: Vec<String> = Vec::new();
|
||||
for w in want {
|
||||
if let Some(idx) = exts_norm.iter().position(|e| e == w) {
|
||||
front.push(exts[idx].clone());
|
||||
}
|
||||
}
|
||||
let rest: Vec<String> = exts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| {
|
||||
let up = &exts_norm[*i];
|
||||
up != ".BAT" && up != ".CMD"
|
||||
})
|
||||
.map(|(_, e)| e)
|
||||
.collect();
|
||||
let mut combined = Vec::new();
|
||||
combined.extend(front);
|
||||
combined.extend(rest);
|
||||
env_map.insert("PATHEXT".into(), combined.join(";"));
|
||||
}
|
||||
|
||||
fn ensure_denybin(tools: &[&str], denybin_dir: Option<&Path>) -> Result<PathBuf> {
|
||||
let base = match denybin_dir {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => {
|
||||
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("no home dir"))?;
|
||||
home.join(".sbx-denybin")
|
||||
}
|
||||
};
|
||||
fs::create_dir_all(&base)?;
|
||||
for tool in tools {
|
||||
for ext in [".bat", ".cmd"] {
|
||||
let path = base.join(format!("{}{}", tool, ext));
|
||||
if !path.exists() {
|
||||
let mut f = File::create(&path)?;
|
||||
f.write_all(b"@echo off\\r\\nexit /b 1\\r\\n")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
pub fn apply_no_network_to_env(env_map: &mut HashMap<String, String>) -> Result<()> {
|
||||
env_map.insert("SBX_NONET_ACTIVE".into(), "1".into());
|
||||
env_map
|
||||
.entry("HTTP_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("HTTPS_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("ALL_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("NO_PROXY".into())
|
||||
.or_insert_with(|| "localhost,127.0.0.1,::1".into());
|
||||
env_map
|
||||
.entry("PIP_NO_INDEX".into())
|
||||
.or_insert_with(|| "1".into());
|
||||
env_map
|
||||
.entry("PIP_DISABLE_PIP_VERSION_CHECK".into())
|
||||
.or_insert_with(|| "1".into());
|
||||
env_map
|
||||
.entry("NPM_CONFIG_OFFLINE".into())
|
||||
.or_insert_with(|| "true".into());
|
||||
env_map
|
||||
.entry("CARGO_NET_OFFLINE".into())
|
||||
.or_insert_with(|| "true".into());
|
||||
env_map
|
||||
.entry("GIT_HTTP_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("GIT_HTTPS_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("GIT_SSH_COMMAND".into())
|
||||
.or_insert_with(|| "cmd /c exit 1".into());
|
||||
env_map
|
||||
.entry("GIT_ALLOW_PROTOCOLS".into())
|
||||
.or_insert_with(|| "".into());
|
||||
|
||||
// Block interactive network tools that bypass HTTP(S) proxy settings, but
|
||||
// allow curl/wget to run so commands like `curl --version` still succeed.
|
||||
// Network access is disabled via proxy envs above.
|
||||
let base = ensure_denybin(&["ssh", "scp"], None)?;
|
||||
// Clean up any stale stubs from previous runs so real curl/wget can run.
|
||||
for tool in ["curl", "wget"] {
|
||||
for ext in [".bat", ".cmd"] {
|
||||
let p = base.join(format!("{}{}", tool, ext));
|
||||
if p.exists() {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
prepend_path(env_map, &base.to_string_lossy());
|
||||
reorder_pathext_for_stubs(env_map);
|
||||
Ok(())
|
||||
}
|
||||
452
codex-rs/windows-sandbox-rs/src/lib.rs
Normal file
452
codex-rs/windows-sandbox-rs/src/lib.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
macro_rules! windows_modules {
|
||||
($($name:ident),+ $(,)?) => {
|
||||
$(#[cfg(target_os = "windows")] mod $name;)+
|
||||
};
|
||||
}
|
||||
|
||||
windows_modules!(acl, allow, audit, cap, env, logging, policy, token, winutil);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::preflight_audit_everyone_writable;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::run_windows_sandbox_capture;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::CaptureResult;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::preflight_audit_everyone_writable;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::run_windows_sandbox_capture;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::CaptureResult;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_impl {
|
||||
use super::acl::add_allow_ace;
|
||||
use super::acl::allow_null_device;
|
||||
use super::acl::revoke_ace;
|
||||
use super::allow::compute_allow_paths;
|
||||
use super::audit;
|
||||
use super::cap::cap_sid_file;
|
||||
use super::cap::load_or_create_cap_sids;
|
||||
use super::env::apply_no_network_to_env;
|
||||
use super::env::ensure_non_interactive_pager;
|
||||
use super::env::normalize_null_device_env;
|
||||
use super::logging::debug_log;
|
||||
use super::logging::log_failure;
|
||||
use super::logging::log_start;
|
||||
use super::logging::log_success;
|
||||
use super::policy::SandboxMode;
|
||||
use super::policy::SandboxPolicy;
|
||||
use super::token::convert_string_sid_to_sid;
|
||||
use super::winutil::format_last_error;
|
||||
use super::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::SetHandleInformation;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
|
||||
use windows_sys::Win32::System::Pipes::CreatePipe;
|
||||
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
||||
|
||||
type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE));
|
||||
|
||||
fn ensure_dir(p: &Path) -> Result<()> {
|
||||
if let Some(d) = p.parent() {
|
||||
std::fs::create_dir_all(d)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
||||
let mut items: Vec<(String, String)> =
|
||||
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
items.sort_by(|a, b| {
|
||||
a.0.to_uppercase()
|
||||
.cmp(&b.0.to_uppercase())
|
||||
.then(a.0.cmp(&b.0))
|
||||
});
|
||||
let mut w: Vec<u16> = Vec::new();
|
||||
for (k, v) in items {
|
||||
let mut s = to_wide(format!("{}={}", k, v));
|
||||
s.pop();
|
||||
w.extend_from_slice(&s);
|
||||
w.push(0);
|
||||
}
|
||||
w.push(0);
|
||||
w
|
||||
}
|
||||
|
||||
// Quote a single Windows command-line argument following the rules used by
|
||||
// CommandLineToArgvW/CRT so that spaces, quotes, and backslashes are preserved.
|
||||
// Reference behavior matches Rust std::process::Command on Windows.
|
||||
fn quote_windows_arg(arg: &str) -> String {
|
||||
let needs_quotes = arg.is_empty()
|
||||
|| arg
|
||||
.chars()
|
||||
.any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"'));
|
||||
if !needs_quotes {
|
||||
return arg.to_string();
|
||||
}
|
||||
|
||||
let mut quoted = String::with_capacity(arg.len() + 2);
|
||||
quoted.push('"');
|
||||
let mut backslashes = 0;
|
||||
for ch in arg.chars() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
backslashes += 1;
|
||||
}
|
||||
'"' => {
|
||||
quoted.push_str(&"\\".repeat(backslashes * 2 + 1));
|
||||
quoted.push('"');
|
||||
backslashes = 0;
|
||||
}
|
||||
_ => {
|
||||
if backslashes > 0 {
|
||||
quoted.push_str(&"\\".repeat(backslashes));
|
||||
backslashes = 0;
|
||||
}
|
||||
quoted.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if backslashes > 0 {
|
||||
quoted.push_str(&"\\".repeat(backslashes * 2));
|
||||
}
|
||||
quoted.push('"');
|
||||
quoted
|
||||
}
|
||||
|
||||
unsafe fn setup_stdio_pipes() -> io::Result<PipeHandles> {
|
||||
let mut in_r: HANDLE = 0;
|
||||
let mut in_w: HANDLE = 0;
|
||||
let mut out_r: HANDLE = 0;
|
||||
let mut out_w: HANDLE = 0;
|
||||
let mut err_r: HANDLE = 0;
|
||||
let mut err_w: HANDLE = 0;
|
||||
if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w)))
|
||||
}
|
||||
|
||||
pub struct CaptureResult {
|
||||
pub exit_code: i32,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
pub fn preflight_audit_everyone_writable(
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
audit::audit_everyone_writable(cwd, env_map)
|
||||
}
|
||||
|
||||
pub fn run_windows_sandbox_capture(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
) -> Result<CaptureResult> {
|
||||
let policy = SandboxPolicy::parse(policy_json_or_preset)?;
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
apply_no_network_to_env(&mut env_map)?;
|
||||
|
||||
let current_dir = cwd.to_path_buf();
|
||||
// for now, don't fail if we detect world-writable directories
|
||||
// audit::audit_everyone_writable(¤t_dir, &env_map)?;
|
||||
log_start(&command);
|
||||
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
||||
match &policy.0 {
|
||||
SandboxMode::ReadOnly => {
|
||||
let caps = load_or_create_cap_sids(sandbox_policy_cwd);
|
||||
ensure_dir(&cap_sid_file(sandbox_policy_cwd))?;
|
||||
fs::write(
|
||||
cap_sid_file(sandbox_policy_cwd),
|
||||
serde_json::to_string(&caps)?,
|
||||
)?;
|
||||
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
|
||||
super::token::create_readonly_token_with_cap(psid)?
|
||||
}
|
||||
SandboxMode::WorkspaceWrite => {
|
||||
let caps = load_or_create_cap_sids(sandbox_policy_cwd);
|
||||
ensure_dir(&cap_sid_file(sandbox_policy_cwd))?;
|
||||
fs::write(
|
||||
cap_sid_file(sandbox_policy_cwd),
|
||||
serde_json::to_string(&caps)?,
|
||||
)?;
|
||||
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
super::token::create_workspace_write_token_with_cap(psid)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if matches!(policy.0, SandboxMode::WorkspaceWrite) {
|
||||
if let Ok(base) = super::token::get_current_token_for_restriction() {
|
||||
if let Ok(bytes) = super::token::get_logon_sid_bytes(base) {
|
||||
let mut tmp = bytes.clone();
|
||||
let psid2 = tmp.as_mut_ptr() as *mut c_void;
|
||||
allow_null_device(psid2);
|
||||
}
|
||||
windows_sys::Win32::Foundation::CloseHandle(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let persist_aces = matches!(policy.0, SandboxMode::WorkspaceWrite);
|
||||
let allow = compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
||||
unsafe {
|
||||
for p in &allow {
|
||||
if let Ok(added) = add_allow_ace(p, psid_to_use) {
|
||||
if added {
|
||||
if persist_aces {
|
||||
if p.is_dir() {
|
||||
// best-effort seeding omitted intentionally
|
||||
}
|
||||
} else {
|
||||
guards.push((p.clone(), psid_to_use));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allow_null_device(psid_to_use);
|
||||
}
|
||||
|
||||
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
|
||||
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
|
||||
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
si.dwFlags |= STARTF_USESTDHANDLES;
|
||||
si.hStdInput = in_r;
|
||||
si.hStdOutput = out_w;
|
||||
si.hStdError = err_w;
|
||||
|
||||
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let cmdline_str = command
|
||||
.iter()
|
||||
.map(|a| quote_windows_arg(a))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||||
let env_block = make_env_block(&env_map);
|
||||
let desktop = to_wide("Winsta0\\Default");
|
||||
si.lpDesktop = desktop.as_ptr() as *mut u16;
|
||||
let spawn_res = unsafe {
|
||||
CreateProcessAsUserW(
|
||||
h_token,
|
||||
ptr::null(),
|
||||
cmdline.as_mut_ptr(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
env_block.as_ptr() as *mut c_void,
|
||||
to_wide(cwd).as_ptr(),
|
||||
&si,
|
||||
&mut pi,
|
||||
)
|
||||
};
|
||||
if spawn_res == 0 {
|
||||
let err = unsafe { GetLastError() } as i32;
|
||||
let dbg = format!(
|
||||
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
|
||||
err,
|
||||
format_last_error(err),
|
||||
cwd.display(),
|
||||
cmdline_str,
|
||||
env_block.len(),
|
||||
si.dwFlags,
|
||||
);
|
||||
debug_log(&dbg);
|
||||
unsafe {
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(in_w);
|
||||
CloseHandle(out_r);
|
||||
CloseHandle(out_w);
|
||||
CloseHandle(err_r);
|
||||
CloseHandle(err_w);
|
||||
CloseHandle(h_token);
|
||||
}
|
||||
return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
CloseHandle(in_r);
|
||||
// Close the parent's stdin write end so the child sees EOF immediately.
|
||||
CloseHandle(in_w);
|
||||
CloseHandle(out_w);
|
||||
CloseHandle(err_w);
|
||||
}
|
||||
|
||||
let (tx_out, rx_out) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||
let (tx_err, rx_err) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||
let t_out = std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
out_r,
|
||||
tmp.as_mut_ptr(),
|
||||
tmp.len() as u32,
|
||||
&mut read_bytes,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||
}
|
||||
let _ = tx_out.send(buf);
|
||||
});
|
||||
let t_err = std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
err_r,
|
||||
tmp.as_mut_ptr(),
|
||||
tmp.len() as u32,
|
||||
&mut read_bytes,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||
}
|
||||
let _ = tx_err.send(buf);
|
||||
});
|
||||
|
||||
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
||||
let res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||
let timed_out = res == 0x0000_0102;
|
||||
let mut exit_code_u32: u32 = 1;
|
||||
if !timed_out {
|
||||
unsafe {
|
||||
GetExitCodeProcess(pi.hProcess, &mut exit_code_u32);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
if pi.hThread != 0 {
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
if pi.hProcess != 0 {
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
CloseHandle(h_token);
|
||||
}
|
||||
let _ = t_out.join();
|
||||
let _ = t_err.join();
|
||||
let stdout = rx_out.recv().unwrap_or_default();
|
||||
let stderr = rx_err.recv().unwrap_or_default();
|
||||
let exit_code = if timed_out {
|
||||
128 + 64
|
||||
} else {
|
||||
exit_code_u32 as i32
|
||||
};
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command);
|
||||
} else {
|
||||
log_failure(&command, &format!("exit code {}", exit_code));
|
||||
}
|
||||
|
||||
if !persist_aces {
|
||||
unsafe {
|
||||
for (p, sid) in guards {
|
||||
revoke_ace(&p, sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CaptureResult {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
timed_out,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod stub {
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CaptureResult {
|
||||
pub exit_code: i32,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
pub fn preflight_audit_everyone_writable(
|
||||
_cwd: &Path,
|
||||
_env_map: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
|
||||
pub fn run_windows_sandbox_capture(
|
||||
_policy_json_or_preset: &str,
|
||||
_sandbox_policy_cwd: &Path,
|
||||
_command: Vec<String>,
|
||||
_cwd: &Path,
|
||||
_env_map: HashMap<String, String>,
|
||||
_timeout_ms: Option<u64>,
|
||||
) -> Result<CaptureResult> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
}
|
||||
47
codex-rs/windows-sandbox-rs/src/logging.rs
Normal file
47
codex-rs/windows-sandbox-rs/src/logging.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
||||
pub const LOG_FILE_NAME: &str = "sandbox_commands.rust.log";
|
||||
|
||||
fn preview(command: &[String]) -> String {
|
||||
let joined = command.join(" ");
|
||||
if joined.len() <= LOG_COMMAND_PREVIEW_LIMIT {
|
||||
joined
|
||||
} else {
|
||||
joined[..LOG_COMMAND_PREVIEW_LIMIT].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn append_line(line: &str) {
|
||||
if let Ok(mut f) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(LOG_FILE_NAME)
|
||||
{
|
||||
let _ = writeln!(f, "{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_start(command: &[String]) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("START: {}", p));
|
||||
}
|
||||
|
||||
pub fn log_success(command: &[String]) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("SUCCESS: {}", p));
|
||||
}
|
||||
|
||||
pub fn log_failure(command: &[String], detail: &str) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("FAILURE: {} ({})", p, detail));
|
||||
}
|
||||
|
||||
// Debug logging helper. Emits only when SBX_DEBUG=1 to avoid noisy logs.
|
||||
pub fn debug_log(msg: &str) {
|
||||
if std::env::var("SBX_DEBUG").ok().as_deref() == Some("1") {
|
||||
append_line(&format!("DEBUG: {}", msg));
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
}
|
||||
36
codex-rs/windows-sandbox-rs/src/policy.rs
Normal file
36
codex-rs/windows-sandbox-rs/src/policy.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PolicyJson {
|
||||
pub mode: String,
|
||||
#[serde(default)]
|
||||
pub workspace_roots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SandboxMode {
|
||||
ReadOnly,
|
||||
WorkspaceWrite,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SandboxPolicy(pub SandboxMode);
|
||||
|
||||
impl SandboxPolicy {
|
||||
pub fn parse(value: &str) -> Result<Self> {
|
||||
match value {
|
||||
"read-only" => Ok(SandboxPolicy(SandboxMode::ReadOnly)),
|
||||
"workspace-write" => Ok(SandboxPolicy(SandboxMode::WorkspaceWrite)),
|
||||
other => {
|
||||
let pj: PolicyJson = serde_json::from_str(other)?;
|
||||
Ok(match pj.mode.as_str() {
|
||||
"read-only" => SandboxPolicy(SandboxMode::ReadOnly),
|
||||
"workspace-write" => SandboxPolicy(SandboxMode::WorkspaceWrite),
|
||||
_ => SandboxPolicy(SandboxMode::ReadOnly),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
codex-rs/windows-sandbox-rs/src/process.rs
Normal file
193
codex-rs/windows-sandbox-rs/src/process.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use crate::logging;
|
||||
use crate::winutil::format_last_error;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::SetHandleInformation;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::System::Console::GetStdHandle;
|
||||
use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;
|
||||
use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
||||
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
|
||||
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
|
||||
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
|
||||
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
|
||||
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
|
||||
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
||||
|
||||
pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
||||
let mut items: Vec<(String, String)> =
|
||||
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
items.sort_by(|a, b| {
|
||||
a.0.to_uppercase()
|
||||
.cmp(&b.0.to_uppercase())
|
||||
.then(a.0.cmp(&b.0))
|
||||
});
|
||||
let mut w: Vec<u16> = Vec::new();
|
||||
for (k, v) in items {
|
||||
let mut s = to_wide(format!("{}={}", k, v));
|
||||
s.pop();
|
||||
w.extend_from_slice(&s);
|
||||
w.push(0);
|
||||
}
|
||||
w.push(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn quote_arg(a: &str) -> String {
|
||||
let needs_quote = a.is_empty() || a.chars().any(|ch| ch.is_whitespace() || ch == '"');
|
||||
if !needs_quote {
|
||||
return a.to_string();
|
||||
}
|
||||
let mut out = String::from("\"");
|
||||
let mut bs: usize = 0;
|
||||
for ch in a.chars() {
|
||||
if (ch as u32) == 92 {
|
||||
bs += 1;
|
||||
continue;
|
||||
}
|
||||
if ch == '"' {
|
||||
out.push_str(&"\\".repeat(bs * 2 + 1));
|
||||
out.push('"');
|
||||
bs = 0;
|
||||
continue;
|
||||
}
|
||||
if bs > 0 {
|
||||
out.push_str(&"\\".repeat(bs * 2));
|
||||
bs = 0;
|
||||
}
|
||||
out.push(ch);
|
||||
}
|
||||
if bs > 0 {
|
||||
out.push_str(&"\\".repeat(bs * 2));
|
||||
}
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> {
|
||||
for kind in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
|
||||
let h = GetStdHandle(kind);
|
||||
if h == 0 || h == INVALID_HANDLE_VALUE {
|
||||
return Err(anyhow!("GetStdHandle failed: {}", GetLastError()));
|
||||
}
|
||||
if SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(anyhow!("SetHandleInformation failed: {}", GetLastError()));
|
||||
}
|
||||
}
|
||||
si.dwFlags |= STARTF_USESTDHANDLES;
|
||||
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe fn create_process_as_user(
|
||||
h_token: HANDLE,
|
||||
argv: &[String],
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> {
|
||||
let cmdline_str = argv
|
||||
.iter()
|
||||
.map(|a| quote_arg(a))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||||
let env_block = make_env_block(env_map);
|
||||
let mut si: STARTUPINFOW = std::mem::zeroed();
|
||||
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
// Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED
|
||||
// if lpDesktop is not set when launching with a restricted token.
|
||||
// Point explicitly at the interactive desktop.
|
||||
let desktop = to_wide("Winsta0\\Default");
|
||||
si.lpDesktop = desktop.as_ptr() as *mut u16;
|
||||
ensure_inheritable_stdio(&mut si)?;
|
||||
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
|
||||
let ok = CreateProcessAsUserW(
|
||||
h_token,
|
||||
std::ptr::null(),
|
||||
cmdline.as_mut_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
1,
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
env_block.as_ptr() as *mut c_void,
|
||||
to_wide(cwd).as_ptr(),
|
||||
&si,
|
||||
&mut pi,
|
||||
);
|
||||
if ok == 0 {
|
||||
let err = GetLastError() as i32;
|
||||
let msg = format!(
|
||||
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
|
||||
err,
|
||||
format_last_error(err),
|
||||
cwd.display(),
|
||||
cmdline_str,
|
||||
env_block.len(),
|
||||
si.dwFlags,
|
||||
);
|
||||
logging::debug_log(&msg);
|
||||
return Err(anyhow!("CreateProcessAsUserW failed: {}", err));
|
||||
}
|
||||
Ok((pi, si))
|
||||
}
|
||||
|
||||
pub unsafe fn wait_process_and_exitcode(pi: &PROCESS_INFORMATION) -> Result<i32> {
|
||||
let res = WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
if res != 0 {
|
||||
return Err(anyhow!("WaitForSingleObject failed: {}", GetLastError()));
|
||||
}
|
||||
let mut code: u32 = 0;
|
||||
if GetExitCodeProcess(pi.hProcess, &mut code) == 0 {
|
||||
return Err(anyhow!("GetExitCodeProcess failed: {}", GetLastError()));
|
||||
}
|
||||
Ok(code as i32)
|
||||
}
|
||||
|
||||
pub unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
|
||||
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
|
||||
if h == 0 {
|
||||
return Err(anyhow!("CreateJobObjectW failed: {}", GetLastError()));
|
||||
}
|
||||
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
let ok = SetInformationJobObject(
|
||||
h,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&mut limits as *mut _ as *mut c_void,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!(
|
||||
"SetInformationJobObject failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
pub unsafe fn assign_to_job(h_job: HANDLE, h_process: HANDLE) -> Result<()> {
|
||||
if AssignProcessToJobObject(h_job, h_process) == 0 {
|
||||
return Err(anyhow!(
|
||||
"AssignProcessToJobObject failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
272
codex-rs/windows-sandbox-rs/src/token.rs
Normal file
272
codex-rs/windows-sandbox-rs/src/token.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use std::ffi::c_void;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::LUID;
|
||||
use windows_sys::Win32::Security::AdjustTokenPrivileges;
|
||||
use windows_sys::Win32::Security::CopySid;
|
||||
use windows_sys::Win32::Security::CreateRestrictedToken;
|
||||
use windows_sys::Win32::Security::CreateWellKnownSid;
|
||||
use windows_sys::Win32::Security::GetLengthSid;
|
||||
use windows_sys::Win32::Security::GetTokenInformation;
|
||||
use windows_sys::Win32::Security::LookupPrivilegeValueW;
|
||||
|
||||
use windows_sys::Win32::Security::TokenGroups;
|
||||
use windows_sys::Win32::Security::SID_AND_ATTRIBUTES;
|
||||
use windows_sys::Win32::Security::TOKEN_ADJUST_DEFAULT;
|
||||
use windows_sys::Win32::Security::TOKEN_ADJUST_PRIVILEGES;
|
||||
use windows_sys::Win32::Security::TOKEN_ADJUST_SESSIONID;
|
||||
use windows_sys::Win32::Security::TOKEN_ASSIGN_PRIMARY;
|
||||
use windows_sys::Win32::Security::TOKEN_DUPLICATE;
|
||||
use windows_sys::Win32::Security::TOKEN_PRIVILEGES;
|
||||
use windows_sys::Win32::Security::TOKEN_QUERY;
|
||||
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
||||
|
||||
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
||||
const LUA_TOKEN: u32 = 0x04;
|
||||
const WRITE_RESTRICTED: u32 = 0x08;
|
||||
const WIN_WORLD_SID: i32 = 1;
|
||||
const SE_GROUP_LOGON_ID: u32 = 0xC0000000;
|
||||
|
||||
pub unsafe fn world_sid() -> Result<Vec<u8>> {
|
||||
let mut size: u32 = 0;
|
||||
CreateWellKnownSid(
|
||||
WIN_WORLD_SID,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut size,
|
||||
);
|
||||
let mut buf: Vec<u8> = vec![0u8; size as usize];
|
||||
let ok = CreateWellKnownSid(
|
||||
WIN_WORLD_SID,
|
||||
std::ptr::null_mut(),
|
||||
buf.as_mut_ptr() as *mut c_void,
|
||||
&mut size,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateWellKnownSid failed: {}", GetLastError()));
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
|
||||
#[link(name = "advapi32")]
|
||||
extern "system" {
|
||||
fn ConvertStringSidToSidW(StringSid: *const u16, Sid: *mut *mut c_void) -> i32;
|
||||
}
|
||||
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||
let ok = unsafe { ConvertStringSidToSidW(to_wide(s).as_ptr(), &mut psid) };
|
||||
if ok != 0 {
|
||||
Some(psid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_current_token_for_restriction() -> Result<HANDLE> {
|
||||
let desired = TOKEN_DUPLICATE
|
||||
| TOKEN_QUERY
|
||||
| TOKEN_ASSIGN_PRIMARY
|
||||
| TOKEN_ADJUST_DEFAULT
|
||||
| TOKEN_ADJUST_SESSIONID
|
||||
| TOKEN_ADJUST_PRIVILEGES;
|
||||
let mut h: HANDLE = 0;
|
||||
#[link(name = "advapi32")]
|
||||
extern "system" {
|
||||
fn OpenProcessToken(
|
||||
ProcessHandle: HANDLE,
|
||||
DesiredAccess: u32,
|
||||
TokenHandle: *mut HANDLE,
|
||||
) -> i32;
|
||||
}
|
||||
let ok = unsafe { OpenProcessToken(GetCurrentProcess(), desired, &mut h) };
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("OpenProcessToken failed: {}", GetLastError()));
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
pub unsafe fn get_logon_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>> {
|
||||
unsafe fn scan_token_groups_for_logon(h: HANDLE) -> Option<Vec<u8>> {
|
||||
let mut needed: u32 = 0;
|
||||
GetTokenInformation(h, TokenGroups, std::ptr::null_mut(), 0, &mut needed);
|
||||
if needed == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buf: Vec<u8> = vec![0u8; needed as usize];
|
||||
let ok = GetTokenInformation(
|
||||
h,
|
||||
TokenGroups,
|
||||
buf.as_mut_ptr() as *mut c_void,
|
||||
needed,
|
||||
&mut needed,
|
||||
);
|
||||
if ok == 0 || (needed as usize) < std::mem::size_of::<u32>() {
|
||||
return None;
|
||||
}
|
||||
let group_count = std::ptr::read_unaligned(buf.as_ptr() as *const u32) as usize;
|
||||
// TOKEN_GROUPS layout is: DWORD GroupCount; SID_AND_ATTRIBUTES Groups[];
|
||||
// On 64-bit, Groups is aligned to pointer alignment after 4-byte GroupCount.
|
||||
let after_count = unsafe { buf.as_ptr().add(std::mem::size_of::<u32>()) } as usize;
|
||||
let align = std::mem::align_of::<SID_AND_ATTRIBUTES>();
|
||||
let aligned = (after_count + (align - 1)) & !(align - 1);
|
||||
let groups_ptr = aligned as *const SID_AND_ATTRIBUTES;
|
||||
for i in 0..group_count {
|
||||
let entry: SID_AND_ATTRIBUTES = std::ptr::read_unaligned(groups_ptr.add(i));
|
||||
if (entry.Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID {
|
||||
let sid = entry.Sid;
|
||||
let sid_len = GetLengthSid(sid);
|
||||
if sid_len == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut out = vec![0u8; sid_len as usize];
|
||||
if CopySid(sid_len, out.as_mut_ptr() as *mut c_void, sid) == 0 {
|
||||
return None;
|
||||
}
|
||||
return Some(out);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
if let Some(v) = scan_token_groups_for_logon(h_token) {
|
||||
return Ok(v);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct TOKEN_LINKED_TOKEN {
|
||||
linked_token: HANDLE,
|
||||
}
|
||||
const TOKEN_LINKED_TOKEN_CLASS: i32 = 19; // TokenLinkedToken
|
||||
let mut ln_needed: u32 = 0;
|
||||
GetTokenInformation(
|
||||
h_token,
|
||||
TOKEN_LINKED_TOKEN_CLASS,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut ln_needed,
|
||||
);
|
||||
if ln_needed >= std::mem::size_of::<TOKEN_LINKED_TOKEN>() as u32 {
|
||||
let mut ln_buf: Vec<u8> = vec![0u8; ln_needed as usize];
|
||||
let ok = GetTokenInformation(
|
||||
h_token,
|
||||
TOKEN_LINKED_TOKEN_CLASS,
|
||||
ln_buf.as_mut_ptr() as *mut c_void,
|
||||
ln_needed,
|
||||
&mut ln_needed,
|
||||
);
|
||||
if ok != 0 {
|
||||
let lt: TOKEN_LINKED_TOKEN =
|
||||
std::ptr::read_unaligned(ln_buf.as_ptr() as *const TOKEN_LINKED_TOKEN);
|
||||
if lt.linked_token != 0 {
|
||||
let res = scan_token_groups_for_logon(lt.linked_token);
|
||||
CloseHandle(lt.linked_token);
|
||||
if let Some(v) = res {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("Logon SID not present on token"))
|
||||
}
|
||||
unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
||||
let mut luid = LUID {
|
||||
LowPart: 0,
|
||||
HighPart: 0,
|
||||
};
|
||||
let ok = LookupPrivilegeValueW(std::ptr::null(), to_wide(name).as_ptr(), &mut luid);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("LookupPrivilegeValueW failed: {}", GetLastError()));
|
||||
}
|
||||
let mut tp: TOKEN_PRIVILEGES = std::mem::zeroed();
|
||||
tp.PrivilegeCount = 1;
|
||||
tp.Privileges[0].Luid = luid;
|
||||
tp.Privileges[0].Attributes = 0x00000002; // SE_PRIVILEGE_ENABLED
|
||||
let ok2 = AdjustTokenPrivileges(h_token, 0, &tp, 0, std::ptr::null_mut(), std::ptr::null_mut());
|
||||
if ok2 == 0 {
|
||||
return Err(anyhow!("AdjustTokenPrivileges failed: {}", GetLastError()));
|
||||
}
|
||||
let err = GetLastError();
|
||||
if err != 0 {
|
||||
return Err(anyhow!("AdjustTokenPrivileges error {}", err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// removed unused create_write_restricted_token_strict
|
||||
|
||||
pub unsafe fn create_workspace_write_token_with_cap(
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let base = get_current_token_for_restriction()?;
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base)?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid()?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
// Exact set and order: Capability, Logon, Everyone
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
let mut new_token: HANDLE = 0;
|
||||
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||
let ok = CreateRestrictedToken(
|
||||
base,
|
||||
flags,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
3,
|
||||
entries.as_mut_ptr(),
|
||||
&mut new_token,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
|
||||
pub unsafe fn create_readonly_token_with_cap(
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let base = get_current_token_for_restriction()?;
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base)?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid()?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
// Exact set and order: Capability, Logon, Everyone
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
let mut new_token: HANDLE = 0;
|
||||
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||
let ok = CreateRestrictedToken(
|
||||
base,
|
||||
flags,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
3,
|
||||
entries.as_mut_ptr(),
|
||||
&mut new_token,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
43
codex-rs/windows-sandbox-rs/src/winutil.rs
Normal file
43
codex-rs/windows-sandbox-rs/src/winutil.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_FROM_SYSTEM;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||
|
||||
pub fn to_wide<S: AsRef<OsStr>>(s: S) -> Vec<u16> {
|
||||
let mut v: Vec<u16> = s.as_ref().encode_wide().collect();
|
||||
v.push(0);
|
||||
v
|
||||
}
|
||||
|
||||
// Produce a readable description for a Win32 error code.
|
||||
pub fn format_last_error(err: i32) -> String {
|
||||
unsafe {
|
||||
let mut buf_ptr: *mut u16 = std::ptr::null_mut();
|
||||
let flags = FORMAT_MESSAGE_ALLOCATE_BUFFER
|
||||
| FORMAT_MESSAGE_FROM_SYSTEM
|
||||
| FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||
let len = FormatMessageW(
|
||||
flags,
|
||||
std::ptr::null(),
|
||||
err as u32,
|
||||
0,
|
||||
// FORMAT_MESSAGE_ALLOCATE_BUFFER expects a pointer to receive the allocated buffer.
|
||||
// Cast &mut *mut u16 to *mut u16 as required by windows-sys.
|
||||
(&mut buf_ptr as *mut *mut u16) as *mut u16,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if len == 0 || buf_ptr.is_null() {
|
||||
return format!("Win32 error {}", err);
|
||||
}
|
||||
let slice = std::slice::from_raw_parts(buf_ptr, len as usize);
|
||||
let mut s = String::from_utf16_lossy(slice);
|
||||
s = s.trim().to_string();
|
||||
let _ = LocalFree(buf_ptr as HLOCAL);
|
||||
s
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user