mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Allow trusted symlinks beneath CODEX_HOME on macOS (#42716)
## What changed - Add the macOS-only `allow_symlinked_codex_home` user setting. When enabled in the execution host's `$CODEX_HOME/config.toml`, writable roots at or beneath that home may traverse symlinks. - Keep the exception disabled by default and prevent project config, command-line overrides, and ignored user config from enabling it. Other writable roots remain subject to symlink checks. - Propagate the resolved setting through local, interactive, and exec-server sandbox paths, including `CODEX_HOME` aliases. - Explain the opt-out in symlink rejection errors, including that it trusts targets outside `CODEX_HOME` and targets that change between commands. ## Testing - Add macOS coverage for shell commands, patches, filesystem helpers, process execution, and interactive startup with enabled, disabled, aliased, and out-of-scope homes. GitOrigin-RevId: 99fcdf611200c9e1b7713cf06f7fdea5bfa7f089
This commit is contained in:
@@ -129,6 +129,10 @@ impl FileSystemSandboxRunner {
|
||||
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
||||
let helper = &self.runtime_paths.codex_self_exe;
|
||||
let sandbox_manager = SandboxManager::for_file_system_helpers();
|
||||
#[cfg(target_os = "macos")]
|
||||
let sandbox_manager = sandbox_manager.with_allowed_symlinked_codex_home(
|
||||
self.runtime_paths.allowed_symlinked_codex_home.clone(),
|
||||
);
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
permission_profile,
|
||||
SandboxablePreference::Require,
|
||||
|
||||
@@ -185,6 +185,9 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
|
||||
network_policy,
|
||||
);
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
let sandbox_manager = sandbox_manager
|
||||
.with_allowed_symlinked_codex_home(runtime_paths.allowed_symlinked_codex_home.clone());
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
&permissions,
|
||||
SandboxablePreference::Require,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
/// Runtime paths needed by exec-server child processes.
|
||||
/// Paths and sandbox settings initialized when creating an executor.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExecServerRuntimePaths {
|
||||
/// Stable path to the Codex executable used to launch hidden helper modes.
|
||||
@@ -10,6 +10,9 @@ pub struct ExecServerRuntimePaths {
|
||||
/// Path to the Linux sandbox helper alias used when the platform sandbox
|
||||
/// needs to re-enter Codex by argv0.
|
||||
pub codex_linux_sandbox_exe: Option<AbsolutePathBuf>,
|
||||
/// User-config opt-out of writable-root symlink checks beneath this host's home.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub allowed_symlinked_codex_home: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
impl ExecServerRuntimePaths {
|
||||
@@ -33,8 +36,20 @@ impl ExecServerRuntimePaths {
|
||||
Ok(Self {
|
||||
codex_self_exe: absolute_path(codex_self_exe)?,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.map(absolute_path).transpose()?,
|
||||
#[cfg(target_os = "macos")]
|
||||
allowed_symlinked_codex_home: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies the symlink opt-in resolved by the execution host's config loader.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn with_allowed_symlinked_codex_home(
|
||||
mut self,
|
||||
allowed_symlinked_codex_home: Option<AbsolutePathBuf>,
|
||||
) -> Self {
|
||||
self.allowed_symlinked_codex_home = allowed_symlinked_codex_home;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn absolute_path(path: PathBuf) -> std::io::Result<AbsolutePathBuf> {
|
||||
|
||||
@@ -102,7 +102,13 @@ impl ExecServerHarness {
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::inherit());
|
||||
command.kill_on_drop(true);
|
||||
command.env("CODEX_HOME", codex_home.path());
|
||||
if !command
|
||||
.as_std()
|
||||
.get_envs()
|
||||
.any(|(key, value)| key == "CODEX_HOME" && value.is_some())
|
||||
{
|
||||
command.env("CODEX_HOME", codex_home.path());
|
||||
}
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let websocket_url = read_listen_url_from_stdout(&mut child).await?;
|
||||
|
||||
@@ -222,13 +222,32 @@ fn maybe_run_exec_server_from_test_binary(guard: Option<&TestBinaryDispatchGuard
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let exit_code = match runtime.block_on(codex_exec_server::run_main_with_telemetry(
|
||||
&listen_url,
|
||||
runtime_paths,
|
||||
ExecServerTelemetry::default(),
|
||||
http_client_factory,
|
||||
request_dispatch_mode,
|
||||
)) {
|
||||
let exit_code = match runtime.block_on(async {
|
||||
#[cfg(target_os = "macos")]
|
||||
let runtime_paths = {
|
||||
let home = codex_utils_home_dir::find_codex_home()?;
|
||||
let config = codex_config::loader::load_config_layers_state(
|
||||
&codex_exec_server::LocalFileSystem::unsandboxed(),
|
||||
home.as_path(),
|
||||
/*cwd*/ None,
|
||||
&[],
|
||||
codex_config::LoaderOverrides::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
runtime_paths.with_allowed_symlinked_codex_home(
|
||||
codex_config::allowed_symlinked_codex_home(&config, &home),
|
||||
)
|
||||
};
|
||||
codex_exec_server::run_main_with_telemetry(
|
||||
&listen_url,
|
||||
runtime_paths,
|
||||
ExecServerTelemetry::default(),
|
||||
http_client_factory,
|
||||
request_dispatch_mode,
|
||||
)
|
||||
.await
|
||||
}) {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("exec-server failed: {error}");
|
||||
|
||||
@@ -102,6 +102,78 @@ async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn codex_home_symlink_opt_out_respects_host_config_and_scope() -> Result<()> {
|
||||
use codex_exec_server::WriteFileOptions;
|
||||
use common::exec_server::exec_server_with_env;
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let workspace = TempDir::new()?;
|
||||
let home = TempDir::new()?;
|
||||
let target = TempDir::new()?;
|
||||
let alias = home.path().join("visualizations");
|
||||
let other_alias = workspace.path().join(".codex/visualizations");
|
||||
std::fs::create_dir(workspace.path().join(".codex"))?;
|
||||
symlink(target.path(), &alias)?;
|
||||
symlink(target.path(), &other_alias)?;
|
||||
std::fs::write(
|
||||
workspace.path().join(".codex/config.toml"),
|
||||
"allow_symlinked_codex_home = true\n",
|
||||
)?;
|
||||
|
||||
for enabled in [None, Some(false), Some(true)] {
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
enabled.map_or_else(String::new, |enabled| {
|
||||
format!("allow_symlinked_codex_home = {enabled}\n")
|
||||
}),
|
||||
)?;
|
||||
let mut server = exec_server_with_env([("CODEX_HOME", home.path())], &[]).await?;
|
||||
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
|
||||
for root in [alias.as_path(), other_alias.as_path(), workspace.path()] {
|
||||
let mut policy = FileSystemSandboxPolicy::read_only();
|
||||
policy.entries.push(FileSystemSandboxEntry::new(
|
||||
PathUri::from_host_native_path(root)?.into(),
|
||||
FileSystemAccessMode::Write,
|
||||
));
|
||||
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
|
||||
PermissionProfile::from_runtime_permissions(
|
||||
&policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
),
|
||||
PathUri::from_host_native_path(workspace.path())?,
|
||||
);
|
||||
let result = environment
|
||||
.get_filesystem()
|
||||
.write_file(
|
||||
&PathUri::from_host_native_path(root.join("output"))?,
|
||||
b"written".to_vec(),
|
||||
WriteFileOptions::default(),
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
root == workspace.path() || (enabled == Some(true) && root == alias),
|
||||
"root={root:?}, enabled={enabled:?}: {result:?}"
|
||||
);
|
||||
if let Err(error) = result {
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("symlinked writable roots are not supported"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
server.shutdown().await?;
|
||||
}
|
||||
assert_eq!(std::fs::read(target.path().join("output"))?, b"written");
|
||||
assert_eq!(std::fs::read(workspace.path().join("output"))?, b"written");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test_case(false, false, false, false, "bash"; "local_pipe")]
|
||||
#[test_case(false, true, false, false, "bash"; "local_tty")]
|
||||
|
||||
Reference in New Issue
Block a user