refactor: identify bubblewrap sandbox violations

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-06-12 13:47:05 -07:00
parent 33735280cf
commit 27facbf916
14 changed files with 196 additions and 137 deletions

View File

@@ -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`].
///

View File

@@ -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,

View File

@@ -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)]

View File

@@ -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 {

View File

@@ -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,
})?;

View File

@@ -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,
})

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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)?;

View File

@@ -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())
);
}

View File

@@ -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,

View File

@@ -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()),