mirror of
https://github.com/openai/codex.git
synced 2026-03-11 00:53:25 +00:00
Compare commits
1 Commits
fix/notify
...
dev/icewea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9d7c59a4 |
91
codex-rs/Cargo.lock
generated
91
codex-rs/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
219
codex-rs/windows-sandbox-rs/src/bin/command_runner.rs
Normal file
219
codex-rs/windows-sandbox-rs/src/bin/command_runner.rs
Normal 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);
|
||||
}
|
||||
70
codex-rs/windows-sandbox-rs/src/bin/logon_smoke.rs
Normal file
70
codex-rs/windows-sandbox-rs/src/bin/logon_smoke.rs
Normal 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(())
|
||||
}
|
||||
33
codex-rs/windows-sandbox-rs/src/bin/runner_smoke.rs
Normal file
33
codex-rs/windows-sandbox-rs/src/bin/runner_smoke.rs
Normal 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(())
|
||||
}
|
||||
105
codex-rs/windows-sandbox-rs/src/bin/runner_stub.rs
Normal file
105
codex-rs/windows-sandbox-rs/src/bin/runner_stub.rs
Normal 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(())
|
||||
}
|
||||
784
codex-rs/windows-sandbox-rs/src/bin/setup.rs
Normal file
784
codex-rs/windows-sandbox-rs/src/bin/setup.rs
Normal 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(())
|
||||
}
|
||||
81
codex-rs/windows-sandbox-rs/src/dpapi.rs
Normal file
81
codex-rs/windows-sandbox-rs/src/dpapi.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptProtectData;
|
||||
use windows_sys::Win32::Security::Cryptography::CryptUnprotectData;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB;
|
||||
use windows_sys::Win32::Security::Cryptography::CRYPTPROTECT_UI_FORBIDDEN;
|
||||
|
||||
fn make_blob(data: &[u8]) -> CRYPT_INTEGER_BLOB {
|
||||
CRYPT_INTEGER_BLOB {
|
||||
cbData: data.len() as u32,
|
||||
pbData: data.as_ptr() as *mut u8,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_mut_passed)]
|
||||
pub fn protect(data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut in_blob = make_blob(data);
|
||||
let mut out_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
let ok = unsafe {
|
||||
CryptProtectData(
|
||||
&mut in_blob,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CRYPTPROTECT_UI_FORBIDDEN,
|
||||
&mut out_blob,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CryptProtectData failed: {}", unsafe { GetLastError() }));
|
||||
}
|
||||
let slice =
|
||||
unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec();
|
||||
unsafe {
|
||||
if !out_blob.pbData.is_null() {
|
||||
LocalFree(out_blob.pbData as HLOCAL);
|
||||
}
|
||||
}
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_mut_passed)]
|
||||
pub fn unprotect(blob: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut in_blob = make_blob(blob);
|
||||
let mut out_blob = CRYPT_INTEGER_BLOB {
|
||||
cbData: 0,
|
||||
pbData: std::ptr::null_mut(),
|
||||
};
|
||||
let ok = unsafe {
|
||||
CryptUnprotectData(
|
||||
&mut in_blob,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CRYPTPROTECT_UI_FORBIDDEN,
|
||||
&mut out_blob,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(anyhow!(
|
||||
"CryptUnprotectData failed: {}",
|
||||
unsafe { GetLastError() }
|
||||
));
|
||||
}
|
||||
let slice =
|
||||
unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec();
|
||||
unsafe {
|
||||
if !out_blob.pbData.is_null() {
|
||||
LocalFree(out_blob.pbData as HLOCAL);
|
||||
}
|
||||
}
|
||||
Ok(slice)
|
||||
}
|
||||
136
codex-rs/windows-sandbox-rs/src/identity.rs
Normal file
136
codex-rs/windows-sandbox-rs/src/identity.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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, ¤t_dir, &env_map);
|
||||
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
||||
unsafe {
|
||||
for p in &allow {
|
||||
if let Ok(added) = add_allow_ace(p, psid_to_use) {
|
||||
if added {
|
||||
if persist_aces {
|
||||
if p.is_dir() {
|
||||
// best-effort seeding omitted intentionally
|
||||
}
|
||||
} else {
|
||||
guards.push((p.clone(), psid_to_use));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
298
codex-rs/windows-sandbox-rs/src/setup.rs
Normal file
298
codex-rs/windows-sandbox-rs/src/setup.rs
Normal 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)
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user