mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Preserve custom arg0 for sandboxed exec-server processes (#34497)
## Why Sandbox wrappers replaced the process launch command and did not carry an `ExecParams.arg0` override through to the inner process. ## What changed - Route sandboxed Unix launches with a custom `arg0` through a helper mode that re-execs the requested program with the override. - Expose the helper executable to the filesystem sandbox and dispatch its mode from Codex and exec-server test binaries. ## Testing Add coverage for the prepared sandbox command and an end-to-end remote process that verifies both the custom `arg0` and filesystem restrictions. GitOrigin-RevId: c9f8eef3906d184e670184c2eeed250d5895a9ca
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! This mirrors `//codex-rs/exec-server/testing:exec-server` so Cargo-backed
|
||||
//! app-server integration tests can receive `CARGO_BIN_EXE_exec-server`. It
|
||||
//! also handles the filesystem-helper argv mode because exec-server re-execs
|
||||
//! `codex_self_exe` for sandboxed filesystem requests.
|
||||
//! also handles the helper argv modes because exec-server re-execs
|
||||
//! `codex_self_exe` for sandboxed filesystem and process requests.
|
||||
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use std::ffi::OsStr;
|
||||
@@ -13,7 +13,12 @@ const CODEX_LINUX_SANDBOX_EXE_ENV_VAR: &str = "CODEX_TEST_LINUX_SANDBOX_EXE";
|
||||
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut args = std::env::args_os();
|
||||
let _ = args.next();
|
||||
if args.next().as_deref() == Some(OsStr::new(codex_exec_server::CODEX_FS_HELPER_ARG1)) {
|
||||
let argv1 = args.next();
|
||||
#[cfg(unix)]
|
||||
if argv1.as_deref() == Some(OsStr::new(codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1)) {
|
||||
codex_exec_server::run_arg0_exec_helper_main();
|
||||
}
|
||||
if argv1.as_deref() == Some(OsStr::new(codex_exec_server::CODEX_FS_HELPER_ARG1)) {
|
||||
codex_exec_server::run_fs_helper_main();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
#[cfg(unix)]
|
||||
use codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1;
|
||||
use codex_exec_server::CODEX_FS_HELPER_ARG1;
|
||||
use codex_install_context::InstallContext;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
@@ -98,6 +100,10 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
}
|
||||
|
||||
let argv1 = args.next().unwrap_or_default();
|
||||
#[cfg(unix)]
|
||||
if argv1 == CODEX_ARG0_EXEC_HELPER_ARG1 {
|
||||
codex_exec_server::run_arg0_exec_helper_main();
|
||||
}
|
||||
if argv1 == CODEX_FS_HELPER_ARG1 {
|
||||
codex_exec_server::run_fs_helper_main();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Aggregates all former standalone integration tests as modules.
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
#[cfg(unix)]
|
||||
use codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1;
|
||||
use codex_exec_server::CODEX_FS_HELPER_ARG1;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_test_binary_support::TestBinaryDispatchGuard;
|
||||
@@ -17,6 +19,10 @@ pub static CODEX_ALIASES_TEMP_DIR: Option<TestBinaryDispatchGuard> = {
|
||||
if argv1 == Some(CODEX_CORE_APPLY_PATCH_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if argv1 == Some(CODEX_ARG0_EXEC_HELPER_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#[cfg(unix)]
|
||||
use std::process::Command;
|
||||
|
||||
pub const CODEX_ARG0_EXEC_HELPER_ARG1: &str = "--codex-run-as-arg0-exec-helper";
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn main() -> ! {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
let mut args = std::env::args_os();
|
||||
let _program = args.next();
|
||||
let _helper_mode = args.next();
|
||||
let Some(arg0) = args.next() else {
|
||||
eprintln!("missing arg0 for exec helper");
|
||||
std::process::exit(1);
|
||||
};
|
||||
let Some(program) = args.next() else {
|
||||
eprintln!("missing program for exec helper");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let error = Command::new(&program).arg0(arg0).args(args).exec();
|
||||
eprintln!("failed to exec {program:?}: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn main() -> ! {
|
||||
eprintln!("arg0 exec helper is only supported on Unix");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod arg0_exec_helper;
|
||||
mod capability_discovery;
|
||||
mod capability_discovery_cache;
|
||||
mod client;
|
||||
@@ -35,6 +36,8 @@ mod websocket_pong_watchdog;
|
||||
|
||||
use codex_exec_server_protocol as protocol;
|
||||
|
||||
pub use arg0_exec_helper::CODEX_ARG0_EXEC_HELPER_ARG1;
|
||||
pub use arg0_exec_helper::main as run_arg0_exec_helper_main;
|
||||
pub use capability_discovery::CapabilityDiscoveryError;
|
||||
pub use capability_discovery::discover_capability_roots;
|
||||
pub use capability_discovery_cache::ExecutorCapabilityDiscoveryCache;
|
||||
|
||||
@@ -26,6 +26,8 @@ use codex_sandboxing::with_managed_mitm_ca_readable_root;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
#[cfg(unix)]
|
||||
use crate::CODEX_ARG0_EXEC_HELPER_ARG1;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::rpc::internal_error;
|
||||
@@ -121,17 +123,27 @@ pub(crate) async fn prepare_exec_request(
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
|
||||
#[cfg(unix)]
|
||||
let sandbox_helper_paths = params
|
||||
.arg0
|
||||
.iter()
|
||||
.map(|_| runtime_paths.codex_self_exe.clone())
|
||||
.collect::<Vec<_>>();
|
||||
// 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 sandbox_helper_paths = {
|
||||
let mut sandbox_helper_paths = sandbox_helper_paths;
|
||||
if !sandbox_helper_paths.contains(&runtime_paths.codex_self_exe) {
|
||||
sandbox_helper_paths.push(runtime_paths.codex_self_exe.clone());
|
||||
}
|
||||
sandbox_helper_paths.extend(runtime_paths.codex_linux_sandbox_exe.iter().cloned());
|
||||
sandbox_helper_paths
|
||||
};
|
||||
#[cfg(unix)]
|
||||
let file_system_policy = file_system_policy
|
||||
.with_additional_readable_roots(native_sandbox_policy_cwd.as_path(), &sandbox_helper_paths);
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(unix)]
|
||||
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permissions.enforcement(),
|
||||
&file_system_policy,
|
||||
@@ -154,16 +166,34 @@ pub(crate) async fn prepare_exec_request(
|
||||
.argv
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
|
||||
#[cfg(unix)]
|
||||
let (program, args) = params.arg0.as_ref().map_or_else(
|
||||
|| (program.into(), args.to_vec()),
|
||||
|arg0| {
|
||||
let mut helper_args = Vec::with_capacity(params.argv.len() + 2);
|
||||
helper_args.push(CODEX_ARG0_EXEC_HELPER_ARG1.to_string());
|
||||
helper_args.push(arg0.clone());
|
||||
helper_args.extend(params.argv.iter().cloned());
|
||||
(
|
||||
runtime_paths
|
||||
.codex_self_exe
|
||||
.as_path()
|
||||
.as_os_str()
|
||||
.to_owned(),
|
||||
helper_args,
|
||||
)
|
||||
},
|
||||
);
|
||||
#[cfg(not(unix))]
|
||||
let (program, args) = (program.into(), args.to_vec());
|
||||
let transform_request = SandboxDirectSpawnTransformRequest {
|
||||
workspace_roots,
|
||||
windows_sandbox_proxy_settings_mode:
|
||||
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||
transform: SandboxTransformRequest {
|
||||
// TODO(jif): Preserve params.arg0 for the inner command across the sandbox
|
||||
// wrapper, or reject sandboxed requests with a custom arg0.
|
||||
command: SandboxCommand {
|
||||
program: program.into(),
|
||||
args: args.to_vec(),
|
||||
program,
|
||||
args,
|
||||
cwd: params.cwd.clone(),
|
||||
env,
|
||||
managed_network,
|
||||
|
||||
@@ -11,6 +11,8 @@ use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
#[cfg(any(unix, windows))]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -19,6 +21,8 @@ use tokio::io::AsyncWriteExt;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::prepare_exec_request;
|
||||
#[cfg(unix)]
|
||||
use crate::CODEX_ARG0_EXEC_HELPER_ARG1;
|
||||
use crate::ExecParams;
|
||||
#[cfg(any(unix, windows))]
|
||||
use crate::ExecServerRuntimePaths;
|
||||
@@ -93,6 +97,61 @@ async fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_routes_custom_arg0_to_inner_helper() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
.expect("absolute cwd");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let self_exe = std::env::current_exe().expect("current executable");
|
||||
let runtime_paths =
|
||||
ExecServerRuntimePaths::new(self_exe.clone(), Some(self_exe)).expect("runtime paths");
|
||||
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
|
||||
PermissionProfile::workspace_write(),
|
||||
cwd_uri.clone(),
|
||||
);
|
||||
let params = ExecParams {
|
||||
process_id: ProcessId::from("process-custom-arg0"),
|
||||
argv: vec!["/bin/sh".to_string(), "-c".to_string(), "true".to_string()],
|
||||
cwd: cwd_uri,
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: Some("custom-arg0".to_string()),
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
network_proxy: None,
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
.await
|
||||
.expect("prepare sandboxed request");
|
||||
let helper_mode = prepared
|
||||
.command
|
||||
.iter()
|
||||
.position(|arg| arg == CODEX_ARG0_EXEC_HELPER_ARG1)
|
||||
.expect("sandboxed command should invoke arg0 helper");
|
||||
|
||||
assert_eq!(
|
||||
prepared.command[helper_mode..],
|
||||
[
|
||||
CODEX_ARG0_EXEC_HELPER_ARG1,
|
||||
"custom-arg0",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"true",
|
||||
]
|
||||
);
|
||||
#[cfg(target_os = "linux")]
|
||||
assert_eq!(prepared.arg0, Some(CODEX_LINUX_SANDBOX_ARG0.to_string()));
|
||||
#[cfg(target_os = "macos")]
|
||||
assert_eq!(prepared.arg0, None);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
//! Minimal exec-server fixture for Bazel-only integration tests.
|
||||
//!
|
||||
//! Linking only exec-server avoids depending on the full Codex CLI binary
|
||||
//! when a test only needs a WebSocket executor endpoint.
|
||||
//! when a test only needs a WebSocket executor endpoint. It handles the arg0
|
||||
//! helper mode because sandboxed process requests re-exec this binary.
|
||||
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
#[cfg(unix)]
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut args = std::env::args_os();
|
||||
let _ = args.next();
|
||||
if args.next().as_deref()
|
||||
== Some(OsStr::new(
|
||||
codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1,
|
||||
))
|
||||
{
|
||||
codex_exec_server::run_arg0_exec_helper_main();
|
||||
}
|
||||
}
|
||||
|
||||
let current_exe = std::env::current_exe()?;
|
||||
let runtime_paths =
|
||||
ExecServerRuntimePaths::new(current_exe, /*codex_linux_sandbox_exe*/ None)?;
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1;
|
||||
use codex_exec_server::CODEX_FS_HELPER_ARG1;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
@@ -25,6 +26,9 @@ const DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG: &str = "--codex-test-delayed-output-a
|
||||
#[ctor]
|
||||
pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
|
||||
let guard = configure_test_binary_dispatch("codex-exec-server-tests", |exe_name, argv1| {
|
||||
if argv1 == Some(CODEX_ARG0_EXEC_HELPER_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,75 @@ async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_sandboxed_process_preserves_custom_arg0() -> 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 outside_workspace = TempDir::new()?;
|
||||
let denied_file = outside_workspace.path().join("denied.txt");
|
||||
std::fs::write(&denied_file, b"denied")?;
|
||||
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-custom-arg0"),
|
||||
argv: vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"printf '%s' \"$0\"; if /bin/cat \"$CODEX_TEST_DENIED_FILE\" >/dev/null 2>&1; then exit 42; fi"
|
||||
.to_string(),
|
||||
],
|
||||
cwd,
|
||||
env_policy: None,
|
||||
env: HashMap::from([
|
||||
("PATH".to_string(), std::env::var("PATH")?),
|
||||
(
|
||||
"CODEX_TEST_DENIED_FILE".to_string(),
|
||||
denied_file.to_string_lossy().into_owned(),
|
||||
),
|
||||
]),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: Some("custom-arg0".to_string()),
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
network_proxy: None,
|
||||
})
|
||||
.await?;
|
||||
let output = collect_process_output_from_events(session.process).await?;
|
||||
|
||||
assert_eq!(
|
||||
output,
|
||||
("custom-arg0".to_string(), String::new(), Some(0), true)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let session = context
|
||||
|
||||
Reference in New Issue
Block a user