Compare commits

...

1 Commits

Author SHA1 Message Date
iceweasel-oai
ce9d7c59a4 checkpoint 2025-12-03 14:45:52 -08:00
15 changed files with 2375 additions and 148 deletions

91
codex-rs/Cargo.lock generated
View File

@@ -1684,6 +1684,8 @@ name = "codex-windows-sandbox"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"chrono",
"codex-protocol",
"dirs-next",
"dunce",
@@ -1691,6 +1693,7 @@ dependencies = [
"serde",
"serde_json",
"tempfile",
"windows 0.58.0",
"windows-sys 0.52.0",
]
@@ -3111,7 +3114,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
"windows-core 0.61.2",
]
[[package]]
@@ -4746,7 +4749,7 @@ dependencies = [
"nix 0.30.1",
"tokio",
"tracing",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -7405,6 +7408,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
@@ -7412,7 +7425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
"windows-core 0.61.2",
"windows-future",
"windows-link 0.1.3",
"windows-numerics",
@@ -7424,7 +7437,20 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
@@ -7433,11 +7459,11 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link 0.1.3",
"windows-result",
"windows-strings",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
@@ -7446,11 +7472,22 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@@ -7462,6 +7499,17 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
@@ -7491,7 +7539,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-core 0.61.2",
"windows-link 0.1.3",
]
@@ -7502,8 +7550,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result",
"windows-strings",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
@@ -7515,6 +7572,16 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.4.2"

View File

@@ -8,11 +8,35 @@ version.workspace = true
name = "codex_windows_sandbox"
path = "src/lib.rs"
[[bin]]
name = "codex-windows-sandbox-setup"
path = "src/bin/setup.rs"
[[bin]]
name = "codex-command-runner"
path = "src/bin/command_runner.rs"
[[bin]]
name = "runner-smoke"
path = "src/bin/runner_smoke.rs"
[[bin]]
name = "runner-stub"
path = "src/bin/runner_stub.rs"
[dependencies]
anyhow = "1.0"
base64 = { workspace = true }
dunce = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", 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"
@@ -40,10 +64,14 @@ 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]

View File

@@ -160,6 +160,7 @@ pub unsafe fn dacl_effective_allows_write(p_dacl: *mut ACL, psid: *mut c_void) -
// Fallback: simple allow ACE scan (already ignores inherit-only)
dacl_has_write_allow_for_sid(p_dacl, psid)
}
#[allow(clippy::missing_safety_doc)]
pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
let mut p_sd: *mut c_void = std::ptr::null_mut();
let mut p_dacl: *mut ACL = std::ptr::null_mut();
@@ -217,6 +218,10 @@ pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
Ok(added)
}
/// Adds a deny ACE to prevent write/append/delete for the given SID on the target path.
///
/// # Safety
/// Caller must ensure `psid` points to a valid SID and `path` refers to an existing file or directory.
pub unsafe fn add_deny_write_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
let mut p_sd: *mut c_void = std::ptr::null_mut();
let mut p_dacl: *mut ACL = std::ptr::null_mut();
@@ -330,6 +335,10 @@ pub unsafe fn revoke_ace(path: &Path, psid: *mut c_void) {
}
}
/// Grants RX to the null device for the given SID to support stdout/stderr redirection.
///
/// # Safety
/// Caller must ensure `psid` is a valid SID pointer.
pub unsafe fn allow_null_device(psid: *mut c_void) {
let desired = 0x00020000 | 0x00040000; // READ_CONTROL | WRITE_DAC
let h = CreateFileW(

View File

@@ -0,0 +1,219 @@
use anyhow::{Context, Result};
use codex_windows_sandbox::{
allow_null_device, cap_sid_file, convert_string_sid_to_sid, create_process_as_user,
create_readonly_token_with_cap_from, create_workspace_write_token_with_cap_from,
get_current_token_for_restriction, load_or_create_cap_sids, log_note, parse_policy, to_wide,
SandboxPolicy,
};
use serde::Deserialize;
use std::collections::HashMap;
use std::ffi::c_void;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, OPEN_EXISTING,
};
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
use windows_sys::Win32::System::Threading::INFINITE;
#[derive(Debug, Deserialize)]
struct RunnerRequest {
policy_json_or_preset: String,
#[allow(dead_code)]
sandbox_policy_cwd: PathBuf,
codex_home: PathBuf,
command: Vec<String>,
cwd: PathBuf,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
stdin_pipe: String,
stdout_pipe: String,
stderr_pipe: String,
}
// Best-effort early marker to detect image load before main.
#[used]
#[allow(dead_code)]
static LOAD_MARKER: fn() = load_marker;
#[allow(dead_code)]
const fn load_marker() {
// const fn placeholder; actual work is in write_load_marker, invoked at start of main.
}
fn write_load_marker() {
if let Some(mut p) = dirs_next::home_dir() {
p.push(".codex");
let _ = std::fs::create_dir_all(&p);
p.push("runner_load_marker.txt");
let _ = std::fs::write(&p, "loaded");
}
}
unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
if h == 0 {
return Err(anyhow::anyhow!("CreateJobObjectW failed"));
}
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let ok = SetInformationJobObject(
h,
JobObjectExtendedLimitInformation,
&mut limits as *mut _ as *mut _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
);
if ok == 0 {
return Err(anyhow::anyhow!("SetInformationJobObject failed"));
}
Ok(h)
}
fn main() -> Result<()> {
write_load_marker();
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("read request")?;
let req: RunnerRequest =
serde_json::from_str(&input).context("parse runner request json from stdin")?;
log_note(
&format!(
"runner start cwd={} cmd={:?}",
req.cwd.display(),
req.command
),
Some(&req.codex_home),
);
log_note(
&format!(
"stdin_pipe={} stdout_pipe={} stderr_pipe={}",
req.stdin_pipe, req.stdout_pipe, req.stderr_pipe
),
Some(&req.codex_home),
);
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
// Ensure cap SIDs exist.
let caps = load_or_create_cap_sids(&req.codex_home);
let cap_sid_path = cap_sid_file(&req.codex_home);
fs::write(&cap_sid_path, serde_json::to_string(&caps)?).context("write cap sid file")?;
let psid_cap: *mut c_void = match &policy {
SandboxPolicy::ReadOnly => unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
SandboxPolicy::WorkspaceWrite { .. } => unsafe {
convert_string_sid_to_sid(&caps.workspace).unwrap()
},
SandboxPolicy::DangerFullAccess => {
anyhow::bail!("DangerFullAccess is not supported for runner")
}
};
// Create restricted token from current process token.
let base = unsafe { get_current_token_for_restriction()? };
let token_res: Result<(HANDLE, *mut c_void)> = unsafe {
match &policy {
SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap),
SandboxPolicy::WorkspaceWrite { .. } => {
create_workspace_write_token_with_cap_from(base, psid_cap)
}
SandboxPolicy::DangerFullAccess => unreachable!(),
}
};
let (h_token, psid_to_use) = token_res?;
unsafe {
CloseHandle(base);
}
unsafe {
allow_null_device(psid_to_use);
}
// Open named pipes for stdio.
let open_pipe = |name: &str, access: u32| -> Result<HANDLE> {
let path = to_wide(name);
let handle = unsafe {
CreateFileW(
path.as_ptr(),
access,
0,
std::ptr::null_mut(),
OPEN_EXISTING,
0,
0,
)
};
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
let err = unsafe { GetLastError() };
log_note(
&format!("CreateFileW failed for pipe {name}: {err}"),
Some(&req.codex_home),
);
return Err(anyhow::anyhow!("CreateFileW failed for pipe {name}: {err}"));
}
Ok(handle)
};
let h_stdin = open_pipe(&req.stdin_pipe, FILE_GENERIC_READ)?;
let h_stdout = open_pipe(&req.stdout_pipe, FILE_GENERIC_WRITE)?;
let h_stderr = open_pipe(&req.stderr_pipe, FILE_GENERIC_WRITE)?;
log_note("pipes opened", Some(&req.codex_home));
// Build command and env, spawn with CreateProcessWithTokenW.
let (proc_info, _si) = unsafe {
create_process_as_user(
h_token,
&req.command,
&req.cwd,
&req.env_map,
Some(&req.codex_home),
Some((h_stdin, h_stdout, h_stderr)),
)?
};
log_note("spawned child process", Some(&req.codex_home));
// Optional job kill on close.
let h_job = unsafe { create_job_kill_on_close().ok() };
if let Some(job) = h_job {
unsafe {
let _ = AssignProcessToJobObject(job, proc_info.hProcess);
}
}
// Wait for process.
let _ = unsafe {
WaitForSingleObject(
proc_info.hProcess,
req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE),
)
};
let mut exit_code: u32 = 1;
unsafe {
windows_sys::Win32::System::Threading::GetExitCodeProcess(
proc_info.hProcess,
&mut exit_code,
);
if proc_info.hThread != 0 {
CloseHandle(proc_info.hThread);
}
if proc_info.hProcess != 0 {
CloseHandle(proc_info.hProcess);
}
CloseHandle(h_token);
if let Some(job) = h_job {
CloseHandle(job);
}
}
log_note(
&format!("runner exiting with code {}", exit_code),
Some(&req.codex_home),
);
std::process::exit(exit_code as i32);
}

View File

@@ -0,0 +1,70 @@
use anyhow::Result;
use codex_windows_sandbox::to_wide;
use codex_windows_sandbox::{require_logon_sandbox_creds, SandboxPolicy};
use std::collections::HashMap;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::System::Threading::CreateProcessWithLogonW;
use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE;
use windows_sys::Win32::System::Threading::{PROCESS_INFORMATION, STARTUPINFOW};
fn main() -> Result<()> {
let cwd = std::env::current_dir()?;
let codex_home = dirs_next::home_dir().unwrap_or(cwd.clone()).join(".codex");
let policy = SandboxPolicy::ReadOnly;
let _policy_json = serde_json::to_string(&policy)?;
let env_map: HashMap<String, String> = HashMap::new();
// Fetch sandbox creds (will prompt setup if missing).
let creds = require_logon_sandbox_creds(&policy, &cwd, &cwd, &env_map, &codex_home)?;
// Optional target override:
// - "stub" to launch runner-stub.exe
// - any other argument list is treated as the full command line to run.
let args: Vec<String> = std::env::args().skip(1).collect();
let target = args.first().cloned().unwrap_or_else(|| "cmd".to_string());
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let user_w = to_wide(&creds.username);
let domain_w = to_wide(".");
let password_w = to_wide(&creds.password);
let cmdline = if target == "stub" {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("runner-stub.exe")))
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or_else(|| "runner-stub.exe".to_string())
} else if !args.is_empty() {
args.join(" ")
} else {
"cmd /c whoami".to_string()
};
let cmd_w = to_wide(&cmdline);
let cwd_w = to_wide(&cwd);
let ok = unsafe {
CreateProcessWithLogonW(
user_w.as_ptr(),
domain_w.as_ptr(),
password_w.as_ptr(),
LOGON_WITH_PROFILE,
std::ptr::null(),
cmd_w.as_ptr() as *mut _,
0,
std::ptr::null(),
cwd_w.as_ptr(),
&si,
&mut pi,
)
};
if ok == 0 {
let err = unsafe { GetLastError() };
println!("CreateProcessWithLogonW failed: {}", err);
return Ok(());
}
println!(
"CreateProcessWithLogonW succeeded pid={} (target={})",
pi.dwProcessId, target
);
Ok(())
}

View File

@@ -0,0 +1,33 @@
use anyhow::Result;
use codex_windows_sandbox::{run_windows_sandbox_capture, SandboxPolicy};
use std::collections::HashMap;
fn main() -> Result<()> {
let cwd = std::env::current_dir()?;
let codex_home = dirs_next::home_dir().unwrap_or(cwd.clone()).join(".codex");
let policy = SandboxPolicy::ReadOnly;
let policy_json = serde_json::to_string(&policy)?;
let mut env_map = HashMap::new();
env_map.insert("SBX_DEBUG".to_string(), "1".to_string());
let res = run_windows_sandbox_capture(
&policy_json,
&cwd,
&codex_home,
vec![
"cmd".to_string(),
"/c".to_string(),
"echo smoke-runner".to_string(),
],
&cwd,
env_map,
Some(10_000),
)?;
println!("exit_code={}", res.exit_code);
println!("stdout={}", String::from_utf8_lossy(&res.stdout));
println!("stderr={}", String::from_utf8_lossy(&res.stderr));
println!("timed_out={}", res.timed_out);
Ok(())
}

View File

@@ -0,0 +1,105 @@
use anyhow::Result;
use codex_windows_sandbox::{
convert_string_sid_to_sid, create_readonly_token_with_cap_from,
get_current_token_for_restriction, load_or_create_cap_sids, to_wide,
};
use std::collections::HashMap;
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError};
use windows_sys::Win32::System::Threading::{
CreateProcessAsUserW, WaitForSingleObject, CREATE_UNICODE_ENVIRONMENT, INFINITE,
PROCESS_INFORMATION, STARTUPINFOW,
};
fn main() -> Result<()> {
// Log current environment for diagnostics to a file under the sandbox user's profile.
let env_dump = std::env::vars()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("\n");
// Attempt multiple destinations; log errors to stderr.
if let Some(mut p) = dirs_next::home_dir() {
p.push(".codex");
if let Err(e) = std::fs::create_dir_all(&p) {
eprintln!("failed to create {:?}: {e}", p);
}
p.push("runner_stub_env.txt");
if let Err(e) = std::fs::write(&p, &env_dump) {
eprintln!("failed to write {:?}: {e}", p);
}
} else {
eprintln!("home_dir not available");
}
let public_path = std::path::Path::new(r"C:\Users\Public\runner_stub_env.txt");
if let Err(e) = std::fs::write(public_path, &env_dump) {
eprintln!("failed to write {:?}: {e}", public_path);
}
let cwd_path = std::env::current_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("."))
.join("runner_stub_env.txt");
if let Err(e) = std::fs::write(&cwd_path, &env_dump) {
eprintln!("failed to write {:?}: {e}", cwd_path);
}
// Create restricted token with readonly capability.
let codex_home = dirs_next::home_dir()
.unwrap_or_else(std::env::temp_dir)
.join(".codex");
let caps = load_or_create_cap_sids(&codex_home);
let psid_cap = unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() };
let base = unsafe { get_current_token_for_restriction()? };
let (restricted, _psid_used) = unsafe { create_readonly_token_with_cap_from(base, psid_cap)? };
unsafe {
CloseHandle(base);
}
// Launch a trivial command with the restricted token.
let cmd = "cmd";
let args = "/c echo restricted-stub";
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let mut cmdline = to_wide(format!("{cmd} {args}"));
let cwd = std::env::current_dir()?;
let mut env_block: Vec<u16> = Vec::new();
let env_map: HashMap<String, String> = std::env::vars().collect();
for (k, v) in env_map {
let mut w = to_wide(format!("{k}={v}"));
w.pop();
env_block.extend_from_slice(&w);
env_block.push(0);
}
env_block.push(0);
let ok = unsafe {
CreateProcessAsUserW(
restricted,
std::ptr::null(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
CREATE_UNICODE_ENVIRONMENT,
env_block.as_ptr() as *const _,
to_wide(&cwd).as_ptr(),
&mut si,
&mut pi,
)
};
if ok == 0 {
eprintln!("CreateProcessAsUserW failed: {}", unsafe { GetLastError() });
} else {
unsafe {
WaitForSingleObject(pi.hProcess, INFINITE);
if pi.hThread != 0 {
CloseHandle(pi.hThread);
}
if pi.hProcess != 0 {
CloseHandle(pi.hProcess);
}
}
}
unsafe {
CloseHandle(restricted);
}
Ok(())
}

View File

@@ -0,0 +1,784 @@
use anyhow::Context;
use anyhow::Result;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use codex_windows_sandbox::add_allow_ace;
use codex_windows_sandbox::dpapi_protect;
use codex_windows_sandbox::sandbox_dir;
use codex_windows_sandbox::string_from_sid_bytes;
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::GetEffectiveRightsFromAclW;
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
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_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,
}
#[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,
}
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_to_string(sid: &[u8]) -> Result<String> {
string_from_sid_bytes(sid).map_err(anyhow::Error::msg)
}
fn sid_bytes_to_psid(sid: &[u8]) -> Result<*mut c_void> {
let sid_str = sid_to_string(sid)?;
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 trustee_has_rx(path: &Path, trustee: &str) -> Result<bool> {
let sid = resolve_sid(trustee)?;
unsafe {
let sid_str = sid_to_string(&sid)?;
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()
));
}
let path_w = to_wide(path.as_os_str());
let mut existing_dacl: *mut ACL = std::ptr::null_mut();
let mut sd: *mut c_void = std::ptr::null_mut();
let get_res = GetNamedSecurityInfoW(
path_w.as_ptr() as *mut u16,
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut existing_dacl,
std::ptr::null_mut(),
&mut sd,
);
if get_res != 0 {
return Err(anyhow::anyhow!(
"GetNamedSecurityInfoW failed for {}: {}",
path.display(),
get_res
));
}
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 mut mask: u32 = 0;
let eff = GetEffectiveRightsFromAclW(existing_dacl, &trustee, &mut mask);
if eff != 0 {
return Err(anyhow::anyhow!(
"GetEffectiveRightsFromAclW failed for {}: {}",
path.display(),
eff
));
}
if !sd.is_null() {
LocalFree(sd as HLOCAL);
}
if !psid.is_null() {
LocalFree(psid as HLOCAL);
}
let needed = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
Ok((mask & needed) == needed)
}
}
fn collect_system_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Ok(sr) = std::env::var("SystemRoot") {
roots.push(PathBuf::from(sr));
} else {
roots.push(PathBuf::from(r"C:\Windows"));
}
if let Ok(pf) = std::env::var("ProgramFiles") {
roots.push(PathBuf::from(pf));
} else {
roots.push(PathBuf::from(r"C:\Program Files"));
}
if let Ok(pf86) = std::env::var("ProgramFiles(x86)") {
roots.push(PathBuf::from(pf86));
} else {
roots.push(PathBuf::from(r"C:\Program Files (x86)"));
}
if let Ok(pd) = std::env::var("ProgramData") {
roots.push(PathBuf::from(pd));
} else {
roots.push(PathBuf::from(r"C:\ProgramData"));
}
roots
}
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 = sid_to_string(sid)?;
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 path_w = to_wide(path.as_os_str());
let mut existing_dacl: *mut ACL = std::ptr::null_mut();
let mut sd: *mut c_void = std::ptr::null_mut();
let get_res = GetNamedSecurityInfoW(
path_w.as_ptr() as *mut u16,
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut existing_dacl,
std::ptr::null_mut(),
&mut sd,
);
if get_res != 0 {
return Err(anyhow::anyhow!(
"GetNamedSecurityInfoW failed for {}: {}",
path.display(),
get_res
));
}
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(
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 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, 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,
),
];
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 {
let sid_str = sid_to_string(&sid_bytes)?;
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,
) -> 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(),
};
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(())
}
fn 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 log_path = payload.codex_home.join("codex_sbx_setup.log");
std::fs::create_dir_all(&payload.codex_home)?;
let mut log = File::options()
.create(true)
.append(true)
.open(&log_path)
.context("open log")?;
log_line(&mut log, "setup binary started")?;
let offline_pwd = random_password();
let online_pwd = random_password();
log_line(
&mut log,
&format!(
"ensuring sandbox users offline={} online={}",
payload.offline_username, payload.online_username
),
)?;
ensure_local_user(&payload.offline_username, &offline_pwd, &mut log)?;
ensure_local_user(&payload.online_username, &online_pwd, &mut 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 system_roots = collect_system_roots();
let offline_sid_str = sid_to_string(&offline_sid)?;
log_line(
&mut log,
&format!(
"resolved SIDs offline={} online={}",
offline_sid_str,
sid_to_string(&online_sid)?
),
)?;
run_netsh_firewall(&offline_sid_str, &mut log)?;
for root in &payload.read_roots {
if !root.exists() {
continue;
}
let mut skipped = false;
for trustee in ["Users", "Authenticated Users", "Everyone"] {
if trustee_has_rx(root, trustee).unwrap_or(false) {
log_line(
&mut log,
&format!("{trustee} already has RX on {}; skipping", root.display()),
)?;
skipped = true;
break;
}
}
if skipped {
continue;
}
if system_roots.contains(root) {
log_line(
&mut log,
&format!(
"system root {} missing RX for Users/AU/Everyone; skipping to avoid hang",
root.display()
),
)?;
continue;
}
log_line(
&mut log,
&format!("granting read ACE to {} for sandbox users", root.display()),
)?;
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,
&mut log,
Duration::from_millis(25),
) {
Ok(_) => {}
Err(e) => {
log_line(
&mut log,
&format!(
"grant read ACE timed out/failed on {} for {label}: {e}",
root.display()
),
)?;
// Best-effort: skip to next root.
continue;
}
}
}
log_line(&mut log, &format!("granted read ACE to {}", root.display()))?;
}
for root in &payload.write_roots {
if !root.exists() {
continue;
}
log_line(
&mut log,
&format!("granting write ACE to {} for sandbox users", root.display()),
)?;
unsafe {
add_allow_ace(root, offline_psid)
.with_context(|| format!("failed to grant write ACE on {}", root.display()))?;
add_allow_ace(root, online_psid)
.with_context(|| format!("failed to grant write ACE on {}", root.display()))?;
}
log_line(
&mut log,
&format!("granted write ACE to {}", root.display()),
)?;
}
lock_sandbox_dir(
&sandbox_dir(&payload.codex_home),
&payload.real_user,
&mut log,
)?;
log_line(&mut log, "sandbox dir ACL applied")?;
write_secrets(
&payload.codex_home,
&payload.offline_username,
&offline_pwd,
&payload.online_username,
&online_pwd,
)?;
log_line(
&mut 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);
}
}
log_line(&mut log, "setup binary completed")?;
Ok(())
}

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,136 @@
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 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,
#[allow(dead_code)]
offline: bool,
}
#[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 offline = !policy.has_full_network_access();
let chosen = if offline {
users.offline
} else {
users.online
};
let password = decode_password(&chosen)?;
Ok(Some(SandboxIdentity {
username: chosen.username.clone(),
password,
offline,
}))
}
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 mut identity = select_identity(policy, codex_home)?;
if identity.is_none() {
run_elevated_setup(policy, policy_cwd, command_cwd, env_map, codex_home)?;
identity = select_identity(policy, 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,18 +4,55 @@ 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, setup, token, winutil
);
#[cfg(target_os = "windows")]
pub use acl::{add_allow_ace, add_deny_write_ace, allow_null_device};
#[cfg(target_os = "windows")]
pub use allow::compute_allow_paths;
#[cfg(target_os = "windows")]
pub use audit::apply_world_writable_scan_and_denies;
#[cfg(target_os = "windows")]
pub use cap::{cap_sid_file, 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 policy::{parse_policy, SandboxPolicy};
#[cfg(target_os = "windows")]
pub use process::create_process_as_user;
#[cfg(target_os = "windows")]
pub use setup::run_elevated_setup;
#[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, create_readonly_token_with_cap_from,
create_workspace_write_token_with_cap_from, get_current_token_for_restriction,
};
#[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(target_os = "windows")]
pub use winutil::to_wide;
#[cfg(not(target_os = "windows"))]
pub use stub::apply_world_writable_scan_and_denies;
#[cfg(not(target_os = "windows"))]
pub use stub::run_elevated_setup;
#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_capture;
#[cfg(not(target_os = "windows"))]
pub use stub::CaptureResult;
@@ -33,8 +70,10 @@ mod windows_impl {
use super::env::apply_no_network_to_env;
use super::env::ensure_non_interactive_pager;
use super::env::normalize_null_device_env;
use super::identity::require_logon_sandbox_creds;
use super::logging::debug_log;
use super::logging::log_failure;
use super::logging::log_note;
use super::logging::log_start;
use super::logging::log_success;
use super::policy::parse_policy;
@@ -43,29 +82,45 @@ mod windows_impl {
use super::winutil::format_last_error;
use super::winutil::to_wide;
use anyhow::Result;
use rand::rngs::SmallRng;
use rand::Rng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::ffi::c_void;
use std::fs;
use std::io;
use std::os::windows::io::FromRawHandle;
use std::path::Path;
use std::path::PathBuf;
use std::ptr;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::SetHandleInformation;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
use windows_sys::Win32::System::Pipes::CreatePipe;
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
use windows_sys::Win32::System::Pipes::ConnectNamedPipe;
use windows_sys::Win32::System::Pipes::CreateNamedPipeW;
// PIPE_ACCESS_DUPLEX is 0x00000003; not exposed in windows-sys 0.52, so use the value directly.
const PIPE_ACCESS_DUPLEX: u32 = 0x0000_0003;
use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
use windows_sys::Win32::Security::LogonUserW;
use windows_sys::Win32::Security::LOGON32_LOGON_INTERACTIVE;
use windows_sys::Win32::Security::LOGON32_PROVIDER_DEFAULT;
use windows_sys::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES};
use windows_sys::Win32::System::Environment::CreateEnvironmentBlock;
use windows_sys::Win32::System::Environment::DestroyEnvironmentBlock;
use windows_sys::Win32::System::Pipes::PIPE_READMODE_BYTE;
use windows_sys::Win32::System::Pipes::PIPE_TYPE_BYTE;
use windows_sys::Win32::System::Pipes::PIPE_WAIT;
use windows_sys::Win32::System::Threading::CreateProcessWithLogonW;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
use windows_sys::Win32::System::Threading::INFINITE;
use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
use windows_sys::Win32::System::Threading::STARTUPINFOW;
type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE));
use windows_sys::Win32::UI::Shell::LoadUserProfileA;
use windows_sys::Win32::UI::Shell::UnloadUserProfile;
use windows_sys::Win32::UI::Shell::PROFILEINFOA;
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
!policy.has_full_network_access()
@@ -83,6 +138,26 @@ mod windows_impl {
Ok(())
}
fn find_runner_exe() -> PathBuf {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let candidate = dir.join("codex-command-runner.exe");
if candidate.exists() {
return candidate;
}
let release_candidate = dir
.parent()
.map(|p| p.join("release").join("codex-command-runner.exe"));
if let Some(rel) = release_candidate {
if rel.exists() {
return rel;
}
}
}
}
PathBuf::from("codex-command-runner.exe")
}
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();
@@ -143,32 +218,64 @@ mod windows_impl {
quoted
}
unsafe fn setup_stdio_pipes() -> io::Result<PipeHandles> {
let mut in_r: HANDLE = 0;
let mut in_w: HANDLE = 0;
let mut out_r: HANDLE = 0;
let mut out_w: HANDLE = 0;
let mut err_r: HANDLE = 0;
let mut err_w: HANDLE = 0;
if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
fn pipe_name(suffix: &str) -> String {
let mut rng = SmallRng::from_entropy();
format!(r"\\.\pipe\codex-runner-{:x}-{}", rng.gen::<u128>(), suffix)
}
fn create_named_pipe(name: &str, access: u32) -> io::Result<HANDLE> {
// Allow sandbox users to connect by granting Everyone full access on the pipe.
let sddl = to_wide("D:(A;;GA;;;WD)");
let mut sd: PSECURITY_DESCRIPTOR = ptr::null_mut();
let ok = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
1, // SDDL_REVISION_1
&mut sd,
ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::from_raw_os_error(unsafe {
GetLastError() as i32
}));
}
if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
let mut sa = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: sd,
bInheritHandle: 0,
};
let wide = to_wide(name);
let h = unsafe {
CreateNamedPipeW(
wide.as_ptr(),
access,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1,
65536,
65536,
0,
&mut sa as *mut SECURITY_ATTRIBUTES,
)
};
if h == 0 || h == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
return Err(io::Error::from_raw_os_error(unsafe {
GetLastError() as i32
}));
}
if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 {
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
Ok(h)
}
fn connect_pipe(h: HANDLE) -> io::Result<()> {
let ok = unsafe { ConnectNamedPipe(h, ptr::null_mut()) };
if ok == 0 {
let err = unsafe { GetLastError() };
const ERROR_PIPE_CONNECTED: u32 = 535;
if err != ERROR_PIPE_CONNECTED {
return Err(io::Error::from_raw_os_error(err as i32));
}
}
if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
}
if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
}
if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
}
Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w)))
Ok(())
}
pub struct CaptureResult {
@@ -178,6 +285,20 @@ mod windows_impl {
pub timed_out: bool,
}
#[derive(serde::Serialize)]
struct RunnerPayload {
policy_json_or_preset: String,
sandbox_policy_cwd: PathBuf,
codex_home: PathBuf,
command: Vec<String>,
cwd: PathBuf,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
stdin_pipe: String,
stdout_pipe: String,
stderr_pipe: String,
}
pub fn run_windows_sandbox_capture(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
@@ -201,99 +322,270 @@ mod windows_impl {
log_start(&command, logs_base_dir);
let cap_sid_path = cap_sid_file(codex_home);
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
let sandbox_creds =
require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?;
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
match &policy {
SandboxPolicy::ReadOnly => {
let caps = load_or_create_cap_sids(codex_home);
ensure_dir(&cap_sid_path)?;
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
super::token::create_readonly_token_with_cap(psid)?
}
SandboxPolicy::WorkspaceWrite { .. } => {
let caps = load_or_create_cap_sids(codex_home);
ensure_dir(&cap_sid_path)?;
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
super::token::create_workspace_write_token_with_cap(psid)?
}
SandboxPolicy::DangerFullAccess => {
anyhow::bail!("DangerFullAccess is not supported for sandboxing")
}
// Build capability SID for ACL grants.
let psid_to_use = match &policy {
SandboxPolicy::ReadOnly => {
let caps = load_or_create_cap_sids(codex_home);
ensure_dir(&cap_sid_path)?;
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() }
}
SandboxPolicy::WorkspaceWrite { .. } => {
let caps = load_or_create_cap_sids(codex_home);
ensure_dir(&cap_sid_path)?;
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() }
}
SandboxPolicy::DangerFullAccess => {
anyhow::bail!("DangerFullAccess is not supported for sandboxing")
}
};
unsafe {
if is_workspace_write {
if let Ok(base) = super::token::get_current_token_for_restriction() {
if let Ok(bytes) = super::token::get_logon_sid_bytes(base) {
let mut tmp = bytes.clone();
let psid2 = tmp.as_mut_ptr() as *mut c_void;
allow_null_device(psid2);
}
windows_sys::Win32::Foundation::CloseHandle(base);
}
}
}
let persist_aces = is_workspace_write;
let AllowDenyPaths { allow, deny } =
compute_allow_paths(&policy, sandbox_policy_cwd, &current_dir, &env_map);
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
unsafe {
for p in &allow {
if let Ok(added) = add_allow_ace(p, psid_to_use) {
if added {
if persist_aces {
if p.is_dir() {
// best-effort seeding omitted intentionally
}
} else {
guards.push((p.clone(), psid_to_use));
}
}
}
}
for p in &deny {
for p in &deny {
unsafe {
if let Ok(added) = add_deny_write_ace(p, psid_to_use) {
if added && !persist_aces {
guards.push((p.clone(), psid_to_use));
}
}
}
}
if is_workspace_write {
for p in &allow {
unsafe {
if let Ok(added) = add_allow_ace(p, psid_to_use) {
if added && !persist_aces {
guards.push((p.clone(), psid_to_use));
}
}
}
}
}
unsafe {
allow_null_device(psid_to_use);
}
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
// Prepare named pipes for runner.
let stdin_name = pipe_name("stdin");
let stdout_name = pipe_name("stdout");
let stderr_name = pipe_name("stderr");
log_note(
&format!(
"preparing pipes stdin={} stdout={} stderr={}",
stdin_name, stdout_name, stderr_name
),
logs_base_dir,
);
let h_stdin_pipe = create_named_pipe(
&stdin_name,
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
)?;
let h_stdout_pipe = create_named_pipe(
&stdout_name,
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
)?;
let h_stderr_pipe = create_named_pipe(
&stderr_name,
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
)?;
// Build runner payload.
let payload = RunnerPayload {
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
codex_home: codex_home.to_path_buf(),
command: command.clone(),
cwd: cwd.to_path_buf(),
env_map: env_map.clone(),
timeout_ms,
stdin_pipe: stdin_name.clone(),
stdout_pipe: stdout_name.clone(),
stderr_pipe: stderr_name.clone(),
};
let payload_json = serde_json::to_string(&payload)?;
// Launch runner as sandbox user via CreateProcessWithLogonW.
let runner_exe = find_runner_exe();
let runner_cmdline = runner_exe
.to_str()
.map(|s| s.to_string())
.unwrap_or_else(|| "codex-command-runner.exe".to_string());
log_note(
&format!(
"launching runner exe={} as user={} cwd={}",
runner_cmdline,
sandbox_creds.username,
cwd.display()
),
logs_base_dir,
);
let cmdline_str = quote_windows_arg(&runner_cmdline);
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
fn build_sandbox_env_block(
username: &str,
password: &str,
logs_base_dir: Option<&Path>,
) -> Option<Vec<u16>> {
unsafe {
let user_w = to_wide(username);
let domain_w = to_wide(".");
let password_w = to_wide(password);
let mut h_tok: HANDLE = 0;
let ok = LogonUserW(
user_w.as_ptr(),
domain_w.as_ptr(),
password_w.as_ptr(),
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
&mut h_tok,
);
if ok == 0 || h_tok == 0 {
log_note(
&format!(
"build_sandbox_env_block: LogonUserW failed for {} err={}",
username,
GetLastError()
),
logs_base_dir,
);
return None;
}
let mut profile: PROFILEINFOA = std::mem::zeroed();
profile.dwSize = std::mem::size_of::<PROFILEINFOA>() as u32;
profile.lpUserName = user_w.as_ptr() as *mut _;
let profile_loaded = LoadUserProfileA(h_tok, &mut profile as *mut _);
if profile_loaded == 0 {
log_note(
&format!(
"build_sandbox_env_block: LoadUserProfile failed err={}",
GetLastError()
),
logs_base_dir,
);
}
let mut env_block_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
let env_ok = CreateEnvironmentBlock(&mut env_block_ptr, h_tok, 0);
if env_ok == 0 || env_block_ptr.is_null() {
log_note(
&format!(
"build_sandbox_env_block: CreateEnvironmentBlock failed err={}",
GetLastError()
),
logs_base_dir,
);
if profile_loaded != 0 {
let _ = UnloadUserProfile(h_tok, profile.hProfile);
}
CloseHandle(h_tok);
return None;
}
// Convert env block to map for patch/logging.
let mut map = HashMap::new();
let mut ptr_u16 = env_block_ptr as *const u16;
loop {
// find len to null
let mut len = 0;
while *ptr_u16.add(len) != 0 {
len += 1;
}
if len == 0 {
break;
}
let slice = std::slice::from_raw_parts(ptr_u16, len);
if let Ok(s) = String::from_utf16(slice) {
if let Some((k, v)) = s.split_once('=') {
map.insert(k.to_string(), v.to_string());
}
}
ptr_u16 = ptr_u16.add(len + 1);
}
// Patch critical vars to the sandbox profile.
let profile_dir = format!(r"C:\Users\{}", username);
map.insert("USERPROFILE".to_string(), profile_dir.clone());
map.insert("HOMEDRIVE".to_string(), "C:".to_string());
map.insert("HOMEPATH".to_string(), format!(r"\Users\{}", username));
map.entry("SystemRoot".to_string())
.or_insert_with(|| "C:\\Windows".to_string());
map.entry("WINDIR".to_string())
.or_insert_with(|| "C:\\Windows".to_string());
let local_app = format!(r"{}\AppData\Local", profile_dir);
let appdata = format!(r"{}\AppData\Roaming", profile_dir);
map.insert("LOCALAPPDATA".to_string(), local_app.clone());
map.insert("APPDATA".to_string(), appdata);
let temp = format!(r"{}\Temp", local_app);
map.insert("TEMP".to_string(), temp.clone());
map.insert("TMP".to_string(), temp);
// Log env
let mut vars: Vec<String> =
map.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
vars.sort();
log_note(
&format!(
"build_sandbox_env_block for {}:\n{}",
username,
vars.join("\n")
),
logs_base_dir,
);
// Rebuild env block
let env_block = make_env_block(&map);
DestroyEnvironmentBlock(env_block_ptr);
if profile_loaded != 0 {
let _ = UnloadUserProfile(h_tok, profile.hProfile);
}
CloseHandle(h_tok);
Some(env_block)
}
}
let env_block = build_sandbox_env_block(
&sandbox_creds.username,
&sandbox_creds.password,
logs_base_dir,
);
let env_log = if env_block.is_some() {
"runner env_block: custom sandbox profile env"
} else {
"runner env_block: inherit (sandbox user profile defaults)"
};
log_note(env_log, logs_base_dir);
let desktop = to_wide("Winsta0\\Default");
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput = in_r;
si.hStdOutput = out_w;
si.hStdError = err_w;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let cmdline_str = command
.iter()
.map(|a| quote_windows_arg(a))
.collect::<Vec<_>>()
.join(" ");
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
let env_block = make_env_block(&env_map);
let desktop = to_wide("Winsta0\\Default");
si.lpDesktop = desktop.as_ptr() as *mut u16;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let user_w = to_wide(&sandbox_creds.username);
let domain_w = to_wide(".");
let password_w = to_wide(&sandbox_creds.password);
let spawn_res = unsafe {
CreateProcessAsUserW(
h_token,
CreateProcessWithLogonW(
user_w.as_ptr(),
domain_w.as_ptr(),
password_w.as_ptr(),
LOGON_WITH_PROFILE,
ptr::null(),
cmdline.as_mut_ptr(),
ptr::null_mut(),
ptr::null_mut(),
1,
CREATE_UNICODE_ENVIRONMENT,
env_block.as_ptr() as *mut c_void,
env_block
.as_ref()
.map(|b| b.as_ptr() as *const c_void)
.unwrap_or(ptr::null()),
to_wide(cwd).as_ptr(),
&si,
&mut pi,
@@ -302,35 +594,33 @@ mod windows_impl {
if spawn_res == 0 {
let err = unsafe { GetLastError() } as i32;
let dbg = format!(
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
"CreateProcessWithLogonW failed: {} ({}) | cwd={} | cmd={} | env=inherit | si_flags={}",
err,
format_last_error(err),
cwd.display(),
cmdline_str,
env_block.len(),
si.dwFlags,
);
debug_log(&dbg, logs_base_dir);
unsafe {
CloseHandle(in_r);
CloseHandle(in_w);
CloseHandle(out_r);
CloseHandle(out_w);
CloseHandle(err_r);
CloseHandle(err_w);
CloseHandle(h_token);
}
return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err));
log_note(&dbg, logs_base_dir);
return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {}", err));
}
log_note("runner process launched", logs_base_dir);
// Connect pipes and send payload.
connect_pipe(h_stdin_pipe)?;
connect_pipe(h_stdout_pipe)?;
connect_pipe(h_stderr_pipe)?;
{
use std::io::Write;
let mut writer = unsafe { std::fs::File::from_raw_handle(h_stdin_pipe as _) };
writer.write_all(payload_json.as_bytes())?;
}
unsafe {
CloseHandle(in_r);
// Close the parent's stdin write end so the child sees EOF immediately.
CloseHandle(in_w);
CloseHandle(out_w);
CloseHandle(err_w);
CloseHandle(h_stdin_pipe);
}
// Read stdout/stderr.
let (tx_out, rx_out) = std::sync::mpsc::channel::<Vec<u8>>();
let (tx_err, rx_err) = std::sync::mpsc::channel::<Vec<u8>>();
let t_out = std::thread::spawn(move || {
@@ -340,7 +630,7 @@ mod windows_impl {
let mut read_bytes: u32 = 0;
let ok = unsafe {
windows_sys::Win32::Storage::FileSystem::ReadFile(
out_r,
h_stdout_pipe,
tmp.as_mut_ptr(),
tmp.len() as u32,
&mut read_bytes,
@@ -361,7 +651,7 @@ mod windows_impl {
let mut read_bytes: u32 = 0;
let ok = unsafe {
windows_sys::Win32::Storage::FileSystem::ReadFile(
err_r,
h_stderr_pipe,
tmp.as_mut_ptr(),
tmp.len() as u32,
&mut read_bytes,
@@ -389,6 +679,13 @@ mod windows_impl {
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
}
}
log_note(
&format!(
"runner exited timed_out={} code={}",
timed_out, exit_code_u32
),
logs_base_dir,
);
unsafe {
if pi.hThread != 0 {
@@ -397,7 +694,8 @@ mod windows_impl {
if pi.hProcess != 0 {
CloseHandle(pi.hProcess);
}
CloseHandle(h_token);
CloseHandle(h_stdout_pipe);
CloseHandle(h_stderr_pipe);
}
let _ = t_out.join();
let _ = t_err.join();
@@ -499,4 +797,14 @@ mod stub {
) -> Result<()> {
bail!("Windows sandbox is only available on Windows")
}
pub fn run_elevated_setup(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
) -> Result<()> {
bail!("Windows sandbox is only available on Windows")
}
}

View File

@@ -21,11 +21,12 @@ use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
use windows_sys::Win32::System::Threading::CreateProcessWithTokenW;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
use windows_sys::Win32::System::Threading::INFINITE;
use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
use windows_sys::Win32::System::Threading::STARTUPINFOW;
@@ -79,6 +80,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] {
let h = GetStdHandle(kind);
@@ -96,12 +98,16 @@ unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> {
Ok(())
}
/// # 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.
pub unsafe fn create_process_as_user(
h_token: HANDLE,
argv: &[String],
cwd: &Path,
env_map: &HashMap<String, String>,
logs_base_dir: Option<&Path>,
stdio: Option<(HANDLE, HANDLE, HANDLE)>,
) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> {
let cmdline_str = argv
.iter()
@@ -117,17 +123,22 @@ pub unsafe fn create_process_as_user(
// Point explicitly at the interactive desktop.
let desktop = to_wide("Winsta0\\Default");
si.lpDesktop = desktop.as_ptr() as *mut u16;
ensure_inheritable_stdio(&mut si)?;
if let Some((stdin_h, stdout_h, stderr_h)) = stdio {
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput = stdin_h;
si.hStdOutput = stdout_h;
si.hStdError = stderr_h;
} else {
ensure_inheritable_stdio(&mut si)?;
}
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
let ok = CreateProcessAsUserW(
let ok = CreateProcessWithTokenW(
h_token,
LOGON_WITH_PROFILE,
std::ptr::null(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
1,
CREATE_UNICODE_ENVIRONMENT,
env_block.as_ptr() as *mut c_void,
env_block.as_ptr() as *const c_void,
to_wide(cwd).as_ptr(),
&si,
&mut pi,
@@ -135,7 +146,7 @@ pub unsafe fn create_process_as_user(
if ok == 0 {
let err = GetLastError() as i32;
let msg = format!(
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
"CreateProcessWithTokenW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
err,
format_last_error(err),
cwd.display(),
@@ -144,11 +155,14 @@ pub unsafe fn create_process_as_user(
si.dwFlags,
);
logging::debug_log(&msg, logs_base_dir);
return Err(anyhow!("CreateProcessAsUserW failed: {}", err));
return Err(anyhow!("CreateProcessWithTokenW failed: {}", err));
}
Ok((pi, si))
}
/// # Safety
/// Caller must provide valid process information handles.
#[allow(dead_code)]
pub unsafe fn wait_process_and_exitcode(pi: &PROCESS_INFORMATION) -> Result<i32> {
let res = WaitForSingleObject(pi.hProcess, INFINITE);
if res != 0 {
@@ -161,6 +175,9 @@ pub unsafe fn wait_process_and_exitcode(pi: &PROCESS_INFORMATION) -> Result<i32>
Ok(code as i32)
}
/// # Safety
/// Caller must close the returned job handle.
#[allow(dead_code)]
pub unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
if h == 0 {
@@ -183,6 +200,9 @@ pub unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
Ok(h)
}
/// # Safety
/// Caller must pass valid handles for a job object and a process.
#[allow(dead_code)]
pub unsafe fn assign_to_job(h_job: HANDLE, h_process: HANDLE) -> Result<()> {
if AssignProcessToJobObject(h_job, h_process) == 0 {
return Err(anyhow!(

View File

@@ -0,0 +1,298 @@
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::ffi::c_void;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use crate::allow::compute_allow_paths;
use crate::allow::AllowDenyPaths;
use crate::policy::SandboxPolicy;
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 = 1;
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")
}
#[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()
}
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)
}
fn gather_write_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
) -> Vec<PathBuf> {
let AllowDenyPaths { allow, .. } =
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
canonical_existing(&allow.into_iter().collect::<Vec<_>>())
}
#[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,
}
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)
.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();
// Default show window.
sei.nShow = 1;
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,
) -> Result<()> {
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()),
};
let needs_elevation = !is_elevated()?;
run_setup_exe(&payload, needs_elevation)
}

View File

@@ -24,6 +24,7 @@ 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;
@@ -52,6 +53,8 @@ pub unsafe fn world_sid() -> Result<Vec<u8>> {
Ok(buf)
}
/// # Safety
/// Caller is responsible for freeing the returned SID with `LocalFree`.
pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
#[link(name = "advapi32")]
extern "system" {
@@ -66,6 +69,9 @@ pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
}
}
/// # Safety
/// Caller must close the returned token handle.
#[allow(dead_code)]
pub unsafe fn get_current_token_for_restriction() -> Result<HANDLE> {
let desired = TOKEN_DUPLICATE
| TOKEN_QUERY
@@ -197,13 +203,55 @@ unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
Ok(())
}
// removed unused create_write_restricted_token_strict
/// # 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)]
pub unsafe fn create_workspace_write_token_with_cap(
psid_capability: *mut c_void,
) -> Result<(HANDLE, *mut c_void)> {
let base = get_current_token_for_restriction()?;
let mut logon_sid_bytes = get_logon_sid_bytes(base)?;
let res = create_workspace_write_token_with_cap_from(base, psid_capability);
CloseHandle(base);
res
}
/// # Safety
/// Caller must close the returned token handle.
#[allow(dead_code)]
pub unsafe fn create_readonly_token_with_cap(
psid_capability: *mut c_void,
) -> Result<(HANDLE, *mut c_void)> {
let base = get_current_token_for_restriction()?;
let res = create_readonly_token_with_cap_from(base, psid_capability);
CloseHandle(base);
res
}
/// # Safety
/// Caller must close the returned token handle; base_token must be a valid primary token.
pub unsafe fn create_workspace_write_token_with_cap_from(
base_token: HANDLE,
psid_capability: *mut c_void,
) -> Result<(HANDLE, *mut c_void)> {
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
let mut everyone = world_sid()?;
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
@@ -218,7 +266,7 @@ pub unsafe fn create_workspace_write_token_with_cap(
let mut new_token: HANDLE = 0;
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
let ok = CreateRestrictedToken(
base,
base_token,
flags,
0,
std::ptr::null(),
@@ -235,11 +283,13 @@ pub unsafe fn create_workspace_write_token_with_cap(
Ok((new_token, psid_capability))
}
pub unsafe fn create_readonly_token_with_cap(
/// # Safety
/// Caller must close the returned token handle; base_token must be a valid primary token.
pub unsafe fn create_readonly_token_with_cap_from(
base_token: HANDLE,
psid_capability: *mut c_void,
) -> Result<(HANDLE, *mut c_void)> {
let base = get_current_token_for_restriction()?;
let mut logon_sid_bytes = get_logon_sid_bytes(base)?;
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
let mut everyone = world_sid()?;
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
@@ -254,7 +304,7 @@ pub unsafe fn create_readonly_token_with_cap(
let mut new_token: HANDLE = 0;
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
let ok = CreateRestrictedToken(
base,
base_token,
flags,
0,
std::ptr::null(),

View File

@@ -6,6 +6,7 @@ use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW;
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER;
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_FROM_SYSTEM;
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_IGNORE_INSERTS;
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
pub fn to_wide<S: AsRef<OsStr>>(s: S) -> Vec<u16> {
let mut v: Vec<u16> = s.as_ref().encode_wide().collect();
@@ -41,3 +42,21 @@ pub fn format_last_error(err: i32) -> String {
s
}
}
pub fn string_from_sid_bytes(sid: &[u8]) -> Result<String, String> {
unsafe {
let mut str_ptr: *mut u16 = std::ptr::null_mut();
let ok = ConvertSidToStringSidW(sid.as_ptr() as *mut std::ffi::c_void, &mut str_ptr);
if ok == 0 || str_ptr.is_null() {
return Err(format!("ConvertSidToStringSidW failed: {}", std::io::Error::last_os_error()));
}
let mut len = 0;
while *str_ptr.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(str_ptr, len);
let out = String::from_utf16_lossy(slice);
let _ = LocalFree(str_ptr as HLOCAL);
Ok(out)
}
}