Use a private desktop for Windows sandbox instead of Winsta0\Default (#14400)

## Summary
- launch Windows sandboxed children on a private desktop instead of
`Winsta0\Default`
- make private desktop the default while keeping
`windows.sandbox_private_desktop=false` as the escape hatch
- centralize process launch through the shared
`create_process_as_user(...)` path
- scope the private desktop ACL to the launching logon SID

## Why
Today sandboxed Windows commands run on the visible shared desktop. That
leaves an avoidable same-desktop attack surface for window interaction,
spoofing, and related UI/input issues. This change moves sandboxed
commands onto a dedicated per-launch desktop by default so the sandbox
no longer shares `Winsta0\Default` with the user session.

The implementation stays conservative on security with no silent
fallback back to `Winsta0\Default`

If private-desktop setup fails on a machine, users can still opt out
explicitly with `windows.sandbox_private_desktop=false`.

## Validation
- `cargo build -p codex-cli`
- elevated-path `codex exec` desktop-name probe returned
`CodexSandboxDesktop-*`
- elevated-path `codex exec` smoke sweep for shell commands, nested
`pwsh`, jobs, and hidden `notepad` launch
- unelevated-path full private-desktop compatibility sweep via `codex
exec` with `-c windows.sandbox=unelevated`
This commit is contained in:
iceweasel-oai authored and GitHub committed 2026-03-13 10:13:39 -07:00
1 parent 9c9867c9fa
commit 6b3d82daca
30 files changed
+416 -70

No files matched your search

+3
View File
@@ -73,6 +73,9 @@ features = [
"Win32_System_Com",
"Win32_Security_Cryptography",
"Win32_Security_Authentication_Identity",
"Win32_Graphics_Gdi",
"Win32_System_StationsAndDesktops",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
"Win32_System_Registry",
]
@@ -27,6 +27,7 @@ use windows_sys::Win32::Storage::FileSystem::CreateFileW;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
@@ -55,6 +56,7 @@ struct RunnerRequest {
cwd: PathBuf,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
use_private_desktop: bool,
stdin_pipe: String,
stdout_pipe: String,
stderr_pipe: String,
@@ -103,6 +105,8 @@ pub fn main() -> Result<()> {
let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?;
let log_dir = Some(req.codex_home.as_path());
hide_current_user_profile_dir(req.codex_home.as_path());
// Suppress Windows error UI from sandboxed child crashes so callers only observe exit codes.
let _ = unsafe { SetErrorMode(0x0001 | 0x0002) }; // SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX
log_note(
&format!(
"runner start cwd={} cmd={:?} real_codex_home={}",
@@ -233,9 +237,10 @@ pub fn main() -> Result<()> {
&req.env_map,
Some(&req.codex_home),
stdio,
req.use_private_desktop,
)
};
let (proc_info, _si) = match spawn_result {
let created = match spawn_result {
Ok(v) => v,
Err(e) => {
log_note(&format!("runner: spawn failed: {e:?}"), log_dir);
@@ -248,6 +253,8 @@ pub fn main() -> Result<()> {
return Err(e);
}
};
let proc_info = created.process_info;
let _desktop = created;
// Optional job kill on close.
let h_job = unsafe { create_job_kill_on_close().ok() };
+196
View File
@@ -0,0 +1,196 @@
use crate::logging;
use crate::token::get_current_token_for_restriction;
use crate::token::get_logon_sid_bytes;
use crate::winutil::format_last_error;
use crate::winutil::to_wide;
use anyhow::Result;
use rand::Rng;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use std::path::Path;
use std::ffi::c_void;
use std::ptr;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
use windows_sys::Win32::Security::Authorization::SE_WINDOW_OBJECT;
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
use windows_sys::Win32::Security::Authorization::SetSecurityInfo;
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
use windows_sys::Win32::System::StationsAndDesktops::CloseDesktop;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEMENU;
use windows_sys::Win32::System::StationsAndDesktops::CreateDesktopW;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEWINDOW;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_DELETE;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_ENUMERATE;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_HOOKCONTROL;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_JOURNALPLAYBACK;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_JOURNALRECORD;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READOBJECTS;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READ_CONTROL;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_SWITCHDESKTOP;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITE_DAC;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITE_OWNER;
use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITEOBJECTS;
const DESKTOP_ALL_ACCESS: u32 = DESKTOP_READOBJECTS
| DESKTOP_CREATEWINDOW
| DESKTOP_CREATEMENU
| DESKTOP_HOOKCONTROL
| DESKTOP_JOURNALRECORD
| DESKTOP_JOURNALPLAYBACK
| DESKTOP_ENUMERATE
| DESKTOP_WRITEOBJECTS
| DESKTOP_SWITCHDESKTOP
| DESKTOP_DELETE
| DESKTOP_READ_CONTROL
| DESKTOP_WRITE_DAC
| DESKTOP_WRITE_OWNER;
pub struct LaunchDesktop {
_private_desktop: Option<PrivateDesktop>,
startup_name: Vec<u16>,
}
impl LaunchDesktop {
pub fn prepare(use_private_desktop: bool, logs_base_dir: Option<&Path>) -> Result<Self> {
if use_private_desktop {
let private_desktop = PrivateDesktop::create(logs_base_dir)?;
let startup_name = to_wide(format!("Winsta0\\{}", private_desktop.name));
Ok(Self {
_private_desktop: Some(private_desktop),
startup_name,
})
} else {
Ok(Self {
_private_desktop: None,
startup_name: to_wide("Winsta0\\Default"),
})
}
}
pub fn startup_info_desktop(&self) -> *mut u16 {
self.startup_name.as_ptr() as *mut u16
}
}
struct PrivateDesktop {
handle: isize,
name: String,
}
impl PrivateDesktop {
fn create(logs_base_dir: Option<&Path>) -> Result<Self> {
let mut rng = SmallRng::from_entropy();
let name = format!("CodexSandboxDesktop-{:x}", rng.gen::<u128>());
let name_wide = to_wide(&name);
let handle = unsafe {
CreateDesktopW(
name_wide.as_ptr(),
ptr::null(),
ptr::null_mut(),
0,
DESKTOP_ALL_ACCESS,
ptr::null_mut(),
)
};
if handle == 0 {
let err = unsafe { GetLastError() } as i32;
logging::debug_log(
&format!(
"CreateDesktopW failed for {name}: {} ({})",
err,
format_last_error(err),
),
logs_base_dir,
);
return Err(anyhow::anyhow!("CreateDesktopW failed: {err}"));
}
unsafe {
if let Err(err) = grant_desktop_access(handle, logs_base_dir) {
let _ = CloseDesktop(handle);
return Err(err);
}
}
Ok(Self { handle, name })
}
}
unsafe fn grant_desktop_access(handle: isize, logs_base_dir: Option<&Path>) -> Result<()> {
let token = get_current_token_for_restriction()?;
let mut logon_sid = get_logon_sid_bytes(token)?;
CloseHandle(token);
let entries = [EXPLICIT_ACCESS_W {
grfAccessPermissions: DESKTOP_ALL_ACCESS,
grfAccessMode: GRANT_ACCESS,
grfInheritance: 0,
Trustee: TRUSTEE_W {
pMultipleTrustee: ptr::null_mut(),
MultipleTrusteeOperation: 0,
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: TRUSTEE_IS_UNKNOWN,
ptstrName: logon_sid.as_mut_ptr() as *mut c_void as *mut u16,
},
}];
let mut updated_dacl = ptr::null_mut();
let set_entries_code = SetEntriesInAclW(
entries.len() as u32,
entries.as_ptr(),
ptr::null_mut(),
&mut updated_dacl,
);
if set_entries_code != ERROR_SUCCESS {
logging::debug_log(
&format!("SetEntriesInAclW failed for private desktop: {set_entries_code}"),
logs_base_dir,
);
return Err(anyhow::anyhow!(
"SetEntriesInAclW failed for private desktop: {set_entries_code}"
));
}
let set_security_code = SetSecurityInfo(
handle,
SE_WINDOW_OBJECT,
DACL_SECURITY_INFORMATION,
ptr::null_mut(),
ptr::null_mut(),
updated_dacl,
ptr::null_mut(),
);
if !updated_dacl.is_null() {
LocalFree(updated_dacl as HLOCAL);
}
if set_security_code != ERROR_SUCCESS {
logging::debug_log(
&format!("SetSecurityInfo failed for private desktop: {set_security_code}"),
logs_base_dir,
);
return Err(anyhow::anyhow!(
"SetSecurityInfo failed for private desktop: {set_security_code}"
));
}
Ok(())
}
impl Drop for PrivateDesktop {
fn drop(&mut self) {
unsafe {
if self.handle != 0 {
let _ = CloseDesktop(self.handle);
}
}
}
}
@@ -196,12 +196,14 @@ mod windows_impl {
cwd: PathBuf,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
use_private_desktop: bool,
stdin_pipe: String,
stdout_pipe: String,
stderr_pipe: String,
}
/// Launches the command runner under the sandbox user and captures its output.
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
@@ -210,6 +212,7 @@ mod windows_impl {
cwd: &Path,
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
use_private_desktop: bool,
) -> Result<CaptureResult> {
let policy = parse_policy(policy_json_or_preset)?;
normalize_null_device_env(&mut env_map);
@@ -302,6 +305,7 @@ mod windows_impl {
cwd: cwd.to_path_buf(),
env_map: env_map.clone(),
timeout_ms,
use_private_desktop,
stdin_pipe: stdin_name.clone(),
stdout_pipe: stdout_name.clone(),
stderr_pipe: stderr_name.clone(),
@@ -530,6 +534,7 @@ mod stub {
}
/// Stub implementation for non-Windows targets; sandboxing only works on Windows.
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
_policy_json_or_preset: &str,
_sandbox_policy_cwd: &Path,
@@ -538,6 +543,7 @@ mod stub {
_cwd: &Path,
_env_map: HashMap<String, String>,
_timeout_ms: Option<u64>,
_use_private_desktop: bool,
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}
+31 -60
View File
@@ -9,6 +9,7 @@ windows_modules!(
allow,
audit,
cap,
desktop,
dpapi,
env,
helper_materialization,
@@ -126,6 +127,8 @@ pub use windows_impl::run_windows_sandbox_legacy_preflight;
#[cfg(target_os = "windows")]
pub use windows_impl::CaptureResult;
#[cfg(target_os = "windows")]
pub use winutil::quote_windows_arg;
#[cfg(target_os = "windows")]
pub use winutil::string_from_sid_bytes;
#[cfg(target_os = "windows")]
pub use winutil::to_wide;
@@ -158,19 +161,15 @@ 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::logging::debug_log;
use super::logging::log_failure;
use super::logging::log_start;
use super::logging::log_success;
use super::path_normalization::canonicalize_path;
use super::policy::parse_policy;
use super::policy::SandboxPolicy;
use super::process::make_env_block;
use super::process::create_process_as_user;
use super::token::convert_string_sid_to_sid;
use super::token::create_workspace_write_token_with_caps_from;
use super::winutil::format_last_error;
use super::winutil::quote_windows_arg;
use super::winutil::to_wide;
use super::workspace_acl::is_command_cwd_root;
use super::workspace_acl::protect_workspace_agents_dir;
use super::workspace_acl::protect_workspace_codex_dir;
@@ -187,14 +186,9 @@ mod windows_impl {
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::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::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));
@@ -242,6 +236,7 @@ mod windows_impl {
pub timed_out: bool,
}
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
@@ -250,6 +245,7 @@ mod windows_impl {
cwd: &Path,
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
use_private_desktop: bool,
) -> Result<CaptureResult> {
let policy = parse_policy(policy_json_or_preset)?;
let apply_network_block = should_apply_network_block(&policy);
@@ -358,61 +354,34 @@ mod windows_impl {
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);
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 spawn_res = unsafe {
CreateProcessAsUserW(
create_process_as_user(
h_token,
ptr::null(),
cmdline.as_mut_ptr(),
ptr::null_mut(),
ptr::null_mut(),
1,
CREATE_UNICODE_ENVIRONMENT,
env_block.as_ptr() as *mut c_void,
to_wide(cwd).as_ptr(),
&si,
&mut pi,
&command,
cwd,
&env_map,
logs_base_dir,
Some((in_r, out_w, err_w)),
use_private_desktop,
)
};
if spawn_res == 0 {
let err = unsafe { GetLastError() } as i32;
let dbg = format!(
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | 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);
let created = match spawn_res {
Ok(v) => v,
Err(err) => {
unsafe {
CloseHandle(in_r);
CloseHandle(in_w);
CloseHandle(out_r);
CloseHandle(out_w);
CloseHandle(err_r);
CloseHandle(err_w);
CloseHandle(h_token);
}
return Err(err);
}
return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err));
}
};
let pi = created.process_info;
let _desktop = created;
unsafe {
CloseHandle(in_r);
@@ -617,6 +586,7 @@ mod stub {
pub timed_out: bool,
}
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
_policy_json_or_preset: &str,
_sandbox_policy_cwd: &Path,
@@ -625,6 +595,7 @@ mod stub {
_cwd: &Path,
_env_map: HashMap<String, String>,
_timeout_ms: Option<u64>,
_use_private_desktop: bool,
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}
+26 -9
View File
@@ -1,3 +1,4 @@
use crate::desktop::LaunchDesktop;
use crate::logging;
use crate::winutil::format_last_error;
use crate::winutil::quote_windows_arg;
@@ -22,6 +23,12 @@ use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
use windows_sys::Win32::System::Threading::STARTUPINFOW;
pub struct CreatedProcess {
pub process_info: PROCESS_INFORMATION,
pub startup_info: STARTUPINFOW,
_desktop: LaunchDesktop,
}
pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
let mut items: Vec<(String, String)> =
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
@@ -68,7 +75,8 @@ pub unsafe fn create_process_as_user(
env_map: &HashMap<String, String>,
logs_base_dir: Option<&Path>,
stdio: Option<(HANDLE, HANDLE, HANDLE)>,
) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> {
use_private_desktop: bool,
) -> Result<CreatedProcess> {
let cmdline_str = argv
.iter()
.map(|a| quote_windows_arg(a))
@@ -80,9 +88,9 @@ pub unsafe fn create_process_as_user(
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
// Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED
// if lpDesktop is not set when launching with a restricted token.
// Point explicitly at the interactive desktop.
let desktop = to_wide("Winsta0\\Default");
si.lpDesktop = desktop.as_ptr() as *mut u16;
// Point explicitly at the interactive desktop or a private desktop.
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
si.lpDesktop = desktop.startup_info_desktop();
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
// Ensure handles are inheritable when custom stdio is supplied.
let inherit_handles = match stdio {
@@ -107,6 +115,10 @@ pub unsafe fn create_process_as_user(
}
};
let creation_flags = CREATE_UNICODE_ENVIRONMENT;
let cwd_wide = to_wide(cwd);
let env_block_len = env_block.len();
let ok = CreateProcessAsUserW(
h_token,
std::ptr::null(),
@@ -114,25 +126,30 @@ pub unsafe fn create_process_as_user(
std::ptr::null_mut(),
std::ptr::null_mut(),
inherit_handles as i32,
CREATE_UNICODE_ENVIRONMENT,
creation_flags,
env_block.as_ptr() as *mut c_void,
to_wide(cwd).as_ptr(),
cwd_wide.as_ptr(),
&si,
&mut pi,
);
if ok == 0 {
let err = GetLastError() as i32;
let msg = format!(
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}",
err,
format_last_error(err),
cwd.display(),
cmdline_str,
env_block.len(),
env_block_len,
si.dwFlags,
creation_flags,
);
logging::debug_log(&msg, logs_base_dir);
return Err(anyhow!("CreateProcessAsUserW failed: {}", err));
}
Ok((pi, si))
Ok(CreatedProcess {
process_info: pi,
startup_info: si,
_desktop: desktop,
})
}