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:
parent
9c9867c9fa
commit
6b3d82daca
30 changed files with 416 additions and 70 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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}"))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
codex-rs/windows-sandbox-rs/src/desktop.rs
Normal file
196
codex-rs/windows-sandbox-rs/src/desktop.rs
Normal 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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue