mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Consolidate Windows filesystem helper sandbox preparation (#61276)
## What changed Remove `SandboxDirectSpawnTransformRequest` and `transform_for_direct_spawn` in favor of `SandboxManager::for_file_system_helpers().transform()`. Keep Windows wrapper preparation specific to filesystem helpers, and return `SandboxUnavailable` when a Windows token sandbox is requested for a filesystem helper on a non-Windows executor. Record the helper permission entry count during launch policy preparation, before adding runtime read grants. ## Testing Cover Windows helper executable read access and explicit workspace write grants, including empty workspace roots. Verify rejection of non-native policy working directories and Windows token helper sandboxes on Unix. Extend the helper isolation test to exercise sandboxed writes as well as reads. GitOrigin-RevId: f95bb94e12d8d7c6e5c773644abb7b59b47ffc4a
This commit is contained in:
@@ -38,12 +38,10 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxSelection;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::WindowsSandboxProxySettingsMode;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_windows_sandbox_test_support::WindowsSandboxAccountTestGuard;
|
||||
use core_test_support::PathExt;
|
||||
@@ -504,33 +502,31 @@ async fn windows_elevated_temp_only_core_and_direct_spawn_enforce_carveouts() ->
|
||||
let command = format!("echo CHILD-TEMP:{child_var} & {command}");
|
||||
let (exit_code, stdout, stderr) = if direct_spawn {
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let request = SandboxManager::new().transform_for_direct_spawn(
|
||||
SandboxDirectSpawnTransformRequest {
|
||||
workspace_roots: std::slice::from_ref(&cwd),
|
||||
windows_sandbox_proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve,
|
||||
transform: SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: "cmd.exe".into(),
|
||||
args: vec!["/D".into(), "/C".into(), command],
|
||||
cwd: cwd_uri.clone(),
|
||||
env,
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permission_profile,
|
||||
workspace_roots: None,
|
||||
runtime_readable_paths: &[],
|
||||
selection: SandboxSelection::Explicit(SandboxType::WindowsRestrictedToken),
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
sandbox_exe: Some(codex.as_path()),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Elevated,
|
||||
// Exec-server spawns the Windows sandbox wrapper returned by transform for filesystem
|
||||
// helpers. Use cmd.exe as the inner command to probe file access through that wrapper.
|
||||
let request = SandboxManager::for_file_system_helpers()
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: "cmd.exe".into(),
|
||||
args: vec!["/D".into(), "/C".into(), command],
|
||||
cwd: cwd_uri.clone(),
|
||||
env,
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
},
|
||||
},
|
||||
)?;
|
||||
permissions: &permission_profile,
|
||||
workspace_roots: Some(std::slice::from_ref(&cwd_uri)),
|
||||
runtime_readable_paths: &[],
|
||||
selection: SandboxSelection::Explicit(SandboxType::WindowsRestrictedToken),
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
sandbox_exe: Some(codex.as_path()),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Elevated,
|
||||
})?
|
||||
.into_command_request();
|
||||
let output = Command::new(&request.command[0])
|
||||
.args(&request.command[1..])
|
||||
.current_dir(request.cwd.to_abs_path()?)
|
||||
|
||||
@@ -153,8 +153,6 @@ impl FileSystemSandboxRunner {
|
||||
})?
|
||||
.into_command_request();
|
||||
request.sandbox_override = sandbox.sandbox_override;
|
||||
let policy = request.permission_profile.file_system_sandbox_policy();
|
||||
tracing::Span::current().record("permission_entries", policy.entries.len());
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
@@ -535,6 +533,32 @@ mod tests {
|
||||
assert_eq!(request.env.get(&path_key), Some(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_command_rejects_non_native_policy_cwd() {
|
||||
#[cfg(unix)]
|
||||
let foreign_uri = "file://server/share/checkout";
|
||||
#[cfg(windows)]
|
||||
let foreign_uri = "file:///usr/local/checkout";
|
||||
let cwd = PathUri::parse(foreign_uri).expect("valid foreign URI");
|
||||
let sandbox = sandbox_context_with_cwd(&FileSystemSandboxPolicy::read_only(), cwd.clone());
|
||||
let paths = ExecServerRuntimeOptions::new(
|
||||
std::env::current_exe().expect("current executable"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
|
||||
let error = FileSystemSandboxRunner::new(paths)
|
||||
.sandbox_command(&sandbox)
|
||||
.expect_err("non-native policy cwd should be rejected");
|
||||
assert_eq!(
|
||||
error,
|
||||
crate::rpc::invalid_request(format!(
|
||||
"'{cwd}' is invalid on '{}'",
|
||||
std::env::consts::OS
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/// Removing the selected directory must not change permission anchoring or prevent launch.
|
||||
#[test]
|
||||
fn sandbox_exec_request_uses_filesystem_root_and_preserves_policy_cwd() {
|
||||
@@ -655,45 +679,113 @@ mod tests {
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn helper_permissions_include_only_the_helper_executable() {
|
||||
let codex_self_exe = std::env::current_exe().expect("current exe");
|
||||
let codex_self_exe = codex_self_exe.canonicalize().expect("canonical helper");
|
||||
let runtime_paths =
|
||||
ExecServerRuntimeOptions::new(codex_self_exe, /*codex_linux_sandbox_exe*/ None)
|
||||
.expect("runtime paths");
|
||||
// :root covers the cwd's drive. The helper needs an exact read on its own drive.
|
||||
let drive = if runtime_paths.codex_self_exe.as_path().starts_with(r"C:\") {
|
||||
r"D:\"
|
||||
} else {
|
||||
r"C:\"
|
||||
};
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(drive).expect("absolute cwd");
|
||||
let parent = runtime_paths
|
||||
.codex_self_exe
|
||||
.parent()
|
||||
.expect("current exe parent");
|
||||
let sibling = parent.join("credentials.json");
|
||||
fn windows_helper_binds_executable_and_workspace_grants() {
|
||||
const CHILD: &str = "CODEX_TEST_WINDOWS_HELPER_GRANTS_CHILD";
|
||||
if std::env::var_os(CHILD).is_none() {
|
||||
// Preparing the token wrapper persists capability SIDs under CODEX_HOME.
|
||||
let home = tempfile::tempdir().expect("isolated Codex home");
|
||||
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
|
||||
.args([
|
||||
"--exact",
|
||||
"fs_sandbox::tests::windows_helper_binds_executable_and_workspace_grants",
|
||||
])
|
||||
.env(CHILD, "1")
|
||||
.env("CODEX_HOME", home.path())
|
||||
.output()
|
||||
.expect("run isolated Windows helper test");
|
||||
assert!(
|
||||
output.status.success()
|
||||
&& String::from_utf8_lossy(&output.stdout).contains("1 passed"),
|
||||
"isolated helper test: {output:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let context = crate::FileSystemSandboxContext {
|
||||
windows_sandbox_selection: codex_file_system::WindowsSandboxSelection::RestrictedToken,
|
||||
..sandbox_context_with_cwd(
|
||||
&FileSystemSandboxPolicy::read_only(),
|
||||
PathUri::from_abs_path(&cwd),
|
||||
)
|
||||
let workspace = tempfile::tempdir().expect("workspace");
|
||||
let other = tempfile::tempdir().expect("other workspace");
|
||||
let roots = [workspace.path(), other.path()].map(|path| {
|
||||
AbsolutePathBuf::from_absolute_path(path.canonicalize().expect("canonical workspace"))
|
||||
.expect("absolute workspace")
|
||||
});
|
||||
let drive = roots[0].ancestors().last().expect("workspace drive");
|
||||
// An unused drive also avoids resolving a mapped drive back into the workspace.
|
||||
let helper = ('D'..='Z')
|
||||
.rev()
|
||||
.map(|letter| std::path::PathBuf::from(format!(r"{letter}:\")))
|
||||
.find(|root| !root.exists())
|
||||
.expect("unused drive for the helper policy")
|
||||
.join(r"codex-test-runtime\helper.exe");
|
||||
let helper = AbsolutePathBuf::from_absolute_path(helper).expect("helper");
|
||||
let helper_dir = helper.parent().expect("helper directory");
|
||||
let runtime_paths = ExecServerRuntimeOptions::new(
|
||||
helper.to_path_buf(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
let runner = FileSystemSandboxRunner {
|
||||
runtime_paths,
|
||||
helper_env: HashMap::new(),
|
||||
};
|
||||
let request = FileSystemSandboxRunner::new(runtime_paths.clone())
|
||||
.sandbox_command(&context)
|
||||
.expect("sandbox command");
|
||||
let policy = request.permission_profile.file_system_sandbox_policy();
|
||||
|
||||
assert!(
|
||||
policy.can_read_local_path_with_cwd(
|
||||
runtime_paths.codex_self_exe.as_path(),
|
||||
cwd.as_path(),
|
||||
)
|
||||
);
|
||||
assert!(!policy.can_read_local_path_with_cwd(parent.as_path(), cwd.as_path()));
|
||||
assert!(!policy.can_read_local_path_with_cwd(sibling.as_path(), cwd.as_path()));
|
||||
let policy = restricted_policy(vec![
|
||||
special_entry(FileSystemSpecialPath::Root, FileSystemAccessMode::Read),
|
||||
special_entry(
|
||||
FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
FileSystemAccessMode::Write,
|
||||
),
|
||||
]);
|
||||
for (grants, policy_cwd, wrapper_roots, writable) in [
|
||||
(&roots[..], &roots[0], &roots[..], [true, true]),
|
||||
// The wrapper encodes the command's drive root when no write roots were granted.
|
||||
(
|
||||
&roots[..0],
|
||||
&roots[0],
|
||||
std::slice::from_ref(&drive),
|
||||
[false, false],
|
||||
),
|
||||
(&roots[1..], &roots[1], &roots[1..], [false, true]),
|
||||
] {
|
||||
let sandbox = crate::FileSystemSandboxContext {
|
||||
workspace_roots: grants.iter().map(PathUri::from_abs_path).collect(),
|
||||
windows_sandbox_selection:
|
||||
codex_file_system::WindowsSandboxSelection::RestrictedToken,
|
||||
..sandbox_context_with_cwd(&policy, PathUri::from_abs_path(policy_cwd))
|
||||
};
|
||||
let request = runner
|
||||
.sandbox_command(&sandbox)
|
||||
.expect("prepare Windows helper");
|
||||
let args = &request.command;
|
||||
assert_eq!(
|
||||
args.windows(2)
|
||||
.filter(|pair| pair[0] == "--workspace-root")
|
||||
.map(|pair| std::path::Path::new(&pair[1]))
|
||||
.collect::<Vec<_>>(),
|
||||
wrapper_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::as_path)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let profile = args
|
||||
.windows(2)
|
||||
.find(|pair| pair[0] == "--permission-profile")
|
||||
.expect("serialized helper policy");
|
||||
let profile: PermissionProfile =
|
||||
serde_json::from_str(&profile[1]).expect("helper policy");
|
||||
let fs = profile.file_system_sandbox_policy();
|
||||
assert_eq!(
|
||||
roots
|
||||
.each_ref()
|
||||
.map(|root| fs.can_write_local_path_with_cwd(root, policy_cwd)),
|
||||
writable
|
||||
);
|
||||
assert_eq!(
|
||||
fs.resolve_access_for_local_path_with_cwd(&helper, policy_cwd),
|
||||
FileSystemAccessMode::Read
|
||||
);
|
||||
assert!(!fs.can_read_local_path_with_cwd(&helper_dir, policy_cwd));
|
||||
assert!(
|
||||
!fs.can_read_local_path_with_cwd(&helper_dir.join("credentials.json"), policy_cwd)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -1225,14 +1225,19 @@ async fn sandboxed_file_operations_cannot_read_or_write_helper_siblings() -> Res
|
||||
std::os::unix::fs::symlink(&sibling, &escaping_link)?;
|
||||
let sandbox = workspace_write_sandbox(workspace.clone());
|
||||
let allowed_file = workspace.join("allowed.txt");
|
||||
std::fs::write(&allowed_file, b"allowed")?;
|
||||
let allowed_contents = file_system
|
||||
.read_file(
|
||||
&PathUri::from_host_native_path(&allowed_file)?,
|
||||
let allowed_uri = PathUri::from_host_native_path(&allowed_file)?;
|
||||
file_system
|
||||
.write_file(
|
||||
&allowed_uri,
|
||||
b"allowed".to_vec(),
|
||||
Default::default(),
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(std::fs::read(&allowed_file)?, b"allowed");
|
||||
let allowed_contents = file_system
|
||||
.read_file(&allowed_uri, Default::default(), Some(&sandbox))
|
||||
.await?;
|
||||
assert_eq!(allowed_contents, b"allowed");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -113,7 +113,10 @@ impl PreparedLaunchPolicy {
|
||||
});
|
||||
};
|
||||
let cwd = native_path(policy_cwd, "sandbox policy cwd")?;
|
||||
let workspace_roots = native_workspace_roots(roots)?;
|
||||
let workspace_roots = roots
|
||||
.iter()
|
||||
.map(|root| native_path(root, "sandbox workspace root"))
|
||||
.collect::<io::Result<Vec<_>>>()?;
|
||||
let (file_system, network) = permissions.to_runtime_permissions();
|
||||
for entry in &file_system.entries {
|
||||
if let FileSystemPath::Path { path } = &entry.path {
|
||||
@@ -123,6 +126,7 @@ impl PreparedLaunchPolicy {
|
||||
let mut file_system =
|
||||
file_system.materialize_project_roots_with_workspace_roots(&workspace_roots);
|
||||
let permissions = if is_file_system_helper {
|
||||
tracing::Span::current().record("permission_entries", file_system.entries.len());
|
||||
if !file_system.has_full_disk_read_access() {
|
||||
let minimal = FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
@@ -170,13 +174,6 @@ impl PreparedLaunchPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn native_workspace_roots(roots: &[PathUri]) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||
roots
|
||||
.iter()
|
||||
.map(|root| native_path(root, "sandbox workspace root"))
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
}
|
||||
|
||||
fn native_path(path: &PathUri, field: &str) -> io::Result<AbsolutePathBuf> {
|
||||
path.to_abs_path()
|
||||
.map_err(|err| io::Error::new(err.kind(), format!("invalid {field}: {err}")))
|
||||
|
||||
@@ -31,7 +31,6 @@ pub use denial::is_likely_executor_managed_sandbox_denied;
|
||||
pub use denial::is_likely_sandbox_denied;
|
||||
pub use linux_pid_namespace::LinuxSandboxPidNamespace;
|
||||
pub use manager::SandboxCommand;
|
||||
pub use manager::SandboxDirectSpawnTransformRequest;
|
||||
pub use manager::SandboxExecRequest;
|
||||
pub use manager::SandboxManager;
|
||||
pub use manager::SandboxSelection;
|
||||
|
||||
@@ -149,17 +149,6 @@ pub struct SandboxTransformRequest<'a> {
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
}
|
||||
|
||||
/// 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],
|
||||
pub windows_sandbox_proxy_settings_mode: codex_windows_sandbox::WindowsSandboxProxySettingsMode,
|
||||
}
|
||||
|
||||
// TODO(anp): Revisit this preparation type once this module's PathUri migration is complete.
|
||||
struct PendingSandboxedExecRequest {
|
||||
native_command_cwd: AbsolutePathBuf,
|
||||
@@ -624,15 +613,22 @@ impl SandboxManager {
|
||||
}
|
||||
SandboxType::WindowsRestrictedToken => {
|
||||
exec.permission_profile = pending?.effective_permission_profile;
|
||||
#[cfg(windows)]
|
||||
if is_file_system_helper {
|
||||
wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
&mut exec,
|
||||
&policy.workspace_roots,
|
||||
/*codex_home*/ None,
|
||||
request.sandbox_exe,
|
||||
codex_windows_sandbox::WindowsSandboxProxySettingsMode::Preserve,
|
||||
)?;
|
||||
#[cfg(not(windows))]
|
||||
return Err(SandboxTransformError::SandboxUnavailable);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let codex_home =
|
||||
codex_utils_home_dir::find_codex_home().map_err(|err| {
|
||||
SandboxTransformError::WindowsSandboxPreparation(err.to_string())
|
||||
})?;
|
||||
wrap_windows_file_system_helper(
|
||||
&mut exec,
|
||||
&policy.workspace_roots,
|
||||
&codex_home,
|
||||
request.sandbox_exe,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -679,67 +675,15 @@ impl SandboxManager {
|
||||
exec.arg0 = Some(arg0.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transform_for_direct_spawn(
|
||||
&self,
|
||||
request: SandboxDirectSpawnTransformRequest<'_>,
|
||||
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.transform_for_direct_spawn_with_codex_home(request, /*codex_home*/ None)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
self.transform(request.transform)
|
||||
.map(WrappedSandboxCommand::into_command_request)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn transform_for_direct_spawn_with_codex_home(
|
||||
&self,
|
||||
request: SandboxDirectSpawnTransformRequest<'_>,
|
||||
codex_home: Option<&Path>,
|
||||
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
||||
let roots = request.transform.workspace_roots;
|
||||
let fallback_roots = request.workspace_roots;
|
||||
let proxy_settings_mode = request.windows_sandbox_proxy_settings_mode;
|
||||
let sandbox_exe = request.transform.sandbox_exe;
|
||||
let mut request = self.transform(request.transform)?.into_command_request();
|
||||
if request.sandbox != SandboxType::WindowsRestrictedToken {
|
||||
return Ok(request);
|
||||
}
|
||||
let workspace_roots = roots
|
||||
.map(launch::native_workspace_roots)
|
||||
.transpose()
|
||||
.map_err(SandboxTransformError::InvalidPolicy)?;
|
||||
wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
&mut request,
|
||||
workspace_roots.as_deref().unwrap_or(fallback_roots),
|
||||
codex_home,
|
||||
sandbox_exe,
|
||||
proxy_settings_mode,
|
||||
)?;
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
fn wrap_windows_file_system_helper(
|
||||
request: &mut SandboxExecRequest,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
codex_home: Option<&Path>,
|
||||
codex_home: &Path,
|
||||
sandbox_exe: Option<&Path>,
|
||||
proxy_settings_mode: codex_windows_sandbox::WindowsSandboxProxySettingsMode,
|
||||
) -> Result<(), SandboxTransformError> {
|
||||
let owned_codex_home;
|
||||
let codex_home = if let Some(codex_home) = codex_home {
|
||||
codex_home
|
||||
} else {
|
||||
owned_codex_home = codex_utils_home_dir::find_codex_home()
|
||||
.map_err(|err| SandboxTransformError::WindowsSandboxPreparation(err.to_string()))?;
|
||||
owned_codex_home.as_path()
|
||||
};
|
||||
let sandbox_exe = sandbox_exe.ok_or_else(|| {
|
||||
SandboxTransformError::WindowsSandboxPreparation(
|
||||
"missing Codex executable path".to_string(),
|
||||
@@ -821,7 +765,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
request.windows_sandbox_level,
|
||||
proxy_enforced,
|
||||
network_proxy_restricting_sid.as_deref(),
|
||||
proxy_settings_mode,
|
||||
codex_windows_sandbox::WindowsSandboxProxySettingsMode::Preserve,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
write_roots_override,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use super::SandboxCommand;
|
||||
#[cfg(windows)]
|
||||
use super::SandboxDirectSpawnTransformRequest;
|
||||
use super::SandboxManager;
|
||||
use super::SandboxSelection;
|
||||
use super::SandboxTransformRequest;
|
||||
@@ -394,35 +392,58 @@ fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn sandbox_transform_request(cwd: &PathUri, sandbox: SandboxType) -> SandboxTransformRequest<'_> {
|
||||
SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &PermissionProfile::Disabled,
|
||||
workspace_roots: None,
|
||||
runtime_readable_paths: &[],
|
||||
selection: SandboxSelection::Explicit(sandbox),
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd,
|
||||
sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn windows_token_filesystem_helper_is_unavailable_on_unix() {
|
||||
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("current dir"));
|
||||
let request = SandboxTransformRequest {
|
||||
permissions: &PermissionProfile::read_only(),
|
||||
workspace_roots: Some(std::slice::from_ref(&cwd)),
|
||||
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
|
||||
..sandbox_transform_request(&cwd, SandboxType::WindowsRestrictedToken)
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
SandboxManager::for_file_system_helpers().transform(request),
|
||||
Err(super::SandboxTransformError::SandboxUnavailable)
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn transform_linux_seccomp_request(
|
||||
codex_linux_sandbox_exe: &std::path::Path,
|
||||
manager: SandboxManager,
|
||||
) -> super::WrappedSandboxCommand {
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let permissions = PermissionProfile::Disabled;
|
||||
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("current dir"));
|
||||
manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd_uri.clone(),
|
||||
env: HashMap::new(),
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
workspace_roots: None,
|
||||
runtime_readable_paths: &[],
|
||||
selection: SandboxSelection::Explicit(SandboxType::LinuxSeccomp),
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
sandbox_exe: Some(codex_linux_sandbox_exe),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
..sandbox_transform_request(&cwd, SandboxType::LinuxSeccomp)
|
||||
})
|
||||
.expect("transform")
|
||||
}
|
||||
@@ -700,7 +721,7 @@ async fn linux_unix_socket_grant_uses_effective_managed_policy() -> anyhow::Resu
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn transform_for_direct_spawn_windows_preserves_only_wrapper_setup_environment() {
|
||||
fn windows_wrapper_preserves_only_setup_environment() {
|
||||
let mut env = HashMap::from([
|
||||
("Path".to_string(), r"C:\Windows\System32".to_string()),
|
||||
("username".to_string(), "wrong-user".to_string()),
|
||||
@@ -760,7 +781,7 @@ fn wrapper_runtime_selection_uses_the_parent_not_environment_overrides() {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn transform_for_direct_spawn_windows_materializes_inner_helper() {
|
||||
fn windows_file_system_wrapper_materializes_inner_helper() {
|
||||
let codex_home = tempfile::TempDir::new().expect("codex home");
|
||||
let helper_dir = tempfile::TempDir::new().expect("helper dir");
|
||||
let configured_helper = helper_dir.path().join("configured-codex-helper.exe");
|
||||
@@ -796,50 +817,37 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() {
|
||||
]),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
let other_workspace = tempfile::TempDir::new().expect("other workspace");
|
||||
let other_workspace_root = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(other_workspace.path()).expect("canonical other workspace"),
|
||||
let policy = super::PreparedLaunchPolicy::resolve(
|
||||
permissions,
|
||||
&cwd_uri,
|
||||
Some(std::slice::from_ref(&cwd_uri)),
|
||||
&[],
|
||||
&super::SandboxOperation::FileSystemHelper,
|
||||
)
|
||||
.expect("absolute other workspace");
|
||||
let workspace_roots = [cwd, other_workspace_root];
|
||||
let manager = SandboxManager::new();
|
||||
let prepare = |roots, policy_cwd| {
|
||||
manager
|
||||
.transform_for_direct_spawn_with_codex_home(
|
||||
SandboxDirectSpawnTransformRequest {
|
||||
workspace_roots: workspace_roots.as_slice(),
|
||||
windows_sandbox_proxy_settings_mode:
|
||||
codex_windows_sandbox::WindowsSandboxProxySettingsMode::Preserve,
|
||||
transform: SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
program: configured_helper.as_os_str().to_owned(),
|
||||
args: vec!["--codex-run-as-fs-helper".to_string()],
|
||||
cwd: cwd_uri.clone(),
|
||||
env: HashMap::from([(
|
||||
"Path".to_string(),
|
||||
r"C:\Windows\System32".to_string(),
|
||||
)]),
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
workspace_roots: roots,
|
||||
runtime_readable_paths: &[],
|
||||
selection: SandboxSelection::Explicit(SandboxType::WindowsRestrictedToken),
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: policy_cwd,
|
||||
sandbox_exe: Some(configured_helper.as_path()),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
|
||||
},
|
||||
},
|
||||
Some(codex_home.path()),
|
||||
)
|
||||
.expect("transform for direct spawn")
|
||||
.expect("resolve helper policy");
|
||||
let mut exec_request = super::SandboxExecRequest {
|
||||
sandbox_override: super::SandboxOverride::NoOverride,
|
||||
command: vec![
|
||||
configured_helper.display().to_string(),
|
||||
"--codex-run-as-fs-helper".to_string(),
|
||||
],
|
||||
cwd: cwd_uri.clone(),
|
||||
sandbox_policy_cwd: cwd_uri,
|
||||
env: HashMap::from([("Path".to_string(), r"C:\Windows\System32".to_string())]),
|
||||
network: None,
|
||||
network_environment_id: None,
|
||||
sandbox: SandboxType::WindowsRestrictedToken,
|
||||
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
|
||||
permission_profile: policy.permissions,
|
||||
arg0: None,
|
||||
};
|
||||
let exec_request = prepare(/*roots*/ None, &cwd_uri);
|
||||
super::wrap_windows_file_system_helper(
|
||||
&mut exec_request,
|
||||
&policy.workspace_roots,
|
||||
codex_home.path(),
|
||||
Some(&configured_helper),
|
||||
)
|
||||
.expect("wrap filesystem helper");
|
||||
|
||||
let inner_env_json = exec_request
|
||||
.command
|
||||
@@ -885,54 +893,6 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() {
|
||||
exec_request.command[separator_index + 2],
|
||||
"--codex-run-as-fs-helper"
|
||||
);
|
||||
let explicit_root = PathUri::from_abs_path(&workspace_roots[1]);
|
||||
// The wrapper encodes the cwd for an empty root list. Its serialized policy must
|
||||
// already have removed the workspace write grant.
|
||||
for (request, expected_roots, expected_writes) in [
|
||||
(&exec_request, &workspace_roots[..], None),
|
||||
(
|
||||
&prepare(Some(&[]), &cwd_uri),
|
||||
&workspace_roots[..1],
|
||||
Some([false, false]),
|
||||
),
|
||||
(
|
||||
// The legacy workspace-write projection always includes its policy cwd.
|
||||
// Anchor it at the grant and check the command cwd independently.
|
||||
&prepare(Some(std::slice::from_ref(&explicit_root)), &explicit_root),
|
||||
&workspace_roots[1..],
|
||||
Some([false, true]),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
request
|
||||
.command
|
||||
.windows(2)
|
||||
.filter_map(|args| {
|
||||
(args[0] == "--workspace-root").then_some(std::path::PathBuf::from(&args[1]))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
expected_roots
|
||||
.iter()
|
||||
.map(|root| root.as_path().to_path_buf())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
if let Some(expected_writes) = expected_writes {
|
||||
let profile_json = request
|
||||
.command
|
||||
.windows(2)
|
||||
.find(|args| args[0] == "--permission-profile")
|
||||
.expect("serialized wrapper policy");
|
||||
let policy = serde_json::from_str::<PermissionProfile>(&profile_json[1])
|
||||
.expect("decode wrapper policy")
|
||||
.file_system_sandbox_policy();
|
||||
assert_eq!(
|
||||
workspace_roots
|
||||
.each_ref()
|
||||
.map(|root| policy.can_write_local_path_with_cwd(root, &workspace_roots[0])),
|
||||
expected_writes
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
materialized_helper
|
||||
.parent()
|
||||
|
||||
Reference in New Issue
Block a user