Codex Handbook
sandboxing/src/manager.rs 422 lines
#[cfg(target_os = "linux")]use crate::bwrap::WSL1_BWRAP_WARNING;#[cfg(target_os = "linux")]use crate::bwrap::is_wsl1;use crate::landlock::CODEX_LINUX_SANDBOX_ARG0;use crate::landlock::allow_network_for_proxy;use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;use crate::policy_transforms::effective_permission_profile;use crate::policy_transforms::should_require_platform_sandbox;use codex_network_proxy::NetworkProxy;use codex_protocol::config_types::WindowsSandboxLevel;use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::models::PermissionProfile;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::permissions::NetworkSandboxPolicy;use codex_protocol::protocol::SandboxPolicy;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;use std::collections::HashMap;use std::ffi::OsString;use std::io;use std::path::Path;#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum SandboxType {    None,    MacosSeatbelt,    LinuxSeccomp,    WindowsRestrictedToken,}impl SandboxType {    pub fn as_metric_tag(self) -> &'static str {        match self {            SandboxType::None => "none",            SandboxType::MacosSeatbelt => "seatbelt",            SandboxType::LinuxSeccomp => "seccomp",            SandboxType::WindowsRestrictedToken => "windows_sandbox",        }    }}#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum SandboxablePreference {    Auto,    Require,    Forbid,}pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {    if cfg!(target_os = "macos") {        Some(SandboxType::MacosSeatbelt)    } else if cfg!(target_os = "linux") {        Some(SandboxType::LinuxSeccomp)    } else if cfg!(target_os = "windows") {        if windows_sandbox_enabled {            Some(SandboxType::WindowsRestrictedToken)        } else {            None        }    } else {        None    }}pub fn with_managed_mitm_ca_readable_root(    permission_profile: PermissionProfile,    managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,    sandbox_policy_cwd: &Path,) -> PermissionProfile {    let Some(managed_mitm_ca_trust_bundle_path) = managed_mitm_ca_trust_bundle_path else {        return permission_profile;    };    let (file_system_sandbox_policy, network_sandbox_policy) =        permission_profile.to_runtime_permissions();    let file_system_sandbox_policy = file_system_sandbox_policy.with_additional_readable_roots(        sandbox_policy_cwd,        std::slice::from_ref(managed_mitm_ca_trust_bundle_path),    );    PermissionProfile::from_runtime_permissions_with_enforcement(        permission_profile.enforcement(),        &file_system_sandbox_policy,        network_sandbox_policy,    )}#[derive(Debug)]pub struct SandboxCommand {    pub program: OsString,    pub args: Vec<String>,    pub cwd: PathUri,    pub env: HashMap<String, String>,    pub additional_permissions: Option<AdditionalPermissionProfile>,}/// A host-native launch request produced after [`SandboxManager::transform`] validates URI inputs./// Build this only at the execution boundary: in exec-server, or in its logical equivalent within/// app-server. Orchestration and transport code should retain [`PathUri`] values and defer/// conversion to native paths until this request is created.#[derive(Debug)]pub struct SandboxExecRequest {    pub command: Vec<String>,    pub cwd: AbsolutePathBuf,    pub sandbox_policy_cwd: AbsolutePathBuf,    pub env: HashMap<String, String>,    pub network: Option<NetworkProxy>,    pub sandbox: SandboxType,    pub windows_sandbox_level: WindowsSandboxLevel,    pub windows_sandbox_private_desktop: bool,    pub permission_profile: PermissionProfile,    pub file_system_sandbox_policy: FileSystemSandboxPolicy,    pub network_sandbox_policy: NetworkSandboxPolicy,    pub arg0: Option<String>,}/// Bundled arguments for sandbox transformation.////// This keeps call sites self-documenting when several fields are optional.pub struct SandboxTransformRequest<'a> {    pub command: SandboxCommand,    pub permissions: &'a PermissionProfile,    pub sandbox: SandboxType,    pub enforce_managed_network: bool,    // TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>    // to make shared ownership explicit across runtime/sandbox plumbing.    pub network: Option<&'a NetworkProxy>,    pub sandbox_policy_cwd: &'a PathUri,    pub codex_linux_sandbox_exe: Option<&'a Path>,    pub use_legacy_landlock: bool,    pub windows_sandbox_level: WindowsSandboxLevel,    pub windows_sandbox_private_desktop: bool,}#[derive(Debug)]pub enum SandboxTransformError {    InvalidCommandCwd {        cwd: PathUri,        source: io::Error,    },    InvalidSandboxPolicyCwd {        cwd: PathUri,        source: io::Error,    },    MissingLinuxSandboxExecutable,    #[cfg(target_os = "linux")]    Wsl1UnsupportedForBubblewrap,    #[cfg(not(target_os = "macos"))]    SeatbeltUnavailable,}impl std::fmt::Display for SandboxTransformError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            Self::InvalidCommandCwd { cwd, source } => {                write!(                    f,                    "command cwd URI `{cwd}` is not valid on this host: {source}"                )            }            Self::InvalidSandboxPolicyCwd { cwd, source } => write!(                f,                "sandbox policy cwd URI `{cwd}` is not valid on this host: {source}"            ),            Self::MissingLinuxSandboxExecutable => {                write!(f, "missing codex-linux-sandbox executable path")            }            #[cfg(target_os = "linux")]            Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),            #[cfg(not(target_os = "macos"))]            Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),        }    }}impl std::error::Error for SandboxTransformError {    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {        match self {            Self::InvalidCommandCwd { source, .. }            | Self::InvalidSandboxPolicyCwd { source, .. } => Some(source),            Self::MissingLinuxSandboxExecutable => None,            #[cfg(target_os = "linux")]            Self::Wsl1UnsupportedForBubblewrap => None,            #[cfg(not(target_os = "macos"))]            Self::SeatbeltUnavailable => None,        }    }}#[derive(Default)]pub struct SandboxManager;impl SandboxManager {    pub fn new() -> Self {        Self    }    pub fn select_initial(        &self,        file_system_policy: &FileSystemSandboxPolicy,        network_policy: NetworkSandboxPolicy,        pref: SandboxablePreference,        windows_sandbox_level: WindowsSandboxLevel,        has_managed_network_requirements: bool,    ) -> SandboxType {        match pref {            SandboxablePreference::Forbid => SandboxType::None,            SandboxablePreference::Require => {                get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)                    .unwrap_or(SandboxType::None)            }            SandboxablePreference::Auto => {                if should_require_platform_sandbox(                    file_system_policy,                    network_policy,                    has_managed_network_requirements,                ) {                    get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)                        .unwrap_or(SandboxType::None)                } else {                    SandboxType::None                }            }        }    }    pub fn transform(        &self,        request: SandboxTransformRequest<'_>,    ) -> Result<SandboxExecRequest, SandboxTransformError> {        let SandboxTransformRequest {            mut command,            permissions,            sandbox,            enforce_managed_network,            network,            sandbox_policy_cwd,            codex_linux_sandbox_exe,            use_legacy_landlock,            windows_sandbox_level,            windows_sandbox_private_desktop,        } = request;        let native_command_cwd = command.cwd.to_abs_path().map_err(|source| {            SandboxTransformError::InvalidCommandCwd {                cwd: command.cwd.clone(),                source,            }        })?;        let native_sandbox_policy_cwd = sandbox_policy_cwd.to_abs_path().map_err(|source| {            SandboxTransformError::InvalidSandboxPolicyCwd {                cwd: sandbox_policy_cwd.clone(),                source,            }        })?;        let additional_permissions = command.additional_permissions.take();        let managed_mitm_ca_trust_bundle_path =            network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);        let effective_permission_profile =            effective_permission_profile(permissions, additional_permissions.as_ref());        let effective_permission_profile = with_managed_mitm_ca_readable_root(            effective_permission_profile,            managed_mitm_ca_trust_bundle_path.as_ref(),            native_sandbox_policy_cwd.as_path(),        );        let (effective_file_system_policy, effective_network_policy) =            effective_permission_profile.to_runtime_permissions();        let mut argv = Vec::with_capacity(1 + command.args.len());        argv.push(command.program);        argv.extend(command.args.into_iter().map(OsString::from));        let (argv, arg0_override) = match sandbox {            SandboxType::None => (os_argv_to_strings(argv), None),            #[cfg(target_os = "macos")]            SandboxType::MacosSeatbelt => {                use crate::seatbelt::CreateSeatbeltCommandArgsParams;                use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;                use crate::seatbelt::create_seatbelt_command_args;                let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {                    command: os_argv_to_strings(argv),                    file_system_sandbox_policy: &effective_file_system_policy,                    network_sandbox_policy: effective_network_policy,                    sandbox_policy_cwd: native_sandbox_policy_cwd.as_path(),                    enforce_managed_network,                    network,                    extra_allow_unix_sockets: &[],                });                let mut full_command = Vec::with_capacity(1 + args.len());                full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());                full_command.append(&mut args);                (full_command, None)            }            #[cfg(not(target_os = "macos"))]            SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),            SandboxType::LinuxSeccomp => {                let exe = codex_linux_sandbox_exe                    .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;                let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);                #[cfg(target_os = "linux")]                ensure_linux_bubblewrap_is_supported(                    &effective_file_system_policy,                    use_legacy_landlock,                    allow_proxy_network,                    is_wsl1(),                )?;                let mut args = create_linux_sandbox_command_args_for_permission_profile(                    os_argv_to_strings(argv),                    native_command_cwd.as_path(),                    &effective_permission_profile,                    native_sandbox_policy_cwd.as_path(),                    use_legacy_landlock,                    allow_proxy_network,                );                let mut full_command = Vec::with_capacity(1 + args.len());                full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));                full_command.append(&mut args);                (full_command, Some(linux_sandbox_arg0_override(exe)))            }            #[cfg(target_os = "windows")]            SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),            #[cfg(not(target_os = "windows"))]            SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),        };        Ok(SandboxExecRequest {            command: argv,            cwd: native_command_cwd,            sandbox_policy_cwd: native_sandbox_policy_cwd,            env: command.env,            network: network.cloned(),            sandbox,            windows_sandbox_level,            windows_sandbox_private_desktop,            permission_profile: effective_permission_profile,            file_system_sandbox_policy: effective_file_system_policy,            network_sandbox_policy: effective_network_policy,            arg0: arg0_override,        })    }}pub fn compatibility_sandbox_policy_for_permission_profile(    permissions: &PermissionProfile,    cwd: &Path,) -> SandboxPolicy {    permissions        .to_legacy_sandbox_policy(cwd)        .unwrap_or_else(|_| {            let (file_system_policy, network_policy) = permissions.to_runtime_permissions();            compatibility_workspace_write_policy(file_system_policy, network_policy, cwd)        })}fn compatibility_workspace_write_policy(    file_system_policy: FileSystemSandboxPolicy,    network_policy: NetworkSandboxPolicy,    cwd: &Path,) -> SandboxPolicy {    let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();    let writable_roots = file_system_policy        .get_writable_roots_with_cwd(cwd)        .into_iter()        .map(|root| root.root)        .filter(|root| cwd_abs.as_ref() != Some(root))        .collect();    let tmpdir_writable = std::env::var_os("TMPDIR")        .filter(|tmpdir| !tmpdir.is_empty())        .and_then(|tmpdir| {            AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()        })        .is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));    let slash_tmp = Path::new("/tmp");    let slash_tmp_writable = slash_tmp.is_absolute()        && slash_tmp.is_dir()        && file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);    SandboxPolicy::WorkspaceWrite {        writable_roots,        network_access: network_policy.is_enabled(),        exclude_tmpdir_env_var: !tmpdir_writable,        exclude_slash_tmp: !slash_tmp_writable,    }}#[cfg(target_os = "linux")]fn ensure_linux_bubblewrap_is_supported(    file_system_sandbox_policy: &FileSystemSandboxPolicy,    use_legacy_landlock: bool,    allow_network_for_proxy: bool,    is_wsl1: bool,) -> Result<(), SandboxTransformError> {    let requires_bubblewrap = allow_network_for_proxy        || (!use_legacy_landlock && !file_system_sandbox_policy.has_full_disk_write_access());    if is_wsl1 && requires_bubblewrap {        return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap);    }    Ok(())}fn os_argv_to_strings(argv: Vec<OsString>) -> Vec<String> {    argv.into_iter()        .map(os_string_to_command_component)        .collect()}fn os_string_to_command_component(value: OsString) -> String {    value        .into_string()        .unwrap_or_else(|value| value.to_string_lossy().into_owned())}fn linux_sandbox_arg0_override(exe: &Path) -> String {    if exe.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) {        os_string_to_command_component(exe.as_os_str().to_owned())    } else {        CODEX_LINUX_SANDBOX_ARG0.to_string()    }}#[cfg(test)]#[path = "manager_tests.rs"]mod tests;