mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
refactor: identify bubblewrap sandbox violations
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -919,7 +919,8 @@ pub struct Config {
|
||||
pub codex_self_exe: Option<PathBuf>,
|
||||
|
||||
/// Path to the `codex-linux-sandbox` executable. This must be set if
|
||||
/// [`codex_sandboxing::SandboxType::LinuxSeccomp`] is used. Note that this
|
||||
/// [`codex_sandboxing::SandboxType::LinuxBubblewrap`] or
|
||||
/// [`codex_sandboxing::SandboxType::LinuxLegacyLandlock`] is used. Note that this
|
||||
/// cannot be set in the config file: it must be set in code via
|
||||
/// [`ConfigOverrides`].
|
||||
///
|
||||
|
||||
@@ -349,12 +349,15 @@ pub fn build_exec_request(
|
||||
let enforce_managed_network = network.is_some();
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let sandbox_type = select_process_exec_tool_sandbox_type(
|
||||
let mut sandbox_type = select_process_exec_tool_sandbox_type(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
windows_sandbox_level,
|
||||
enforce_managed_network,
|
||||
);
|
||||
if use_legacy_landlock && sandbox_type == SandboxType::LinuxBubblewrap {
|
||||
sandbox_type = SandboxType::LinuxLegacyLandlock;
|
||||
}
|
||||
tracing::debug!("Sandbox type: {sandbox_type:?}");
|
||||
|
||||
if let Some(network) = network.as_ref() {
|
||||
@@ -396,7 +399,6 @@ pub fn build_exec_request(
|
||||
network: network.as_ref(),
|
||||
sandbox_policy_cwd: &sandbox_policy_cwd_uri,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
})
|
||||
@@ -825,68 +827,6 @@ fn finalize_exec_result(
|
||||
}
|
||||
}
|
||||
|
||||
/// We don't have a fully deterministic way to tell if our command failed
|
||||
/// because of the sandbox - a command in the user's zshrc file might hit an
|
||||
/// error, but the command itself might fail or succeed for other reasons.
|
||||
/// For now, we conservatively check for well known command failure exit codes and
|
||||
/// also look for common sandbox denial keywords in the command output.
|
||||
pub(crate) fn is_likely_sandbox_denied(
|
||||
sandbox_type: SandboxType,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
) -> bool {
|
||||
if sandbox_type == SandboxType::None || exec_output.exit_code == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick rejects: well-known non-sandbox shell exit codes
|
||||
// 2: misuse of shell builtins
|
||||
// 126: permission denied
|
||||
// 127: command not found
|
||||
const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [
|
||||
"operation not permitted",
|
||||
"permission denied",
|
||||
"read-only file system",
|
||||
"seccomp",
|
||||
"sandbox",
|
||||
"landlock",
|
||||
"failed to write file",
|
||||
];
|
||||
|
||||
let has_sandbox_keyword = [
|
||||
&exec_output.stderr.text,
|
||||
&exec_output.stdout.text,
|
||||
&exec_output.aggregated_output.text,
|
||||
]
|
||||
.into_iter()
|
||||
.any(|section| {
|
||||
let lower = section.to_lowercase();
|
||||
SANDBOX_DENIED_KEYWORDS
|
||||
.iter()
|
||||
.any(|needle| lower.contains(needle))
|
||||
});
|
||||
|
||||
if has_sandbox_keyword {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QUICK_REJECT_EXIT_CODES: [i32; 3] = [2, 126, 127];
|
||||
if QUICK_REJECT_EXIT_CODES.contains(&exec_output.exit_code) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
const SIGSYS_CODE: i32 = libc::SIGSYS;
|
||||
if sandbox_type == SandboxType::LinuxSeccomp
|
||||
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RawExecToolCallOutput {
|
||||
pub exit_status: ExitStatus,
|
||||
|
||||
@@ -30,7 +30,7 @@ fn make_exec_output(
|
||||
fn sandbox_detection_requires_keywords() {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", "", "");
|
||||
assert!(!is_likely_sandbox_denied(
|
||||
SandboxType::LinuxSeccomp,
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&output
|
||||
));
|
||||
}
|
||||
@@ -38,14 +38,17 @@ fn sandbox_detection_requires_keywords() {
|
||||
#[test]
|
||||
fn sandbox_detection_identifies_keyword_in_stderr() {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", "Operation not permitted", "");
|
||||
assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output));
|
||||
assert!(is_likely_sandbox_denied(
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&output
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_detection_respects_quick_reject_exit_codes() {
|
||||
let output = make_exec_output(/*exit_code*/ 127, "", "command not found", "");
|
||||
assert!(!is_likely_sandbox_denied(
|
||||
SandboxType::LinuxSeccomp,
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&output
|
||||
));
|
||||
}
|
||||
@@ -91,7 +94,7 @@ fn sandbox_detection_ignores_network_policy_text_with_zero_exit_code() {
|
||||
);
|
||||
|
||||
assert!(!is_likely_sandbox_denied(
|
||||
SandboxType::LinuxSeccomp,
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&output
|
||||
));
|
||||
}
|
||||
@@ -1027,7 +1030,12 @@ fn build_exec_request_preserves_windows_workspace_roots() -> Result<()> {
|
||||
fn sandbox_detection_flags_sigsys_exit_code() {
|
||||
let exit_code = EXIT_CODE_SIGNAL_BASE + libc::SIGSYS;
|
||||
let output = make_exec_output(exit_code, "", "", "");
|
||||
assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output));
|
||||
for sandbox_type in [
|
||||
SandboxType::LinuxBubblewrap,
|
||||
SandboxType::LinuxLegacyLandlock,
|
||||
] {
|
||||
assert!(is_likely_sandbox_denied(sandbox_type, &output));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -260,6 +260,13 @@ impl ToolOrchestrator {
|
||||
|
||||
// Platform-specific flag gating is handled by SandboxManager::select_initial.
|
||||
let use_legacy_landlock = turn_ctx.config.features.use_legacy_landlock();
|
||||
let initial_sandbox =
|
||||
if use_legacy_landlock && initial_sandbox == SandboxType::LinuxBubblewrap {
|
||||
SandboxType::LinuxLegacyLandlock
|
||||
} else {
|
||||
initial_sandbox
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
let sandbox_policy_cwd = tool
|
||||
.sandbox_cwd(req)
|
||||
@@ -444,6 +451,12 @@ impl ToolOrchestrator {
|
||||
} else {
|
||||
SandboxType::None
|
||||
};
|
||||
let retry_sandbox =
|
||||
if use_legacy_landlock && retry_sandbox == SandboxType::LinuxBubblewrap {
|
||||
SandboxType::LinuxLegacyLandlock
|
||||
} else {
|
||||
retry_sandbox
|
||||
};
|
||||
let retry_codex_linux_sandbox_exe = if unsandboxed_allowed {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -999,13 +999,16 @@ impl CoreShellCommandExecutor {
|
||||
.split_first()
|
||||
.ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?;
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
let mut sandbox = sandbox_manager.select_initial(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
self.windows_sandbox_level,
|
||||
self.network.is_some(),
|
||||
);
|
||||
if self.use_legacy_landlock && sandbox == SandboxType::LinuxBubblewrap {
|
||||
sandbox = SandboxType::LinuxLegacyLandlock;
|
||||
}
|
||||
let cwd = PathUri::from_abs_path(workdir);
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&self.sandbox_policy_cwd);
|
||||
let command = SandboxCommand {
|
||||
@@ -1029,7 +1032,6 @@ impl CoreShellCommandExecutor {
|
||||
network: self.network.as_ref(),
|
||||
sandbox_policy_cwd: &sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})?;
|
||||
|
||||
@@ -459,7 +459,6 @@ impl<'a> SandboxAttempt<'a> {
|
||||
codex_linux_sandbox_exe: self
|
||||
.codex_linux_sandbox_exe
|
||||
.map(std::path::PathBuf::as_path),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_sandboxing::SandboxDirectSpawnTransformRequest;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
@@ -103,13 +104,16 @@ impl FileSystemSandboxRunner {
|
||||
let helper = &self.runtime_paths.codex_self_exe;
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
let mut sandbox = sandbox_manager.select_initial(
|
||||
&file_system_policy,
|
||||
network_policy,
|
||||
SandboxablePreference::Auto,
|
||||
sandbox_context.windows_sandbox_level,
|
||||
/*has_managed_network_requirements*/ false,
|
||||
);
|
||||
if sandbox_context.use_legacy_landlock && sandbox == SandboxType::LinuxBubblewrap {
|
||||
sandbox = SandboxType::LinuxLegacyLandlock;
|
||||
}
|
||||
let command = SandboxCommand {
|
||||
program: helper.as_path().as_os_str().to_owned(),
|
||||
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
|
||||
@@ -142,7 +146,6 @@ impl FileSystemSandboxRunner {
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd.uri,
|
||||
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: sandbox_context
|
||||
.windows_sandbox_private_desktop,
|
||||
|
||||
@@ -78,13 +78,16 @@ pub(crate) fn prepare_exec_request(
|
||||
);
|
||||
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
let mut sandbox = sandbox_manager.select_initial(
|
||||
&file_system_policy,
|
||||
network_policy,
|
||||
SandboxablePreference::Require,
|
||||
sandbox_context.windows_sandbox_level,
|
||||
params.enforce_managed_network,
|
||||
);
|
||||
if sandbox_context.use_legacy_landlock && sandbox == SandboxType::LinuxBubblewrap {
|
||||
sandbox = SandboxType::LinuxLegacyLandlock;
|
||||
}
|
||||
match sandbox {
|
||||
SandboxType::None => {
|
||||
return Err(invalid_params(
|
||||
@@ -98,7 +101,9 @@ pub(crate) fn prepare_exec_request(
|
||||
"sandboxed remote process launch is not supported on Windows".to_string(),
|
||||
));
|
||||
}
|
||||
SandboxType::MacosSeatbelt | SandboxType::LinuxSeccomp => {}
|
||||
SandboxType::MacosSeatbelt
|
||||
| SandboxType::LinuxBubblewrap
|
||||
| SandboxType::LinuxLegacyLandlock => {}
|
||||
}
|
||||
let (program, args) = params
|
||||
.argv
|
||||
@@ -127,7 +132,6 @@ pub(crate) fn prepare_exec_request(
|
||||
network: None,
|
||||
sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,11 @@ use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
|
||||
use crate::SandboxType;
|
||||
|
||||
/// Returns whether a failed command was likely denied by the selected sandbox.
|
||||
/// We don't have a fully deterministic way to tell if our command failed
|
||||
/// because of the sandbox - a command in the user's zshrc file might hit an
|
||||
/// error, but the command itself might fail or succeed for other reasons.
|
||||
/// For now, we conservatively check for well known command failure exit codes and
|
||||
/// also look for common sandbox denial keywords in the command output.
|
||||
pub fn is_likely_sandbox_denied(
|
||||
sandbox_type: SandboxType,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
@@ -11,6 +15,10 @@ pub fn is_likely_sandbox_denied(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick rejects: well-known non-sandbox shell exit codes
|
||||
// 2: misuse of shell builtins
|
||||
// 126: permission denied
|
||||
// 127: command not found
|
||||
const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [
|
||||
"operation not permitted",
|
||||
"permission denied",
|
||||
@@ -46,8 +54,11 @@ pub fn is_likely_sandbox_denied(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
const EXIT_CODE_SIGNAL_BASE: i32 = 128;
|
||||
if sandbox_type == SandboxType::LinuxSeccomp
|
||||
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS
|
||||
const SIGSYS_CODE: i32 = libc::SIGSYS;
|
||||
if matches!(
|
||||
sandbox_type,
|
||||
SandboxType::LinuxBubblewrap | SandboxType::LinuxLegacyLandlock
|
||||
) && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use manager::with_managed_mitm_ca_readable_root;
|
||||
pub use violation::FileSystemSandboxViolation;
|
||||
pub use violation::FileSystemSandboxViolationReason;
|
||||
pub use violation::NetworkSandboxViolation;
|
||||
pub use violation::SandboxViolationBackend;
|
||||
pub use violation::SandboxViolationEvent;
|
||||
pub use violation::record_filesystem_sandbox_violation;
|
||||
pub use violation::record_network_sandbox_violation;
|
||||
|
||||
@@ -35,17 +35,19 @@ const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USER
|
||||
pub enum SandboxType {
|
||||
None,
|
||||
MacosSeatbelt,
|
||||
LinuxSeccomp,
|
||||
LinuxBubblewrap,
|
||||
LinuxLegacyLandlock,
|
||||
WindowsRestrictedToken,
|
||||
}
|
||||
|
||||
impl SandboxType {
|
||||
pub fn as_metric_tag(self) -> &'static str {
|
||||
pub const fn as_metric_tag(self) -> &'static str {
|
||||
match self {
|
||||
SandboxType::None => "none",
|
||||
SandboxType::MacosSeatbelt => "seatbelt",
|
||||
SandboxType::LinuxSeccomp => "seccomp",
|
||||
SandboxType::WindowsRestrictedToken => "windows_sandbox",
|
||||
Self::None => "none",
|
||||
Self::MacosSeatbelt => "seatbelt",
|
||||
Self::LinuxBubblewrap => "bubblewrap",
|
||||
Self::LinuxLegacyLandlock => "legacy_landlock",
|
||||
Self::WindowsRestrictedToken => "windows_sandbox",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +63,7 @@ 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)
|
||||
Some(SandboxType::LinuxBubblewrap)
|
||||
} else if cfg!(target_os = "windows") {
|
||||
if windows_sandbox_enabled {
|
||||
Some(SandboxType::WindowsRestrictedToken)
|
||||
@@ -139,7 +141,6 @@ pub struct SandboxTransformRequest<'a> {
|
||||
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,
|
||||
}
|
||||
@@ -331,10 +332,10 @@ impl SandboxManager {
|
||||
network,
|
||||
sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
} = request;
|
||||
let use_legacy_landlock = sandbox == SandboxType::LinuxLegacyLandlock;
|
||||
#[cfg(target_os = "macos")]
|
||||
let managed_network = command.managed_network.as_ref();
|
||||
let additional_permissions = command.additional_permissions.take();
|
||||
@@ -382,7 +383,7 @@ impl SandboxManager {
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
|
||||
SandboxType::LinuxSeccomp => {
|
||||
SandboxType::LinuxBubblewrap | SandboxType::LinuxLegacyLandlock => {
|
||||
let pending = pending_sandboxed_request?;
|
||||
let exe = codex_linux_sandbox_exe
|
||||
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
|
||||
|
||||
@@ -102,7 +102,6 @@ fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_poli
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -158,7 +157,6 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -229,7 +227,6 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -303,8 +300,9 @@ fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn transform_linux_seccomp_request(
|
||||
fn transform_linux_request(
|
||||
codex_linux_sandbox_exe: &std::path::Path,
|
||||
sandbox: SandboxType,
|
||||
) -> super::SandboxExecRequest {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
@@ -321,13 +319,12 @@ fn transform_linux_seccomp_request(
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
sandbox: SandboxType::LinuxSeccomp,
|
||||
sandbox,
|
||||
enforce_managed_network: false,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -405,9 +402,10 @@ fn wsl1_allows_non_bubblewrap_linux_paths() {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() {
|
||||
fn transform_linux_bubblewrap_preserves_helper_path_in_arg0_when_available() {
|
||||
let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex-linux-sandbox");
|
||||
let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe);
|
||||
let exec_request =
|
||||
transform_linux_request(&codex_linux_sandbox_exe, SandboxType::LinuxBubblewrap);
|
||||
|
||||
assert_eq!(
|
||||
exec_request.arg0,
|
||||
@@ -417,9 +415,10 @@ fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path() {
|
||||
fn transform_linux_bubblewrap_uses_helper_alias_when_launcher_is_not_helper_path() {
|
||||
let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex");
|
||||
let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe);
|
||||
let exec_request =
|
||||
transform_linux_request(&codex_linux_sandbox_exe, SandboxType::LinuxBubblewrap);
|
||||
|
||||
assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string()));
|
||||
}
|
||||
@@ -520,7 +519,6 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() {
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd_uri,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Elevated,
|
||||
windows_sandbox_private_desktop: false,
|
||||
},
|
||||
@@ -583,3 +581,19 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() {
|
||||
);
|
||||
assert!(materialized_helper.exists());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn transform_linux_landlock_uses_legacy_backend() {
|
||||
let exec_request = transform_linux_request(
|
||||
std::path::Path::new("/tmp/codex-linux-sandbox"),
|
||||
SandboxType::LinuxLegacyLandlock,
|
||||
);
|
||||
|
||||
assert_eq!(exec_request.sandbox, SandboxType::LinuxLegacyLandlock);
|
||||
assert!(
|
||||
exec_request
|
||||
.command
|
||||
.contains(&"--use-legacy-landlock".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ const SANDBOX_DENIED_KEYWORDS: [(FileSystemSandboxViolationReason, &str); 7] = [
|
||||
FileSystemSandboxViolationReason::ReadOnlyFileSystem,
|
||||
"read-only file system",
|
||||
),
|
||||
(FileSystemSandboxViolationReason::Seccomp, "seccomp"),
|
||||
(FileSystemSandboxViolationReason::Sandbox, "sandbox"),
|
||||
(FileSystemSandboxViolationReason::Landlock, "landlock"),
|
||||
(FileSystemSandboxViolationReason::PolicyDenied, "seccomp"),
|
||||
(FileSystemSandboxViolationReason::PolicyDenied, "sandbox"),
|
||||
(FileSystemSandboxViolationReason::PolicyDenied, "landlock"),
|
||||
(
|
||||
FileSystemSandboxViolationReason::FailedToWriteFile,
|
||||
"failed to write file",
|
||||
@@ -43,10 +43,32 @@ pub enum SandboxViolationEvent {
|
||||
Network(NetworkSandboxViolation),
|
||||
}
|
||||
|
||||
/// Enforcement backend that observed a sandbox violation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SandboxViolationBackend {
|
||||
Bubblewrap,
|
||||
LegacyLandlock,
|
||||
ManagedNetworkProxy,
|
||||
Seatbelt,
|
||||
WindowsSandbox,
|
||||
}
|
||||
|
||||
impl SandboxViolationBackend {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bubblewrap => "bubblewrap",
|
||||
Self::LegacyLandlock => "legacy_landlock",
|
||||
Self::ManagedNetworkProxy => "managed_network_proxy",
|
||||
Self::Seatbelt => "seatbelt",
|
||||
Self::WindowsSandbox => "windows_sandbox",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A filesystem sandbox denial inferred from a sandboxed process result.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FileSystemSandboxViolation {
|
||||
pub sandbox_type: SandboxType,
|
||||
pub backend: SandboxViolationBackend,
|
||||
pub reason: FileSystemSandboxViolationReason,
|
||||
pub path: Option<String>,
|
||||
pub output_snippet: String,
|
||||
@@ -58,9 +80,7 @@ pub enum FileSystemSandboxViolationReason {
|
||||
OperationNotPermitted,
|
||||
PermissionDenied,
|
||||
ReadOnlyFileSystem,
|
||||
Seccomp,
|
||||
Sandbox,
|
||||
Landlock,
|
||||
PolicyDenied,
|
||||
FailedToWriteFile,
|
||||
SignalSyscall,
|
||||
}
|
||||
@@ -71,9 +91,7 @@ impl FileSystemSandboxViolationReason {
|
||||
Self::OperationNotPermitted => "operation_not_permitted",
|
||||
Self::PermissionDenied => "permission_denied",
|
||||
Self::ReadOnlyFileSystem => "read_only_file_system",
|
||||
Self::Seccomp => "seccomp",
|
||||
Self::Sandbox => "sandbox",
|
||||
Self::Landlock => "landlock",
|
||||
Self::PolicyDenied => "policy_denied",
|
||||
Self::FailedToWriteFile => "failed_to_write_file",
|
||||
Self::SignalSyscall => "sigsys",
|
||||
}
|
||||
@@ -83,6 +101,7 @@ impl FileSystemSandboxViolationReason {
|
||||
/// A network sandbox denial reported by the managed network proxy.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NetworkSandboxViolation {
|
||||
pub backend: SandboxViolationBackend,
|
||||
pub host: String,
|
||||
pub reason: String,
|
||||
pub client: Option<String>,
|
||||
@@ -98,6 +117,7 @@ pub struct NetworkSandboxViolation {
|
||||
impl NetworkSandboxViolation {
|
||||
pub fn from_blocked_request(blocked: &BlockedRequest) -> Self {
|
||||
Self {
|
||||
backend: SandboxViolationBackend::ManagedNetworkProxy,
|
||||
host: blocked.host.clone(),
|
||||
reason: blocked.reason.clone(),
|
||||
client: blocked.client.clone(),
|
||||
@@ -117,13 +137,20 @@ fn classify_filesystem_sandbox_violation(
|
||||
sandbox_type: SandboxType,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
) -> Option<FileSystemSandboxViolation> {
|
||||
if sandbox_type == SandboxType::None || exec_output.exit_code == 0 {
|
||||
if exec_output.exit_code == 0 {
|
||||
return None;
|
||||
}
|
||||
let backend = match sandbox_type {
|
||||
SandboxType::None => return None,
|
||||
SandboxType::MacosSeatbelt => SandboxViolationBackend::Seatbelt,
|
||||
SandboxType::LinuxBubblewrap => SandboxViolationBackend::Bubblewrap,
|
||||
SandboxType::LinuxLegacyLandlock => SandboxViolationBackend::LegacyLandlock,
|
||||
SandboxType::WindowsRestrictedToken => SandboxViolationBackend::WindowsSandbox,
|
||||
};
|
||||
|
||||
if let Some(reason) = filesystem_reason_from_output(exec_output) {
|
||||
return Some(FileSystemSandboxViolation {
|
||||
sandbox_type,
|
||||
backend,
|
||||
reason,
|
||||
path: extract_denied_path(exec_output),
|
||||
output_snippet: output_snippet(exec_output),
|
||||
@@ -136,11 +163,13 @@ fn classify_filesystem_sandbox_violation(
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if sandbox_type == SandboxType::LinuxSeccomp
|
||||
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS
|
||||
if matches!(
|
||||
sandbox_type,
|
||||
SandboxType::LinuxBubblewrap | SandboxType::LinuxLegacyLandlock
|
||||
) && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS
|
||||
{
|
||||
return Some(FileSystemSandboxViolation {
|
||||
sandbox_type,
|
||||
backend,
|
||||
reason: FileSystemSandboxViolationReason::SignalSyscall,
|
||||
path: None,
|
||||
output_snippet: output_snippet(exec_output),
|
||||
@@ -174,15 +203,16 @@ pub fn record_sandbox_violation(event: &SandboxViolationEvent) {
|
||||
SandboxViolationEvent::FileSystem(violation) => {
|
||||
let path = violation.path.as_deref().unwrap_or("unknown");
|
||||
warn!(
|
||||
"recorded sandbox violation: resource=filesystem sandbox={} reason={} path={}",
|
||||
violation.sandbox_type.as_metric_tag(),
|
||||
"recorded sandbox violation: resource=filesystem backend={} reason={} path={}",
|
||||
violation.backend.as_str(),
|
||||
violation.reason.as_str(),
|
||||
path
|
||||
);
|
||||
}
|
||||
SandboxViolationEvent::Network(violation) => {
|
||||
warn!(
|
||||
"recorded sandbox violation: resource=network protocol={} host={} port={:?} reason={} method={:?} mode={:?} client={:?} decision={:?} source={:?}",
|
||||
"recorded sandbox violation: resource=network backend={} protocol={} host={} port={:?} reason={} method={:?} mode={:?} client={:?} decision={:?} source={:?}",
|
||||
violation.backend.as_str(),
|
||||
violation.protocol,
|
||||
violation.host,
|
||||
violation.port,
|
||||
|
||||
@@ -37,12 +37,29 @@ fn classifies_legacy_denial_keywords() {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", keyword, "");
|
||||
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output).is_some(),
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxBubblewrap, &output).is_some(),
|
||||
"{keyword}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_backend_keywords_as_policy_denied() {
|
||||
for keyword in ["seccomp", "sandbox", "landlock"] {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", keyword, "");
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxBubblewrap, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::Bubblewrap,
|
||||
reason: FileSystemSandboxViolationReason::PolicyDenied,
|
||||
path: None,
|
||||
output_snippet: keyword.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_legacy_denial_ordering() {
|
||||
let quick_reject_without_keyword =
|
||||
@@ -56,21 +73,24 @@ fn preserves_legacy_denial_ordering() {
|
||||
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(
|
||||
SandboxType::LinuxSeccomp,
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&quick_reject_without_keyword
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(
|
||||
SandboxType::LinuxSeccomp,
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&quick_reject_with_keyword
|
||||
)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &zero_exit_with_keyword)
|
||||
.is_none()
|
||||
classify_filesystem_sandbox_violation(
|
||||
SandboxType::LinuxBubblewrap,
|
||||
&zero_exit_with_keyword
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::None, &non_sandbox_with_keyword)
|
||||
@@ -90,7 +110,7 @@ fn classifies_filesystem_violation_with_path() {
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
sandbox_type: SandboxType::MacosSeatbelt,
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::OperationNotPermitted,
|
||||
path: Some("/private/tmp/denied".to_string()),
|
||||
output_snippet: "bash: /private/tmp/denied: Operation not permitted".to_string(),
|
||||
@@ -110,7 +130,7 @@ fn classifies_filesystem_violation_with_unicode_before_marker() {
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
sandbox_type: SandboxType::MacosSeatbelt,
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::OperationNotPermitted,
|
||||
path: Some("/private/tmp/\u{130}-denied".to_string()),
|
||||
output_snippet: "bash: /private/tmp/\u{130}-denied: Operation not permitted"
|
||||
@@ -131,7 +151,7 @@ fn classifies_filesystem_violation_from_aggregated_output() {
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
sandbox_type: SandboxType::MacosSeatbelt,
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::ReadOnlyFileSystem,
|
||||
path: None,
|
||||
output_snippet: "cargo failed: Read-only file system when writing target".to_string(),
|
||||
@@ -149,15 +169,26 @@ fn classifies_linux_sigsys_exit() {
|
||||
"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
sandbox_type: SandboxType::LinuxSeccomp,
|
||||
reason: FileSystemSandboxViolationReason::SignalSyscall,
|
||||
path: None,
|
||||
output_snippet: String::new(),
|
||||
})
|
||||
);
|
||||
for (sandbox_type, backend) in [
|
||||
(
|
||||
SandboxType::LinuxBubblewrap,
|
||||
SandboxViolationBackend::Bubblewrap,
|
||||
),
|
||||
(
|
||||
SandboxType::LinuxLegacyLandlock,
|
||||
SandboxViolationBackend::LegacyLandlock,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(sandbox_type, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend,
|
||||
reason: FileSystemSandboxViolationReason::SignalSyscall,
|
||||
path: None,
|
||||
output_snippet: String::new(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -184,6 +215,7 @@ fn converts_blocked_request_to_network_violation() {
|
||||
assert_eq!(
|
||||
NetworkSandboxViolation::from_blocked_request(&blocked),
|
||||
NetworkSandboxViolation {
|
||||
backend: SandboxViolationBackend::ManagedNetworkProxy,
|
||||
host: "example.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
client: Some("curl".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user