Skip to content

Commit 3e81ed4

Browse files
Elevated Sandbox 3 (#7809)
dedicated sandbox command runner exe.
1 parent c4f3f56 commit 3e81ed4

File tree

4 files changed

+257
-0
lines changed

4 files changed

+257
-0
lines changed

codex-rs/windows-sandbox-rs/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ path = "src/lib.rs"
1313
name = "codex-windows-sandbox-setup"
1414
path = "src/bin/setup_main.rs"
1515

16+
[[bin]]
17+
name = "codex-command-runner"
18+
path = "src/bin/command_runner.rs"
19+
1620
[dependencies]
1721
anyhow = "1.0"
1822
base64 = { workspace = true }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#[path = "../command_runner_win.rs"]
2+
mod win;
3+
4+
#[cfg(target_os = "windows")]
5+
fn main() -> anyhow::Result<()> {
6+
win::main()
7+
}
8+
9+
#[cfg(not(target_os = "windows"))]
10+
fn main() {
11+
panic!("codex-command-runner is Windows-only");
12+
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#![cfg(target_os = "windows")]
2+
3+
use anyhow::Context;
4+
use anyhow::Result;
5+
use codex_windows_sandbox::allow_null_device;
6+
use codex_windows_sandbox::convert_string_sid_to_sid;
7+
use codex_windows_sandbox::create_process_as_user;
8+
use codex_windows_sandbox::create_readonly_token_with_cap_from;
9+
use codex_windows_sandbox::create_workspace_write_token_with_cap_from;
10+
use codex_windows_sandbox::get_current_token_for_restriction;
11+
use codex_windows_sandbox::log_note;
12+
use codex_windows_sandbox::parse_policy;
13+
use codex_windows_sandbox::to_wide;
14+
use codex_windows_sandbox::SandboxPolicy;
15+
use serde::Deserialize;
16+
use std::collections::HashMap;
17+
use std::ffi::c_void;
18+
use std::path::PathBuf;
19+
use windows_sys::Win32::Foundation::CloseHandle;
20+
use windows_sys::Win32::Foundation::GetLastError;
21+
use windows_sys::Win32::Foundation::HANDLE;
22+
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
23+
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
24+
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
25+
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
26+
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
27+
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
28+
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
29+
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
30+
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
31+
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
32+
use windows_sys::Win32::System::Threading::TerminateProcess;
33+
use windows_sys::Win32::System::Threading::WaitForSingleObject;
34+
use windows_sys::Win32::System::Threading::INFINITE;
35+
36+
#[derive(Debug, Deserialize)]
37+
struct RunnerRequest {
38+
policy_json_or_preset: String,
39+
// Writable location for logs (sandbox user's .codex).
40+
codex_home: PathBuf,
41+
// Real user's CODEX_HOME for shared data (caps, config).
42+
real_codex_home: PathBuf,
43+
cap_sid: String,
44+
command: Vec<String>,
45+
cwd: PathBuf,
46+
env_map: HashMap<String, String>,
47+
timeout_ms: Option<u64>,
48+
stdin_pipe: String,
49+
stdout_pipe: String,
50+
stderr_pipe: String,
51+
}
52+
53+
const WAIT_TIMEOUT: u32 = 0x0000_0102;
54+
55+
unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
56+
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
57+
if h == 0 {
58+
return Err(anyhow::anyhow!("CreateJobObjectW failed"));
59+
}
60+
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
61+
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
62+
let ok = SetInformationJobObject(
63+
h,
64+
JobObjectExtendedLimitInformation,
65+
&mut limits as *mut _ as *mut _,
66+
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
67+
);
68+
if ok == 0 {
69+
return Err(anyhow::anyhow!("SetInformationJobObject failed"));
70+
}
71+
Ok(h)
72+
}
73+
74+
pub fn main() -> Result<()> {
75+
let mut input = String::new();
76+
let mut args = std::env::args().skip(1);
77+
if let Some(first) = args.next() {
78+
if let Some(rest) = first.strip_prefix("--request-file=") {
79+
let req_path = PathBuf::from(rest);
80+
input = std::fs::read_to_string(&req_path).context("read request file")?;
81+
}
82+
}
83+
if input.is_empty() {
84+
anyhow::bail!("runner: no request-file provided");
85+
}
86+
let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?;
87+
let log_dir = Some(req.codex_home.as_path());
88+
log_note(
89+
&format!(
90+
"runner start cwd={} cmd={:?} real_codex_home={}",
91+
req.cwd.display(),
92+
req.command,
93+
req.real_codex_home.display()
94+
),
95+
Some(&req.codex_home),
96+
);
97+
98+
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
99+
let psid_cap: *mut c_void = unsafe { convert_string_sid_to_sid(&req.cap_sid).unwrap() };
100+
101+
// Create restricted token from current process token.
102+
let base = unsafe { get_current_token_for_restriction()? };
103+
let token_res: Result<(HANDLE, *mut c_void)> = unsafe {
104+
match &policy {
105+
SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap),
106+
SandboxPolicy::WorkspaceWrite { .. } => {
107+
create_workspace_write_token_with_cap_from(base, psid_cap)
108+
}
109+
SandboxPolicy::DangerFullAccess => unreachable!(),
110+
}
111+
};
112+
let (h_token, psid_to_use) = token_res?;
113+
unsafe {
114+
CloseHandle(base);
115+
}
116+
unsafe {
117+
allow_null_device(psid_to_use);
118+
}
119+
120+
// Open named pipes for stdio.
121+
let open_pipe = |name: &str, access: u32| -> Result<HANDLE> {
122+
let path = to_wide(name);
123+
let handle = unsafe {
124+
CreateFileW(
125+
path.as_ptr(),
126+
access,
127+
0,
128+
std::ptr::null_mut(),
129+
OPEN_EXISTING,
130+
0,
131+
0,
132+
)
133+
};
134+
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
135+
let err = unsafe { GetLastError() };
136+
log_note(
137+
&format!("CreateFileW failed for pipe {name}: {err}"),
138+
Some(&req.codex_home),
139+
);
140+
return Err(anyhow::anyhow!("CreateFileW failed for pipe {name}: {err}"));
141+
}
142+
Ok(handle)
143+
};
144+
let h_stdin = open_pipe(&req.stdin_pipe, FILE_GENERIC_READ)?;
145+
let h_stdout = open_pipe(&req.stdout_pipe, FILE_GENERIC_WRITE)?;
146+
let h_stderr = open_pipe(&req.stderr_pipe, FILE_GENERIC_WRITE)?;
147+
148+
// Build command and env, spawn with CreateProcessAsUserW.
149+
let spawn_result = unsafe {
150+
create_process_as_user(
151+
h_token,
152+
&req.command,
153+
&req.cwd,
154+
&req.env_map,
155+
Some(&req.codex_home),
156+
Some((h_stdin, h_stdout, h_stderr)),
157+
)
158+
};
159+
let (proc_info, _si) = match spawn_result {
160+
Ok(v) => v,
161+
Err(e) => {
162+
log_note(&format!("runner: spawn failed: {e:?}"), log_dir);
163+
unsafe {
164+
CloseHandle(h_stdin);
165+
CloseHandle(h_stdout);
166+
CloseHandle(h_stderr);
167+
CloseHandle(h_token);
168+
}
169+
return Err(e);
170+
}
171+
};
172+
173+
// Optional job kill on close.
174+
let h_job = unsafe { create_job_kill_on_close().ok() };
175+
if let Some(job) = h_job {
176+
unsafe {
177+
let _ = AssignProcessToJobObject(job, proc_info.hProcess);
178+
}
179+
}
180+
181+
// Wait for process.
182+
let wait_res = unsafe {
183+
WaitForSingleObject(
184+
proc_info.hProcess,
185+
req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE),
186+
)
187+
};
188+
let timed_out = wait_res == WAIT_TIMEOUT;
189+
190+
let exit_code: i32;
191+
unsafe {
192+
if timed_out {
193+
let _ = TerminateProcess(proc_info.hProcess, 1);
194+
exit_code = 128 + 64;
195+
} else {
196+
let mut raw_exit: u32 = 1;
197+
windows_sys::Win32::System::Threading::GetExitCodeProcess(
198+
proc_info.hProcess,
199+
&mut raw_exit,
200+
);
201+
exit_code = raw_exit as i32;
202+
}
203+
if proc_info.hThread != 0 {
204+
CloseHandle(proc_info.hThread);
205+
}
206+
if proc_info.hProcess != 0 {
207+
CloseHandle(proc_info.hProcess);
208+
}
209+
CloseHandle(h_stdin);
210+
CloseHandle(h_stdout);
211+
CloseHandle(h_stderr);
212+
CloseHandle(h_token);
213+
if let Some(job) = h_job {
214+
CloseHandle(job);
215+
}
216+
}
217+
if exit_code != 0 {
218+
eprintln!("runner child exited with code {}", exit_code);
219+
}
220+
log_note(
221+
&format!("runner exit pid={} code={}", proc_info.hProcess, exit_code),
222+
log_dir,
223+
);
224+
std::process::exit(exit_code);
225+
}

codex-rs/windows-sandbox-rs/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ windows_modules!(
1212
#[path = "setup_orchestrator.rs"]
1313
mod setup;
1414

15+
#[cfg(target_os = "windows")]
16+
pub use acl::allow_null_device;
1517
#[cfg(target_os = "windows")]
1618
pub use acl::ensure_allow_write_aces;
1719
#[cfg(target_os = "windows")]
@@ -33,6 +35,12 @@ pub use logging::log_note;
3335
#[cfg(target_os = "windows")]
3436
pub use logging::LOG_FILE_NAME;
3537
#[cfg(target_os = "windows")]
38+
pub use policy::parse_policy;
39+
#[cfg(target_os = "windows")]
40+
pub use policy::SandboxPolicy;
41+
#[cfg(target_os = "windows")]
42+
pub use process::create_process_as_user;
43+
#[cfg(target_os = "windows")]
3644
pub use setup::run_elevated_setup;
3745
#[cfg(target_os = "windows")]
3846
pub use setup::run_setup_refresh;
@@ -43,11 +51,19 @@ pub use setup::SETUP_VERSION;
4351
#[cfg(target_os = "windows")]
4452
pub use token::convert_string_sid_to_sid;
4553
#[cfg(target_os = "windows")]
54+
pub use token::create_readonly_token_with_cap_from;
55+
#[cfg(target_os = "windows")]
56+
pub use token::create_workspace_write_token_with_cap_from;
57+
#[cfg(target_os = "windows")]
58+
pub use token::get_current_token_for_restriction;
59+
#[cfg(target_os = "windows")]
4660
pub use windows_impl::run_windows_sandbox_capture;
4761
#[cfg(target_os = "windows")]
4862
pub use windows_impl::CaptureResult;
4963
#[cfg(target_os = "windows")]
5064
pub use winutil::string_from_sid_bytes;
65+
#[cfg(target_os = "windows")]
66+
pub use winutil::to_wide;
5167

5268
#[cfg(not(target_os = "windows"))]
5369
pub use stub::apply_world_writable_scan_and_denies;

0 commit comments

Comments
 (0)