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:
pakrym-oai
2026-07-09 18:28:55 -07:00
committed by GitHub
parent 2b44896c5a
commit 1f0566d3f5
2 changed files with 160 additions and 0 deletions
@@ -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,
+144
View File
@@ -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>,