Elevated Sandbox 2 (#7792)
- DPAPI helpers for storing Sandbox user passwords securely - creation of Offline/Online sandbox users - ACL setup for sandbox users - firewall rule setup
This commit is contained in:
parent
83aac0f985
commit
13c0919bff
13 changed files with 1681 additions and 33 deletions
|
|
@ -3,17 +3,29 @@ edition = "2021"
|
|||
license.workspace = true
|
||||
name = "codex-windows-sandbox"
|
||||
version.workspace = true
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "codex_windows_sandbox"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "codex-windows-sandbox-setup"
|
||||
path = "src/bin/setup_main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
|
||||
base64 = { workspace = true }
|
||||
dunce = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_WindowsFirewall",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Variant",
|
||||
] }
|
||||
[dependencies.codex-protocol]
|
||||
package = "codex-protocol"
|
||||
path = "../protocol"
|
||||
|
|
@ -41,11 +53,18 @@ features = [
|
|||
"Win32_System_Console",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_NetworkManagement_NetManagement",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_Security_Authentication_Identity",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_Registry",
|
||||
]
|
||||
version = "0.52"
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
|
|
|||
5
codex-rs/windows-sandbox-rs/build.rs
Normal file
5
codex-rs/windows-sandbox-rs/build.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_manifest_file("codex-windows-sandbox-setup.manifest");
|
||||
let _ = res.compile();
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
|
|
@ -34,6 +34,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
|
|||
use windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_DELETE_CHILD;
|
||||
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;
|
||||
|
|
@ -45,12 +46,16 @@ use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA;
|
|||
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA;
|
||||
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
|
||||
use windows_sys::Win32::Storage::FileSystem::READ_CONTROL;
|
||||
use windows_sys::Win32::Storage::FileSystem::DELETE;
|
||||
const SE_KERNEL_OBJECT: u32 = 6;
|
||||
const INHERIT_ONLY_ACE: u8 = 0x08;
|
||||
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
|
||||
const DENY_ACCESS: i32 = 3;
|
||||
|
||||
/// Fetch DACL via handle-based query; caller must LocalFree the returned SD.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must free the returned security descriptor with `LocalFree` and pass an existing path.
|
||||
pub unsafe fn fetch_dacl_handle(path: &Path) -> Result<(*mut ACL, *mut c_void)> {
|
||||
let wpath = to_wide(path);
|
||||
let h = CreateFileW(
|
||||
|
|
@ -88,11 +93,13 @@ pub unsafe fn fetch_dacl_handle(path: &Path) -> Result<(*mut ACL, *mut c_void)>
|
|||
Ok((p_dacl, p_sd))
|
||||
}
|
||||
|
||||
/// Fast mask-based check: does any ACE for provided SIDs grant at least one desired bit? Skips inherit-only.
|
||||
pub unsafe fn dacl_quick_mask_allows(
|
||||
/// Fast mask-based check: does an ACE for provided SIDs grant the desired mask? Skips inherit-only.
|
||||
/// When `require_all_bits` is true, all bits in `desired_mask` must be present; otherwise any bit suffices.
|
||||
pub unsafe fn dacl_mask_allows(
|
||||
p_dacl: *mut ACL,
|
||||
psids: &[*mut c_void],
|
||||
desired_mask: u32,
|
||||
require_all_bits: bool,
|
||||
) -> bool {
|
||||
if p_dacl.is_null() {
|
||||
return false;
|
||||
|
|
@ -141,22 +148,25 @@ pub unsafe fn dacl_quick_mask_allows(
|
|||
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
|
||||
let mut mask = ace.Mask;
|
||||
MapGenericMask(&mut mask, &mapping);
|
||||
if (mask & desired_mask) != 0 {
|
||||
if (require_all_bits && (mask & desired_mask) == desired_mask)
|
||||
|| (!require_all_bits && (mask & desired_mask) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Path-based wrapper around the quick mask check (single DACL fetch).
|
||||
pub fn path_quick_mask_allows(
|
||||
/// Path-based wrapper around the mask check (single DACL fetch).
|
||||
pub fn path_mask_allows(
|
||||
path: &Path,
|
||||
psids: &[*mut c_void],
|
||||
desired_mask: u32,
|
||||
require_all_bits: bool,
|
||||
) -> Result<bool> {
|
||||
unsafe {
|
||||
let (p_dacl, sd) = fetch_dacl_handle(path)?;
|
||||
let has = dacl_quick_mask_allows(p_dacl, psids, desired_mask);
|
||||
let has = dacl_mask_allows(p_dacl, psids, desired_mask, require_all_bits);
|
||||
if !sd.is_null() {
|
||||
LocalFree(sd as HLOCAL);
|
||||
}
|
||||
|
|
@ -326,16 +336,23 @@ pub unsafe fn dacl_effective_allows_mask(
|
|||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
|
||||
const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ
|
||||
| FILE_GENERIC_WRITE
|
||||
| FILE_GENERIC_EXECUTE
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
|
||||
/// Ensure all provided SIDs have a write-capable allow ACE on the path.
|
||||
/// Returns true if any ACE was added.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must pass valid SID pointers and an existing path; free the returned security descriptor with `LocalFree`.
|
||||
#[allow(dead_code)]
|
||||
pub unsafe fn ensure_allow_write_aces(path: &Path, sids: &[*mut c_void]) -> Result<bool> {
|
||||
let (p_dacl, p_sd) = fetch_dacl_handle(path)?;
|
||||
let mut entries: Vec<EXPLICIT_ACCESS_W> = Vec::new();
|
||||
for sid in sids {
|
||||
if dacl_quick_mask_allows(p_dacl, &[*sid], WRITE_ALLOW_MASK) {
|
||||
if dacl_mask_allows(p_dacl, &[*sid], WRITE_ALLOW_MASK, true) {
|
||||
continue;
|
||||
}
|
||||
entries.push(EXPLICIT_ACCESS_W {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::acl::add_deny_write_ace;
|
||||
use crate::acl::path_quick_mask_allows;
|
||||
use crate::acl::path_mask_allows;
|
||||
use crate::cap::cap_sid_file;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::logging::{debug_log, log_note};
|
||||
|
|
@ -84,7 +84,7 @@ unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
|
|||
let mut world = world_sid()?;
|
||||
let psid_world = world.as_mut_ptr() as *mut c_void;
|
||||
let write_mask = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
|
||||
path_quick_mask_allows(path, &[psid_world], write_mask)
|
||||
path_mask_allows(path, &[psid_world], write_mask, false)
|
||||
}
|
||||
|
||||
pub fn audit_everyone_writable(
|
||||
|
|
|
|||
12
codex-rs/windows-sandbox-rs/src/bin/setup_main.rs
Normal file
12
codex-rs/windows-sandbox-rs/src/bin/setup_main.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#[path = "../setup_main_win.rs"]
|
||||
mod win;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn main() -> anyhow::Result<()> {
|
||||
win::main()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn main() {
|
||||
panic!("codex-windows-sandbox-setup is Windows-only");
|
||||
}
|
||||
81
codex-rs/windows-sandbox-rs/src/dpapi.rs
Normal file
81
codex-rs/windows-sandbox-rs/src/dpapi.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptProtectData;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptUnprotectData;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPTPROTECT_UI_FORBIDDEN;
|
||||
|
||||
fn make_blob(data: &[u8]) -> CRYPT_INTEGER_BLOB {
|
||||
CRYPT_INTEGER_BLOB {
|
||||
cbData: data.len() as u32,
|
||||
pbData: data.as_ptr() as *mut u8,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_mut_passed)]
|
||||
pub fn protect(data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut in_blob = make_blob(data);
|
||||
let mut out_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
let ok = unsafe {
|
||||
CryptProtectData(
|
||||
&mut in_blob,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CRYPTPROTECT_UI_FORBIDDEN,
|
||||
&mut out_blob,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CryptProtectData failed: {}", unsafe { GetLastError() }));
|
||||
}
|
||||
let slice =
|
||||
unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec();
|
||||
unsafe {
|
||||
if !out_blob.pbData.is_null() {
|
||||
LocalFree(out_blob.pbData as HLOCAL);
|
||||
}
|
||||
}
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_mut_passed)]
|
||||
pub fn unprotect(blob: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut in_blob = make_blob(blob);
|
||||
let mut out_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
let ok = unsafe {
|
||||
CryptUnprotectData(
|
||||
&mut in_blob,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CRYPTPROTECT_UI_FORBIDDEN,
|
||||
&mut out_blob,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(anyhow!(
|
||||
"CryptUnprotectData failed: {}",
|
||||
unsafe { GetLastError() }
|
||||
));
|
||||
}
|
||||
let slice =
|
||||
unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec();
|
||||
unsafe {
|
||||
if !out_blob.pbData.is_null() {
|
||||
LocalFree(out_blob.pbData as HLOCAL);
|
||||
}
|
||||
}
|
||||
Ok(slice)
|
||||
}
|
||||
171
codex-rs/windows-sandbox-rs/src/identity.rs
Normal file
171
codex-rs/windows-sandbox-rs/src/identity.rs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
use crate::dpapi;
|
||||
use crate::logging::debug_log;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::setup::run_elevated_setup;
|
||||
use crate::setup::sandbox_users_path;
|
||||
use crate::setup::setup_marker_path;
|
||||
use crate::setup::SandboxUserRecord;
|
||||
use crate::setup::SandboxUsersFile;
|
||||
use crate::setup::SetupMarker;
|
||||
use crate::setup::{gather_read_roots, gather_write_roots};
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SandboxIdentity {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SandboxCreds {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
fn load_marker(codex_home: &Path) -> Result<Option<SetupMarker>> {
|
||||
let path = setup_marker_path(codex_home);
|
||||
let marker = match fs::read_to_string(&path) {
|
||||
Ok(contents) => match serde_json::from_str::<SetupMarker>(&contents) {
|
||||
Ok(m) => Some(m),
|
||||
Err(err) => {
|
||||
debug_log(
|
||||
&format!("sandbox setup marker parse failed: {err}"),
|
||||
Some(codex_home),
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(err) => {
|
||||
debug_log(
|
||||
&format!("sandbox setup marker read failed: {err}"),
|
||||
Some(codex_home),
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(marker)
|
||||
}
|
||||
|
||||
fn load_users(codex_home: &Path) -> Result<Option<SandboxUsersFile>> {
|
||||
let path = sandbox_users_path(codex_home);
|
||||
let file = match fs::read_to_string(&path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(err) => {
|
||||
debug_log(
|
||||
&format!("sandbox users read failed: {err}"),
|
||||
Some(codex_home),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
match serde_json::from_str::<SandboxUsersFile>(&file) {
|
||||
Ok(users) => Ok(Some(users)),
|
||||
Err(err) => {
|
||||
debug_log(
|
||||
&format!("sandbox users parse failed: {err}"),
|
||||
Some(codex_home),
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_password(record: &SandboxUserRecord) -> Result<String> {
|
||||
let blob = BASE64_STANDARD
|
||||
.decode(record.password.as_bytes())
|
||||
.context("base64 decode password")?;
|
||||
let decrypted = dpapi::unprotect(&blob)?;
|
||||
let pwd = String::from_utf8(decrypted).context("sandbox password not utf-8")?;
|
||||
Ok(pwd)
|
||||
}
|
||||
|
||||
fn select_identity(policy: &SandboxPolicy, codex_home: &Path) -> Result<Option<SandboxIdentity>> {
|
||||
let _marker = match load_marker(codex_home)? {
|
||||
Some(m) if m.version_matches() => m,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let users = match load_users(codex_home)? {
|
||||
Some(u) if u.version_matches() => u,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let chosen = if !policy.has_full_network_access() {
|
||||
users.offline
|
||||
} else {
|
||||
users.online
|
||||
};
|
||||
let password = decode_password(&chosen)?;
|
||||
Ok(Some(SandboxIdentity {
|
||||
username: chosen.username.clone(),
|
||||
password,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn require_logon_sandbox_creds(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
) -> Result<SandboxCreds> {
|
||||
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
|
||||
let needed_read = gather_read_roots(command_cwd, policy, policy_cwd);
|
||||
let mut needed_write = gather_write_roots(policy, policy_cwd, command_cwd, env_map);
|
||||
// Ensure the sandbox directory itself is writable by sandbox users.
|
||||
needed_write.push(sandbox_dir.clone());
|
||||
let mut setup_reason: Option<String> = None;
|
||||
let mut _existing_marker: Option<SetupMarker> = None;
|
||||
|
||||
let mut identity = match load_marker(codex_home)? {
|
||||
Some(marker) if marker.version_matches() => {
|
||||
_existing_marker = Some(marker.clone());
|
||||
let selected = select_identity(policy, codex_home)?;
|
||||
if selected.is_none() {
|
||||
setup_reason =
|
||||
Some("sandbox users missing or incompatible with marker version".to_string());
|
||||
}
|
||||
selected
|
||||
}
|
||||
_ => {
|
||||
setup_reason = Some("sandbox setup marker missing or incompatible".to_string());
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if identity.is_none() {
|
||||
if let Some(reason) = &setup_reason {
|
||||
crate::logging::log_note(&format!("sandbox setup required: {reason}"), Some(&sandbox_dir));
|
||||
} else {
|
||||
crate::logging::log_note("sandbox setup required", Some(&sandbox_dir));
|
||||
}
|
||||
run_elevated_setup(
|
||||
policy,
|
||||
policy_cwd,
|
||||
command_cwd,
|
||||
env_map,
|
||||
codex_home,
|
||||
Some(needed_read.clone()),
|
||||
Some(needed_write.clone()),
|
||||
)?;
|
||||
identity = select_identity(policy, codex_home)?;
|
||||
}
|
||||
// Always refresh ACLs (non-elevated) for current roots via the setup binary.
|
||||
crate::setup::run_setup_refresh(policy, policy_cwd, command_cwd, env_map, codex_home)?;
|
||||
let identity = identity.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Windows sandbox setup is missing or out of date; rerun the sandbox setup with elevation"
|
||||
)
|
||||
})?;
|
||||
Ok(SandboxCreds {
|
||||
username: identity.username,
|
||||
password: identity.password,
|
||||
})
|
||||
}
|
||||
|
|
@ -4,14 +4,50 @@ macro_rules! windows_modules {
|
|||
};
|
||||
}
|
||||
|
||||
windows_modules!(acl, allow, audit, cap, env, logging, policy, token, winutil);
|
||||
windows_modules!(
|
||||
acl, allow, audit, cap, dpapi, env, identity, logging, policy, process, token, winutil
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "setup_orchestrator.rs"]
|
||||
mod setup;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::ensure_allow_write_aces;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::fetch_dacl_handle;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::path_mask_allows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use audit::apply_world_writable_scan_and_denies;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use cap::load_or_create_cap_sids;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use dpapi::protect as dpapi_protect;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use dpapi::unprotect as dpapi_unprotect;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use identity::require_logon_sandbox_creds;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::log_note;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::LOG_FILE_NAME;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_elevated_setup;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_setup_refresh;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::sandbox_dir;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SETUP_VERSION;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::convert_string_sid_to_sid;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::run_windows_sandbox_capture;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::CaptureResult;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use winutil::string_from_sid_bytes;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::apply_world_writable_scan_and_denies;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
|||
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
||||
|
||||
#[allow(dead_code)]
|
||||
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();
|
||||
|
|
@ -49,6 +50,7 @@ pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
|||
w
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn quote_arg(a: &str) -> String {
|
||||
let needs_quote = a.is_empty() || a.chars().any(|ch| ch.is_whitespace() || ch == '"');
|
||||
if !needs_quote {
|
||||
|
|
@ -79,6 +81,7 @@ fn quote_arg(a: &str) -> String {
|
|||
out.push('"');
|
||||
out
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> {
|
||||
for kind in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
|
||||
|
|
@ -100,6 +103,7 @@ unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> {
|
|||
/// # Safety
|
||||
/// Caller must provide a valid primary token handle (`h_token`) with appropriate access,
|
||||
/// and the `argv`, `cwd`, and `env_map` must remain valid for the duration of the call.
|
||||
#[allow(dead_code)]
|
||||
pub unsafe fn create_process_as_user(
|
||||
h_token: HANDLE,
|
||||
argv: &[String],
|
||||
|
|
@ -156,7 +160,7 @@ pub unsafe fn create_process_as_user(
|
|||
CREATE_UNICODE_ENVIRONMENT,
|
||||
env_block.as_ptr() as *mut c_void,
|
||||
to_wide(cwd).as_ptr(),
|
||||
&mut si,
|
||||
&si,
|
||||
&mut pi,
|
||||
);
|
||||
if ok == 0 {
|
||||
|
|
|
|||
913
codex-rs/windows-sandbox-rs/src/setup_main_win.rs
Normal file
913
codex-rs/windows-sandbox-rs/src/setup_main_win.rs
Normal file
|
|
@ -0,0 +1,913 @@
|
|||
#![cfg(target_os = "windows")]
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use base64::Engine;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::dpapi_protect;
|
||||
use codex_windows_sandbox::ensure_allow_write_aces;
|
||||
use codex_windows_sandbox::fetch_dacl_handle;
|
||||
use codex_windows_sandbox::load_or_create_cap_sids;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::path_mask_allows;
|
||||
use codex_windows_sandbox::sandbox_dir;
|
||||
use codex_windows_sandbox::string_from_sid_bytes;
|
||||
use codex_windows_sandbox::LOG_FILE_NAME;
|
||||
use codex_windows_sandbox::SETUP_VERSION;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::ffi::c_void;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use windows::core::Interface;
|
||||
use windows::core::BSTR;
|
||||
use windows::Win32::Foundation::VARIANT_TRUE;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::INetFwPolicy2;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::INetFwRule3;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_ACTION_BLOCK;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_IP_PROTOCOL_ANY;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_PROFILE2_ALL;
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_RULE_DIR_OUT;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_INPROC_SERVER;
|
||||
use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::NERR_Success;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAddMembers;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::NetUserAdd;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::NetUserSetInfo;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_MEMBERS_INFO_3;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::UF_DONT_EXPIRE_PASSWD;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::UF_SCRIPT;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1003;
|
||||
use windows_sys::Win32::NetworkManagement::NetManagement::USER_PRIV_USER;
|
||||
use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW;
|
||||
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
||||
use windows_sys::Win32::Security::Authorization::SetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||
use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
|
||||
use windows_sys::Win32::Security::Authorization::SE_FILE_OBJECT;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||
use windows_sys::Win32::Security::LookupAccountNameW;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::CONTAINER_INHERIT_ACE;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
use windows_sys::Win32::Security::OBJECT_INHERIT_ACE;
|
||||
use windows_sys::Win32::Security::SID_NAME_USE;
|
||||
use windows_sys::Win32::Storage::FileSystem::DELETE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_DELETE_CHILD;
|
||||
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;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Payload {
|
||||
version: u32,
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
codex_home: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
real_user: String,
|
||||
#[serde(default)]
|
||||
refresh_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SandboxUserRecord {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SandboxUsersFile {
|
||||
version: u32,
|
||||
offline: SandboxUserRecord,
|
||||
online: SandboxUserRecord,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SetupMarker {
|
||||
version: u32,
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
created_at: String,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn log_line(log: &mut File, msg: &str) -> Result<()> {
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
writeln!(log, "[{ts}] {msg}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_wide(s: &OsStr) -> Vec<u16> {
|
||||
let mut v: Vec<u16> = s.encode_wide().collect();
|
||||
v.push(0);
|
||||
v
|
||||
}
|
||||
|
||||
fn random_password() -> String {
|
||||
const CHARS: &[u8] =
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let mut buf = [0u8; 24];
|
||||
rng.fill_bytes(&mut buf);
|
||||
buf.iter()
|
||||
.map(|b| {
|
||||
let idx = (*b as usize) % CHARS.len();
|
||||
CHARS[idx] as char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sid_bytes_to_psid(sid: &[u8]) -> Result<*mut c_void> {
|
||||
let sid_str = string_from_sid_bytes(sid).map_err(anyhow::Error::msg)?;
|
||||
let sid_w = to_wide(OsStr::new(&sid_str));
|
||||
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||
if unsafe { ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) } == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"ConvertStringSidToSidW failed: {}",
|
||||
unsafe { GetLastError() }
|
||||
));
|
||||
}
|
||||
Ok(psid)
|
||||
}
|
||||
|
||||
fn ensure_local_user(name: &str, password: &str, log: &mut File) -> Result<()> {
|
||||
let name_w = to_wide(OsStr::new(name));
|
||||
let pwd_w = to_wide(OsStr::new(password));
|
||||
unsafe {
|
||||
let info = USER_INFO_1 {
|
||||
usri1_name: name_w.as_ptr() as *mut u16,
|
||||
usri1_password: pwd_w.as_ptr() as *mut u16,
|
||||
usri1_password_age: 0,
|
||||
usri1_priv: USER_PRIV_USER,
|
||||
usri1_home_dir: std::ptr::null_mut(),
|
||||
usri1_comment: std::ptr::null_mut(),
|
||||
usri1_flags: UF_SCRIPT | UF_DONT_EXPIRE_PASSWD,
|
||||
usri1_script_path: std::ptr::null_mut(),
|
||||
};
|
||||
let status = NetUserAdd(
|
||||
std::ptr::null(),
|
||||
1,
|
||||
&info as *const _ as *mut u8,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if status != NERR_Success {
|
||||
// Try update password via level 1003.
|
||||
let pw_info = USER_INFO_1003 {
|
||||
usri1003_password: pwd_w.as_ptr() as *mut u16,
|
||||
};
|
||||
let upd = NetUserSetInfo(
|
||||
std::ptr::null(),
|
||||
name_w.as_ptr(),
|
||||
1003,
|
||||
&pw_info as *const _ as *mut u8,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if upd != NERR_Success {
|
||||
log_line(log, &format!("NetUserSetInfo failed for {name} code {upd}"))?;
|
||||
return Err(anyhow::anyhow!(
|
||||
"failed to create/update user {name}, code {status}/{upd}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let group = to_wide(OsStr::new("Users"));
|
||||
let member = LOCALGROUP_MEMBERS_INFO_3 {
|
||||
lgrmi3_domainandname: name_w.as_ptr() as *mut u16,
|
||||
};
|
||||
let _ = NetLocalGroupAddMembers(
|
||||
std::ptr::null(),
|
||||
group.as_ptr(),
|
||||
3,
|
||||
&member as *const _ as *mut u8,
|
||||
1,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_sid(name: &str) -> Result<Vec<u8>> {
|
||||
let name_w = to_wide(OsStr::new(name));
|
||||
let mut sid_buffer = vec![0u8; 68];
|
||||
let mut sid_len: u32 = sid_buffer.len() as u32;
|
||||
let mut domain: Vec<u16> = Vec::new();
|
||||
let mut domain_len: u32 = 0;
|
||||
let mut use_type: SID_NAME_USE = 0;
|
||||
loop {
|
||||
let ok = unsafe {
|
||||
LookupAccountNameW(
|
||||
std::ptr::null(),
|
||||
name_w.as_ptr(),
|
||||
sid_buffer.as_mut_ptr() as *mut c_void,
|
||||
&mut sid_len,
|
||||
domain.as_mut_ptr(),
|
||||
&mut domain_len,
|
||||
&mut use_type,
|
||||
)
|
||||
};
|
||||
if ok != 0 {
|
||||
sid_buffer.truncate(sid_len as usize);
|
||||
return Ok(sid_buffer);
|
||||
}
|
||||
let err = unsafe { GetLastError() };
|
||||
if err == ERROR_INSUFFICIENT_BUFFER {
|
||||
sid_buffer.resize(sid_len as usize, 0);
|
||||
domain.resize(domain_len as usize, 0);
|
||||
continue;
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"LookupAccountNameW failed for {name}: {}",
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_inheritable_allow_no_log(path: &Path, sid: &[u8], mask: u32) -> Result<()> {
|
||||
unsafe {
|
||||
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||
let sid_str = string_from_sid_bytes(sid).map_err(anyhow::Error::msg)?;
|
||||
let sid_w = to_wide(OsStr::new(&sid_str));
|
||||
if ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"ConvertStringSidToSidW failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
let (existing_dacl, sd) = fetch_dacl_handle(path)?;
|
||||
let trustee = TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_SID,
|
||||
ptstrName: psid as *mut u16,
|
||||
};
|
||||
let ea = EXPLICIT_ACCESS_W {
|
||||
grfAccessPermissions: mask,
|
||||
grfAccessMode: GRANT_ACCESS,
|
||||
grfInheritance: OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
|
||||
Trustee: trustee,
|
||||
};
|
||||
let mut new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let set = SetEntriesInAclW(1, &ea, existing_dacl, &mut new_dacl);
|
||||
if set != 0 {
|
||||
return Err(anyhow::anyhow!("SetEntriesInAclW failed: {}", set));
|
||||
}
|
||||
let res = SetNamedSecurityInfoW(
|
||||
to_wide(path.as_os_str()).as_ptr() as *mut u16,
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if res != 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"SetNamedSecurityInfoW failed for {}: {}",
|
||||
path.display(),
|
||||
res
|
||||
));
|
||||
}
|
||||
if !new_dacl.is_null() {
|
||||
LocalFree(new_dacl as HLOCAL);
|
||||
}
|
||||
if !sd.is_null() {
|
||||
LocalFree(sd as HLOCAL);
|
||||
}
|
||||
if !psid.is_null() {
|
||||
LocalFree(psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_add_inheritable_allow_with_timeout(
|
||||
path: &Path,
|
||||
sid: &[u8],
|
||||
mask: u32,
|
||||
_log: &mut File,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let (tx, rx) = mpsc::channel::<Result<()>>();
|
||||
let path_buf = path.to_path_buf();
|
||||
let sid_vec = sid.to_vec();
|
||||
std::thread::spawn(move || {
|
||||
let res = add_inheritable_allow_no_log(&path_buf, &sid_vec, mask);
|
||||
let _ = tx.send(res);
|
||||
});
|
||||
match rx.recv_timeout(timeout) {
|
||||
Ok(res) => res,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => Err(anyhow::anyhow!(
|
||||
"ACL grant timed out on {} after {:?}",
|
||||
path.display(),
|
||||
timeout
|
||||
)),
|
||||
Err(e) => Err(anyhow::anyhow!(
|
||||
"ACL grant channel error on {}: {e}",
|
||||
path.display()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_netsh_firewall(sid: &str, log: &mut File) -> Result<()> {
|
||||
let local_user_spec = format!("O:LSD:(A;;CC;;;{sid})");
|
||||
let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
|
||||
if hr.is_err() {
|
||||
return Err(anyhow::anyhow!("CoInitializeEx failed: {hr:?}"));
|
||||
}
|
||||
let result = unsafe {
|
||||
(|| -> Result<()> {
|
||||
let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER)
|
||||
.map_err(|e| anyhow::anyhow!("CoCreateInstance NetFwPolicy2: {e:?}"))?;
|
||||
let rules = policy
|
||||
.Rules()
|
||||
.map_err(|e| anyhow::anyhow!("INetFwPolicy2::Rules: {e:?}"))?;
|
||||
let name = BSTR::from("Codex Sandbox Offline - Block Outbound");
|
||||
let rule: INetFwRule3 = match rules.Item(&name) {
|
||||
Ok(existing) => existing.cast().map_err(|e| {
|
||||
anyhow::anyhow!("cast existing firewall rule to INetFwRule3: {e:?}")
|
||||
})?,
|
||||
Err(_) => {
|
||||
let new_rule: INetFwRule3 =
|
||||
CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER)
|
||||
.map_err(|e| anyhow::anyhow!("CoCreateInstance NetFwRule: {e:?}"))?;
|
||||
new_rule
|
||||
.SetName(&name)
|
||||
.map_err(|e| anyhow::anyhow!("SetName: {e:?}"))?;
|
||||
new_rule
|
||||
.SetDirection(NET_FW_RULE_DIR_OUT)
|
||||
.map_err(|e| anyhow::anyhow!("SetDirection: {e:?}"))?;
|
||||
new_rule
|
||||
.SetAction(NET_FW_ACTION_BLOCK)
|
||||
.map_err(|e| anyhow::anyhow!("SetAction: {e:?}"))?;
|
||||
new_rule
|
||||
.SetEnabled(VARIANT_TRUE)
|
||||
.map_err(|e| anyhow::anyhow!("SetEnabled: {e:?}"))?;
|
||||
new_rule
|
||||
.SetProfiles(NET_FW_PROFILE2_ALL.0)
|
||||
.map_err(|e| anyhow::anyhow!("SetProfiles: {e:?}"))?;
|
||||
new_rule
|
||||
.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0)
|
||||
.map_err(|e| anyhow::anyhow!("SetProtocol: {e:?}"))?;
|
||||
rules
|
||||
.Add(&new_rule)
|
||||
.map_err(|e| anyhow::anyhow!("Rules::Add: {e:?}"))?;
|
||||
new_rule
|
||||
}
|
||||
};
|
||||
rule.SetLocalUserAuthorizedList(&BSTR::from(local_user_spec.as_str()))
|
||||
.map_err(|e| anyhow::anyhow!("SetLocalUserAuthorizedList: {e:?}"))?;
|
||||
rule.SetEnabled(VARIANT_TRUE)
|
||||
.map_err(|e| anyhow::anyhow!("SetEnabled: {e:?}"))?;
|
||||
rule.SetProfiles(NET_FW_PROFILE2_ALL.0)
|
||||
.map_err(|e| anyhow::anyhow!("SetProfiles: {e:?}"))?;
|
||||
rule.SetAction(NET_FW_ACTION_BLOCK)
|
||||
.map_err(|e| anyhow::anyhow!("SetAction: {e:?}"))?;
|
||||
rule.SetDirection(NET_FW_RULE_DIR_OUT)
|
||||
.map_err(|e| anyhow::anyhow!("SetDirection: {e:?}"))?;
|
||||
rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0)
|
||||
.map_err(|e| anyhow::anyhow!("SetProtocol: {e:?}"))?;
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"firewall rule configured via COM with LocalUserAuthorizedList={local_user_spec}"
|
||||
),
|
||||
)?;
|
||||
Ok(())
|
||||
})()
|
||||
};
|
||||
unsafe {
|
||||
CoUninitialize();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn lock_sandbox_dir(
|
||||
dir: &Path,
|
||||
real_user: &str,
|
||||
sandbox_user_sids: &[Vec<u8>],
|
||||
log: &mut File,
|
||||
) -> Result<()> {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let system_sid = resolve_sid("SYSTEM")?;
|
||||
let admins_sid = resolve_sid("Administrators")?;
|
||||
let real_sid = resolve_sid(real_user)?;
|
||||
let entries = [
|
||||
(
|
||||
system_sid,
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE,
|
||||
),
|
||||
(
|
||||
admins_sid,
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE,
|
||||
),
|
||||
(
|
||||
real_sid,
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE,
|
||||
),
|
||||
];
|
||||
let sandbox_entries: Vec<(Vec<u8>, u32)> = sandbox_user_sids
|
||||
.iter()
|
||||
.map(|sid| {
|
||||
(
|
||||
sid.clone(),
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
unsafe {
|
||||
let mut eas: Vec<EXPLICIT_ACCESS_W> = Vec::new();
|
||||
let mut sids: Vec<*mut c_void> = Vec::new();
|
||||
for (sid_bytes, mask) in entries
|
||||
.iter()
|
||||
.map(|(s, m)| (s, *m))
|
||||
.chain(sandbox_entries.iter().map(|(s, m)| (s, *m)))
|
||||
{
|
||||
let sid_str = string_from_sid_bytes(sid_bytes).map_err(anyhow::Error::msg)?;
|
||||
let sid_w = to_wide(OsStr::new(&sid_str));
|
||||
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||
if ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"ConvertStringSidToSidW failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
sids.push(psid);
|
||||
eas.push(EXPLICIT_ACCESS_W {
|
||||
grfAccessPermissions: mask,
|
||||
grfAccessMode: GRANT_ACCESS,
|
||||
grfInheritance: OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
|
||||
Trustee: TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_SID,
|
||||
ptstrName: psid as *mut u16,
|
||||
},
|
||||
});
|
||||
}
|
||||
let mut new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let set = SetEntriesInAclW(
|
||||
eas.len() as u32,
|
||||
eas.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
&mut new_dacl,
|
||||
);
|
||||
if set != 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"SetEntriesInAclW sandbox dir failed: {}",
|
||||
set
|
||||
));
|
||||
}
|
||||
let path_w = to_wide(dir.as_os_str());
|
||||
let res = SetNamedSecurityInfoW(
|
||||
path_w.as_ptr() as *mut u16,
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if res != 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"SetNamedSecurityInfoW sandbox dir failed: {}",
|
||||
res
|
||||
));
|
||||
}
|
||||
if !new_dacl.is_null() {
|
||||
LocalFree(new_dacl as HLOCAL);
|
||||
}
|
||||
for sid in sids {
|
||||
if !sid.is_null() {
|
||||
LocalFree(sid as HLOCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
log_line(
|
||||
log,
|
||||
&format!("sandbox dir ACL applied at {}", dir.display()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_secrets(
|
||||
codex_home: &Path,
|
||||
offline_user: &str,
|
||||
offline_pwd: &str,
|
||||
online_user: &str,
|
||||
online_pwd: &str,
|
||||
_read_roots: &[PathBuf],
|
||||
_write_roots: &[PathBuf],
|
||||
) -> Result<()> {
|
||||
let sandbox_dir = sandbox_dir(codex_home);
|
||||
std::fs::create_dir_all(&sandbox_dir)?;
|
||||
let offline_blob = dpapi_protect(offline_pwd.as_bytes())?;
|
||||
let online_blob = dpapi_protect(online_pwd.as_bytes())?;
|
||||
let users = SandboxUsersFile {
|
||||
version: SETUP_VERSION,
|
||||
offline: SandboxUserRecord {
|
||||
username: offline_user.to_string(),
|
||||
password: BASE64.encode(offline_blob),
|
||||
},
|
||||
online: SandboxUserRecord {
|
||||
username: online_user.to_string(),
|
||||
password: BASE64.encode(online_blob),
|
||||
},
|
||||
};
|
||||
let marker = SetupMarker {
|
||||
version: SETUP_VERSION,
|
||||
offline_username: offline_user.to_string(),
|
||||
online_username: online_user.to_string(),
|
||||
created_at: chrono::Utc::now().to_rfc3339(),
|
||||
read_roots: Vec::new(),
|
||||
write_roots: Vec::new(),
|
||||
};
|
||||
let users_path = sandbox_dir.join("sandbox_users.json");
|
||||
let marker_path = sandbox_dir.join("setup_marker.json");
|
||||
std::fs::write(users_path, serde_json::to_vec_pretty(&users)?)?;
|
||||
std::fs::write(marker_path, serde_json::to_vec_pretty(&marker)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let ret = real_main();
|
||||
if let Err(e) = &ret {
|
||||
// Best-effort: log unexpected top-level errors.
|
||||
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
||||
let sbx_dir = sandbox_dir(Path::new(&codex_home));
|
||||
let _ = std::fs::create_dir_all(&sbx_dir);
|
||||
let log_path = sbx_dir.join(LOG_FILE_NAME);
|
||||
if let Ok(mut f) = File::options().create(true).append(true).open(&log_path) {
|
||||
let _ = writeln!(
|
||||
f,
|
||||
"[{}] top-level error: {}",
|
||||
chrono::Utc::now().to_rfc3339(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn real_main() -> Result<()> {
|
||||
let mut args = std::env::args().collect::<Vec<_>>();
|
||||
if args.len() != 2 {
|
||||
anyhow::bail!("expected payload argument");
|
||||
}
|
||||
let payload_b64 = args.remove(1);
|
||||
let payload_json = BASE64
|
||||
.decode(payload_b64)
|
||||
.context("failed to decode payload b64")?;
|
||||
let payload: Payload =
|
||||
serde_json::from_slice(&payload_json).context("failed to parse payload json")?;
|
||||
if payload.version != SETUP_VERSION {
|
||||
anyhow::bail!("setup version mismatch");
|
||||
}
|
||||
let sbx_dir = sandbox_dir(&payload.codex_home);
|
||||
std::fs::create_dir_all(&sbx_dir)?;
|
||||
let log_path = sbx_dir.join(LOG_FILE_NAME);
|
||||
let mut log = File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.context("open log")?;
|
||||
log_line(&mut log, "setup binary started")?;
|
||||
log_note("setup binary started", Some(sbx_dir.as_path()));
|
||||
let result = run_setup(&payload, &mut log, &sbx_dir);
|
||||
if let Err(err) = &result {
|
||||
let _ = log_line(&mut log, &format!("setup error: {err:?}"));
|
||||
log_note(&format!("setup error: {err:?}"), Some(sbx_dir.as_path()));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn run_setup(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<()> {
|
||||
let refresh_only = payload.refresh_only;
|
||||
let offline_pwd = if refresh_only {
|
||||
None
|
||||
} else {
|
||||
Some(random_password())
|
||||
};
|
||||
let online_pwd = if refresh_only {
|
||||
None
|
||||
} else {
|
||||
Some(random_password())
|
||||
};
|
||||
if refresh_only {
|
||||
log_line(log, "refresh-only mode: skipping user creation/firewall")?;
|
||||
} else {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"ensuring sandbox users offline={} online={}",
|
||||
payload.offline_username, payload.online_username
|
||||
),
|
||||
)?;
|
||||
ensure_local_user(
|
||||
&payload.offline_username,
|
||||
offline_pwd.as_ref().unwrap(),
|
||||
log,
|
||||
)?;
|
||||
ensure_local_user(&payload.online_username, online_pwd.as_ref().unwrap(), log)?;
|
||||
}
|
||||
let offline_sid = resolve_sid(&payload.offline_username)?;
|
||||
let online_sid = resolve_sid(&payload.online_username)?;
|
||||
let offline_psid = sid_bytes_to_psid(&offline_sid)?;
|
||||
let online_psid = sid_bytes_to_psid(&online_sid)?;
|
||||
let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?;
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"resolved SIDs offline={} online={}",
|
||||
offline_sid_str,
|
||||
string_from_sid_bytes(&online_sid).map_err(anyhow::Error::msg)?
|
||||
),
|
||||
)?;
|
||||
let caps = load_or_create_cap_sids(&payload.codex_home);
|
||||
let cap_psid = unsafe {
|
||||
convert_string_sid_to_sid(&caps.workspace)
|
||||
.ok_or_else(|| anyhow::anyhow!("convert capability SID failed"))?
|
||||
};
|
||||
let mut refresh_errors: Vec<String> = Vec::new();
|
||||
let users_sid = resolve_sid("Users")?;
|
||||
let users_psid = sid_bytes_to_psid(&users_sid)?;
|
||||
let auth_sid = resolve_sid("Authenticated Users")?;
|
||||
let auth_psid = sid_bytes_to_psid(&auth_sid)?;
|
||||
let everyone_sid = resolve_sid("Everyone")?;
|
||||
let everyone_psid = sid_bytes_to_psid(&everyone_sid)?;
|
||||
let rx_psids = vec![users_psid, auth_psid, everyone_psid];
|
||||
log_line(log, &format!("resolved capability SID {}", caps.workspace))?;
|
||||
if !refresh_only {
|
||||
run_netsh_firewall(&offline_sid_str, log)?;
|
||||
}
|
||||
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"refresh: processing {} read roots, {} write roots",
|
||||
payload.read_roots.len(),
|
||||
payload.write_roots.len()
|
||||
),
|
||||
)?;
|
||||
for root in &payload.read_roots {
|
||||
if !root.exists() {
|
||||
log_line(
|
||||
log,
|
||||
&format!("read root {} missing; skipping", root.display()),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
match path_mask_allows(
|
||||
root,
|
||||
&rx_psids,
|
||||
FILE_GENERIC_READ | FILE_GENERIC_EXECUTE,
|
||||
true,
|
||||
) {
|
||||
Ok(has) => {
|
||||
if has {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"Users/AU/Everyone already has RX on {}; skipping",
|
||||
root.display()
|
||||
),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
refresh_errors.push(format!(
|
||||
"read mask check failed on {}: {}",
|
||||
root.display(),
|
||||
e
|
||||
));
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"read mask check failed on {}: {}; continuing",
|
||||
root.display(),
|
||||
e
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
log_line(
|
||||
log,
|
||||
&format!("granting read ACE to {} for sandbox users", root.display()),
|
||||
)?;
|
||||
let mut successes = 0usize;
|
||||
let read_mask = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
|
||||
for (label, sid_bytes) in [("offline", &offline_sid), ("online", &online_sid)] {
|
||||
match try_add_inheritable_allow_with_timeout(
|
||||
root,
|
||||
sid_bytes,
|
||||
read_mask,
|
||||
log,
|
||||
Duration::from_millis(100),
|
||||
) {
|
||||
Ok(_) => {
|
||||
successes += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"grant read ACE timed out/failed on {} for {label}: {e}",
|
||||
root.display()
|
||||
),
|
||||
)?;
|
||||
// Best-effort: continue to next SID/root.
|
||||
}
|
||||
}
|
||||
}
|
||||
if successes == 2 {
|
||||
log_line(log, &format!("granted read ACE to {}", root.display()))?;
|
||||
} else {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"read ACE incomplete on {} (success {}/2)",
|
||||
root.display(),
|
||||
successes
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for root in &payload.write_roots {
|
||||
if !root.exists() {
|
||||
log_line(
|
||||
log,
|
||||
&format!("write root {} missing; skipping", root.display()),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
let sids = vec![offline_psid, online_psid, cap_psid];
|
||||
let write_mask = FILE_GENERIC_READ
|
||||
| FILE_GENERIC_WRITE
|
||||
| FILE_GENERIC_EXECUTE
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
let mut need_grant = false;
|
||||
for (label, psid) in [
|
||||
("offline", offline_psid),
|
||||
("online", online_psid),
|
||||
("cap", cap_psid),
|
||||
] {
|
||||
let has = match path_mask_allows(root, &[psid], write_mask, true) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
refresh_errors.push(format!(
|
||||
"write mask check failed on {} for {label}: {}",
|
||||
root.display(),
|
||||
e
|
||||
));
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"write mask check failed on {} for {label}: {}; continuing",
|
||||
root.display(),
|
||||
e
|
||||
),
|
||||
)?;
|
||||
false
|
||||
}
|
||||
};
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"write check {label} on {} => {}",
|
||||
root.display(),
|
||||
if has { "present" } else { "missing" }
|
||||
),
|
||||
)?;
|
||||
if !has {
|
||||
need_grant = true;
|
||||
}
|
||||
}
|
||||
if need_grant {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"granting write ACE to {} for sandbox users and capability SID",
|
||||
root.display()
|
||||
),
|
||||
)?;
|
||||
match unsafe { ensure_allow_write_aces(root, &sids) } {
|
||||
Ok(res) => {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"write ACE {} on {}",
|
||||
if res { "added" } else { "already present" },
|
||||
root.display()
|
||||
),
|
||||
)?;
|
||||
}
|
||||
Err(e) => {
|
||||
refresh_errors.push(format!("write ACE failed on {}: {}", root.display(), e));
|
||||
log_line(
|
||||
log,
|
||||
&format!("write ACE grant failed on {}: {}", root.display(), e),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"write ACE already present for all sandbox SIDs on {}",
|
||||
root.display()
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
if refresh_only {
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"setup refresh: processed {} read roots, {} write roots; errors={:?}",
|
||||
payload.read_roots.len(),
|
||||
payload.write_roots.len(),
|
||||
refresh_errors
|
||||
),
|
||||
)?;
|
||||
}
|
||||
if !refresh_only {
|
||||
lock_sandbox_dir(
|
||||
&sandbox_dir(&payload.codex_home),
|
||||
&payload.real_user,
|
||||
&[offline_sid.clone(), online_sid.clone()],
|
||||
log,
|
||||
)?;
|
||||
log_line(log, "sandbox dir ACL applied")?;
|
||||
write_secrets(
|
||||
&payload.codex_home,
|
||||
&payload.offline_username,
|
||||
offline_pwd.as_ref().unwrap(),
|
||||
&payload.online_username,
|
||||
online_pwd.as_ref().unwrap(),
|
||||
&payload.read_roots,
|
||||
&payload.write_roots,
|
||||
)?;
|
||||
log_line(
|
||||
log,
|
||||
"sandbox users and marker written (sandbox_users.json, setup_marker.json)",
|
||||
)?;
|
||||
}
|
||||
unsafe {
|
||||
if !offline_psid.is_null() {
|
||||
LocalFree(offline_psid as HLOCAL);
|
||||
}
|
||||
if !online_psid.is_null() {
|
||||
LocalFree(online_psid as HLOCAL);
|
||||
}
|
||||
if !cap_psid.is_null() {
|
||||
LocalFree(cap_psid as HLOCAL);
|
||||
}
|
||||
if !users_psid.is_null() {
|
||||
LocalFree(users_psid as HLOCAL);
|
||||
}
|
||||
if !auth_psid.is_null() {
|
||||
LocalFree(auth_psid as HLOCAL);
|
||||
}
|
||||
if !everyone_psid.is_null() {
|
||||
LocalFree(everyone_psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
if refresh_only && !refresh_errors.is_empty() {
|
||||
log_line(
|
||||
log,
|
||||
&format!("setup refresh completed with errors: {:?}", refresh_errors),
|
||||
)?;
|
||||
anyhow::bail!("setup refresh had errors");
|
||||
}
|
||||
log_line(log, "setup binary completed")?;
|
||||
log_note("setup binary completed", Some(sbx_dir));
|
||||
Ok(())
|
||||
}
|
||||
392
codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs
Normal file
392
codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use crate::allow::compute_allow_paths;
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::logging::log_note;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Security::AllocateAndInitializeSid;
|
||||
use windows_sys::Win32::Security::CheckTokenMembership;
|
||||
use windows_sys::Win32::Security::FreeSid;
|
||||
use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY;
|
||||
|
||||
pub const SETUP_VERSION: u32 = 2;
|
||||
pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline";
|
||||
pub const ONLINE_USERNAME: &str = "CodexSandboxOnline";
|
||||
const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020;
|
||||
const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220;
|
||||
|
||||
pub fn sandbox_dir(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(".sandbox")
|
||||
}
|
||||
|
||||
pub fn setup_marker_path(codex_home: &Path) -> PathBuf {
|
||||
sandbox_dir(codex_home).join("setup_marker.json")
|
||||
}
|
||||
|
||||
pub fn sandbox_users_path(codex_home: &Path) -> PathBuf {
|
||||
sandbox_dir(codex_home).join("sandbox_users.json")
|
||||
}
|
||||
|
||||
pub fn run_setup_refresh(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
) -> Result<()> {
|
||||
// Skip in danger-full-access.
|
||||
if matches!(policy, SandboxPolicy::DangerFullAccess) {
|
||||
return Ok(());
|
||||
}
|
||||
let payload = ElevationPayload {
|
||||
version: SETUP_VERSION,
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
read_roots: gather_read_roots(command_cwd, policy, policy_cwd),
|
||||
write_roots: gather_write_roots(policy, policy_cwd, command_cwd, env_map),
|
||||
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
||||
refresh_only: true,
|
||||
};
|
||||
let json = serde_json::to_vec(&payload)?;
|
||||
let b64 = BASE64_STANDARD.encode(json);
|
||||
let exe = find_setup_exe();
|
||||
log_note(
|
||||
&format!("setup refresh: invoking {}", exe.display()),
|
||||
Some(&sandbox_dir(codex_home)),
|
||||
);
|
||||
// Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC.
|
||||
let mut cmd = Command::new(&exe);
|
||||
cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null());
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| codex_home.to_path_buf());
|
||||
log_note(
|
||||
&format!(
|
||||
"setup refresh: spawning {} (cwd={}, payload_len={})",
|
||||
exe.display(),
|
||||
cwd.display(),
|
||||
b64.len()
|
||||
),
|
||||
Some(&sandbox_dir(codex_home)),
|
||||
);
|
||||
let status = cmd
|
||||
.status()
|
||||
.map_err(|e| {
|
||||
log_note(
|
||||
&format!("setup refresh: failed to spawn {}: {e}", exe.display()),
|
||||
Some(&sandbox_dir(codex_home)),
|
||||
);
|
||||
e
|
||||
})
|
||||
.context("spawn setup refresh")?;
|
||||
if !status.success() {
|
||||
log_note(
|
||||
&format!("setup refresh: exited with status {status:?}"),
|
||||
Some(&sandbox_dir(codex_home)),
|
||||
);
|
||||
return Err(anyhow!("setup refresh failed with status {}", status));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SetupMarker {
|
||||
pub version: u32,
|
||||
pub offline_username: String,
|
||||
pub online_username: String,
|
||||
#[serde(default)]
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
impl SetupMarker {
|
||||
pub fn version_matches(&self) -> bool {
|
||||
self.version == SETUP_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SandboxUserRecord {
|
||||
pub username: String,
|
||||
/// DPAPI-encrypted password blob, base64 encoded.
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SandboxUsersFile {
|
||||
pub version: u32,
|
||||
pub offline: SandboxUserRecord,
|
||||
pub online: SandboxUserRecord,
|
||||
}
|
||||
|
||||
impl SandboxUsersFile {
|
||||
pub fn version_matches(&self) -> bool {
|
||||
self.version == SETUP_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
fn is_elevated() -> Result<bool> {
|
||||
unsafe {
|
||||
let mut administrators_group: *mut c_void = std::ptr::null_mut();
|
||||
let ok = AllocateAndInitializeSid(
|
||||
&SECURITY_NT_AUTHORITY,
|
||||
2,
|
||||
SECURITY_BUILTIN_DOMAIN_RID,
|
||||
DOMAIN_ALIAS_RID_ADMINS,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
&mut administrators_group,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!(
|
||||
"AllocateAndInitializeSid failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
let mut is_member = 0i32;
|
||||
let check = CheckTokenMembership(0, administrators_group, &mut is_member as *mut _);
|
||||
FreeSid(administrators_group as *mut _);
|
||||
if check == 0 {
|
||||
return Err(anyhow!("CheckTokenMembership failed: {}", GetLastError()));
|
||||
}
|
||||
Ok(is_member != 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_existing(paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
paths
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
if !p.exists() {
|
||||
return None;
|
||||
}
|
||||
Some(dunce::canonicalize(p).unwrap_or_else(|_| p.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn gather_read_roots(
|
||||
command_cwd: &Path,
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut roots: Vec<PathBuf> = Vec::new();
|
||||
for p in [
|
||||
PathBuf::from(r"C:\Windows"),
|
||||
PathBuf::from(r"C:\Program Files"),
|
||||
PathBuf::from(r"C:\Program Files (x86)"),
|
||||
PathBuf::from(r"C:\ProgramData"),
|
||||
] {
|
||||
roots.push(p);
|
||||
}
|
||||
if let Ok(up) = std::env::var("USERPROFILE") {
|
||||
roots.push(PathBuf::from(up));
|
||||
}
|
||||
roots.push(command_cwd.to_path_buf());
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
|
||||
for root in writable_roots {
|
||||
let candidate = if root.is_absolute() {
|
||||
root.clone()
|
||||
} else {
|
||||
policy_cwd.join(root)
|
||||
};
|
||||
roots.push(candidate);
|
||||
}
|
||||
}
|
||||
canonical_existing(&roots)
|
||||
}
|
||||
|
||||
pub(crate) fn gather_write_roots(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut roots: Vec<PathBuf> = Vec::new();
|
||||
// Always include the command CWD for workspace-write.
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
roots.push(command_cwd.to_path_buf());
|
||||
}
|
||||
let AllowDenyPaths { allow, .. } =
|
||||
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
|
||||
roots.extend(allow);
|
||||
canonical_existing(&roots)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ElevationPayload {
|
||||
version: u32,
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
codex_home: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
real_user: String,
|
||||
#[serde(default)]
|
||||
refresh_only: bool,
|
||||
}
|
||||
|
||||
fn quote_arg(arg: &str) -> String {
|
||||
let needs = arg.is_empty()
|
||||
|| arg
|
||||
.chars()
|
||||
.any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"'));
|
||||
if !needs {
|
||||
return arg.to_string();
|
||||
}
|
||||
let mut out = String::from("\"");
|
||||
let mut bs = 0;
|
||||
for ch in arg.chars() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
bs += 1;
|
||||
}
|
||||
'"' => {
|
||||
out.push_str(&"\\".repeat(bs * 2 + 1));
|
||||
out.push('"');
|
||||
bs = 0;
|
||||
}
|
||||
_ => {
|
||||
if bs > 0 {
|
||||
out.push_str(&"\\".repeat(bs));
|
||||
bs = 0;
|
||||
}
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if bs > 0 {
|
||||
out.push_str(&"\\".repeat(bs * 2));
|
||||
}
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
|
||||
fn find_setup_exe() -> PathBuf {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let candidate = dir.join("codex-windows-sandbox-setup.exe");
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from("codex-windows-sandbox-setup.exe")
|
||||
}
|
||||
|
||||
fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<()> {
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::Win32::UI::Shell::ShellExecuteExW;
|
||||
use windows_sys::Win32::UI::Shell::SEE_MASK_NOCLOSEPROCESS;
|
||||
use windows_sys::Win32::UI::Shell::SHELLEXECUTEINFOW;
|
||||
let exe = find_setup_exe();
|
||||
let payload_json = serde_json::to_string(payload)?;
|
||||
let payload_b64 = BASE64_STANDARD.encode(payload_json.as_bytes());
|
||||
|
||||
if !needs_elevation {
|
||||
let status = Command::new(&exe)
|
||||
.arg(&payload_b64)
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.context("failed to launch setup helper")?;
|
||||
if !status.success() {
|
||||
return Err(anyhow!(
|
||||
"setup helper exited with status {:?}",
|
||||
status.code()
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let exe_w = crate::winutil::to_wide(&exe);
|
||||
let params = quote_arg(&payload_b64);
|
||||
let params_w = crate::winutil::to_wide(params);
|
||||
let verb_w = crate::winutil::to_wide("runas");
|
||||
let mut sei: SHELLEXECUTEINFOW = unsafe { std::mem::zeroed() };
|
||||
sei.cbSize = std::mem::size_of::<SHELLEXECUTEINFOW>() as u32;
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
sei.lpVerb = verb_w.as_ptr();
|
||||
sei.lpFile = exe_w.as_ptr();
|
||||
sei.lpParameters = params_w.as_ptr();
|
||||
// Hide the window for the elevated helper.
|
||||
sei.nShow = 0; // SW_HIDE
|
||||
let ok = unsafe { ShellExecuteExW(&mut sei) };
|
||||
if ok == 0 || sei.hProcess == 0 {
|
||||
return Err(anyhow!(
|
||||
"ShellExecuteExW failed to launch setup helper: {}",
|
||||
unsafe { GetLastError() }
|
||||
));
|
||||
}
|
||||
unsafe {
|
||||
WaitForSingleObject(sei.hProcess, INFINITE);
|
||||
let mut code: u32 = 1;
|
||||
GetExitCodeProcess(sei.hProcess, &mut code);
|
||||
CloseHandle(sei.hProcess);
|
||||
if code != 0 {
|
||||
return Err(anyhow!("setup helper exited with status {}", code));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_elevated_setup(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
read_roots_override: Option<Vec<PathBuf>>,
|
||||
write_roots_override: Option<Vec<PathBuf>>,
|
||||
) -> Result<()> {
|
||||
// Ensure the shared sandbox directory exists before we send it to the elevated helper.
|
||||
let sbx_dir = sandbox_dir(codex_home);
|
||||
std::fs::create_dir_all(&sbx_dir)?;
|
||||
let mut write_roots = if let Some(roots) = write_roots_override {
|
||||
roots
|
||||
} else {
|
||||
gather_write_roots(policy, policy_cwd, command_cwd, env_map)
|
||||
};
|
||||
if !write_roots.contains(&sbx_dir) {
|
||||
write_roots.push(sbx_dir.clone());
|
||||
}
|
||||
let read_roots = if let Some(roots) = read_roots_override {
|
||||
roots
|
||||
} else {
|
||||
gather_read_roots(command_cwd, policy, policy_cwd)
|
||||
};
|
||||
let payload = ElevationPayload {
|
||||
version: SETUP_VERSION,
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
read_roots,
|
||||
write_roots,
|
||||
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
||||
refresh_only: false,
|
||||
};
|
||||
let needs_elevation = !is_elevated()?;
|
||||
run_setup_exe(&payload, needs_elevation)
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ 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;
|
||||
use windows_sys::Win32::System::Threading::OpenProcessToken;
|
||||
|
||||
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
||||
const LUA_TOKEN: u32 = 0x04;
|
||||
|
|
@ -192,7 +191,14 @@ unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
|||
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());
|
||||
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()));
|
||||
}
|
||||
|
|
@ -203,24 +209,6 @@ unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Opens the current process token and adjusts privileges; caller should ensure this is needed in the current context.
|
||||
#[allow(dead_code)]
|
||||
pub unsafe fn enable_privilege_on_current(name: &str) -> Result<()> {
|
||||
let mut h: HANDLE = 0;
|
||||
let ok = OpenProcessToken(
|
||||
GetCurrentProcess(),
|
||||
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
||||
&mut h,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("OpenProcessToken failed: {}", GetLastError()));
|
||||
}
|
||||
let res = enable_single_privilege(h, name);
|
||||
CloseHandle(h);
|
||||
res
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle.
|
||||
#[allow(dead_code)]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue