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 2026-03-13 10:13:39 -07:00 committed by GitHub
parent 9c9867c9fa
commit 6b3d82daca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 416 additions and 70 deletions

View file

@ -1695,6 +1695,10 @@ impl CodexMessageProcessor {
.map(codex_core::config::StartedNetworkProxy::proxy),
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
.permissions
.windows_sandbox_private_desktop,
justification: None,
arg0: None,
};

View file

@ -733,6 +733,7 @@ mod tests {
expiration: ExecExpiration::DefaultTimeout,
sandbox: SandboxType::WindowsRestrictedToken,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault,
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
@ -844,6 +845,7 @@ mod tests {
expiration: ExecExpiration::Cancellation(CancellationToken::new()),
sandbox: SandboxType::None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault,
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),

View file

@ -165,6 +165,7 @@ async fn run_command_under_sandbox(
&cwd_clone,
env_map,
None,
config.permissions.windows_sandbox_private_desktop,
)
} else {
run_windows_sandbox_capture(
@ -175,6 +176,7 @@ async fn run_command_under_sandbox(
&cwd_clone,
env_map,
None,
config.permissions.windows_sandbox_private_desktop,
)
}
})

View file

@ -1699,6 +1699,10 @@
"properties": {
"sandbox": {
"$ref": "#/definitions/WindowsSandboxModeToml"
},
"sandbox_private_desktop": {
"description": "Defaults to `true`. Set to `false` to launch the final sandboxed child process on `Winsta0\\\\Default` instead of a private desktop.",
"type": "boolean"
}
},
"type": "object"

View file

@ -4214,6 +4214,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
network: None,
sandbox_permissions,
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
.permissions
.windows_sandbox_private_desktop,
justification: Some("test".to_string()),
arg0: None,
};
@ -4226,6 +4230,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
env: HashMap::new(),
network: None,
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
.permissions
.windows_sandbox_private_desktop,
justification: params.justification.clone(),
arg0: None,
};

View file

@ -125,6 +125,10 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
network: None,
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
.permissions
.windows_sandbox_private_desktop,
justification: Some("test".to_string()),
arg0: None,
};

View file

@ -4082,6 +4082,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
@ -4219,6 +4220,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
@ -4354,6 +4356,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
@ -4475,6 +4478,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),

View file

@ -58,6 +58,7 @@ use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS;
use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use crate::windows_sandbox::resolve_windows_sandbox_mode;
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_protocol::config_types::AltScreenMode;
@ -189,6 +190,8 @@ pub struct Permissions {
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
/// legacy feature keys.
pub windows_sandbox_mode: Option<WindowsSandboxModeToml>,
/// Whether the final Windows sandboxed child should run on a private desktop.
pub windows_sandbox_private_desktop: bool,
/// Optional macOS seatbelt extension profile used to extend default
/// seatbelt permissions when running under seatbelt.
pub macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
@ -1934,6 +1937,8 @@ impl Config {
let configured_features = Features::from_config(&cfg, &config_profile, feature_overrides);
let features = ManagedFeatures::from_configured(configured_features, feature_requirements)?;
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
let windows_sandbox_private_desktop =
resolve_windows_sandbox_private_desktop(&cfg, &config_profile);
let resolved_cwd = normalize_for_native_workdir({
use std::env;
@ -2394,6 +2399,7 @@ impl Config {
allow_login_shell,
shell_environment_policy,
windows_sandbox_mode,
windows_sandbox_private_desktop,
macos_seatbelt_profile_extensions: None,
},
enforce_residency: enforce_residency.value,

View file

@ -41,6 +41,9 @@ pub enum WindowsSandboxModeToml {
#[schemars(deny_unknown_fields)]
pub struct WindowsToml {
pub sandbox: Option<WindowsSandboxModeToml>,
/// Defaults to `true`. Set to `false` to launch the final sandboxed child
/// process on `Winsta0\\Default` instead of a private desktop.
pub sandbox_private_desktop: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]

View file

@ -81,6 +81,7 @@ pub struct ExecParams {
pub network: Option<NetworkProxy>,
pub sandbox_permissions: SandboxPermissions,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub justification: Option<String>,
pub arg0: Option<String>,
}
@ -231,6 +232,7 @@ pub fn build_exec_request(
network,
sandbox_permissions,
windows_sandbox_level,
windows_sandbox_private_desktop,
justification,
arg0: _,
} = params;
@ -271,6 +273,7 @@ pub fn build_exec_request(
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock,
windows_sandbox_level,
windows_sandbox_private_desktop,
})
.map_err(CodexErr::from)?;
Ok(exec_req)
@ -290,6 +293,7 @@ pub(crate) async fn execute_exec_request(
expiration,
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
sandbox_permissions,
sandbox_policy: _sandbox_policy_from_env,
file_system_sandbox_policy,
@ -307,6 +311,7 @@ pub(crate) async fn execute_exec_request(
network: network.clone(),
sandbox_permissions,
windows_sandbox_level,
windows_sandbox_private_desktop,
justification,
arg0,
};
@ -409,6 +414,7 @@ async fn exec_windows_sandbox(
network,
expiration,
windows_sandbox_level,
windows_sandbox_private_desktop,
..
} = params;
if let Some(network) = network.as_ref() {
@ -443,6 +449,7 @@ async fn exec_windows_sandbox(
&cwd,
env,
timeout_ms,
windows_sandbox_private_desktop,
)
} else {
run_windows_sandbox_capture(
@ -453,6 +460,7 @@ async fn exec_windows_sandbox(
&cwd,
env,
timeout_ms,
windows_sandbox_private_desktop,
)
}
})

View file

@ -319,6 +319,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()>
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
};
@ -375,6 +376,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> {
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
};

View file

@ -69,6 +69,7 @@ pub struct ExecRequest {
pub expiration: ExecExpiration,
pub sandbox: SandboxType,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub sandbox_permissions: SandboxPermissions,
pub sandbox_policy: SandboxPolicy,
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
@ -96,6 +97,7 @@ pub(crate) struct SandboxTransformRequest<'a> {
pub codex_linux_sandbox_exe: Option<&'a PathBuf>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
}
pub enum SandboxPreference {
@ -593,6 +595,7 @@ impl SandboxManager {
codex_linux_sandbox_exe,
use_legacy_landlock,
windows_sandbox_level,
windows_sandbox_private_desktop,
} = request;
#[cfg(not(target_os = "macos"))]
let macos_seatbelt_profile_extensions = None;
@ -705,6 +708,7 @@ impl SandboxManager {
expiration: spec.expiration,
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
sandbox_permissions: spec.sandbox_permissions,
sandbox_policy: effective_policy,
file_system_sandbox_policy: effective_file_system_policy,

View file

@ -169,6 +169,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
})
.expect("transform");
@ -502,6 +503,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
})
.expect("transform");
@ -574,6 +576,7 @@ fn transform_additional_permissions_preserves_denied_entries() {
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
})
.expect("transform");

View file

@ -167,6 +167,10 @@ pub(crate) async fn execute_user_shell_command(
expiration: USER_SHELL_TIMEOUT_MS.into(),
sandbox: SandboxType::None,
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
.permissions
.windows_sandbox_private_desktop,
sandbox_permissions: SandboxPermissions::UseDefault,
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),

View file

@ -74,6 +74,10 @@ impl ShellHandler {
network: turn_context.network.clone(),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
.permissions
.windows_sandbox_private_desktop,
justification: params.justification.clone(),
arg0: None,
}
@ -124,6 +128,10 @@ impl ShellCommandHandler {
network: turn_context.network.clone(),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
.config
.permissions
.windows_sandbox_private_desktop,
justification: params.justification.clone(),
arg0: None,
})

View file

@ -1067,6 +1067,10 @@ impl JsReplManager {
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock: turn.features.use_legacy_landlock(),
windows_sandbox_level: turn.windows_sandbox_level,
windows_sandbox_private_desktop: turn
.config
.permissions
.windows_sandbox_private_desktop,
})
.map_err(|err| format!("failed to configure sandbox for js_repl: {err}"))?;

View file

@ -197,6 +197,10 @@ impl ToolOrchestrator {
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
windows_sandbox_private_desktop: turn_ctx
.config
.permissions
.windows_sandbox_private_desktop,
};
let (first_result, first_deferred_network_approval) = Self::run_attempt(
@ -319,6 +323,10 @@ impl ToolOrchestrator {
codex_linux_sandbox_exe: None,
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
windows_sandbox_private_desktop: turn_ctx
.config
.permissions
.windows_sandbox_private_desktop,
};
// Second attempt.

View file

@ -126,6 +126,7 @@ pub(super) async fn try_run_zsh_fork(
expiration: _sandbox_expiration,
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop: _windows_sandbox_private_desktop,
sandbox_permissions,
sandbox_policy,
file_system_sandbox_policy,
@ -924,6 +925,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
expiration: ExecExpiration::Cancellation(cancel_rx),
sandbox: self.sandbox,
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: false,
sandbox_permissions: self.sandbox_permissions,
sandbox_policy: self.sandbox_policy.clone(),
file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
@ -1080,6 +1082,7 @@ impl CoreShellCommandExecutor {
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock: self.use_legacy_landlock,
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: false,
})?;
if let Some(network) = exec_request.network.as_ref() {
network.apply_to_env(&mut exec_request.env);

View file

@ -730,6 +730,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: false,
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()

View file

@ -333,6 +333,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
}
impl<'a> SandboxAttempt<'a> {
@ -356,6 +357,7 @@ impl<'a> SandboxAttempt<'a> {
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe,
use_legacy_landlock: self.use_legacy_landlock,
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
})
}
}

View file

@ -75,6 +75,19 @@ pub fn resolve_windows_sandbox_mode(
.or_else(|| legacy_windows_sandbox_mode(cfg.features.as_ref()))
}
pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml, profile: &ConfigProfile) -> bool {
profile
.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
.or_else(|| {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
})
.unwrap_or(true)
}
fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool {
let Some(entries) = features.map(|features| &features.entries) else {
return false;

View file

@ -77,12 +77,14 @@ fn resolve_windows_sandbox_mode_prefers_profile_windows() {
let cfg = ConfigToml {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Unelevated),
..Default::default()
}),
..Default::default()
};
let profile = ConfigProfile {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Elevated),
..Default::default()
}),
..Default::default()
};
@ -130,3 +132,47 @@ fn resolve_windows_sandbox_mode_profile_legacy_false_blocks_top_level_legacy_tru
assert_eq!(resolve_windows_sandbox_mode(&cfg, &profile), None);
}
#[test]
fn resolve_windows_sandbox_private_desktop_prefers_profile_windows() {
let cfg = ConfigToml {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Unelevated),
sandbox_private_desktop: Some(false),
}),
..Default::default()
};
let profile = ConfigProfile {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Elevated),
sandbox_private_desktop: Some(true),
}),
..Default::default()
};
assert!(resolve_windows_sandbox_private_desktop(&cfg, &profile));
}
#[test]
fn resolve_windows_sandbox_private_desktop_defaults_to_true() {
assert!(resolve_windows_sandbox_private_desktop(
&ConfigToml::default(),
&ConfigProfile::default()
));
}
#[test]
fn resolve_windows_sandbox_private_desktop_respects_explicit_cfg_value() {
let cfg = ConfigToml {
windows: Some(WindowsToml {
sandbox_private_desktop: Some(false),
..Default::default()
}),
..Default::default()
};
assert!(!resolve_windows_sandbox_private_desktop(
&cfg,
&ConfigProfile::default()
));
}

View file

@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
};

View file

@ -120,6 +120,7 @@ async fn run_cmd_result_with_policies(
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
};
@ -352,6 +353,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
};

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",
]

View file

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

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

View file

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

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

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