Compare commits

...

2 Commits

Author SHA1 Message Date
David Wiesen
d215ef9de9 Fix Windows sandbox mapped-drive cwd 2026-04-25 20:42:25 -07:00
David Wiesen
395084ec67 Handle remote Windows sandbox workspaces early 2026-04-25 20:40:46 -07:00
7 changed files with 108 additions and 18 deletions

View File

@@ -20,6 +20,7 @@ pub struct ElevatedSandboxCaptureRequest<'a> {
mod windows_impl {
use super::ElevatedSandboxCaptureRequest;
use crate::acl::allow_null_device;
use crate::canonicalize_path;
use crate::cap::load_or_create_cap_sids;
use crate::env::ensure_non_interactive_pager;
use crate::env::inherit_path_env;
@@ -242,10 +243,11 @@ mod windows_impl {
deny_write_paths_override,
} = request;
let policy = parse_policy(policy_json_or_preset)?;
let current_dir = canonicalize_path(cwd);
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
inherit_path_env(&mut env_map);
inject_git_safe_directory(&mut env_map, cwd, None);
inject_git_safe_directory(&mut env_map, &current_dir, None);
// Use a temp-based log dir that the sandbox user can write.
let sandbox_base = codex_home.join(".sandbox");
ensure_codex_home_exists(&sandbox_base)?;
@@ -255,7 +257,7 @@ mod windows_impl {
let sandbox_creds = require_logon_sandbox_creds(
&policy,
sandbox_policy_cwd,
cwd,
&current_dir,
&env_map,
codex_home,
read_roots_override,
@@ -289,7 +291,7 @@ mod windows_impl {
psid,
vec![
caps.workspace,
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
crate::cap::workspace_cap_sid_for_cwd(codex_home, &current_dir)?,
],
)
}
@@ -321,7 +323,7 @@ mod windows_impl {
);
let mut cmdline_vec: Vec<u16> = to_wide(&runner_full_cmd);
let exe_w: Vec<u16> = to_wide(&runner_cmdline);
let cwd_w: Vec<u16> = to_wide(cwd);
let cwd_w: Vec<u16> = to_wide(&current_dir);
// Minimal CPWL launch: inherit env, no desktop override, no handle inheritance.
let env_block: Option<Vec<u16>> = None;
@@ -339,7 +341,7 @@ mod windows_impl {
"runner launch: exe={} cmdline={} cwd={}",
runner_exe.display(),
runner_full_cmd,
cwd.display()
current_dir.display()
),
logs_base_dir,
);
@@ -413,7 +415,7 @@ mod windows_impl {
message: Message::SpawnRequest {
payload: Box::new(SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
cwd: current_dir.clone(),
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),

View File

@@ -378,7 +378,7 @@ mod windows_impl {
#[allow(clippy::expect_used)]
let psid_generic =
convert_string_sid_to_sid(&caps.workspace).expect("valid workspace SID");
let ws_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
let ws_sid = workspace_cap_sid_for_cwd(codex_home, &current_dir)?;
#[allow(clippy::expect_used)]
let psid_workspace =
convert_string_sid_to_sid(&ws_sid).expect("valid workspace SID");
@@ -458,7 +458,7 @@ mod windows_impl {
create_process_as_user(
h_token,
&command,
cwd,
&current_dir,
&env_map,
logs_base_dir,
Some((in_r, out_w, err_w)),
@@ -603,15 +603,15 @@ mod windows_impl {
}
ensure_codex_home_exists(codex_home)?;
let current_dir = canonicalize_path(cwd);
let caps = load_or_create_cap_sids(codex_home)?;
#[allow(clippy::expect_used)]
let psid_generic =
unsafe { convert_string_sid_to_sid(&caps.workspace) }.expect("valid workspace SID");
let ws_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
let ws_sid = workspace_cap_sid_for_cwd(codex_home, &current_dir)?;
#[allow(clippy::expect_used)]
let psid_workspace =
unsafe { convert_string_sid_to_sid(&ws_sid) }.expect("valid workspace SID");
let current_dir = cwd.to_path_buf();
let AllowDenyPaths { allow, deny } =
compute_allow_paths(sandbox_policy, sandbox_policy_cwd, &current_dir, env_map);
let canonical_cwd = canonicalize_path(&current_dir);

View File

@@ -12,8 +12,54 @@ pub fn canonical_path_key(path: &Path) -> String {
.to_ascii_lowercase()
}
pub fn unsupported_windows_sandbox_workspace_reason(path: &Path) -> Option<String> {
unsupported_windows_sandbox_workspace_kind(path).map(|kind| {
format!(
"windows sandbox does not support {kind} workspace paths ({path}); use a local checkout or disable the sandbox for this session",
path = path.display()
)
})
}
#[cfg(target_os = "windows")]
fn unsupported_windows_sandbox_workspace_kind(path: &Path) -> Option<&'static str> {
use windows_sys::Win32::Storage::FileSystem::DRIVE_REMOTE;
use windows_sys::Win32::Storage::FileSystem::GetDriveTypeW;
let path_text = path.as_os_str().to_string_lossy();
if path_text.starts_with(r"\\") || path_text.starts_with("//") {
return Some("UNC network share");
}
let root = windows_drive_root(path)?;
let root_wide: Vec<u16> = root.encode_utf16().chain(std::iter::once(0)).collect();
let drive_type = unsafe { GetDriveTypeW(root_wide.as_ptr()) };
(drive_type == DRIVE_REMOTE).then_some("mapped network drive")
}
#[cfg(not(target_os = "windows"))]
fn unsupported_windows_sandbox_workspace_kind(_path: &Path) -> Option<&'static str> {
None
}
#[cfg(target_os = "windows")]
fn windows_drive_root(path: &Path) -> Option<String> {
let path_text = path.as_os_str().to_string_lossy();
let bytes = path_text.as_bytes();
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'\\' | b'/')
{
Some(format!("{}:\\", bytes[0] as char))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::unsupported_windows_sandbox_workspace_reason;
use super::canonical_path_key;
use pretty_assertions::assert_eq;
use std::path::Path;
@@ -25,4 +71,11 @@ mod tests {
assert_eq!(canonical_path_key(windows_style), canonical_path_key(slash_style));
}
#[test]
fn unsupported_workspace_reason_flags_unc_paths() {
let reason = unsupported_windows_sandbox_workspace_reason(Path::new(r"\\server\share\repo"))
.expect("UNC path should be rejected");
assert!(reason.contains("UNC network share"));
}
}

View File

@@ -15,6 +15,7 @@ use crate::allow::compute_allow_paths;
use crate::helper_materialization::helper_bin_dir;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::path_normalization::unsupported_windows_sandbox_workspace_reason;
use crate::policy::SandboxPolicy;
use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupFailure;
@@ -165,6 +166,9 @@ fn run_setup_refresh_inner(
) {
return Ok(());
}
if let Some(reason) = unsupported_windows_sandbox_workspace_reason(request.command_cwd) {
anyhow::bail!(reason);
}
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let network_identity =

View File

@@ -13,6 +13,7 @@ use crate::identity::SandboxCreds;
use crate::identity::require_logon_sandbox_creds;
use crate::logging::log_start;
use crate::path_normalization::canonicalize_path;
use crate::path_normalization::unsupported_windows_sandbox_workspace_reason;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::sandbox_utils::ensure_codex_home_exists;
@@ -96,12 +97,16 @@ fn prepare_spawn_context_common(
add_git_safe_directory: bool,
) -> Result<SpawnContext> {
let policy = parse_policy(policy_json_or_preset)?;
let current_dir = canonicalize_path(cwd);
if matches!(
&policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
}
if let Some(reason) = unsupported_windows_sandbox_workspace_reason(cwd) {
anyhow::bail!(reason);
}
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
@@ -109,7 +114,7 @@ fn prepare_spawn_context_common(
inherit_path_env(env_map);
}
if add_git_safe_directory {
inject_git_safe_directory(env_map, cwd);
inject_git_safe_directory(env_map, &current_dir);
}
ensure_codex_home_exists(codex_home)?;
@@ -122,7 +127,7 @@ fn prepare_spawn_context_common(
Ok(SpawnContext {
policy,
current_dir: cwd.to_path_buf(),
current_dir,
sandbox_base,
logs_base_dir,
is_workspace_write,
@@ -290,7 +295,7 @@ pub(crate) fn prepare_elevated_spawn_context(
let sandbox_creds = require_logon_sandbox_creds(
&common.policy,
sandbox_policy_cwd,
cwd,
&common.current_dir,
env_map,
codex_home,
/*read_roots_override*/ None,
@@ -305,7 +310,7 @@ pub(crate) fn prepare_elevated_spawn_context(
vec![caps.readonly.clone()],
),
SandboxPolicy::WorkspaceWrite { .. } => {
let cap_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
let cap_sid = workspace_cap_sid_for_cwd(codex_home, &common.current_dir)?;
(
LocalSid::from_string(&caps.workspace)?,
vec![caps.workspace.clone(), cap_sid],
@@ -408,4 +413,29 @@ mod tests {
Some(&"http://user.proxy:8080".to_string())
);
}
#[test]
fn common_spawn_context_canonicalizes_current_dir() {
let codex_home = TempDir::new().expect("tempdir");
let workspace = TempDir::new().expect("tempdir");
std::fs::create_dir_all(workspace.path().join("nested")).expect("create nested dir");
let spelled_cwd = workspace.path().join(".").join("nested").join("..");
let mut env_map = HashMap::new();
let context = prepare_spawn_context_common(
"workspace-write",
codex_home.path(),
&spelled_cwd,
&mut env_map,
&["cmd.exe".to_string()],
/*inherit_path*/ true,
/*add_git_safe_directory*/ false,
)
.expect("canonical cwd prep");
assert_eq!(
context.current_dir,
dunce::canonicalize(&spelled_cwd).expect("canonical cwd")
);
}
}

View File

@@ -42,7 +42,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
cwd: elevated.common.current_dir.clone(),
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
@@ -55,7 +55,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
use_private_desktop,
};
let codex_home = codex_home.to_path_buf();
let cwd = cwd.to_path_buf();
let cwd = elevated.common.current_dir.clone();
let sandbox_creds = elevated.sandbox_creds.clone();
let logs_base_dir = elevated.common.logs_base_dir.clone();
let transport = tokio::task::spawn_blocking(move || -> Result<_> {

View File

@@ -300,7 +300,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
if !common.policy.has_full_disk_read_access() {
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
}
let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?;
let security =
prepare_legacy_session_security(&common.policy, codex_home, &common.current_dir)?;
allow_null_device_for_workspace_write(common.is_workspace_write);
let persist_aces = common.is_workspace_write;
@@ -333,7 +334,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
} = match spawn_legacy_process(
security.h_token,
&command,
cwd,
&common.current_dir,
&env_map,
use_private_desktop,
tty,