fix: fix old system bubblewrap compatibility without falling back to vendored bwrap (#15693)

Fixes #15283.

## Summary
Older system bubblewrap builds reject `--argv0`, which makes our Linux
sandbox fail before the helper can re-exec. This PR keeps using system
`/usr/bin/bwrap` whenever it exists and only falls back to vendored
bwrap when the system binary is missing. That matters on stricter
AppArmor hosts, where the distro bwrap package also provides the policy
setup needed for user namespaces.

For old system bwrap, we avoid `--argv0` instead of switching binaries:
- pass the sandbox helper a full-path `argv0`,
- keep the existing `current_exe() + --argv0` path when the selected
launcher supports it,
- otherwise omit `--argv0` and re-exec through the helper's own
`argv[0]` path, whose basename still dispatches as
`codex-linux-sandbox`.

Also updates the launcher/warning tests and docs so they match the new
behavior: present-but-old system bwrap uses the compatibility path, and
only absent system bwrap falls back to vendored.

### Validation

1. Install Ubuntu 20.04 in a VM
2. Compile codex and run without bubblewrap installed - see a warning
about falling back to the vendored bwrap
3. Install bwrap and verify version is 0.4.0 without `argv0` support
4. run codex and use apply_patch tool without errors

<img width="802" height="631" alt="Screenshot 2026-03-25 at 11 48 36 PM"
src="https://github.com/user-attachments/assets/77248a29-aa38-4d7c-9833-496ec6a458b8"
/>
<img width="807" height="634" alt="Screenshot 2026-03-25 at 11 47 32 PM"
src="https://github.com/user-attachments/assets/5af8b850-a466-489b-95a6-455b76b5050f"
/>
<img width="812" height="635" alt="Screenshot 2026-03-25 at 11 45 45 PM"
src="https://github.com/user-attachments/assets/438074f0-8435-4274-a667-332efdd5cb57"
/>
<img width="801" height="623" alt="Screenshot 2026-03-25 at 11 43 56 PM"
src="https://github.com/user-attachments/assets/0dc8d3f5-e8cf-4218-b4b4-a4f7d9bf02e3"
/>

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
viyatb-oai
2026-03-25 23:51:39 -07:00
committed by GitHub
parent 6d0525ae70
commit 937cb5081d
29 changed files with 556 additions and 171 deletions

View File

@@ -14,13 +14,21 @@ const SYSTEM_BWRAP_PATH: &str = "/usr/bin/bwrap";
#[derive(Debug, Clone, PartialEq, Eq)]
enum BubblewrapLauncher {
System(AbsolutePathBuf),
System(SystemBwrapLauncher),
Vendored,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SystemBwrapLauncher {
program: AbsolutePathBuf,
supports_argv0: bool,
}
pub(crate) fn exec_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
match preferred_bwrap_launcher() {
BubblewrapLauncher::System(program) => exec_system_bwrap(&program, argv, preserved_files),
BubblewrapLauncher::System(launcher) => {
exec_system_bwrap(&launcher.program, argv, preserved_files)
}
BubblewrapLauncher::Vendored => exec_vendored_bwrap(argv, preserved_files),
}
}
@@ -33,10 +41,18 @@ fn preferred_bwrap_launcher() -> BubblewrapLauncher {
}
fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLauncher {
if !system_bwrap_supports_argv0(system_bwrap_path) {
preferred_bwrap_launcher_for_path_with_probe(system_bwrap_path, system_bwrap_supports_argv0)
}
fn preferred_bwrap_launcher_for_path_with_probe(
system_bwrap_path: &Path,
system_bwrap_supports_argv0: impl FnOnce(&Path) -> bool,
) -> BubblewrapLauncher {
if !system_bwrap_path.is_file() {
return BubblewrapLauncher::Vendored;
}
let supports_argv0 = system_bwrap_supports_argv0(system_bwrap_path);
let system_bwrap_path = match AbsolutePathBuf::from_absolute_path(system_bwrap_path) {
Ok(path) => path,
Err(err) => panic!(
@@ -44,14 +60,25 @@ fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLaun
system_bwrap_path.display()
),
};
BubblewrapLauncher::System(system_bwrap_path)
BubblewrapLauncher::System(SystemBwrapLauncher {
program: system_bwrap_path,
supports_argv0,
})
}
pub(crate) fn preferred_bwrap_supports_argv0() -> bool {
match preferred_bwrap_launcher() {
BubblewrapLauncher::System(launcher) => launcher.supports_argv0,
BubblewrapLauncher::Vendored => true,
}
}
fn system_bwrap_supports_argv0(system_bwrap_path: &Path) -> bool {
// bubblewrap added `--argv0` in v0.9.0:
// https://github.com/containers/bubblewrap/releases/tag/v0.9.0
// Older distro packages (for example Ubuntu 20.04/22.04) ship builds that
// reject `--argv0`, so prefer the vendored build in that case.
// reject `--argv0`, so use the system binary's no-argv0 compatibility path
// in that case.
let output = match Command::new(system_bwrap_path).arg("--help").output() {
Ok(output) => output,
Err(_) => return false,
@@ -126,47 +153,34 @@ fn clear_cloexec(fd: libc::c_int) {
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
use tempfile::TempPath;
#[test]
fn prefers_system_bwrap_when_help_lists_argv0() {
let fake_bwrap = write_fake_bwrap(
r#"#!/bin/sh
if [ "$1" = "--help" ]; then
echo ' --argv0 PROGRAM'
exit 0
fi
exit 1
"#,
);
let fake_bwrap_path: &Path = fake_bwrap.as_ref();
let fake_bwrap = NamedTempFile::new().expect("temp file");
let fake_bwrap_path = fake_bwrap.path();
let expected = AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute");
assert_eq!(
preferred_bwrap_launcher_for_path(fake_bwrap_path),
BubblewrapLauncher::System(expected)
preferred_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| true),
BubblewrapLauncher::System(SystemBwrapLauncher {
program: expected,
supports_argv0: true,
})
);
}
#[test]
fn falls_back_to_vendored_when_system_bwrap_lacks_argv0() {
let fake_bwrap = write_fake_bwrap(
r#"#!/bin/sh
if [ "$1" = "--help" ]; then
echo 'usage: bwrap [OPTION...] COMMAND'
exit 0
fi
exit 1
"#,
);
let fake_bwrap_path: &Path = fake_bwrap.as_ref();
fn prefers_system_bwrap_when_system_bwrap_lacks_argv0() {
let fake_bwrap = NamedTempFile::new().expect("temp file");
let fake_bwrap_path = fake_bwrap.path();
assert_eq!(
preferred_bwrap_launcher_for_path(fake_bwrap_path),
BubblewrapLauncher::Vendored
preferred_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| false),
BubblewrapLauncher::System(SystemBwrapLauncher {
program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"),
supports_argv0: false,
})
);
}
@@ -207,13 +221,4 @@ exit 1
}
flags
}
fn write_fake_bwrap(contents: &str) -> TempPath {
// Linux rejects exec-ing a file that is still open for writing.
let path = NamedTempFile::new().expect("temp file").into_temp_path();
fs::write(&path, contents).expect("write fake bwrap");
let permissions = fs::Permissions::from_mode(0o755);
fs::set_permissions(&path, permissions).expect("chmod fake bwrap");
path
}
}

View File

@@ -12,11 +12,13 @@ use crate::bwrap::BwrapOptions;
use crate::bwrap::create_bwrap_command_args;
use crate::landlock::apply_sandbox_policy_to_current_thread;
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::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
#[derive(Debug, Parser)]
/// CLI surface for the Linux sandbox helper.
@@ -426,13 +428,14 @@ fn run_bwrap_with_proc_fallback(
mount_proc,
network_mode,
};
let bwrap_args = build_bwrap_argv(
let mut bwrap_args = build_bwrap_argv(
inner,
file_system_sandbox_policy,
sandbox_policy_cwd,
command_cwd,
options,
);
apply_inner_command_argv0(&mut bwrap_args.args);
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
}
@@ -456,7 +459,7 @@ fn build_bwrap_argv(
command_cwd: &Path,
options: BwrapOptions,
) -> crate::bwrap::BwrapArgs {
let mut bwrap_args = create_bwrap_command_args(
let bwrap_args = create_bwrap_command_args(
inner,
file_system_sandbox_policy,
sandbox_policy_cwd,
@@ -465,16 +468,6 @@ fn build_bwrap_argv(
)
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
let command_separator_index = bwrap_args
.args
.iter()
.position(|arg| arg == "--")
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
bwrap_args.args.splice(
command_separator_index..command_separator_index,
["--argv0".to_string(), "codex-linux-sandbox".to_string()],
);
let mut argv = vec!["bwrap".to_string()];
argv.extend(bwrap_args.args);
crate::bwrap::BwrapArgs {
@@ -483,6 +476,46 @@ fn build_bwrap_argv(
}
}
fn apply_inner_command_argv0(argv: &mut Vec<String>) {
apply_inner_command_argv0_for_launcher(
argv,
preferred_bwrap_supports_argv0(),
current_process_argv0(),
);
}
fn apply_inner_command_argv0_for_launcher(
argv: &mut Vec<String>,
supports_argv0: bool,
argv0_fallback_command: String,
) {
let command_separator_index = argv
.iter()
.position(|arg| arg == "--")
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
if supports_argv0 {
argv.splice(
command_separator_index..command_separator_index,
["--argv0".to_string(), CODEX_LINUX_SANDBOX_ARG0.to_string()],
);
return;
}
let command_index = command_separator_index + 1;
let Some(command) = argv.get_mut(command_index) else {
panic!("bubblewrap argv is missing inner command after '--'");
};
*command = argv0_fallback_command;
}
fn current_process_argv0() -> String {
match std::env::args_os().next() {
Some(argv0) => argv0.to_string_lossy().into_owned(),
None => panic!("failed to resolve current process argv[0]"),
}
}
fn preflight_proc_mount_support(
sandbox_policy_cwd: &Path,
command_cwd: &Path,

View File

@@ -40,7 +40,7 @@ fn ignores_non_proc_mount_errors() {
#[test]
fn inserts_bwrap_argv0_before_command_separator() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let argv = build_bwrap_argv(
let mut argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
@@ -51,6 +51,11 @@ fn inserts_bwrap_argv0_before_command_separator() {
},
)
.args;
apply_inner_command_argv0_for_launcher(
&mut argv,
true,
"/tmp/codex-arg0-session/codex-linux-sandbox".to_string(),
);
assert_eq!(
argv,
vec![
@@ -74,6 +79,73 @@ fn inserts_bwrap_argv0_before_command_separator() {
);
}
#[test]
fn rewrites_inner_command_path_when_bwrap_lacks_argv0() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let mut argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&FileSystemSandboxPolicy::from(&sandbox_policy),
Path::new("/"),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
},
)
.args;
apply_inner_command_argv0_for_launcher(
&mut argv,
false,
"/tmp/codex-arg0-session/codex-linux-sandbox".to_string(),
);
assert!(!argv.iter().any(|arg| arg == "--argv0"));
assert!(
argv.windows(2)
.any(|window| { window == ["--", "/tmp/codex-arg0-session/codex-linux-sandbox"] })
);
}
#[test]
fn rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appears_later() {
let nested_current_exe = std::env::current_exe()
.expect("current exe")
.to_string_lossy()
.into_owned();
let mut argv = vec![
"bwrap".to_string(),
"--".to_string(),
"/tmp/helper-symlink".to_string(),
"--sandbox-policy-cwd".to_string(),
"/tmp/cwd".to_string(),
"--".to_string(),
nested_current_exe.clone(),
"--codex-run-as-apply-patch".to_string(),
"patch".to_string(),
];
apply_inner_command_argv0_for_launcher(
&mut argv,
false,
"/tmp/argv0-fallback-helper".to_string(),
);
assert_eq!(
argv,
vec![
"bwrap".to_string(),
"--".to_string(),
"/tmp/argv0-fallback-helper".to_string(),
"--sandbox-policy-cwd".to_string(),
"/tmp/cwd".to_string(),
"--".to_string(),
nested_current_exe,
"--codex-run-as-apply-patch".to_string(),
"patch".to_string(),
]
);
}
#[test]
fn inserts_unshare_net_when_network_isolation_requested() {
let sandbox_policy = SandboxPolicy::new_read_only_policy();
@@ -313,6 +385,7 @@ fn resolve_sandbox_policies_rejects_mismatched_legacy_and_split_inputs() {
Some(NetworkSandboxPolicy::Enabled),
)
.expect_err("mismatched legacy and split policies should fail");
assert!(
matches!(
err,