mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
exec-server: expose process helper to outer sandbox (#31937)
## Why Sandboxed exec-server process requests can use a restricted filesystem profile that does not expose the exec-server binary. On Linux, the outer bubblewrap stage re-enters that binary with the `codex-linux-sandbox` argv0 to install seccomp, so hiding the binary prevents the requested process from starting. ## What changed - add the configured `codex_self_exe` to the process permission profile before constructing the outer platform sandbox - add a Linux exec-server integration test that starts a real remote process with restricted reads and verifies it can read an allowed workspace file ## Test plan - `just test -p codex-exec-server process_sandbox` - `just test -p codex-exec-server --test exec_process remote_process_keeps_sandbox_helper_visible_with_restricted_reads`
This commit is contained in:
@@ -77,6 +77,22 @@ pub(crate) fn prepare_exec_request(
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
|
||||
// Bubblewrap launches the configured helper, which may re-enter this executable to apply
|
||||
// seccomp, so the outer filesystem sandbox must expose both paths.
|
||||
#[cfg(target_os = "linux")]
|
||||
let sandbox_helper_paths = std::iter::once(&runtime_paths.codex_self_exe)
|
||||
.chain(runtime_paths.codex_linux_sandbox_exe.as_ref())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
#[cfg(target_os = "linux")]
|
||||
let file_system_policy = file_system_policy
|
||||
.with_additional_readable_roots(native_sandbox_policy_cwd.as_path(), &sandbox_helper_paths);
|
||||
#[cfg(target_os = "linux")]
|
||||
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permissions.enforcement(),
|
||||
&file_system_policy,
|
||||
network_policy,
|
||||
);
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
&file_system_policy,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod common;
|
||||
|
||||
use std::collections::HashMap;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -11,11 +13,27 @@ use codex_exec_server::ExecOutputStream;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecProcess;
|
||||
use codex_exec_server::ExecProcessEvent;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_exec_server::FileSystemSandboxContext;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_exec_server::ProcessSignal;
|
||||
use codex_exec_server::ReadResponse;
|
||||
use codex_exec_server::StartedExecProcess;
|
||||
use codex_exec_server::WriteStatus;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
@@ -96,6 +114,132 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() -> Result<()> {
|
||||
if let Some(warning) = codex_sandboxing::system_bwrap_warning(&PermissionProfile::read_only()) {
|
||||
eprintln!("skipping bwrap test: {warning}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = create_process_context(/*use_remote*/ true).await?;
|
||||
let workspace = TempDir::new()?;
|
||||
let file = workspace.path().join("allowed.txt");
|
||||
std::fs::write(&file, b"allowed")?;
|
||||
let cwd = PathUri::from_host_native_path(workspace.path())?;
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
]);
|
||||
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted),
|
||||
cwd.clone(),
|
||||
);
|
||||
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("proc-restricted-helper"),
|
||||
argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()],
|
||||
cwd,
|
||||
env_policy: None,
|
||||
env: HashMap::from([("PATH".to_string(), std::env::var("PATH")?)]),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
let output = collect_process_output_from_events(session.process).await?;
|
||||
|
||||
assert_eq!(
|
||||
output,
|
||||
("allowed".to_string(), String::new(), Some(0), true)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() -> Result<()> {
|
||||
if let Some(warning) = codex_sandboxing::system_bwrap_warning(&PermissionProfile::read_only()) {
|
||||
eprintln!("skipping bwrap test: {warning}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = create_process_context(/*use_remote*/ true).await?;
|
||||
let workspace = TempDir::new()?;
|
||||
let file = workspace.path().join("allowed.txt");
|
||||
std::fs::write(&file, b"allowed")?;
|
||||
let hostile_helper = workspace.path().join("codex-linux-sandbox");
|
||||
std::fs::write(&hostile_helper, b"#!/bin/sh\nprintf hostile")?;
|
||||
let mut permissions = std::fs::metadata(&hostile_helper)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
std::fs::set_permissions(&hostile_helper, permissions)?;
|
||||
let path = std::env::var_os("PATH").context("PATH is not set")?;
|
||||
let hostile_path = std::env::join_paths(
|
||||
std::iter::once(workspace.path().to_path_buf()).chain(std::env::split_paths(&path)),
|
||||
)?;
|
||||
let cwd = PathUri::from_host_native_path(workspace.path())?;
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
]);
|
||||
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted),
|
||||
cwd.clone(),
|
||||
);
|
||||
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("proc-hostile-helper-path"),
|
||||
argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()],
|
||||
cwd,
|
||||
env_policy: None,
|
||||
env: HashMap::from([(
|
||||
"PATH".to_string(),
|
||||
hostile_path.to_string_lossy().into_owned(),
|
||||
)]),
|
||||
tty: true,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
let output = collect_process_output_from_events(session.process).await?;
|
||||
|
||||
assert_eq!(
|
||||
output,
|
||||
("allowed".to_string(), String::new(), Some(0), true)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_process_until_change(
|
||||
session: Arc<dyn ExecProcess>,
|
||||
wake_rx: &mut watch::Receiver<u64>,
|
||||
|
||||
Reference in New Issue
Block a user