use std::ffi::CStr;
use std::ffi::CString;
use std::fs::File;
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::bundled_bwrap;
use crate::bundled_bwrap::BundledBwrapLauncher;
use crate::exec_util::argv_to_cstrings;
use crate::exec_util::make_files_inheritable;
use zerobox_sandboxing::find_system_bwrap_in_path;
use zerobox_utils_absolute_path::AbsolutePathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
enum BubblewrapLauncher {
System(SystemBwrapLauncher),
Bundled(BundledBwrapLauncher),
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SystemBwrapLauncher {
program: AbsolutePathBuf,
supports_argv0: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SystemBwrapCapabilities {
supports_argv0: bool,
supports_perms: 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::Bundled(launcher) => launcher.exec(argv, preserved_files),
BubblewrapLauncher::Unavailable => {
panic!(
"bubblewrap is unavailable: no system bwrap was found on PATH and no bundled \
codex-resources/bwrap binary was found next to the Codex executable"
)
}
}
}
fn preferred_bwrap_launcher() -> BubblewrapLauncher {
static LAUNCHER: OnceLock<BubblewrapLauncher> = OnceLock::new();
LAUNCHER
.get_or_init(|| {
if let Some(path) = find_system_bwrap_in_path()
&& let Some(launcher) = system_bwrap_launcher_for_path(&path)
{
return BubblewrapLauncher::System(launcher);
}
match bundled_bwrap::launcher() {
Some(launcher) => BubblewrapLauncher::Bundled(launcher),
None => BubblewrapLauncher::Unavailable,
}
})
.clone()
}
fn system_bwrap_launcher_for_path(system_bwrap_path: &Path) -> Option<SystemBwrapLauncher> {
system_bwrap_launcher_for_path_with_probe(system_bwrap_path, system_bwrap_capabilities)
}
fn system_bwrap_launcher_for_path_with_probe(
system_bwrap_path: &Path,
system_bwrap_capabilities: impl FnOnce(&Path) -> Option<SystemBwrapCapabilities>,
) -> Option<SystemBwrapLauncher> {
if !system_bwrap_path.is_file() {
return None;
}
let Some(SystemBwrapCapabilities {
supports_argv0,
supports_perms: true,
}) = system_bwrap_capabilities(system_bwrap_path)
else {
return None;
};
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()
),
};
Some(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::Bundled(_) | BubblewrapLauncher::Unavailable => true,
}
}
fn system_bwrap_capabilities(system_bwrap_path: &Path) -> Option<SystemBwrapCapabilities> {
let output = match Command::new(system_bwrap_path).arg("--help").output() {
Ok(output) => output,
Err(_) => return None,
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
Some(SystemBwrapCapabilities {
supports_argv0: stdout.contains("--argv0") || stderr.contains("--argv0"),
supports_perms: stdout.contains("--perms") || stderr.contains("--perms"),
})
}
fn exec_system_bwrap(
program: &AbsolutePathBuf,
argv: Vec<String>,
preserved_files: Vec<File>,
) -> ! {
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(CString::as_c_str)
.map(CStr::as_ptr)
.collect();
argv_ptrs.push(std::ptr::null());
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}");
}
#[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!(
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
Some(SystemBwrapCapabilities {
supports_argv0: true,
supports_perms: true,
})
}),
Some(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!(
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
Some(SystemBwrapCapabilities {
supports_argv0: false,
supports_perms: true,
})
}),
Some(SystemBwrapLauncher {
program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"),
supports_argv0: false,
})
);
}
#[test]
fn ignores_system_bwrap_when_system_bwrap_lacks_perms() {
let fake_bwrap = NamedTempFile::new().expect("temp file");
assert_eq!(
system_bwrap_launcher_for_path_with_probe(fake_bwrap.path(), |_| {
Some(SystemBwrapCapabilities {
supports_argv0: false,
supports_perms: false,
})
}),
None
);
}
#[test]
fn ignores_system_bwrap_when_system_bwrap_is_missing() {
assert_eq!(
system_bwrap_launcher_for_path(Path::new("/definitely/not/a/bwrap")),
None
);
}
}