Compare commits

...

5 Commits

Author SHA1 Message Date
David Wiesen
36b80dd9be Clarify mapped-drive cwd normalization 2026-04-25 20:44:12 -07:00
David Wiesen
73d8c18fd5 Normalize mapped-drive cwd in legacy sandbox entrypoints 2026-04-25 20:43:50 -07:00
David Wiesen
6d4c2ae753 Handle Windows sandbox mapped-drive workspaces 2026-04-25 20:43:23 -07:00
David Wiesen
f54ba811cd Handle unsupported remote Windows sandbox workspaces 2026-04-25 20:40:28 -07:00
David Wiesen
ec116a6a8e Fix Windows sandbox runner launch cwd 2026-04-25 20:40:21 -07:00
9 changed files with 172 additions and 33 deletions

View File

@@ -80,6 +80,7 @@ features = [
"Win32_Security_Cryptography",
"Win32_Security_Authentication_Identity",
"Win32_Graphics_Gdi",
"Win32_NetworkManagement_WNet",
"Win32_System_StationsAndDesktops",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",

View File

@@ -69,9 +69,16 @@ impl RunnerTransport {
}
}
fn runner_launch_cwd<'a>(codex_home: &'a Path, runner_exe: &'a Path) -> &'a Path {
// The elevated helper only needs a stable local directory to start up and connect its pipes.
// Using the requested workspace as the process CWD breaks mapped-drive/UNC workspaces because
// the sandbox logon session may not inherit the caller's drive mappings.
runner_exe.parent().unwrap_or(codex_home)
}
pub(crate) fn spawn_runner_transport(
codex_home: &Path,
cwd: &Path,
_cwd: &Path,
sandbox_creds: &SandboxCreds,
log_dir: Option<&Path>,
) -> Result<RunnerTransport> {
@@ -94,7 +101,8 @@ pub(crate) fn spawn_runner_transport(
);
let mut cmdline_vec = to_wide(&runner_full_cmd);
let exe_w = to_wide(&runner_cmdline);
let cwd_w = to_wide(cwd);
let launch_cwd = runner_launch_cwd(codex_home, &runner_exe);
let cwd_w = to_wide(launch_cwd);
let user_w = to_wide(&sandbox_creds.username);
let domain_w = to_wide(".");
let password_w = to_wide(&sandbox_creds.password);
@@ -166,6 +174,33 @@ pub(crate) fn spawn_runner_transport(
})
}
#[cfg(test)]
mod tests {
use super::runner_launch_cwd;
use std::path::Path;
#[test]
fn runner_launch_cwd_prefers_local_helper_parent_over_requested_workspace() {
let codex_home = Path::new(r"C:\Users\dev\.codex");
let runner_exe = Path::new(
r"C:\Users\dev\.codex\.sandbox-bin\codex-command-runner.exe",
);
assert_eq!(
runner_launch_cwd(codex_home, runner_exe),
Path::new(r"C:\Users\dev\.codex\.sandbox-bin")
);
}
#[test]
fn runner_launch_cwd_falls_back_to_codex_home_when_parent_is_missing() {
let codex_home = Path::new(r"C:\Users\dev\.codex");
let runner_exe = Path::new("codex-command-runner.exe");
assert_eq!(runner_launch_cwd(codex_home, runner_exe), codex_home);
}
}
fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()> {
let handle = pipe_read.as_raw_handle() as HANDLE;
let deadline = Instant::now() + timeout;

View File

@@ -38,6 +38,7 @@ mod windows_impl {
use crate::logging::log_note;
use crate::logging::log_start;
use crate::logging::log_success;
use crate::path_normalization::normalize_command_cwd;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::token::convert_string_sid_to_sid;
@@ -241,11 +242,12 @@ mod windows_impl {
write_roots_override,
deny_write_paths_override,
} = request;
let normalized_cwd = normalize_command_cwd(cwd);
let policy = parse_policy(policy_json_or_preset)?;
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, &normalized_cwd, 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,
&normalized_cwd,
&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, &normalized_cwd)?,
],
)
}
@@ -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(sandbox_base.as_path());
// 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()
sandbox_base.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: normalized_cwd.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,8 @@ 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 normalized_cwd = super::path_normalization::normalize_command_cwd(cwd);
let ws_sid = workspace_cap_sid_for_cwd(codex_home, &normalized_cwd)?;
#[allow(clippy::expect_used)]
let psid_workspace =
convert_string_sid_to_sid(&ws_sid).expect("valid workspace SID");
@@ -607,11 +608,11 @@ mod windows_impl {
#[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 current_dir = super::path_normalization::normalize_command_cwd(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

@@ -1,6 +1,11 @@
use std::path::Path;
use std::path::PathBuf;
use crate::winutil::to_wide;
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
use windows_sys::Win32::Foundation::NO_ERROR;
use windows_sys::Win32::NetworkManagement::WNet::WNetGetConnectionW;
pub fn canonicalize_path(path: &Path) -> PathBuf {
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
@@ -12,9 +17,57 @@ pub fn canonical_path_key(path: &Path) -> String {
.to_ascii_lowercase()
}
/// Resolve mapped drive working directories into their UNC targets before launching
/// sandboxed processes under an alternate logon session.
pub fn normalize_command_cwd(path: &Path) -> PathBuf {
let simplified = dunce::simplified(path).to_path_buf();
normalize_mapped_drive_path_with(&simplified, mapped_drive_remote_root).unwrap_or(simplified)
}
fn normalize_mapped_drive_path_with<F>(path: &Path, resolve_remote_root: F) -> Option<PathBuf>
where
F: Fn(&str) -> Option<String>,
{
let raw = path.to_string_lossy();
let bytes = raw.as_bytes();
if bytes.len() < 2 || !bytes[0].is_ascii_alphabetic() || bytes[1] != b':' {
return None;
}
let drive = raw[..2].to_ascii_uppercase();
let remote_root = resolve_remote_root(&drive)?;
let suffix = raw[2..].trim_start_matches(['\\', '/']);
let mut normalized = PathBuf::from(remote_root);
if !suffix.is_empty() {
normalized.push(suffix);
}
Some(normalized)
}
fn mapped_drive_remote_root(drive: &str) -> Option<String> {
let drive_wide = to_wide(drive);
let mut len = 260u32;
loop {
let mut buf = vec![0u16; len as usize];
let status =
unsafe { WNetGetConnectionW(drive_wide.as_ptr(), buf.as_mut_ptr(), &mut len) };
match status {
NO_ERROR => {
let end = buf.iter().position(|ch| *ch == 0).unwrap_or(buf.len());
return String::from_utf16(&buf[..end]).ok();
}
ERROR_MORE_DATA => continue,
_ => return None,
}
}
}
#[cfg(test)]
mod tests {
use super::canonical_path_key;
use super::normalize_command_cwd;
use super::normalize_mapped_drive_path_with;
use pretty_assertions::assert_eq;
use std::path::Path;
@@ -23,6 +76,30 @@ mod tests {
let windows_style = Path::new(r"C:\Users\Dev\Repo");
let slash_style = Path::new("c:/users/dev/repo");
assert_eq!(canonical_path_key(windows_style), canonical_path_key(slash_style));
assert_eq!(
canonical_path_key(windows_style),
canonical_path_key(slash_style)
);
}
#[test]
fn mapped_drive_paths_expand_to_unc_roots() {
let path = Path::new(r"L:\cs-web\context");
let normalized = normalize_mapped_drive_path_with(path, |drive| {
(drive == "L:").then(|| r"\\video1\node".to_string())
});
assert_eq!(
normalized,
Some(PathBuf::from(r"\\video1\node\cs-web\context"))
);
}
#[test]
fn local_paths_are_left_alone() {
let path = Path::new(r"C:\Users\Dev\Repo");
assert_eq!(
normalize_command_cwd(path),
PathBuf::from(r"C:\Users\Dev\Repo")
);
}
}

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::normalize_command_cwd;
use crate::policy::SandboxPolicy;
use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupFailure;
@@ -165,17 +166,30 @@ fn run_setup_refresh_inner(
) {
return Ok(());
}
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 =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
let normalized_command_cwd = normalize_command_cwd(request.command_cwd);
let normalized_request = SandboxSetupRequest {
policy: request.policy,
policy_cwd: request.policy_cwd,
command_cwd: &normalized_command_cwd,
env_map: request.env_map,
codex_home: request.codex_home,
proxy_enforced: request.proxy_enforced,
};
let (read_roots, write_roots) = build_payload_roots(&normalized_request, &overrides);
let deny_write_paths =
build_payload_deny_write_paths(&normalized_request, overrides.deny_write_paths);
let network_identity = SandboxNetworkIdentity::from_policy(
normalized_request.policy,
normalized_request.proxy_enforced,
);
let offline_proxy_settings =
offline_proxy_settings_from_env(normalized_request.env_map, network_identity);
let payload = ElevationPayload {
version: SETUP_VERSION,
offline_username: OFFLINE_USERNAME.to_string(),
online_username: ONLINE_USERNAME.to_string(),
codex_home: request.codex_home.to_path_buf(),
command_cwd: request.command_cwd.to_path_buf(),
codex_home: normalized_request.codex_home.to_path_buf(),
command_cwd: normalized_request.command_cwd.to_path_buf(),
read_roots,
write_roots,
deny_write_paths,
@@ -198,14 +212,14 @@ fn run_setup_refresh_inner(
cwd.display(),
b64.len()
),
Some(&sandbox_dir(request.codex_home)),
Some(&sandbox_dir(normalized_request.codex_home)),
);
let status = cmd
.status()
.map_err(|e| {
log_note(
&format!("setup refresh: failed to spawn {}: {e}", exe.display()),
Some(&sandbox_dir(request.codex_home)),
Some(&sandbox_dir(normalized_request.codex_home)),
);
e
})
@@ -213,7 +227,7 @@ fn run_setup_refresh_inner(
if !status.success() {
log_note(
&format!("setup refresh: exited with status {status:?}"),
Some(&sandbox_dir(request.codex_home)),
Some(&sandbox_dir(normalized_request.codex_home)),
);
return Err(anyhow!("setup refresh failed with status {status}"));
}

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::normalize_command_cwd;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::sandbox_utils::ensure_codex_home_exists;
@@ -103,13 +104,15 @@ fn prepare_spawn_context_common(
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
}
let normalized_cwd = normalize_command_cwd(cwd);
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
if inherit_path {
inherit_path_env(env_map);
}
if add_git_safe_directory {
inject_git_safe_directory(env_map, cwd);
inject_git_safe_directory(env_map, &normalized_cwd);
}
ensure_codex_home_exists(codex_home)?;
@@ -122,7 +125,7 @@ fn prepare_spawn_context_common(
Ok(SpawnContext {
policy,
current_dir: cwd.to_path_buf(),
current_dir: normalized_cwd,
sandbox_base,
logs_base_dir,
is_workspace_write,
@@ -168,7 +171,8 @@ pub(crate) fn prepare_legacy_session_security(
}
SandboxPolicy::WorkspaceWrite { .. } => {
let psid_generic = LocalSid::from_string(&caps.workspace)?;
let workspace_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
let workspace_sid =
workspace_cap_sid_for_cwd(codex_home, &normalize_command_cwd(cwd))?;
let psid_workspace = LocalSid::from_string(&workspace_sid)?;
let base = get_current_token_for_restriction()?;
let h_token = create_workspace_write_token_with_caps_from(
@@ -290,7 +294,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 +309,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],

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,12 +55,16 @@ 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 runner_cwd = elevated.common.sandbox_base.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<_> {
let mut transport =
spawn_runner_transport(&codex_home, &cwd, &sandbox_creds, logs_base_dir.as_deref())?;
let mut transport = spawn_runner_transport(
&codex_home,
&runner_cwd,
&sandbox_creds,
logs_base_dir.as_deref(),
)?;
transport.send_spawn_request(spawn_request)?;
transport.read_spawn_ready()?;
Ok(transport)

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,