Files
codex/codex-rs/linux-sandbox/src/launcher.rs
viyatb-oai 937cb5081d 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>
2026-03-25 23:51:39 -07:00

225 lines
7.6 KiB
Rust

use std::ffi::CString;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::os::raw::c_char;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;
use crate::vendored_bwrap::exec_vendored_bwrap;
use codex_utils_absolute_path::AbsolutePathBuf;
const SYSTEM_BWRAP_PATH: &str = "/usr/bin/bwrap";
#[derive(Debug, Clone, PartialEq, Eq)]
enum BubblewrapLauncher {
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(launcher) => {
exec_system_bwrap(&launcher.program, argv, preserved_files)
}
BubblewrapLauncher::Vendored => exec_vendored_bwrap(argv, preserved_files),
}
}
fn preferred_bwrap_launcher() -> BubblewrapLauncher {
static LAUNCHER: OnceLock<BubblewrapLauncher> = OnceLock::new();
LAUNCHER
.get_or_init(|| preferred_bwrap_launcher_for_path(Path::new(SYSTEM_BWRAP_PATH)))
.clone()
}
fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLauncher {
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!(
"failed to normalize system bubblewrap path {}: {err}",
system_bwrap_path.display()
),
};
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 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,
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
stdout.contains("--argv0") || stderr.contains("--argv0")
}
fn exec_system_bwrap(
program: &AbsolutePathBuf,
argv: Vec<String>,
preserved_files: Vec<File>,
) -> ! {
// System bwrap runs across an exec boundary, so preserved fds must survive exec.
make_files_inheritable(&preserved_files);
let program_path = program.as_path().display().to_string();
let program = CString::new(program.as_path().as_os_str().as_bytes())
.unwrap_or_else(|err| panic!("invalid system bubblewrap path: {err}"));
let cstrings = argv_to_cstrings(&argv);
let mut argv_ptrs: Vec<*const c_char> = cstrings.iter().map(|arg| arg.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
// SAFETY: `program` and every entry in `argv_ptrs` are valid C strings for
// the duration of the call. On success `execv` does not return.
unsafe {
libc::execv(program.as_ptr(), argv_ptrs.as_ptr());
}
let err = std::io::Error::last_os_error();
panic!("failed to exec system bubblewrap {program_path}: {err}");
}
fn argv_to_cstrings(argv: &[String]) -> Vec<CString> {
let mut cstrings: Vec<CString> = Vec::with_capacity(argv.len());
for arg in argv {
match CString::new(arg.as_str()) {
Ok(value) => cstrings.push(value),
Err(err) => panic!("failed to convert argv to CString: {err}"),
}
}
cstrings
}
fn make_files_inheritable(files: &[File]) {
for file in files {
clear_cloexec(file.as_raw_fd());
}
}
fn clear_cloexec(fd: libc::c_int) {
// SAFETY: `fd` is an owned descriptor kept alive by `files`.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to read fd flags for preserved bubblewrap file descriptor {fd}: {err}");
}
let cleared_flags = flags & !libc::FD_CLOEXEC;
if cleared_flags == flags {
return;
}
// SAFETY: `fd` is valid and we are only clearing FD_CLOEXEC.
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, cleared_flags) };
if result < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to clear CLOEXEC for preserved bubblewrap file descriptor {fd}: {err}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::NamedTempFile;
#[test]
fn prefers_system_bwrap_when_help_lists_argv0() {
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_with_probe(fake_bwrap_path, |_| true),
BubblewrapLauncher::System(SystemBwrapLauncher {
program: expected,
supports_argv0: true,
})
);
}
#[test]
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_with_probe(fake_bwrap_path, |_| false),
BubblewrapLauncher::System(SystemBwrapLauncher {
program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"),
supports_argv0: false,
})
);
}
#[test]
fn falls_back_to_vendored_when_system_bwrap_is_missing() {
assert_eq!(
preferred_bwrap_launcher_for_path(Path::new("/definitely/not/a/bwrap")),
BubblewrapLauncher::Vendored
);
}
#[test]
fn preserved_files_are_made_inheritable_for_system_exec() {
let file = NamedTempFile::new().expect("temp file");
set_cloexec(file.as_file().as_raw_fd());
make_files_inheritable(std::slice::from_ref(file.as_file()));
assert_eq!(fd_flags(file.as_file().as_raw_fd()) & libc::FD_CLOEXEC, 0);
}
fn set_cloexec(fd: libc::c_int) {
let flags = fd_flags(fd);
// SAFETY: `fd` is valid for the duration of the test.
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
if result < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to set CLOEXEC for test fd {fd}: {err}");
}
}
fn fd_flags(fd: libc::c_int) -> libc::c_int {
// SAFETY: `fd` is valid for the duration of the test.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to read fd flags for test fd {fd}: {err}");
}
flags
}
}