mirror of
https://github.com/openai/codex.git
synced 2026-04-22 07:21:46 +03:00
Compare commits
19 Commits
dev/shaqay
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc1dd287d | ||
|
|
badb1beb36 | ||
|
|
4179952552 | ||
|
|
6270d61439 | ||
|
|
356035c871 | ||
|
|
906cff498c | ||
|
|
46f62ffbb1 | ||
|
|
20e063a18d | ||
|
|
361dae22b2 | ||
|
|
3b0c2edb7f | ||
|
|
83b54acd91 | ||
|
|
ec5a7fd714 | ||
|
|
f45dabf46e | ||
|
|
17e52b756d | ||
|
|
f93598ef4b | ||
|
|
3c69faf447 | ||
|
|
1a2c4fd308 | ||
|
|
b82d0b6748 | ||
|
|
709b9c075f |
@@ -83,6 +83,7 @@ impl BwrapNetworkMode {
|
||||
pub(crate) struct BwrapArgs {
|
||||
pub args: Vec<String>,
|
||||
pub preserved_files: Vec<File>,
|
||||
pub cleanup_mount_points: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
|
||||
@@ -104,6 +105,7 @@ pub(crate) fn create_bwrap_command_args(
|
||||
Ok(BwrapArgs {
|
||||
args: command,
|
||||
preserved_files: Vec::new(),
|
||||
cleanup_mount_points: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
Ok(create_bwrap_flags_full_filesystem(command, options))
|
||||
@@ -143,6 +145,7 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
|
||||
BwrapArgs {
|
||||
args,
|
||||
preserved_files: Vec::new(),
|
||||
cleanup_mount_points: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +160,7 @@ fn create_bwrap_flags(
|
||||
let BwrapArgs {
|
||||
args: filesystem_args,
|
||||
preserved_files,
|
||||
cleanup_mount_points,
|
||||
} = create_filesystem_args(file_system_sandbox_policy, sandbox_policy_cwd)?;
|
||||
let normalized_command_cwd = normalize_command_cwd_for_bwrap(command_cwd);
|
||||
let mut args = Vec::new();
|
||||
@@ -188,6 +192,7 @@ fn create_bwrap_flags(
|
||||
Ok(BwrapArgs {
|
||||
args,
|
||||
preserved_files,
|
||||
cleanup_mount_points,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,6 +300,7 @@ fn create_filesystem_args(
|
||||
args
|
||||
};
|
||||
let mut preserved_files = Vec::new();
|
||||
let mut cleanup_mount_points = Vec::new();
|
||||
let mut allowed_write_paths = Vec::with_capacity(writable_roots.len());
|
||||
for writable_root in &writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
@@ -331,6 +337,7 @@ fn create_filesystem_args(
|
||||
append_unreadable_root_args(
|
||||
&mut args,
|
||||
&mut preserved_files,
|
||||
&mut cleanup_mount_points,
|
||||
unreadable_root,
|
||||
&allowed_write_paths,
|
||||
)?;
|
||||
@@ -366,7 +373,12 @@ fn create_filesystem_args(
|
||||
}
|
||||
read_only_subpaths.sort_by_key(|path| path_depth(path));
|
||||
for subpath in read_only_subpaths {
|
||||
append_read_only_subpath_args(&mut args, &subpath, &allowed_write_paths);
|
||||
append_read_only_subpath_args(
|
||||
&mut args,
|
||||
&mut cleanup_mount_points,
|
||||
&subpath,
|
||||
&allowed_write_paths,
|
||||
);
|
||||
}
|
||||
let mut nested_unreadable_roots: Vec<PathBuf> = unreadable_roots
|
||||
.iter()
|
||||
@@ -382,6 +394,7 @@ fn create_filesystem_args(
|
||||
append_unreadable_root_args(
|
||||
&mut args,
|
||||
&mut preserved_files,
|
||||
&mut cleanup_mount_points,
|
||||
&unreadable_root,
|
||||
&allowed_write_paths,
|
||||
)?;
|
||||
@@ -403,6 +416,7 @@ fn create_filesystem_args(
|
||||
append_unreadable_root_args(
|
||||
&mut args,
|
||||
&mut preserved_files,
|
||||
&mut cleanup_mount_points,
|
||||
&unreadable_root,
|
||||
&allowed_write_paths,
|
||||
)?;
|
||||
@@ -411,6 +425,7 @@ fn create_filesystem_args(
|
||||
Ok(BwrapArgs {
|
||||
args,
|
||||
preserved_files,
|
||||
cleanup_mount_points,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -496,6 +511,7 @@ fn append_mount_target_parent_dir_args(args: &mut Vec<String>, mount_target: &Pa
|
||||
|
||||
fn append_read_only_subpath_args(
|
||||
args: &mut Vec<String>,
|
||||
cleanup_mount_points: &mut Vec<PathBuf>,
|
||||
subpath: &Path,
|
||||
allowed_write_paths: &[PathBuf],
|
||||
) {
|
||||
@@ -512,17 +528,36 @@ fn append_read_only_subpath_args(
|
||||
}
|
||||
|
||||
if !subpath.exists() {
|
||||
// Mask the first missing component so the process cannot create a
|
||||
// protected subtree under a writable root before reaching the leaf.
|
||||
if let Some(first_missing_component) = find_first_non_existent_component(subpath)
|
||||
&& is_within_allowed_write_paths(&first_missing_component, allowed_write_paths)
|
||||
{
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(&first_missing_component));
|
||||
append_bwrap_mount_point_read_only_bind_args(
|
||||
args,
|
||||
cleanup_mount_points,
|
||||
&first_missing_component,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if is_within_allowed_write_paths(subpath, allowed_write_paths) {
|
||||
if subpath.file_name().is_some_and(|name| name == ".codex") && subpath.is_dir() {
|
||||
let subpath = path_to_string(subpath);
|
||||
args.push("--dir".to_string());
|
||||
args.push(subpath.clone());
|
||||
args.push("--ro-bind-try".to_string());
|
||||
args.push(subpath.clone());
|
||||
args.push(subpath.clone());
|
||||
args.push("--remount-ro".to_string());
|
||||
args.push(subpath);
|
||||
return;
|
||||
}
|
||||
if fs::canonicalize(subpath).is_err() {
|
||||
append_bwrap_mount_point_read_only_bind_args(args, cleanup_mount_points, subpath);
|
||||
return;
|
||||
}
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push(path_to_string(subpath));
|
||||
args.push(path_to_string(subpath));
|
||||
@@ -532,6 +567,7 @@ fn append_read_only_subpath_args(
|
||||
fn append_unreadable_root_args(
|
||||
args: &mut Vec<String>,
|
||||
preserved_files: &mut Vec<File>,
|
||||
cleanup_mount_points: &mut Vec<PathBuf>,
|
||||
unreadable_root: &Path,
|
||||
allowed_write_paths: &[PathBuf],
|
||||
) -> Result<()> {
|
||||
@@ -551,9 +587,11 @@ fn append_unreadable_root_args(
|
||||
if let Some(first_missing_component) = find_first_non_existent_component(unreadable_root)
|
||||
&& is_within_allowed_write_paths(&first_missing_component, allowed_write_paths)
|
||||
{
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(&first_missing_component));
|
||||
append_bwrap_mount_point_read_only_bind_args(
|
||||
args,
|
||||
cleanup_mount_points,
|
||||
&first_missing_component,
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -602,18 +640,38 @@ fn append_existing_unreadable_path_args(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
args.push("--perms".to_string());
|
||||
args.push("000".to_string());
|
||||
append_empty_file_read_only_bind_args(args, preserved_files, unreadable_root)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_empty_file_read_only_bind_args(
|
||||
args: &mut Vec<String>,
|
||||
preserved_files: &mut Vec<File>,
|
||||
mount_target: &Path,
|
||||
) -> Result<()> {
|
||||
if preserved_files.is_empty() {
|
||||
preserved_files.push(File::open("/dev/null")?);
|
||||
}
|
||||
let null_fd = preserved_files[0].as_raw_fd().to_string();
|
||||
args.push("--perms".to_string());
|
||||
args.push("000".to_string());
|
||||
args.push("--ro-bind-data".to_string());
|
||||
args.push(null_fd);
|
||||
args.push(path_to_string(unreadable_root));
|
||||
args.push(path_to_string(mount_target));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_bwrap_mount_point_read_only_bind_args(
|
||||
args: &mut Vec<String>,
|
||||
cleanup_mount_points: &mut Vec<PathBuf>,
|
||||
mount_target: &Path,
|
||||
) {
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(mount_target));
|
||||
cleanup_mount_points.push(mount_target.to_path_buf());
|
||||
}
|
||||
|
||||
/// Returns true when `path` is under any allowed writable root.
|
||||
fn is_within_allowed_write_paths(path: &Path, allowed_write_paths: &[PathBuf]) -> bool {
|
||||
allowed_write_paths
|
||||
@@ -1054,33 +1112,122 @@ mod tests {
|
||||
Path::new("/"),
|
||||
)
|
||||
.expect("bwrap fs args");
|
||||
assert_eq!(args.preserved_files.len(), 0);
|
||||
assert_eq!(args.cleanup_mount_points, vec![PathBuf::from("/.codex")]);
|
||||
assert_eq!(
|
||||
args.args,
|
||||
vec![
|
||||
&args.args[..8],
|
||||
[
|
||||
// Start from a read-only view of the full filesystem.
|
||||
"--ro-bind".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
"--ro-bind",
|
||||
"/",
|
||||
"/",
|
||||
// Recreate a writable /dev inside the sandbox.
|
||||
"--dev".to_string(),
|
||||
"/dev".to_string(),
|
||||
"--dev",
|
||||
"/dev",
|
||||
// Make the writable root itself writable again.
|
||||
"--bind".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
// Mask the default protected .codex subpath under that writable
|
||||
// root. Because the root is `/` in this test, the carveout path
|
||||
// appears as `/.codex`.
|
||||
"--ro-bind".to_string(),
|
||||
"/dev/null".to_string(),
|
||||
"/.codex".to_string(),
|
||||
// Rebind /dev after the root bind so device nodes remain
|
||||
// writable/usable inside the writable root.
|
||||
"--bind".to_string(),
|
||||
"/dev".to_string(),
|
||||
"/dev".to_string(),
|
||||
"--bind",
|
||||
"/",
|
||||
"/",
|
||||
]
|
||||
);
|
||||
let codex_mask_index = args
|
||||
.args
|
||||
.windows(3)
|
||||
.position(|window| window == ["--ro-bind", "/dev/null", "/.codex"])
|
||||
.expect("missing protected .codex should be masked under bwrap");
|
||||
let dev_rebind_index = args
|
||||
.args
|
||||
.windows(3)
|
||||
.position(|window| window == ["--bind", "/dev", "/dev"])
|
||||
.expect("expected /dev to be rebound after the writable root");
|
||||
assert!(codex_mask_index < dev_rebind_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_first_missing_component_for_nested_read_only_subpaths() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let protected_path = temp_dir.path().join("missing").join("protected");
|
||||
let first_missing_component = temp_dir.path().join("missing");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(temp_dir.path()).expect("absolute temp dir"),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(protected_path.as_path())
|
||||
.expect("absolute protected path"),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
assert!(
|
||||
args.cleanup_mount_points.contains(&first_missing_component),
|
||||
"missing protected subtree should be registered for cleanup: {:#?}",
|
||||
args.cleanup_mount_points
|
||||
);
|
||||
let first_missing_component = path_to_string(&first_missing_component);
|
||||
let protected_path = path_to_string(&protected_path);
|
||||
|
||||
assert_eq!(args.preserved_files.len(), 0);
|
||||
assert!(
|
||||
args.args.windows(3).any(|window| {
|
||||
window == ["--ro-bind", "/dev/null", first_missing_component.as_str()]
|
||||
}),
|
||||
"missing protected subtree should be masked at first missing component: {:#?}",
|
||||
args.args
|
||||
);
|
||||
assert!(
|
||||
!args
|
||||
.args
|
||||
.windows(3)
|
||||
.any(|window| window == ["--ro-bind", "/dev/null", protected_path.as_str()]),
|
||||
"mask should target the first missing component, not the unreachable leaf: {:#?}",
|
||||
args.args
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_dot_codex_uses_try_bind_fallback_without_cleanup() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let dot_codex = temp_dir.path().join(".codex");
|
||||
std::fs::create_dir(&dot_codex).expect("create .codex");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(temp_dir.path()).expect("absolute temp dir"),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert!(!args.cleanup_mount_points.contains(&dot_codex));
|
||||
let dot_codex = path_to_string(&dot_codex);
|
||||
assert!(
|
||||
args.args
|
||||
.windows(2)
|
||||
.any(|window| window == ["--dir", dot_codex.as_str()])
|
||||
);
|
||||
assert!(
|
||||
args.args
|
||||
.windows(3)
|
||||
.any(|window| window == ["--ro-bind-try", dot_codex.as_str(), dot_codex.as_str()])
|
||||
);
|
||||
assert!(
|
||||
args.args
|
||||
.windows(2)
|
||||
.any(|window| window == ["--remount-ro", dot_codex.as_str()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
162
codex-rs/linux-sandbox/src/bwrap_mount_cleanup.rs
Normal file
162
codex-rs/linux-sandbox/src/bwrap_mount_cleanup.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BwrapMountPointRegistration {
|
||||
mount_point: PathBuf,
|
||||
marker_file: PathBuf,
|
||||
marker_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn register_bwrap_mount_points(
|
||||
mount_points: &[PathBuf],
|
||||
) -> Vec<BwrapMountPointRegistration> {
|
||||
let mut mount_points = mount_points.to_vec();
|
||||
mount_points.sort();
|
||||
mount_points.dedup();
|
||||
|
||||
let mut registrations = Vec::new();
|
||||
for mount_point in mount_points {
|
||||
let marker_dir = bwrap_mount_point_marker_dir(&mount_point);
|
||||
if fs::create_dir_all(&marker_dir).is_err() {
|
||||
continue;
|
||||
}
|
||||
let marker_file = marker_dir.join(std::process::id().to_string());
|
||||
if fs::write(&marker_file, b"").is_err() {
|
||||
continue;
|
||||
}
|
||||
registrations.push(BwrapMountPointRegistration {
|
||||
mount_point,
|
||||
marker_file,
|
||||
marker_dir,
|
||||
});
|
||||
}
|
||||
registrations
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_bwrap_mount_points(registrations: &[BwrapMountPointRegistration]) {
|
||||
for registration in registrations {
|
||||
let _ = fs::remove_file(®istration.marker_file);
|
||||
if has_active_bwrap_mount_point_markers(®istration.marker_dir) {
|
||||
continue;
|
||||
}
|
||||
remove_empty_bwrap_mount_point(®istration.mount_point);
|
||||
let _ = fs::remove_dir(®istration.marker_dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn has_active_bwrap_mount_point_markers(marker_dir: &Path) -> bool {
|
||||
let Ok(entries) = fs::read_dir(marker_dir) else {
|
||||
return false;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let marker_file = entry.path();
|
||||
if marker_pid_is_active(marker_file.file_name()) {
|
||||
return true;
|
||||
}
|
||||
let _ = fs::remove_file(marker_file);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn marker_pid_is_active(pid: Option<&OsStr>) -> bool {
|
||||
let Some(pid) = pid.and_then(OsStr::to_str) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(pid) = pid.parse::<i32>() else {
|
||||
return false;
|
||||
};
|
||||
let kill_res = unsafe { libc::kill(pid, 0) };
|
||||
kill_res == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
|
||||
}
|
||||
|
||||
fn bwrap_mount_point_marker_dir(mount_point: &Path) -> PathBuf {
|
||||
std::env::temp_dir()
|
||||
.join("codex-bwrap-mountpoints")
|
||||
.join(hash_os_str(mount_point.as_os_str()))
|
||||
}
|
||||
|
||||
fn hash_os_str(value: &OsStr) -> String {
|
||||
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
|
||||
const FNV_PRIME: u64 = 0x100000001b3;
|
||||
let mut hash = FNV_OFFSET_BASIS;
|
||||
for byte in value.as_bytes() {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(FNV_PRIME);
|
||||
}
|
||||
format!("{hash:016x}")
|
||||
}
|
||||
|
||||
fn remove_empty_bwrap_mount_point(mount_point: &Path) {
|
||||
let Ok(metadata) = fs::symlink_metadata(mount_point) else {
|
||||
return;
|
||||
};
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_file() && metadata.len() == 0 {
|
||||
let _ = fs::remove_file(mount_point);
|
||||
} else if file_type.is_dir()
|
||||
&& fs::read_dir(mount_point)
|
||||
.map(|mut entries| entries.next().is_none())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = fs::remove_dir(mount_point);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cleanup_bwrap_mount_points_removes_empty_mount_points() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join("empty-file");
|
||||
let empty_dir = temp_dir.path().join("empty-dir");
|
||||
std::fs::write(&empty_file, "").expect("create empty file");
|
||||
std::fs::create_dir(&empty_dir).expect("create empty dir");
|
||||
let registrations = register_bwrap_mount_points(&[empty_file.clone(), empty_dir.clone()]);
|
||||
|
||||
cleanup_bwrap_mount_points(®istrations);
|
||||
|
||||
assert!(!empty_file.exists());
|
||||
assert!(!empty_dir.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_bwrap_mount_points_keeps_non_empty_paths() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let non_empty_file = temp_dir.path().join("non-empty-file");
|
||||
let non_empty_dir = temp_dir.path().join("non-empty-dir");
|
||||
std::fs::write(&non_empty_file, "content").expect("create non-empty file");
|
||||
std::fs::create_dir(&non_empty_dir).expect("create non-empty dir");
|
||||
std::fs::write(non_empty_dir.join("child"), "").expect("create child");
|
||||
let registrations =
|
||||
register_bwrap_mount_points(&[non_empty_file.clone(), non_empty_dir.clone()]);
|
||||
|
||||
cleanup_bwrap_mount_points(®istrations);
|
||||
|
||||
assert!(non_empty_file.exists());
|
||||
assert!(non_empty_dir.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_bwrap_mount_points_defers_when_another_sandbox_is_active() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join("empty-file");
|
||||
std::fs::write(&empty_file, "").expect("create empty file");
|
||||
let registrations = register_bwrap_mount_points(std::slice::from_ref(&empty_file));
|
||||
let active_marker = registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, "").expect("create active marker");
|
||||
|
||||
cleanup_bwrap_mount_points(®istrations);
|
||||
|
||||
assert!(empty_file.exists());
|
||||
std::fs::remove_file(active_marker).expect("remove active marker");
|
||||
let registrations = register_bwrap_mount_points(std::slice::from_ref(&empty_file));
|
||||
cleanup_bwrap_mount_points(®istrations);
|
||||
assert!(!empty_file.exists());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap_mount_cleanup;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod landlock;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod launcher;
|
||||
|
||||
@@ -10,6 +10,8 @@ use std::path::PathBuf;
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::bwrap_mount_cleanup::cleanup_bwrap_mount_points;
|
||||
use crate::bwrap_mount_cleanup::register_bwrap_mount_points;
|
||||
use crate::landlock::apply_sandbox_policy_to_current_thread;
|
||||
use crate::launcher::exec_bwrap;
|
||||
use crate::launcher::preferred_bwrap_supports_argv0;
|
||||
@@ -436,7 +438,13 @@ fn run_bwrap_with_proc_fallback(
|
||||
options,
|
||||
);
|
||||
apply_inner_command_argv0(&mut bwrap_args.args);
|
||||
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
|
||||
let cleanup_mount_points = register_bwrap_mount_points(&bwrap_args.cleanup_mount_points);
|
||||
if cleanup_mount_points.is_empty() {
|
||||
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
|
||||
}
|
||||
let exit_code = run_bwrap_in_child_inherit_stdio(bwrap_args);
|
||||
cleanup_bwrap_mount_points(&cleanup_mount_points);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn bwrap_network_mode(
|
||||
@@ -473,6 +481,7 @@ fn build_bwrap_argv(
|
||||
crate::bwrap::BwrapArgs {
|
||||
args: argv,
|
||||
preserved_files: bwrap_args.preserved_files,
|
||||
cleanup_mount_points: bwrap_args.cleanup_mount_points,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,6 +582,7 @@ fn resolve_true_command() -> String {
|
||||
/// command, and reads are bounded to a fixed max size.
|
||||
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String {
|
||||
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
|
||||
let cleanup_mount_points = register_bwrap_mount_points(&bwrap_args.cleanup_mount_points);
|
||||
|
||||
let mut pipe_fds = [0; 2];
|
||||
let pipe_res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
|
||||
@@ -583,11 +593,7 @@ fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> Str
|
||||
let read_fd = pipe_fds[0];
|
||||
let write_fd = pipe_fds[1];
|
||||
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to fork for bubblewrap: {err}");
|
||||
}
|
||||
let pid = fork_bwrap_or_panic();
|
||||
|
||||
if pid == 0 {
|
||||
// Child: redirect stderr to the pipe, then run bubblewrap.
|
||||
@@ -614,16 +620,56 @@ fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> Str
|
||||
panic!("failed to read bubblewrap stderr: {err}");
|
||||
}
|
||||
|
||||
let mut status: libc::c_int = 0;
|
||||
let wait_res = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, 0) };
|
||||
if wait_res < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("waitpid failed for bubblewrap child: {err}");
|
||||
}
|
||||
let _ = wait_for_bwrap_child(pid);
|
||||
cleanup_bwrap_mount_points(&cleanup_mount_points);
|
||||
|
||||
String::from_utf8_lossy(&stderr_bytes).into_owned()
|
||||
}
|
||||
|
||||
fn run_bwrap_in_child_inherit_stdio(bwrap_args: crate::bwrap::BwrapArgs) -> i32 {
|
||||
let pid = fork_bwrap_or_panic();
|
||||
if pid == 0 {
|
||||
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
|
||||
}
|
||||
wait_for_bwrap_child(pid)
|
||||
}
|
||||
|
||||
fn fork_bwrap_or_panic() -> libc::pid_t {
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to fork for bubblewrap: {err}");
|
||||
}
|
||||
pid
|
||||
}
|
||||
|
||||
fn wait_for_bwrap_child(pid: libc::pid_t) -> i32 {
|
||||
let mut status: libc::c_int = 0;
|
||||
loop {
|
||||
let wait_res = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, 0) };
|
||||
if wait_res == pid {
|
||||
return wait_status_to_exit_code(status);
|
||||
}
|
||||
if wait_res < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EINTR) {
|
||||
continue;
|
||||
}
|
||||
panic!("waitpid failed for bubblewrap child: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_status_to_exit_code(status: libc::c_int) -> i32 {
|
||||
if libc::WIFEXITED(status) {
|
||||
libc::WEXITSTATUS(status)
|
||||
} else if libc::WIFSIGNALED(status) {
|
||||
128 + libc::WTERMSIG(status)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// Close an owned file descriptor and panic with context on failure.
|
||||
///
|
||||
/// We use explicit close() checks here (instead of ignoring return codes)
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
@@ -43,6 +44,18 @@ const NETWORK_TIMEOUT_MS: u64 = 10_000;
|
||||
|
||||
const BWRAP_UNAVAILABLE_ERR: &str = "build-time bubblewrap is not available in this build.";
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn codex_linux_sandbox_exe() -> PathBuf {
|
||||
let path = PathBuf::from(env!("CARGO_BIN_EXE_codex-linux-sandbox"));
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.expect("cwd should exist")
|
||||
.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_env_from_core_vars() -> HashMap<String, String> {
|
||||
let policy = ShellEnvironmentPolicy::default();
|
||||
create_env(&policy, /*thread_id*/ None)
|
||||
@@ -75,12 +88,33 @@ async fn run_cmd_output(
|
||||
.expect("sandboxed command should execute")
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
async fn run_cmd_result_with_writable_roots(
|
||||
cmd: &[&str],
|
||||
writable_roots: &[PathBuf],
|
||||
timeout_ms: u64,
|
||||
use_legacy_landlock: bool,
|
||||
network_access: bool,
|
||||
) -> Result<codex_protocol::exec_output::ExecToolCallOutput> {
|
||||
let cwd = std::env::current_dir().expect("cwd should exist");
|
||||
run_cmd_result_with_writable_roots_in_cwd(
|
||||
cmd,
|
||||
writable_roots,
|
||||
cwd.as_path(),
|
||||
timeout_ms,
|
||||
use_legacy_landlock,
|
||||
network_access,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_cmd_result_with_writable_roots_in_cwd(
|
||||
cmd: &[&str],
|
||||
writable_roots: &[PathBuf],
|
||||
cwd: &Path,
|
||||
timeout_ms: u64,
|
||||
use_legacy_landlock: bool,
|
||||
network_access: bool,
|
||||
) -> Result<codex_protocol::exec_output::ExecToolCallOutput> {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: writable_roots
|
||||
@@ -97,11 +131,12 @@ async fn run_cmd_result_with_writable_roots(
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
run_cmd_result_with_policies(
|
||||
run_cmd_result_with_policies_in_cwd(
|
||||
cmd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
cwd,
|
||||
timeout_ms,
|
||||
use_legacy_landlock,
|
||||
)
|
||||
@@ -117,7 +152,29 @@ async fn run_cmd_result_with_policies(
|
||||
timeout_ms: u64,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Result<codex_protocol::exec_output::ExecToolCallOutput> {
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd should exist");
|
||||
let cwd = std::env::current_dir().expect("cwd should exist");
|
||||
run_cmd_result_with_policies_in_cwd(
|
||||
cmd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
cwd.as_path(),
|
||||
timeout_ms,
|
||||
use_legacy_landlock,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_cmd_result_with_policies_in_cwd(
|
||||
cmd: &[&str],
|
||||
sandbox_policy: SandboxPolicy,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
cwd: &Path,
|
||||
timeout_ms: u64,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Result<codex_protocol::exec_output::ExecToolCallOutput> {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd)?;
|
||||
let sandbox_cwd = cwd.clone();
|
||||
let params = ExecParams {
|
||||
command: cmd.iter().copied().map(str::to_owned).collect(),
|
||||
@@ -132,8 +189,7 @@ async fn run_cmd_result_with_policies(
|
||||
justification: None,
|
||||
arg0: None,
|
||||
};
|
||||
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
|
||||
let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program));
|
||||
let codex_linux_sandbox_exe = Some(codex_linux_sandbox_exe());
|
||||
|
||||
process_exec_tool_call(
|
||||
params,
|
||||
@@ -258,6 +314,45 @@ async fn bwrap_populates_minimal_dev_nodes() {
|
||||
assert_eq!(output.exit_code, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bwrap_dev_nodes_work_and_missing_workspace_dot_codex_stays_blocked() {
|
||||
if should_skip_bwrap_tests().await {
|
||||
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let tmpdir = tempfile::tempdir().expect("tempdir");
|
||||
let writable_roots = vec![tmpdir.path().to_path_buf()];
|
||||
let output = run_cmd_result_with_writable_roots_in_cwd(
|
||||
&[
|
||||
"bash",
|
||||
"-lc",
|
||||
concat!(
|
||||
": >/dev/null && ",
|
||||
"if mkdir .codex 2>/dev/null; then exit 42; fi && ",
|
||||
"head -c 8 /dev/zero | od -An -tx1"
|
||||
),
|
||||
],
|
||||
&writable_roots,
|
||||
tmpdir.path(),
|
||||
LONG_TIMEOUT_MS,
|
||||
/*use_legacy_landlock*/ false,
|
||||
/*network_access*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("sandboxed command should execute");
|
||||
|
||||
assert_eq!(output.exit_code, 0);
|
||||
assert_eq!(
|
||||
output.stdout.text.split_whitespace().collect::<Vec<_>>(),
|
||||
vec!["00", "00", "00", "00", "00", "00", "00", "00"]
|
||||
);
|
||||
assert!(
|
||||
!tmpdir.path().join(".codex").exists(),
|
||||
"bwrap-created .codex mountpoint should be cleaned up after command exit"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bwrap_preserves_writable_dev_shm_bind_mount() {
|
||||
if should_skip_bwrap_tests().await {
|
||||
@@ -394,8 +489,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
|
||||
};
|
||||
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
|
||||
let codex_linux_sandbox_exe: Option<PathBuf> = Some(PathBuf::from(sandbox_program));
|
||||
let codex_linux_sandbox_exe: Option<PathBuf> = Some(codex_linux_sandbox_exe());
|
||||
let result = process_exec_tool_call(
|
||||
params,
|
||||
&sandbox_policy,
|
||||
@@ -554,7 +648,7 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() {
|
||||
let blocked_target = blocked.join("secret.txt");
|
||||
// These tests bypass the usual legacy-policy bridge, so explicitly keep
|
||||
// the sandbox helper binary and minimal runtime paths readable.
|
||||
let sandbox_helper_dir = PathBuf::from(env!("CARGO_BIN_EXE_codex-linux-sandbox"))
|
||||
let sandbox_helper_dir = codex_linux_sandbox_exe()
|
||||
.parent()
|
||||
.expect("sandbox helper should have a parent")
|
||||
.to_path_buf();
|
||||
@@ -627,7 +721,7 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() {
|
||||
let allowed_target = allowed.join("note.txt");
|
||||
// These tests bypass the usual legacy-policy bridge, so explicitly keep
|
||||
// the sandbox helper binary and minimal runtime paths readable.
|
||||
let sandbox_helper_dir = PathBuf::from(env!("CARGO_BIN_EXE_codex-linux-sandbox"))
|
||||
let sandbox_helper_dir = codex_linux_sandbox_exe()
|
||||
.parent()
|
||||
.expect("sandbox helper should have a parent")
|
||||
.to_path_buf();
|
||||
|
||||
Reference in New Issue
Block a user