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
parent 3b2ebb368e
commit 8b95d5467e
3 changed files with 75 additions and 10 deletions
@@ -587,6 +587,59 @@ async fn sandbox_blocks_codex_symlink_replacement_attack() {
assert_ne!(codex_output.exit_code, 0);
}
#[tokio::test]
async fn sandbox_reports_codex_symlink_build_failure_without_panicking() {
if should_skip_bwrap_tests().await {
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
return;
}
use std::os::unix::fs::symlink;
let tmpdir = tempfile::tempdir().expect("tempdir");
let decoy = tmpdir.path().join("decoy-codex");
std::fs::create_dir_all(&decoy).expect("create decoy dir");
let dot_codex = tmpdir.path().join(".codex");
symlink(&decoy, &dot_codex).expect("create .codex symlink");
let output = match run_cmd_result_with_writable_roots(
&["bash", "-lc", "true"],
&[tmpdir.path().to_path_buf()],
LONG_TIMEOUT_MS,
/*use_legacy_landlock*/ false,
/*network_access*/ true,
)
.await
{
Err(CodexErr::Sandbox(SandboxErr::Denied { output, .. })) => *output,
result => panic!(".codex symlink build failure should deny: {result:?}"),
};
assert_eq!(output.exit_code, 1);
assert!(
output
.stderr
.text
.contains("error building bubblewrap command:"),
"stderr: {}",
output.stderr.text
);
assert!(
output
.stderr
.text
.contains("cannot enforce sandbox read-only path"),
"stderr: {}",
output.stderr.text
);
assert!(
!output.stderr.text.contains("panicked at"),
"stderr: {}",
output.stderr.text
);
}
#[tokio::test]
async fn sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata() {
if should_skip_bwrap_tests().await {