fix(linux-sandbox): avoid panic on bwrap build failures (#21127)

## Summary

- Propagate Linux bubblewrap argument-construction failures instead of
panicking in the helper
- Keep mutable-symlink carveouts fail-closed while reporting them as
ordinary sandbox build failures
- Add regression coverage for a protected `.codex` symlink inside a
writable workspace root

## Root cause

Linux bubblewrap intentionally rejects read-only carveouts that cross a
symlink the sandboxed process can still rewrite. That is the correct
security behavior for protected metadata paths such as `.codex`.

The bug was one layer higher: `linux_run_main` treated the expected
build failure as impossible and panicked while constructing the
bubblewrap argv. For issue #20716, that turned a normal fail-closed
sandbox outcome into a noisy panic in the transcript.

## User impact

Users with a project-local `.codex` symlink inside a writable workspace
still get the conservative sandbox decision, but they no longer see a
Rust panic for that condition. The helper now exits with the concise
sandbox-build error so the normal denial / escalation path can handle
it.


Fixes #20716
This commit is contained in:
viyatb-oai
2026-05-05 13:34:08 -07:00
committed by GitHub
Unverified
parent 3b2ebb368e
commit 8b95d5467e
3 changed files with 75 additions and 10 deletions
+17 -10
View File
@@ -25,6 +25,7 @@ use crate::launcher::exec_bwrap;
use crate::launcher::preferred_bwrap_supports_argv0;
use crate::proxy_routing::activate_proxy_routes_in_netns;
use crate::proxy_routing::prepare_host_proxy_route_spec;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::NetworkSandboxPolicy;
@@ -333,6 +334,7 @@ fn run_bwrap_with_proc_fallback(
file_system_sandbox_policy,
network_mode,
)
.unwrap_or_else(|err| exit_with_bwrap_build_error(err))
{
// Keep the retry silent so sandbox-internal diagnostics do not leak into the
// child process stderr stream.
@@ -350,7 +352,8 @@ fn run_bwrap_with_proc_fallback(
sandbox_policy_cwd,
command_cwd,
options,
);
)
.unwrap_or_else(|err| exit_with_bwrap_build_error(err));
apply_inner_command_argv0(&mut bwrap_args.args);
run_or_exec_bwrap(bwrap_args);
}
@@ -374,24 +377,28 @@ fn build_bwrap_argv(
sandbox_policy_cwd: &Path,
command_cwd: &Path,
options: BwrapOptions,
) -> crate::bwrap::BwrapArgs {
) -> CodexResult<crate::bwrap::BwrapArgs> {
let bwrap_args = create_bwrap_command_args(
inner,
file_system_sandbox_policy,
sandbox_policy_cwd,
command_cwd,
options,
)
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
)?;
let mut argv = vec!["bwrap".to_string()];
argv.extend(bwrap_args.args);
crate::bwrap::BwrapArgs {
Ok(crate::bwrap::BwrapArgs {
args: argv,
preserved_files: bwrap_args.preserved_files,
synthetic_mount_targets: bwrap_args.synthetic_mount_targets,
protected_create_targets: bwrap_args.protected_create_targets,
}
})
}
fn exit_with_bwrap_build_error(err: codex_protocol::error::CodexErr) -> ! {
eprintln!("error building bubblewrap command: {err}");
std::process::exit(1);
}
fn apply_inner_command_argv0(argv: &mut Vec<String>) {
@@ -439,15 +446,15 @@ fn preflight_proc_mount_support(
command_cwd: &Path,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_mode: BwrapNetworkMode,
) -> bool {
) -> CodexResult<bool> {
let preflight_argv = build_preflight_bwrap_argv(
sandbox_policy_cwd,
command_cwd,
file_system_sandbox_policy,
network_mode,
);
)?;
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
Ok(!is_proc_mount_failure(stderr.as_str()))
}
fn build_preflight_bwrap_argv(
@@ -455,7 +462,7 @@ fn build_preflight_bwrap_argv(
command_cwd: &Path,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_mode: BwrapNetworkMode,
) -> crate::bwrap::BwrapArgs {
) -> CodexResult<crate::bwrap::BwrapArgs> {
let preflight_command = vec![resolve_true_command()];
build_bwrap_argv(
preflight_command,