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:
iceweasel-oai 2025-12-10 21:23:16 -08:00 committed by GitHub
parent 83aac0f985
commit 13c0919bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1681 additions and 33 deletions

View file

@ -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"

View 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();
}

View file

@ -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>

View file

@ -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 {

View file

@ -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(

View 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");
}

View 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)
}

View 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,
})
}

View file

@ -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;

View file

@ -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 {

View 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(())
}

View 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)
}

View file

@ -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)]