mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Defer exec-server sandbox preparation to the execution host (#60663)
## Why Remote commands can use workspace paths from a different operating system. Preparing them on the client should preserve those paths and command arguments for the execution host. ## What changed - Build exec-server requests directly, carrying the native command and sandbox context without applying the client's sandbox transform. - Resolve workspace permissions and runtime read access in `SandboxManager` on the execution host, including helper binaries and validated managed-network CA bundles. - Add explicit backend selection and required platform sandbox selection, rejecting launches when the required sandbox is unavailable. ## Testing Extend request tests to cover foreign workspace paths, Unicode arguments, and empty arguments. Extend the remote process test on Unix to verify that restricted reads allow the sandbox helper and validated CA bundle while denying an untrusted certificate file. GitOrigin-RevId: 5fc02ac4208912ecefb80a212742389afc14ac05
This commit is contained in:
@@ -2024,7 +2024,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!(
|
||||
"exec-server rejected request (-32602): sandbox cwd URI `{sandbox_cwd}` is not valid on this exec-server host: {source}"
|
||||
"exec-server rejected request (-32602): failed to prepare process sandbox: invalid sandbox policy cwd: {source}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server_protocol::JSONRPCErrorError;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
@@ -16,7 +15,6 @@ use codex_sandboxing::SandboxDirectSpawnTransformRequest;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
@@ -157,17 +155,8 @@ impl FileSystemSandboxRunner {
|
||||
let sandbox_manager = sandbox_manager.with_allowed_symlinked_codex_home(
|
||||
self.runtime_paths.allowed_symlinked_codex_home.clone(),
|
||||
);
|
||||
let (sandbox, windows_sandbox_level) = crate::sandbox_selection::select_sandbox(
|
||||
&sandbox_manager,
|
||||
permission_profile,
|
||||
sandbox_context,
|
||||
/*has_managed_network_requirements*/ false,
|
||||
);
|
||||
if sandbox == SandboxType::None {
|
||||
return Err(invalid_request(
|
||||
"filesystem sandbox cannot be enforced on this executor".to_string(),
|
||||
));
|
||||
}
|
||||
let (selection, windows_sandbox_level) =
|
||||
crate::sandbox_selection::sandbox_selection(sandbox_context);
|
||||
// Requests use absolute paths; the helper can start at the filesystem root even if
|
||||
// the policy cwd was removed. Keep its drive or share for Windows `:root` rules.
|
||||
let helper_cwd = cwd
|
||||
@@ -192,7 +181,8 @@ impl FileSystemSandboxRunner {
|
||||
command,
|
||||
permissions: permission_profile,
|
||||
workspace_roots: None,
|
||||
sandbox,
|
||||
runtime_readable_paths: &[],
|
||||
selection,
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
@@ -203,11 +193,15 @@ impl FileSystemSandboxRunner {
|
||||
self.runtime_paths.codex_linux_sandbox_exe.as_deref()
|
||||
},
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: windows_sandbox_level
|
||||
.unwrap_or(WindowsSandboxLevel::Disabled),
|
||||
windows_sandbox_level,
|
||||
},
|
||||
})
|
||||
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))?;
|
||||
.map_err(|err| match err {
|
||||
codex_sandboxing::SandboxTransformError::SandboxUnavailable => invalid_request(
|
||||
"filesystem sandbox cannot be enforced on this executor".to_string(),
|
||||
),
|
||||
err => invalid_request(format!("failed to prepare fs sandbox: {err}")),
|
||||
})?;
|
||||
request.sandbox_override = sandbox_context.sandbox_override;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
|
||||
use codex_sandboxing::WindowsSandboxProxySettingsMode;
|
||||
use codex_sandboxing::WindowsSandboxSpawnRequest;
|
||||
use codex_sandboxing::with_managed_mitm_ca_readable_root;
|
||||
#[cfg(windows)]
|
||||
use codex_shell_command::shell_detect::fallback_powershell_shell_for_windows_sandbox;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -38,7 +37,7 @@ use crate::process_telemetry::trace_process_id;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_params;
|
||||
use crate::sandbox_selection::select_sandbox;
|
||||
use crate::sandbox_selection::sandbox_selection;
|
||||
|
||||
const CREDENTIAL_MASKING_FAILURE_MODE: CredentialMaskingFailureMode =
|
||||
CredentialMaskingFailureMode::Warn;
|
||||
@@ -152,24 +151,16 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
|
||||
};
|
||||
let runtime_paths = runtime_paths
|
||||
.ok_or_else(|| invalid_params("sandbox runtime paths are not configured".to_string()))?;
|
||||
sandbox_context
|
||||
.validate_file_system_paths_for_current_host()
|
||||
.map_err(|err| invalid_params(err.to_string()))?;
|
||||
let windows_sandbox_proxy_settings_mode = sandbox_context
|
||||
.windows_sandbox_proxy_settings_mode
|
||||
.unwrap_or_default();
|
||||
// TODO(jif): Transport permissions before orchestrator-local paths are materialized,
|
||||
// then resolve executor-local helper and workspace paths here.
|
||||
let permissions = sandbox_context.permissions.clone();
|
||||
let sandbox_policy_cwd = &sandbox_context.cwd;
|
||||
let native_sandbox_policy_cwd = native_path(sandbox_policy_cwd, "sandbox cwd")?;
|
||||
let native_workspace_roots = sandbox_context
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(|root| native_path(root, "sandbox workspace root"))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let workspace_roots = native_workspace_roots.as_slice();
|
||||
let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
let mut runtime_readable_paths = Vec::new();
|
||||
if cfg!(target_os = "linux") || (cfg!(unix) && params.arg0.is_some()) {
|
||||
runtime_readable_paths.push(runtime_paths.codex_self_exe.clone());
|
||||
}
|
||||
if cfg!(target_os = "linux") {
|
||||
runtime_readable_paths.extend(runtime_paths.codex_linux_sandbox_exe.iter().cloned());
|
||||
}
|
||||
let managed_mitm_ca_trust_bundle_path = managed_network.as_ref().and_then(|_| {
|
||||
CUSTOM_CA_ENV_KEYS.iter().find_map(|key| {
|
||||
let path = env.get(*key)?;
|
||||
@@ -179,55 +170,13 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
|
||||
AbsolutePathBuf::from_absolute_path(path).ok()
|
||||
})
|
||||
});
|
||||
let permissions = with_managed_mitm_ca_readable_root(
|
||||
permissions,
|
||||
managed_mitm_ca_trust_bundle_path.as_ref(),
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
#[cfg(unix)]
|
||||
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 = {
|
||||
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(unix)]
|
||||
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permissions.enforcement(),
|
||||
&file_system_policy,
|
||||
network_policy,
|
||||
);
|
||||
runtime_readable_paths.extend(managed_mitm_ca_trust_bundle_path);
|
||||
let sandbox_manager = SandboxManager::new()
|
||||
.with_linux_sandbox_pid_namespace(runtime_paths.linux_sandbox_pid_namespace);
|
||||
#[cfg(target_os = "macos")]
|
||||
let sandbox_manager = sandbox_manager
|
||||
.with_allowed_symlinked_codex_home(runtime_paths.allowed_symlinked_codex_home.clone());
|
||||
let (sandbox, windows_sandbox_level) = select_sandbox(
|
||||
&sandbox_manager,
|
||||
&permissions,
|
||||
sandbox_context,
|
||||
params.enforce_managed_network,
|
||||
);
|
||||
if sandbox == SandboxType::None {
|
||||
return Err(invalid_params(
|
||||
"sandbox intent cannot be enforced on this executor".to_string(),
|
||||
));
|
||||
}
|
||||
let (selection, windows_sandbox_level) = sandbox_selection(sandbox_context);
|
||||
let (program, args) = params
|
||||
.argv
|
||||
.split_first()
|
||||
@@ -276,33 +225,43 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
|
||||
managed_network,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
workspace_roots: None,
|
||||
sandbox,
|
||||
permissions: &sandbox_context.permissions,
|
||||
workspace_roots: Some(&sandbox_context.workspace_roots),
|
||||
runtime_readable_paths: &runtime_readable_paths,
|
||||
selection,
|
||||
enforce_managed_network: params.enforce_managed_network,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd,
|
||||
sandbox_policy_cwd: &sandbox_context.cwd,
|
||||
sandbox_exe: if cfg!(windows) {
|
||||
Some(runtime_paths.codex_self_exe.as_path())
|
||||
} else {
|
||||
runtime_paths.codex_linux_sandbox_exe.as_deref()
|
||||
},
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: windows_sandbox_level.unwrap_or(WindowsSandboxLevel::Disabled),
|
||||
windows_sandbox_level,
|
||||
})
|
||||
.map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?
|
||||
.map_err(|err| match err {
|
||||
codex_sandboxing::SandboxTransformError::SandboxUnavailable => {
|
||||
invalid_params(err.to_string())
|
||||
}
|
||||
err => invalid_params(format!("failed to prepare process sandbox: {err}")),
|
||||
})?
|
||||
.into_command_request();
|
||||
request.sandbox_override = sandbox_context.sandbox_override;
|
||||
crate::run_integrity_checks(&request).await;
|
||||
let windows_sandbox = if sandbox == SandboxType::WindowsRestrictedToken {
|
||||
let windows_sandbox_level = windows_sandbox_level.ok_or_else(|| {
|
||||
invalid_params("restricted token sandbox requires a sandbox level".to_string())
|
||||
})?;
|
||||
let windows_sandbox = if request.sandbox == SandboxType::WindowsRestrictedToken {
|
||||
let permissions = request.permission_profile;
|
||||
let native_sandbox_policy_cwd = native_path(&request.sandbox_policy_cwd, "sandbox cwd")?;
|
||||
let native_workspace_roots = sandbox_context
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(|root| native_path(root, "sandbox workspace root"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
request.arg0 = params.arg0.clone();
|
||||
let proxy_enforced = params.enforce_managed_network;
|
||||
let filesystem_overrides = WindowsSandboxFilesystemOverrides::resolve(
|
||||
sandbox,
|
||||
request.sandbox,
|
||||
&permissions,
|
||||
&native_sandbox_policy_cwd,
|
||||
windows_sandbox_level,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
//! Resolves local sandbox implementations and checks support for an explicitly selected remote MXC.
|
||||
//! Translates Windows sandbox selection and validates explicit remote MXC requests.
|
||||
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_file_system::WindowsSandboxSelection;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxSelection;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
|
||||
pub(crate) fn validate_remote_mxc(info: &crate::EnvironmentInfo) -> std::io::Result<()> {
|
||||
if !info.capabilities.windows_mxc {
|
||||
@@ -18,29 +16,19 @@ pub(crate) fn validate_remote_mxc(info: &crate::EnvironmentInfo) -> std::io::Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn select_sandbox(
|
||||
manager: &SandboxManager,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_context: &FileSystemSandboxContext,
|
||||
has_managed_network_requirements: bool,
|
||||
) -> (SandboxType, Option<WindowsSandboxLevel>) {
|
||||
let windows_sandbox_level = match sandbox_context.windows_sandbox_selection {
|
||||
pub(crate) fn sandbox_selection(
|
||||
context: &FileSystemSandboxContext,
|
||||
) -> (SandboxSelection, WindowsSandboxLevel) {
|
||||
let level = match context.windows_sandbox_selection {
|
||||
WindowsSandboxSelection::Disabled => WindowsSandboxLevel::Disabled,
|
||||
WindowsSandboxSelection::RestrictedToken => WindowsSandboxLevel::RestrictedToken,
|
||||
WindowsSandboxSelection::Elevated => WindowsSandboxLevel::Elevated,
|
||||
WindowsSandboxSelection::Mxc => return (SandboxType::WindowsMxc, None),
|
||||
};
|
||||
let windows_sandbox_type = match windows_sandbox_level {
|
||||
WindowsSandboxLevel::Disabled => SandboxType::None,
|
||||
WindowsSandboxLevel::RestrictedToken | WindowsSandboxLevel::Elevated => {
|
||||
SandboxType::WindowsRestrictedToken
|
||||
WindowsSandboxSelection::Mxc => {
|
||||
return (
|
||||
SandboxSelection::Explicit(SandboxType::WindowsMxc),
|
||||
WindowsSandboxLevel::Disabled,
|
||||
);
|
||||
}
|
||||
};
|
||||
let sandbox_type = manager.select_initial(
|
||||
permission_profile,
|
||||
SandboxablePreference::Require,
|
||||
windows_sandbox_type,
|
||||
has_managed_network_requirements,
|
||||
);
|
||||
(sandbox_type, Some(windows_sandbox_level))
|
||||
(SandboxSelection::RequirePlatform, level)
|
||||
}
|
||||
|
||||
@@ -1272,9 +1272,9 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() -> Result<()> {
|
||||
async fn remote_process_can_read_its_helper_and_generated_ca_with_restricted_reads() -> Result<()> {
|
||||
if let Some(warning) = codex_sandboxing::system_bwrap_warning(
|
||||
&PermissionProfile::read_only(),
|
||||
&std::env::current_dir()?,
|
||||
@@ -1283,7 +1283,16 @@ async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() ->
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = create_process_context(/*use_remote*/ true).await?;
|
||||
let server = exec_server().await?;
|
||||
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
|
||||
let proxy_dir = server.codex_home().canonicalize()?.join("proxy");
|
||||
std::fs::create_dir_all(&proxy_dir)?;
|
||||
// The bundle name contains the SHA-256 of its contents.
|
||||
let bundle = proxy_dir
|
||||
.join("ca-bundle-ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.pem");
|
||||
std::fs::write(&bundle, b"abc")?;
|
||||
let untrusted = proxy_dir.join("untrusted.pem");
|
||||
std::fs::write(&untrusted, b"untrusted")?;
|
||||
let workspace = TempDir::new()?;
|
||||
let file = workspace.path().join("allowed.txt");
|
||||
std::fs::write(&file, b"allowed")?;
|
||||
@@ -1309,22 +1318,30 @@ async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() ->
|
||||
cwd.clone(),
|
||||
);
|
||||
|
||||
let session = context
|
||||
.backend
|
||||
let session = environment
|
||||
.get_exec_backend()
|
||||
.start(ExecParams {
|
||||
metadata: Default::default(),
|
||||
process_id: ProcessId::from("proc-restricted-helper"),
|
||||
argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()],
|
||||
argv: vec![
|
||||
"/bin/sh".to_owned(), "-c".to_owned(),
|
||||
r#"cat "$1" "$CURL_CA_BUNDLE" && ! cat "$SSL_CERT_FILE" 2>/dev/null"#.to_owned(),
|
||||
"sh".to_owned(), file.to_string_lossy().into_owned(),
|
||||
],
|
||||
cwd,
|
||||
shell_snapshot: None,
|
||||
env_policy: /*env_policy*/ None,
|
||||
env: HashMap::from([("PATH".to_string(), std::env::var("PATH")?)]),
|
||||
env: HashMap::from([
|
||||
("PATH".to_owned(), std::env::var("PATH")?),
|
||||
("CURL_CA_BUNDLE".to_owned(), bundle.to_string_lossy().into_owned()),
|
||||
("SSL_CERT_FILE".to_owned(), untrusted.to_string_lossy().into_owned()),
|
||||
]),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
managed_network: Some(codex_network_proxy::ManagedNetworkSandboxContext::default()),
|
||||
network_proxy: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1332,7 +1349,7 @@ async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() ->
|
||||
|
||||
assert_eq!(
|
||||
output,
|
||||
("allowed".to_string(), String::new(), Some(0), true)
|
||||
("allowedabc".to_string(), String::new(), Some(0), true)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user