mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Run fs helper through Windows sandbox wrapper (#28359)
## Why This is the final PR in the Windows fs-helper sandbox stack and contains the actual bug fix. The exec-server filesystem helper is a direct-spawn path: it asks `SandboxManager` for a `SandboxExecRequest`, then launches the returned argv itself. That works on macOS and Linux because the transformed argv is already a self-contained sandbox wrapper. On Windows, the transformed request carried `WindowsRestrictedToken` metadata, but the direct-spawn fs-helper runner still launched the helper argv directly. That means Windows filesystem built-ins backed by the fs-helper could run with the parent Codex process permissions instead of the configured Windows sandbox. This PR makes the direct-spawn transform produce a self-contained Windows wrapper argv before fs-helper launches it. ## What Changed - Added `SandboxManager::transform_for_direct_spawn()` for callers that launch the returned argv themselves. - Wrapped Windows restricted-token direct-spawn requests with `codex.exe --run-as-windows-sandbox` and then marked the outer request as unsandboxed, matching the macOS/Linux wrapper argv shape. - Updated `exec-server/src/fs_sandbox.rs` to use the direct-spawn transform for fs-helper launches. - Materialized the inner `codex.exe --codex-run-as-fs-helper` executable into `.sandbox-bin` so the sandboxed user can run it. - Carried runtime workspace roots through `FileSystemSandboxContext` as `PathUri` values so `:workspace_roots` policies resolve correctly without sending native client paths over exec-server JSON. - Preserved wrapper setup identity environment needed by Windows sandbox setup without changing the serialized inner helper environment. ## Verification - `just bazel-lock-update` - `just bazel-lock-check` - `just test -p codex-sandboxing transform_for_direct_spawn_windows` - `just test -p codex-exec-server fs_sandbox::tests` - `just fix -p codex-windows-sandbox -p codex-sandboxing -p codex-exec-server -p codex-core -p codex-file-system` Local note: `just fmt` completed Rust formatting, but this workstation still fails the non-Rust formatter phases because uv cannot open its cache and the local buildifier/dotslash path is missing.
This commit is contained in:
1 parent
c78911e37f
commit
ef75171f18
16 files changed
+861
-399
No files matched your search
@@ -7,6 +7,12 @@ use crate::landlock::allow_network_for_proxy;
|
||||
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
use crate::policy_transforms::effective_permission_profile;
|
||||
use crate::policy_transforms::should_require_platform_sandbox;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::resolve_windows_elevated_filesystem_overrides;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::resolve_windows_restricted_token_filesystem_overrides;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::windows_sandbox_uses_elevated_backend;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -21,6 +27,9 @@ use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USERPROFILE"];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SandboxType {
|
||||
None,
|
||||
@@ -131,6 +140,16 @@ pub struct SandboxTransformRequest<'a> {
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
}
|
||||
|
||||
/// Bundled arguments for a sandbox transformation whose result will be spawned
|
||||
/// directly from argv.
|
||||
///
|
||||
/// Direct-spawn callers will not run a later platform-specific launcher, so the
|
||||
/// returned command must encode any sandbox wrapper it needs.
|
||||
pub struct SandboxDirectSpawnTransformRequest<'a> {
|
||||
pub transform: SandboxTransformRequest<'a>,
|
||||
pub workspace_roots: &'a [AbsolutePathBuf],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxTransformError {
|
||||
InvalidCommandCwd {
|
||||
@@ -146,6 +165,8 @@ pub enum SandboxTransformError {
|
||||
Wsl1UnsupportedForBubblewrap,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
SeatbeltUnavailable,
|
||||
#[cfg(target_os = "windows")]
|
||||
WindowsSandboxPreparation(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SandboxTransformError {
|
||||
@@ -168,6 +189,10 @@ impl std::fmt::Display for SandboxTransformError {
|
||||
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::WindowsSandboxPreparation(err) => {
|
||||
write!(f, "failed to prepare windows sandbox wrapper: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +207,8 @@ impl std::error::Error for SandboxTransformError {
|
||||
Self::Wsl1UnsupportedForBubblewrap => None,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::SeatbeltUnavailable => None,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::WindowsSandboxPreparation(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,6 +363,142 @@ impl SandboxManager {
|
||||
arg0: arg0_override,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform_for_direct_spawn(
|
||||
&self,
|
||||
request: SandboxDirectSpawnTransformRequest<'_>,
|
||||
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let codex_home = codex_utils_home_dir::find_codex_home()
|
||||
.map_err(|err| SandboxTransformError::WindowsSandboxPreparation(err.to_string()))?;
|
||||
self.transform_for_direct_spawn_with_codex_home(request, codex_home.as_path())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
self.transform(request.transform)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn transform_for_direct_spawn_with_codex_home(
|
||||
&self,
|
||||
request: SandboxDirectSpawnTransformRequest<'_>,
|
||||
codex_home: &Path,
|
||||
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
||||
let workspace_roots = request.workspace_roots;
|
||||
let mut request = self.transform(request.transform)?;
|
||||
if request.sandbox == SandboxType::WindowsRestrictedToken {
|
||||
wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
&mut request,
|
||||
workspace_roots,
|
||||
codex_home,
|
||||
)?;
|
||||
}
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
request: &mut SandboxExecRequest,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
codex_home: &Path,
|
||||
) -> Result<(), SandboxTransformError> {
|
||||
let Some(program) = request.command.first_mut() else {
|
||||
return Err(SandboxTransformError::WindowsSandboxPreparation(
|
||||
"sandbox command was empty".to_string(),
|
||||
));
|
||||
};
|
||||
let source = std::path::PathBuf::from(&program);
|
||||
let helper = codex_windows_sandbox::resolve_exe_for_launch(source.as_path(), codex_home);
|
||||
*program = helper.to_string_lossy().into_owned();
|
||||
|
||||
let inner_command = std::mem::take(&mut request.command);
|
||||
let proxy_enforced = request.network.is_some();
|
||||
let use_elevated =
|
||||
windows_sandbox_uses_elevated_backend(request.windows_sandbox_level, proxy_enforced);
|
||||
let overrides = if use_elevated {
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
request.sandbox,
|
||||
&request.permission_profile,
|
||||
&request.sandbox_policy_cwd,
|
||||
use_elevated,
|
||||
)
|
||||
} else {
|
||||
resolve_windows_restricted_token_filesystem_overrides(
|
||||
request.sandbox,
|
||||
&request.permission_profile,
|
||||
&request.sandbox_policy_cwd,
|
||||
request.windows_sandbox_level,
|
||||
)
|
||||
}
|
||||
.map_err(SandboxTransformError::WindowsSandboxPreparation)?;
|
||||
let empty_paths: &[AbsolutePathBuf] = &[];
|
||||
let read_roots_override = overrides
|
||||
.as_ref()
|
||||
.and_then(|overrides| overrides.read_roots_override.as_deref());
|
||||
let read_roots_include_platform_defaults = overrides
|
||||
.as_ref()
|
||||
.is_some_and(|overrides| overrides.read_roots_include_platform_defaults);
|
||||
let write_roots_override = overrides
|
||||
.as_ref()
|
||||
.and_then(|overrides| overrides.write_roots_override.as_deref());
|
||||
let deny_read_paths_override = overrides.as_ref().map_or(empty_paths, |overrides| {
|
||||
overrides.additional_deny_read_paths.as_slice()
|
||||
});
|
||||
let deny_write_paths_override = overrides.as_ref().map_or(empty_paths, |overrides| {
|
||||
overrides.additional_deny_write_paths.as_slice()
|
||||
});
|
||||
let mut wrapper_args =
|
||||
codex_windows_sandbox::create_windows_sandbox_command_args_for_permission_profile(
|
||||
inner_command,
|
||||
&request.cwd,
|
||||
workspace_roots,
|
||||
&request.env,
|
||||
&request.permission_profile,
|
||||
request.windows_sandbox_level,
|
||||
request.windows_sandbox_private_desktop,
|
||||
proxy_enforced,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
write_roots_override,
|
||||
deny_read_paths_override,
|
||||
deny_write_paths_override,
|
||||
codex_home,
|
||||
);
|
||||
|
||||
request.command = Vec::with_capacity(1 + wrapper_args.len());
|
||||
request.command.push(source.to_string_lossy().into_owned());
|
||||
request.command.append(&mut wrapper_args);
|
||||
request.sandbox = SandboxType::None;
|
||||
request.arg0 = None;
|
||||
add_windows_sandbox_wrapper_setup_env(&mut request.env);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn add_windows_sandbox_wrapper_setup_env(env: &mut HashMap<String, String>) {
|
||||
add_windows_sandbox_wrapper_setup_env_from_vars(env, std::env::vars_os());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn add_windows_sandbox_wrapper_setup_env_from_vars(
|
||||
env: &mut HashMap<String, String>,
|
||||
vars: impl IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
|
||||
) {
|
||||
for (key, value) in vars {
|
||||
let key = key.to_string_lossy().into_owned();
|
||||
if !WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST
|
||||
.iter()
|
||||
.any(|allowed| key.eq_ignore_ascii_case(allowed))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
env.retain(|existing, _| !existing.eq_ignore_ascii_case(&key));
|
||||
env.insert(key, value.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compatibility_sandbox_policy_for_permission_profile(
|
||||
|
||||
Reference in new issue
Block a user