From 3eab33bf0f6dd004e29d16cfb7c8c878bb646a24 Mon Sep 17 00:00:00 2001 From: Eva Wong Date: Wed, 29 Apr 2026 16:40:41 -0700 Subject: [PATCH] Split Linux metadata runtime modules --- codex-rs/linux-sandbox/src/bwrap.rs | 227 +---- .../src/file_system_protected_metadata.rs | 190 ++++ .../file_system_protected_metadata_cleanup.rs | 407 ++++++++ ...system_protected_metadata_cleanup_tests.rs | 133 +++ .../file_system_protected_metadata_monitor.rs | 160 +++ .../file_system_protected_metadata_runtime.rs | 418 ++++++++ ...system_protected_metadata_runtime_tests.rs | 46 + codex-rs/linux-sandbox/src/lib.rs | 8 + codex-rs/linux-sandbox/src/linux_run_main.rs | 950 +----------------- .../linux-sandbox/src/linux_run_main_tests.rs | 178 ---- 10 files changed, 1393 insertions(+), 1324 deletions(-) create mode 100644 codex-rs/linux-sandbox/src/file_system_protected_metadata.rs create mode 100644 codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup.rs create mode 100644 codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup_tests.rs create mode 100644 codex-rs/linux-sandbox/src/file_system_protected_metadata_monitor.rs create mode 100644 codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime.rs create mode 100644 codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime_tests.rs diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 22e7d72531..9d1c54cd4b 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -19,11 +19,14 @@ use std::fs::Metadata; use std::io; use std::os::fd::AsRawFd; use std::os::unix::ffi::OsStringExt; -use std::os::unix::fs::MetadataExt; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use crate::file_system_protected_metadata::FileSystemPermissionsEnforcement; +use crate::file_system_protected_metadata::SyntheticMountTarget; +use crate::file_system_protected_metadata::append_metadata_path_masks_for_writable_root; +use crate::file_system_protected_metadata::append_protected_create_targets_for_writable_root; use codex_protocol::error::CodexErr; use codex_protocol::error::Result; use codex_protocol::permissions::is_protected_metadata_name; @@ -107,121 +110,7 @@ impl BwrapNetworkMode { pub(crate) struct BwrapArgs { pub args: Vec, pub preserved_files: Vec, - pub synthetic_mount_targets: Vec, - pub protected_create_targets: Vec, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct FileIdentity { - dev: u64, - ino: u64, -} - -impl FileIdentity { - fn from_metadata(metadata: &Metadata) -> Self { - Self { - dev: metadata.dev(), - ino: metadata.ino(), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum SyntheticMountTargetKind { - EmptyFile, - EmptyDirectory, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct SyntheticMountTarget { - path: PathBuf, - kind: SyntheticMountTargetKind, - // If an empty metadata path was already present, remember its inode so - // cleanup does not delete a real pre-existing file or directory. - pre_existing_path: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct ProtectedCreateTarget { - path: PathBuf, -} - -impl ProtectedCreateTarget { - pub(crate) fn missing(path: &Path) -> Self { - Self { - path: path.to_path_buf(), - } - } - - pub(crate) fn path(&self) -> &Path { - &self.path - } -} - -impl SyntheticMountTarget { - pub(crate) fn missing(path: &Path) -> Self { - Self { - path: path.to_path_buf(), - kind: SyntheticMountTargetKind::EmptyFile, - pre_existing_path: None, - } - } - - pub(crate) fn missing_empty_directory(path: &Path) -> Self { - Self { - path: path.to_path_buf(), - kind: SyntheticMountTargetKind::EmptyDirectory, - pre_existing_path: None, - } - } - - pub(crate) fn existing_empty_file(path: &Path, metadata: &Metadata) -> Self { - Self { - path: path.to_path_buf(), - kind: SyntheticMountTargetKind::EmptyFile, - pre_existing_path: Some(FileIdentity::from_metadata(metadata)), - } - } - - fn existing_empty_directory(path: &Path, metadata: &Metadata) -> Self { - Self { - path: path.to_path_buf(), - kind: SyntheticMountTargetKind::EmptyDirectory, - pre_existing_path: Some(FileIdentity::from_metadata(metadata)), - } - } - - pub(crate) fn preserves_pre_existing_path(&self) -> bool { - self.pre_existing_path.is_some() - } - - pub(crate) fn path(&self) -> &Path { - &self.path - } - - pub(crate) fn kind(&self) -> SyntheticMountTargetKind { - self.kind - } - - pub(crate) fn should_remove_after_bwrap(&self, metadata: &Metadata) -> bool { - match self.kind { - SyntheticMountTargetKind::EmptyFile => { - if !metadata.file_type().is_file() || metadata.len() != 0 { - return false; - } - } - SyntheticMountTargetKind::EmptyDirectory => { - if !metadata.file_type().is_dir() { - return false; - } - } - } - - match self.pre_existing_path { - Some(pre_existing_path) => pre_existing_path != FileIdentity::from_metadata(metadata), - None => true, - } - } + pub file_system_permissions_enforcement: FileSystemPermissionsEnforcement, } /// Wrap a command with bubblewrap so the filesystem is read-only by default, @@ -247,8 +136,7 @@ pub(crate) fn create_bwrap_command_args( Ok(BwrapArgs { args: command, preserved_files: Vec::new(), - synthetic_mount_targets: Vec::new(), - protected_create_targets: Vec::new(), + file_system_permissions_enforcement: FileSystemPermissionsEnforcement::default(), }) } else { Ok(create_bwrap_flags_full_filesystem(command, options)) @@ -288,8 +176,7 @@ fn create_bwrap_flags_full_filesystem(command: Vec, options: BwrapOption BwrapArgs { args, preserved_files: Vec::new(), - synthetic_mount_targets: Vec::new(), - protected_create_targets: Vec::new(), + file_system_permissions_enforcement: FileSystemPermissionsEnforcement::default(), } } @@ -304,8 +191,7 @@ fn create_bwrap_flags( let BwrapArgs { args: filesystem_args, preserved_files, - synthetic_mount_targets, - protected_create_targets, + file_system_permissions_enforcement, } = create_filesystem_args( file_system_sandbox_policy, sandbox_policy_cwd, @@ -343,8 +229,7 @@ fn create_bwrap_flags( Ok(BwrapArgs { args, preserved_files, - synthetic_mount_targets, - protected_create_targets, + file_system_permissions_enforcement, }) } @@ -513,8 +398,7 @@ fn create_filesystem_args( let mut bwrap_args = BwrapArgs { args, preserved_files: Vec::new(), - synthetic_mount_targets: Vec::new(), - protected_create_targets: Vec::new(), + file_system_permissions_enforcement: FileSystemPermissionsEnforcement::default(), }; let mut allowed_write_paths = Vec::with_capacity(writable_roots.len()); for writable_root in &writable_roots { @@ -586,7 +470,7 @@ fn create_filesystem_args( read_only_subpaths = remap_paths_for_symlink_target(read_only_subpaths, root, target); } append_protected_create_targets_for_writable_root( - &mut bwrap_args, + &mut bwrap_args.file_system_permissions_enforcement, &protected_metadata_names, root, symlink_target.as_deref(), @@ -629,74 +513,6 @@ fn create_filesystem_args( Ok(bwrap_args) } -fn append_protected_create_targets_for_writable_root( - bwrap_args: &mut BwrapArgs, - protected_metadata_names: &[String], - root: &Path, - symlink_target: Option<&Path>, - read_only_subpaths: &[PathBuf], -) { - for name in protected_metadata_names { - let mut path = root.join(name); - if let Some(target) = symlink_target - && let Ok(relative_path) = path.strip_prefix(root) - { - path = target.join(relative_path); - } - if read_only_subpaths.iter().any(|subpath| subpath == &path) || path.exists() { - continue; - } - bwrap_args - .protected_create_targets - .push(ProtectedCreateTarget::missing(&path)); - } -} - -fn append_metadata_path_masks_for_writable_root( - read_only_subpaths: &mut Vec, - root: &Path, - mount_root: &Path, - protected_metadata_names: &[String], -) { - for name in protected_metadata_names { - let path = root.join(name); - if should_leave_missing_git_for_parent_repo_discovery(mount_root, name) { - continue; - } - if !read_only_subpaths.iter().any(|subpath| subpath == &path) { - read_only_subpaths.push(path); - } - } -} - -fn should_leave_missing_git_for_parent_repo_discovery(mount_root: &Path, name: &str) -> bool { - let path = mount_root.join(name); - name == ".git" - && matches!( - path.symlink_metadata(), - Err(err) if err.kind() == io::ErrorKind::NotFound - ) - && mount_root - .ancestors() - .skip(1) - .any(ancestor_has_git_metadata) -} - -fn ancestor_has_git_metadata(ancestor: &Path) -> bool { - let git_path = ancestor.join(".git"); - let Ok(metadata) = git_path.symlink_metadata() else { - return false; - }; - if metadata.is_dir() { - return git_path.join("HEAD").symlink_metadata().is_ok(); - } - if metadata.is_file() { - return fs::read_to_string(git_path) - .is_ok_and(|contents| contents.trim_start().starts_with("gitdir:")); - } - false -} - fn expand_unreadable_globs_with_ripgrep( patterns: &[String], cwd: &Path, @@ -1095,6 +911,7 @@ fn append_missing_read_only_subpath_args(bwrap_args: &mut BwrapArgs, path: &Path if path.file_name().is_some_and(is_protected_metadata_name) { append_empty_directory_args(bwrap_args, path); bwrap_args + .file_system_permissions_enforcement .synthetic_mount_targets .push(SyntheticMountTarget::missing_empty_directory(path)); return Ok(()); @@ -1106,6 +923,7 @@ fn append_missing_read_only_subpath_args(bwrap_args: &mut BwrapArgs, path: &Path fn append_missing_empty_file_bind_data_args(bwrap_args: &mut BwrapArgs, path: &Path) -> Result<()> { append_empty_file_bind_data_args(bwrap_args, path)?; bwrap_args + .file_system_permissions_enforcement .synthetic_mount_targets .push(SyntheticMountTarget::missing(path)); Ok(()) @@ -1118,6 +936,7 @@ fn append_existing_empty_file_bind_data_args( ) -> Result<()> { append_empty_file_bind_data_args(bwrap_args, path)?; bwrap_args + .file_system_permissions_enforcement .synthetic_mount_targets .push(SyntheticMountTarget::existing_empty_file(path, metadata)); Ok(()) @@ -1130,6 +949,7 @@ fn append_existing_empty_directory_args( ) { append_empty_directory_args(bwrap_args, path); bwrap_args + .file_system_permissions_enforcement .synthetic_mount_targets .push(SyntheticMountTarget::existing_empty_directory( path, metadata, @@ -1777,7 +1597,10 @@ mod tests { ); let metadata = std::fs::symlink_metadata(&dot_git).expect("stat .git"); assert!( - !args.synthetic_mount_targets[0].should_remove_after_bwrap(&metadata), + !args + .file_system_permissions_enforcement + .synthetic_mount_targets[0] + .should_remove_after_bwrap(&metadata), "pre-existing empty preserved files must not be cleaned up as synthetic targets", ); } @@ -2552,7 +2375,11 @@ mod tests { let blocked_file_str = path_to_string(blocked_file.as_path()); assert_eq!(args.preserved_files.len(), 1); - assert!(args.synthetic_mount_targets.is_empty()); + assert!( + args.file_system_permissions_enforcement + .synthetic_mount_targets + .is_empty() + ); assert!(args.args.windows(5).any(|window| { window[0] == "--perms" && window[1] == "000" @@ -2692,14 +2519,16 @@ mod tests { } fn synthetic_mount_target_paths(args: &BwrapArgs) -> Vec { - args.synthetic_mount_targets + args.file_system_permissions_enforcement + .synthetic_mount_targets .iter() .map(|target| target.path().to_path_buf()) .collect() } fn protected_create_target_paths(args: &BwrapArgs) -> Vec { - args.protected_create_targets + args.file_system_permissions_enforcement + .protected_create_targets .iter() .map(|target| target.path().to_path_buf()) .collect() diff --git a/codex-rs/linux-sandbox/src/file_system_protected_metadata.rs b/codex-rs/linux-sandbox/src/file_system_protected_metadata.rs new file mode 100644 index 0000000000..1c539f6d6b --- /dev/null +++ b/codex-rs/linux-sandbox/src/file_system_protected_metadata.rs @@ -0,0 +1,190 @@ +use std::fs; +use std::fs::Metadata; +use std::io; +use std::os::unix::fs::MetadataExt; +use std::path::Path; +use std::path::PathBuf; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct FileIdentity { + dev: u64, + ino: u64, +} + +impl FileIdentity { + fn from_metadata(metadata: &Metadata) -> Self { + Self { + dev: metadata.dev(), + ino: metadata.ino(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct SyntheticMountTarget { + path: PathBuf, + is_directory: bool, + // If an empty metadata path was already present, remember its inode so + // cleanup does not delete a real pre-existing file or directory. + pre_existing_path: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ProtectedCreateTarget { + path: PathBuf, +} + +#[derive(Debug, Default)] +pub(crate) struct FileSystemPermissionsEnforcement { + pub(crate) synthetic_mount_targets: Vec, + pub(crate) protected_create_targets: Vec, +} + +impl ProtectedCreateTarget { + pub(crate) fn missing(path: &Path) -> Self { + Self { + path: path.to_path_buf(), + } + } + + pub(crate) fn path(&self) -> &Path { + &self.path + } +} + +impl SyntheticMountTarget { + pub(crate) fn missing(path: &Path) -> Self { + Self { + path: path.to_path_buf(), + is_directory: false, + pre_existing_path: None, + } + } + + pub(crate) fn missing_empty_directory(path: &Path) -> Self { + Self { + path: path.to_path_buf(), + is_directory: true, + pre_existing_path: None, + } + } + + pub(crate) fn existing_empty_file(path: &Path, metadata: &Metadata) -> Self { + Self { + path: path.to_path_buf(), + is_directory: false, + pre_existing_path: Some(FileIdentity::from_metadata(metadata)), + } + } + + pub(crate) fn existing_empty_directory(path: &Path, metadata: &Metadata) -> Self { + Self { + path: path.to_path_buf(), + is_directory: true, + pre_existing_path: Some(FileIdentity::from_metadata(metadata)), + } + } + + pub(crate) fn preserves_pre_existing_path(&self) -> bool { + self.pre_existing_path.is_some() + } + + pub(crate) fn path(&self) -> &Path { + &self.path + } + + pub(crate) fn is_directory(&self) -> bool { + self.is_directory + } + + pub(crate) fn without_pre_existing_path(&self) -> Self { + Self { + path: self.path.clone(), + is_directory: self.is_directory, + pre_existing_path: None, + } + } + + pub(crate) fn should_remove_after_bwrap(&self, metadata: &Metadata) -> bool { + if self.is_directory { + if !metadata.file_type().is_dir() { + return false; + } + } else if !metadata.file_type().is_file() || metadata.len() != 0 { + return false; + } + + match self.pre_existing_path { + Some(pre_existing_path) => pre_existing_path != FileIdentity::from_metadata(metadata), + None => true, + } + } +} + +pub(crate) fn append_protected_create_targets_for_writable_root( + enforcement: &mut FileSystemPermissionsEnforcement, + protected_metadata_names: &[String], + root: &Path, + symlink_target: Option<&Path>, + read_only_subpaths: &[PathBuf], +) { + for name in protected_metadata_names { + let mut path = root.join(name); + if let Some(target) = symlink_target + && let Ok(relative_path) = path.strip_prefix(root) + { + path = target.join(relative_path); + } + if read_only_subpaths.iter().any(|subpath| subpath == &path) || path.exists() { + continue; + } + enforcement + .protected_create_targets + .push(ProtectedCreateTarget::missing(&path)); + } +} + +pub(crate) fn append_metadata_path_masks_for_writable_root( + read_only_subpaths: &mut Vec, + root: &Path, + mount_root: &Path, + protected_metadata_names: &[String], +) { + for name in protected_metadata_names { + let path = root.join(name); + if should_leave_missing_git_for_parent_repo_discovery(mount_root, name) { + continue; + } + if !read_only_subpaths.iter().any(|subpath| subpath == &path) { + read_only_subpaths.push(path); + } + } +} + +fn should_leave_missing_git_for_parent_repo_discovery(mount_root: &Path, name: &str) -> bool { + let path = mount_root.join(name); + name == ".git" + && matches!( + path.symlink_metadata(), + Err(err) if err.kind() == io::ErrorKind::NotFound + ) + && mount_root + .ancestors() + .skip(1) + .any(ancestor_has_git_metadata) +} + +fn ancestor_has_git_metadata(ancestor: &Path) -> bool { + let git_path = ancestor.join(".git"); + let Ok(metadata) = git_path.symlink_metadata() else { + return false; + }; + if metadata.is_dir() { + return git_path.join("HEAD").symlink_metadata().is_ok(); + } + if metadata.is_file() { + return fs::read_to_string(git_path) + .is_ok_and(|contents| contents.trim_start().starts_with("gitdir:")); + } + false +} diff --git a/codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup.rs b/codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup.rs new file mode 100644 index 0000000000..c8e9d8afe5 --- /dev/null +++ b/codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup.rs @@ -0,0 +1,407 @@ +use std::fs; +use std::fs::OpenOptions; +use std::os::fd::AsRawFd; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +use crate::file_system_protected_metadata::ProtectedCreateTarget; +use crate::file_system_protected_metadata::SyntheticMountTarget; + +pub(crate) type ViolationReporter = fn(&Path); + +const SYNTHETIC_MOUNT_MARKER_SYNTHETIC: &[u8] = b"synthetic\n"; +const SYNTHETIC_MOUNT_MARKER_EXISTING: &[u8] = b"existing\n"; +const PROTECTED_CREATE_MARKER: &[u8] = b"protected-create\n"; + +pub(crate) struct TargetRegistration { + target: T, + marker_file: PathBuf, + marker_dir: PathBuf, +} + +pub(crate) fn register_synthetic_mount_targets( + targets: &[SyntheticMountTarget], +) -> Vec> { + with_metadata_runtime_registry_lock(|| { + targets + .iter() + .map(|target| { + let marker_dir = metadata_runtime_marker_dir(target.path()); + fs::create_dir_all(&marker_dir).unwrap_or_else(|err| { + panic!( + "failed to create metadata runtime marker directory {}: {err}", + marker_dir.display() + ) + }); + let target = if target.preserves_pre_existing_path() + && metadata_runtime_marker_dir_has_active_synthetic_owner(&marker_dir) + { + target.without_pre_existing_path() + } else { + target.clone() + }; + let marker_file = marker_dir.join(std::process::id().to_string()); + fs::write(&marker_file, synthetic_mount_marker_contents(&target)).unwrap_or_else( + |err| { + panic!( + "failed to register synthetic bubblewrap mount target {}: {err}", + target.path().display() + ) + }, + ); + TargetRegistration { + target, + marker_file, + marker_dir, + } + }) + .collect() + }) +} + +pub(crate) fn register_protected_create_targets( + targets: &[ProtectedCreateTarget], +) -> Vec> { + with_metadata_runtime_registry_lock(|| { + targets + .iter() + .map(|target| { + let marker_dir = metadata_runtime_marker_dir(target.path()); + fs::create_dir_all(&marker_dir).unwrap_or_else(|err| { + panic!( + "failed to create protected create marker directory {}: {err}", + marker_dir.display() + ) + }); + let marker_file = marker_dir.join(std::process::id().to_string()); + fs::write(&marker_file, PROTECTED_CREATE_MARKER).unwrap_or_else(|err| { + panic!( + "failed to register protected create target {}: {err}", + target.path().display() + ) + }); + TargetRegistration { + target: target.clone(), + marker_file, + marker_dir, + } + }) + .collect() + }) +} + +fn synthetic_mount_marker_contents(target: &SyntheticMountTarget) -> &'static [u8] { + if target.preserves_pre_existing_path() { + SYNTHETIC_MOUNT_MARKER_EXISTING + } else { + SYNTHETIC_MOUNT_MARKER_SYNTHETIC + } +} + +fn metadata_runtime_marker_dir_has_active_synthetic_owner(marker_dir: &Path) -> bool { + metadata_runtime_marker_dir_has_active_process_matching(marker_dir, |path| { + match fs::read(path) { + Ok(contents) => contents == SYNTHETIC_MOUNT_MARKER_SYNTHETIC, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, + Err(err) => panic!( + "failed to read metadata runtime marker {}: {err}", + path.display() + ), + } + }) +} + +fn metadata_runtime_marker_dir_has_active_process(marker_dir: &Path) -> bool { + metadata_runtime_marker_dir_has_active_process_matching(marker_dir, |_| true) +} + +fn metadata_runtime_marker_dir_has_active_process_matching( + marker_dir: &Path, + matches_marker: impl Fn(&Path) -> bool, +) -> bool { + let entries = match fs::read_dir(marker_dir) { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return false, + Err(err) => panic!( + "failed to read metadata runtime marker directory {}: {err}", + marker_dir.display() + ), + }; + for entry in entries { + let entry = entry.unwrap_or_else(|err| { + panic!( + "failed to read metadata runtime marker in {}: {err}", + marker_dir.display() + ) + }); + let path = entry.path(); + let Some(pid) = path + .file_name() + .and_then(|name| name.to_str()) + .and_then(|name| name.parse::().ok()) + else { + continue; + }; + if !process_is_active(pid) { + match fs::remove_file(&path) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => panic!( + "failed to remove stale metadata runtime marker {}: {err}", + path.display() + ), + } + continue; + } + let matches_marker = matches_marker(&path); + if matches_marker { + return true; + } + } + false +} + +pub(crate) fn cleanup_synthetic_mount_targets( + targets: &[TargetRegistration], +) { + with_metadata_runtime_registry_lock(|| { + for target in targets.iter().rev() { + match fs::remove_file(&target.marker_file) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => panic!( + "failed to unregister synthetic bubblewrap mount target {}: {err}", + target.target.path().display() + ), + } + } + + for target in targets.iter().rev() { + if metadata_runtime_marker_dir_has_active_process(&target.marker_dir) { + continue; + } + remove_synthetic_mount_target(&target.target); + match fs::remove_dir(&target.marker_dir) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {} + Err(err) => panic!( + "failed to remove metadata runtime marker directory {}: {err}", + target.marker_dir.display() + ), + } + } + }); +} + +pub(crate) fn cleanup_protected_create_targets( + targets: &[TargetRegistration], + report_violation: ViolationReporter, +) -> bool { + with_metadata_runtime_registry_lock(|| { + for target in targets.iter().rev() { + match fs::remove_file(&target.marker_file) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => panic!( + "failed to unregister protected create target {}: {err}", + target.target.path().display() + ), + } + } + + let mut violation = false; + for target in targets.iter().rev() { + if metadata_runtime_marker_dir_has_active_process(&target.marker_dir) { + if target.target.path().exists() { + violation = true; + } + continue; + } + violation |= remove_protected_create_target(&target.target, report_violation); + match fs::remove_dir(&target.marker_dir) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {} + Err(err) => panic!( + "failed to remove protected create marker directory {}: {err}", + target.marker_dir.display() + ), + } + } + violation + }) +} + +fn remove_protected_create_target( + target: &ProtectedCreateTarget, + report_violation: ViolationReporter, +) -> bool { + for attempt in 0..100 { + match try_remove_protected_create_target(target) { + Ok(true) => { + report_violation(target.path()); + return true; + } + Ok(false) => return false, + Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty && attempt < 99 => { + thread::sleep(Duration::from_millis(1)); + } + Err(err) => { + panic!( + "failed to remove protected create target {}: {err}", + target.path().display() + ); + } + } + } + unreachable!("protected create removal retry loop should return or panic") +} + +pub(crate) fn remove_protected_create_target_best_effort( + target: &ProtectedCreateTarget, + report_violation: ViolationReporter, +) -> bool { + for _ in 0..100 { + match try_remove_protected_create_target(target) { + Ok(true) => { + report_violation(target.path()); + return true; + } + Ok(false) => return false, + Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => { + thread::sleep(Duration::from_millis(1)); + } + Err(_) => return true, + } + } + true +} + +fn try_remove_protected_create_target(target: &ProtectedCreateTarget) -> std::io::Result { + let path = target.path(); + let metadata = match fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err), + }; + + let result = if metadata.is_dir() { + fs::remove_dir_all(path) + } else { + fs::remove_file(path) + }; + match result { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err), + } + Ok(true) +} + +fn remove_synthetic_mount_target(target: &SyntheticMountTarget) { + let path = target.path(); + let metadata = match fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return, + Err(err) => panic!( + "failed to inspect synthetic bubblewrap mount target {}: {err}", + path.display() + ), + }; + if !target.should_remove_after_bwrap(&metadata) { + return; + } + if target.is_directory() { + match fs::remove_dir(path) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {} + Err(err) => panic!( + "failed to remove synthetic bubblewrap mount target {}: {err}", + path.display() + ), + } + } else { + match fs::remove_file(path) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => panic!( + "failed to remove synthetic bubblewrap mount target {}: {err}", + path.display() + ), + } + } +} + +fn process_is_active(pid: libc::pid_t) -> bool { + let result = unsafe { libc::kill(pid, 0) }; + if result == 0 { + return true; + } + let err = std::io::Error::last_os_error(); + !matches!(err.raw_os_error(), Some(libc::ESRCH)) +} + +fn with_metadata_runtime_registry_lock(f: impl FnOnce() -> T) -> T { + let registry_root = metadata_runtime_registry_root(); + fs::create_dir_all(®istry_root).unwrap_or_else(|err| { + panic!( + "failed to create protected metadata runtime registry {}: {err}", + registry_root.display() + ) + }); + let lock_path = registry_root.join("lock"); + let lock_file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&lock_path) + .unwrap_or_else(|err| { + panic!( + "failed to open protected metadata runtime registry lock {}: {err}", + lock_path.display() + ) + }); + if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) } < 0 { + let err = std::io::Error::last_os_error(); + panic!( + "failed to lock protected metadata runtime registry {}: {err}", + lock_path.display() + ); + } + let result = f(); + if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN) } < 0 { + let err = std::io::Error::last_os_error(); + panic!( + "failed to unlock protected metadata runtime registry {}: {err}", + lock_path.display() + ); + } + result +} + +fn metadata_runtime_marker_dir(path: &Path) -> PathBuf { + metadata_runtime_registry_root().join(format!("{:016x}", hash_path(path))) +} + +fn metadata_runtime_registry_root() -> PathBuf { + std::env::temp_dir().join("codex-bwrap-synthetic-mount-targets") +} + +fn hash_path(path: &Path) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in path.as_os_str().as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash +} + +#[cfg(test)] +#[path = "file_system_protected_metadata_cleanup_tests.rs"] +mod tests; diff --git a/codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup_tests.rs b/codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup_tests.rs new file mode 100644 index 0000000000..5d4224efc4 --- /dev/null +++ b/codex-rs/linux-sandbox/src/file_system_protected_metadata_cleanup_tests.rs @@ -0,0 +1,133 @@ +use super::*; +use crate::file_system_protected_metadata::ProtectedCreateTarget; +use crate::file_system_protected_metadata::SyntheticMountTarget; +use pretty_assertions::assert_eq; + +fn ignore_metadata_violation(_: &std::path::Path) {} + +#[test] +fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let empty_file = temp_dir.path().join(".git"); + let empty_dir = temp_dir.path().join(".agents"); + let non_empty_file = temp_dir.path().join("non-empty"); + let missing_file = temp_dir.path().join(".missing"); + std::fs::write(&empty_file, "").expect("write empty file"); + std::fs::create_dir(&empty_dir).expect("create empty dir"); + std::fs::write(&non_empty_file, "keep").expect("write nonempty file"); + + let registrations = register_synthetic_mount_targets(&[ + SyntheticMountTarget::missing(&empty_file), + SyntheticMountTarget::missing_empty_directory(&empty_dir), + SyntheticMountTarget::missing(&non_empty_file), + SyntheticMountTarget::missing(&missing_file), + ]); + cleanup_synthetic_mount_targets(®istrations); + + assert!(!empty_file.exists()); + assert!(!empty_dir.exists()); + assert_eq!( + std::fs::read_to_string(&non_empty_file).expect("read nonempty file"), + "keep" + ); + assert!(!missing_file.exists()); +} + +#[test] +fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let empty_file = temp_dir.path().join(".git"); + std::fs::write(&empty_file, "").expect("write empty file"); + let target = SyntheticMountTarget::missing(&empty_file); + + let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target)); + let active_marker = registrations[0].marker_dir.join("1"); + std::fs::write(&active_marker, "").expect("write active marker"); + + cleanup_synthetic_mount_targets(®istrations); + assert!(empty_file.exists()); + + std::fs::remove_file(active_marker).expect("remove active marker"); + let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target)); + cleanup_synthetic_mount_targets(®istrations); + + assert!(!empty_file.exists()); +} + +#[test] +fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let empty_file = temp_dir.path().join(".git"); + let first_target = SyntheticMountTarget::missing(&empty_file); + + let first_registrations = register_synthetic_mount_targets(&[first_target]); + std::fs::write(&empty_file, "").expect("write transient empty file"); + let active_marker = first_registrations[0].marker_dir.join("1"); + std::fs::write(&active_marker, SYNTHETIC_MOUNT_MARKER_SYNTHETIC).expect("write active marker"); + let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file"); + let second_target = SyntheticMountTarget::existing_empty_file(&empty_file, &metadata); + let second_registrations = register_synthetic_mount_targets(&[second_target]); + + cleanup_synthetic_mount_targets(&first_registrations); + assert!(empty_file.exists()); + + std::fs::remove_file(active_marker).expect("remove active marker"); + cleanup_synthetic_mount_targets(&second_registrations); + + assert!(!empty_file.exists()); +} + +#[test] +fn cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let empty_file = temp_dir.path().join(".git"); + std::fs::write(&empty_file, "").expect("write pre-existing empty file"); + let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file"); + let first_target = SyntheticMountTarget::existing_empty_file(&empty_file, &metadata); + let second_target = SyntheticMountTarget::existing_empty_file(&empty_file, &metadata); + + let first_registrations = register_synthetic_mount_targets(&[first_target]); + let second_registrations = register_synthetic_mount_targets(&[second_target]); + + cleanup_synthetic_mount_targets(&first_registrations); + cleanup_synthetic_mount_targets(&second_registrations); + + assert!(empty_file.exists()); +} + +#[test] +fn cleanup_protected_create_targets_removes_created_path_and_reports_violation() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let dot_git = temp_dir.path().join(".git"); + let target = ProtectedCreateTarget::missing(&dot_git); + + let registrations = register_protected_create_targets(&[target]); + std::fs::create_dir(&dot_git).expect("create protected path"); + let violation = cleanup_protected_create_targets(®istrations, ignore_metadata_violation); + + assert!(violation); + assert!(!dot_git.exists()); +} + +#[test] +fn cleanup_protected_create_targets_waits_for_other_active_registrations() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let dot_git = temp_dir.path().join(".git"); + let target = ProtectedCreateTarget::missing(&dot_git); + + let registrations = register_protected_create_targets(std::slice::from_ref(&target)); + let active_marker = registrations[0].marker_dir.join("1"); + std::fs::write(&active_marker, PROTECTED_CREATE_MARKER).expect("write active marker"); + std::fs::write(&dot_git, "").expect("create protected path"); + + let violation = cleanup_protected_create_targets(®istrations, ignore_metadata_violation); + assert!(violation); + assert!(dot_git.exists()); + + std::fs::remove_file(active_marker).expect("remove active marker"); + let registrations = register_protected_create_targets(std::slice::from_ref(&target)); + let violation = cleanup_protected_create_targets(®istrations, ignore_metadata_violation); + + assert!(violation); + assert!(!dot_git.exists()); +} diff --git a/codex-rs/linux-sandbox/src/file_system_protected_metadata_monitor.rs b/codex-rs/linux-sandbox/src/file_system_protected_metadata_monitor.rs new file mode 100644 index 0000000000..a2df1e6a8f --- /dev/null +++ b/codex-rs/linux-sandbox/src/file_system_protected_metadata_monitor.rs @@ -0,0 +1,160 @@ +use std::ffi::CString; +use std::os::unix::ffi::OsStrExt; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +use crate::file_system_protected_metadata::ProtectedCreateTarget; +use crate::file_system_protected_metadata_cleanup::ViolationReporter; +use crate::file_system_protected_metadata_cleanup::remove_protected_create_target_best_effort; + +pub(crate) struct CreateMonitor { + stop: Arc, + violation: Arc, + handle: thread::JoinHandle<()>, +} + +struct CreateWatcher { + fd: libc::c_int, + _watches: Vec, +} + +impl CreateMonitor { + pub(crate) fn start( + targets: &[ProtectedCreateTarget], + report_violation: ViolationReporter, + ) -> Option { + if targets.is_empty() { + return None; + } + + let targets = targets.to_vec(); + let stop = Arc::new(AtomicBool::new(false)); + let violation = Arc::new(AtomicBool::new(false)); + let monitor_stop = Arc::clone(&stop); + let monitor_violation = Arc::clone(&violation); + let handle = thread::spawn(move || { + let watcher = CreateWatcher::new(&targets); + while !monitor_stop.load(Ordering::SeqCst) { + for target in &targets { + if remove_protected_create_target_best_effort(target, report_violation) { + monitor_violation.store(true, Ordering::SeqCst); + } + } + if let Some(watcher) = &watcher { + watcher.wait_for_create_event(&monitor_stop); + } else { + thread::sleep(Duration::from_millis(1)); + } + } + }); + + Some(Self { + stop, + violation, + handle, + }) + } + + pub(crate) fn stop(self) -> bool { + self.stop.store(true, Ordering::SeqCst); + self.handle + .join() + .unwrap_or_else(|_| panic!("protected create monitor thread panicked")); + self.violation.load(Ordering::SeqCst) + } +} + +impl CreateWatcher { + fn new(targets: &[ProtectedCreateTarget]) -> Option { + let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) }; + if fd < 0 { + return None; + } + + let mut watched_parents = Vec::::new(); + let mut watches = Vec::new(); + for target in targets { + let Some(parent) = target.path().parent() else { + continue; + }; + if watched_parents.iter().any(|watched| watched == parent) { + continue; + } + watched_parents.push(parent.to_path_buf()); + let Ok(parent_cstr) = CString::new(parent.as_os_str().as_bytes()) else { + continue; + }; + let mask = + libc::IN_CREATE | libc::IN_MOVED_TO | libc::IN_DELETE_SELF | libc::IN_MOVE_SELF; + let watch = unsafe { libc::inotify_add_watch(fd, parent_cstr.as_ptr(), mask) }; + if watch >= 0 { + watches.push(watch); + } + } + + if watches.is_empty() { + unsafe { + libc::close(fd); + } + return None; + } + + Some(Self { + fd, + _watches: watches, + }) + } + + fn wait_for_create_event(&self, stop: &AtomicBool) { + let mut poll_fd = libc::pollfd { + fd: self.fd, + events: libc::POLLIN, + revents: 0, + }; + while !stop.load(Ordering::SeqCst) { + let res = unsafe { libc::poll(&mut poll_fd, 1, 10) }; + if res > 0 { + self.drain_events(); + return; + } + if res == 0 { + return; + } + let err = std::io::Error::last_os_error(); + if err.kind() == std::io::ErrorKind::Interrupted { + continue; + } + return; + } + } + + fn drain_events(&self) { + let mut buf = [0_u8; 4096]; + loop { + let read = unsafe { libc::read(self.fd, buf.as_mut_ptr().cast(), buf.len()) }; + if read > 0 { + continue; + } + if read == 0 { + return; + } + let err = std::io::Error::last_os_error(); + if err.kind() == std::io::ErrorKind::Interrupted { + continue; + } + return; + } + } +} + +impl Drop for CreateWatcher { + fn drop(&mut self) { + unsafe { + libc::close(self.fd); + } + } +} diff --git a/codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime.rs b/codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime.rs new file mode 100644 index 0000000000..7ffb25a097 --- /dev/null +++ b/codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime.rs @@ -0,0 +1,418 @@ +use std::fs::File; +use std::io::Read; +use std::os::fd::FromRawFd; +use std::path::Path; +use std::sync::atomic::AtomicI32; +use std::sync::atomic::Ordering; + +use crate::bwrap::BwrapArgs; +use crate::file_system_protected_metadata_cleanup::cleanup_protected_create_targets; +use crate::file_system_protected_metadata_cleanup::cleanup_synthetic_mount_targets; +use crate::file_system_protected_metadata_cleanup::register_protected_create_targets; +use crate::file_system_protected_metadata_cleanup::register_synthetic_mount_targets; +use crate::file_system_protected_metadata_monitor::CreateMonitor; +use crate::launcher::exec_bwrap; + +static BWRAP_CHILD_PID: AtomicI32 = AtomicI32::new(0); +static PENDING_FORWARDED_SIGNAL: AtomicI32 = AtomicI32::new(0); + +const FORWARDED_SIGNALS: &[libc::c_int] = + &[libc::SIGHUP, libc::SIGINT, libc::SIGQUIT, libc::SIGTERM]; + +pub(crate) fn run_or_exec_bwrap(bwrap_args: BwrapArgs) -> ! { + let enforcement = &bwrap_args.file_system_permissions_enforcement; + if enforcement.synthetic_mount_targets.is_empty() + && enforcement.protected_create_targets.is_empty() + { + exec_bwrap(bwrap_args.args, bwrap_args.preserved_files); + } + run_bwrap_with_file_system_protected_metadata_runtime(bwrap_args); +} + +fn run_bwrap_with_file_system_protected_metadata_runtime(bwrap_args: BwrapArgs) -> ! { + let BwrapArgs { + args, + preserved_files, + file_system_permissions_enforcement, + } = bwrap_args; + let synthetic_mount_targets = file_system_permissions_enforcement.synthetic_mount_targets; + let protected_create_targets = file_system_permissions_enforcement.protected_create_targets; + let setup_signal_mask = ForwardedSignalMask::block(); + let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets); + let protected_create_registrations = + register_protected_create_targets(&protected_create_targets); + let exec_start_pipe = create_exec_start_pipe(!protected_create_targets.is_empty()); + let parent_pid = unsafe { libc::getpid() }; + let pid = unsafe { libc::fork() }; + if pid < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to fork for bubblewrap: {err}"); + } + + if pid == 0 { + reset_forwarded_signal_handlers_to_default(); + setup_signal_mask.restore(); + let setpgid_res = unsafe { libc::setpgid(0, 0) }; + if setpgid_res < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to place bubblewrap child in its own process group: {err}"); + } + terminate_with_parent(parent_pid); + wait_for_parent_exec_start(exec_start_pipe[0], exec_start_pipe[1]); + exec_bwrap(args, preserved_files); + } + + close_child_exec_start_read(exec_start_pipe[0]); + let protected_create_monitor = CreateMonitor::start( + &protected_create_targets, + report_file_system_protected_metadata_violation, + ); + let signal_forwarders = install_bwrap_signal_forwarders(pid); + release_child_exec_start(exec_start_pipe[1]); + setup_signal_mask.restore(); + let status = wait_for_bwrap_child(pid); + let cleanup_signal_mask = ForwardedSignalMask::block(); + BWRAP_CHILD_PID.store(0, Ordering::SeqCst); + let protected_create_monitor_violation = protected_create_monitor + .map(CreateMonitor::stop) + .unwrap_or(false); + cleanup_synthetic_mount_targets(&synthetic_mount_registrations); + let protected_create_violation = protected_create_monitor_violation + || cleanup_protected_create_targets( + &protected_create_registrations, + report_file_system_protected_metadata_violation, + ); + signal_forwarders.restore(); + cleanup_signal_mask.restore(); + exit_with_wait_status_or_policy_violation(status, protected_create_violation); +} + +fn create_exec_start_pipe(enabled: bool) -> [libc::c_int; 2] { + if !enabled { + return [-1, -1]; + } + let mut pipe = [-1, -1]; + if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) } < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to create bubblewrap exec start pipe: {err}"); + } + pipe +} + +fn wait_for_parent_exec_start(read_fd: libc::c_int, write_fd: libc::c_int) { + if write_fd >= 0 { + unsafe { + libc::close(write_fd); + } + } + if read_fd < 0 { + return; + } + + let mut byte = [0_u8; 1]; + loop { + let read = unsafe { libc::read(read_fd, byte.as_mut_ptr().cast(), byte.len()) }; + if read >= 0 { + break; + } + let err = std::io::Error::last_os_error(); + if err.kind() != std::io::ErrorKind::Interrupted { + break; + } + } + unsafe { + libc::close(read_fd); + } +} + +fn close_child_exec_start_read(read_fd: libc::c_int) { + if read_fd >= 0 { + unsafe { + libc::close(read_fd); + } + } +} + +fn release_child_exec_start(write_fd: libc::c_int) { + if write_fd < 0 { + return; + } + let byte = [0_u8; 1]; + unsafe { + libc::write(write_fd, byte.as_ptr().cast(), byte.len()); + libc::close(write_fd); + } +} + +struct ForwardedSignalMask { + previous: libc::sigset_t, +} + +struct ForwardedSignalHandlers { + previous: Vec<(libc::c_int, libc::sigaction)>, +} + +impl ForwardedSignalMask { + fn block() -> Self { + let mut blocked: libc::sigset_t = unsafe { std::mem::zeroed() }; + let mut previous: libc::sigset_t = unsafe { std::mem::zeroed() }; + unsafe { + libc::sigemptyset(&mut blocked); + for signal in FORWARDED_SIGNALS { + libc::sigaddset(&mut blocked, *signal); + } + if libc::sigprocmask(libc::SIG_BLOCK, &blocked, &mut previous) < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to block bubblewrap forwarded signals: {err}"); + } + } + Self { previous } + } + + fn restore(&self) { + let mut restored = self.previous; + unsafe { + for signal in FORWARDED_SIGNALS { + libc::sigdelset(&mut restored, *signal); + } + if libc::sigprocmask(libc::SIG_SETMASK, &restored, std::ptr::null_mut()) < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to restore bubblewrap forwarded signals: {err}"); + } + } + } +} + +fn terminate_with_parent(parent_pid: libc::pid_t) { + let res = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) }; + if res < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to set bubblewrap child parent-death signal: {err}"); + } + if unsafe { libc::getppid() } != parent_pid { + unsafe { + libc::raise(libc::SIGTERM); + } + } +} + +impl ForwardedSignalHandlers { + fn restore(self) { + BWRAP_CHILD_PID.store(0, Ordering::SeqCst); + PENDING_FORWARDED_SIGNAL.store(0, Ordering::SeqCst); + for (signal, previous_action) in self.previous { + unsafe { + if libc::sigaction(signal, &previous_action, std::ptr::null_mut()) < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to restore bubblewrap signal handler for {signal}: {err}"); + } + } + } + } +} + +fn install_bwrap_signal_forwarders(pid: libc::pid_t) -> ForwardedSignalHandlers { + BWRAP_CHILD_PID.store(pid, Ordering::SeqCst); + let mut previous = Vec::with_capacity(FORWARDED_SIGNALS.len()); + for signal in FORWARDED_SIGNALS { + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + let mut previous_action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = forward_signal_to_bwrap_child as *const () as libc::sighandler_t; + unsafe { + libc::sigemptyset(&mut action.sa_mask); + if libc::sigaction(*signal, &action, &mut previous_action) < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to install bubblewrap signal forwarder for {signal}: {err}"); + } + } + previous.push((*signal, previous_action)); + } + replay_pending_forwarded_signal(pid); + ForwardedSignalHandlers { previous } +} + +extern "C" fn forward_signal_to_bwrap_child(signal: libc::c_int) { + PENDING_FORWARDED_SIGNAL.store(signal, Ordering::SeqCst); + let pid = BWRAP_CHILD_PID.load(Ordering::SeqCst); + if pid > 0 { + send_signal_to_bwrap_child(pid, signal); + } +} + +fn replay_pending_forwarded_signal(pid: libc::pid_t) { + let signal = PENDING_FORWARDED_SIGNAL.swap(0, Ordering::SeqCst); + if signal > 0 { + send_signal_to_bwrap_child(pid, signal); + } +} + +fn send_signal_to_bwrap_child(pid: libc::pid_t, signal: libc::c_int) { + unsafe { + libc::kill(-pid, signal); + libc::kill(pid, signal); + } +} + +fn reset_forwarded_signal_handlers_to_default() { + for signal in FORWARDED_SIGNALS { + unsafe { + if libc::signal(*signal, libc::SIG_DFL) == libc::SIG_ERR { + let err = std::io::Error::last_os_error(); + panic!("failed to reset bubblewrap signal handler for {signal}: {err}"); + } + } + } +} + +fn wait_for_bwrap_child(pid: libc::pid_t) -> libc::c_int { + loop { + 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 { + return status; + } + 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 exit_with_wait_status(status: libc::c_int) -> ! { + if libc::WIFEXITED(status) { + std::process::exit(libc::WEXITSTATUS(status)); + } + + if libc::WIFSIGNALED(status) { + let signal = libc::WTERMSIG(status); + unsafe { + libc::signal(signal, libc::SIG_DFL); + libc::kill(libc::getpid(), signal); + } + std::process::exit(128 + signal); + } + + std::process::exit(1); +} + +fn exit_with_wait_status_or_policy_violation( + status: libc::c_int, + protected_create_violation: bool, +) -> ! { + if protected_create_violation && libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 { + std::process::exit(1); + } + + exit_with_wait_status(status); +} + +/// Run a short-lived bubblewrap preflight in a child process and capture stderr. +/// +/// Strategy: +/// - This is used only by `preflight_proc_mount_support`, which runs `/bin/true` +/// under bubblewrap with `--proc /proc`. +/// - The goal is to detect environments where mounting `/proc` fails (for +/// example, restricted containers), so we can retry the real run with +/// `--no-proc`. +/// - We capture stderr from that preflight to match known mount-failure text. +/// We do not stream it because this is a one-shot probe with a trivial +/// command, and reads are bounded to a fixed max size. +pub(crate) fn run_bwrap_in_child_capture_stderr(bwrap_args: BwrapArgs) -> String { + const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024; + let BwrapArgs { + args, + preserved_files, + file_system_permissions_enforcement, + } = bwrap_args; + let synthetic_mount_targets = file_system_permissions_enforcement.synthetic_mount_targets; + let protected_create_targets = file_system_permissions_enforcement.protected_create_targets; + let setup_signal_mask = ForwardedSignalMask::block(); + let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets); + let protected_create_registrations = + register_protected_create_targets(&protected_create_targets); + + let mut pipe_fds = [0; 2]; + let pipe_res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) }; + if pipe_res < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to create stderr pipe for bubblewrap: {err}"); + } + 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}"); + } + + if pid == 0 { + reset_forwarded_signal_handlers_to_default(); + setup_signal_mask.restore(); + // Child: redirect stderr to the pipe, then run bubblewrap. + unsafe { + close_fd_or_panic(read_fd, "close read end in bubblewrap child"); + if libc::dup2(write_fd, libc::STDERR_FILENO) < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to redirect stderr for bubblewrap: {err}"); + } + close_fd_or_panic(write_fd, "close write end in bubblewrap child"); + } + + exec_bwrap(args, preserved_files); + } + + let signal_forwarders = install_bwrap_signal_forwarders(pid); + setup_signal_mask.restore(); + // Parent: close the write end and read stderr while the child runs. + close_fd_or_panic(write_fd, "close write end in bubblewrap parent"); + + // SAFETY: `read_fd` is a valid owned fd in the parent. + let mut read_file = unsafe { File::from_raw_fd(read_fd) }; + let mut stderr_bytes = Vec::new(); + let mut limited_reader = (&mut read_file).take(MAX_PREFLIGHT_STDERR_BYTES); + if let Err(err) = limited_reader.read_to_end(&mut stderr_bytes) { + panic!("failed to read bubblewrap stderr: {err}"); + } + + let status = wait_for_bwrap_child(pid); + let cleanup_signal_mask = ForwardedSignalMask::block(); + BWRAP_CHILD_PID.store(0, Ordering::SeqCst); + cleanup_synthetic_mount_targets(&synthetic_mount_registrations); + cleanup_protected_create_targets( + &protected_create_registrations, + report_file_system_protected_metadata_violation, + ); + signal_forwarders.restore(); + cleanup_signal_mask.restore(); + if libc::WIFSIGNALED(status) { + exit_with_wait_status(status); + } + + String::from_utf8_lossy(&stderr_bytes).into_owned() +} + +fn report_file_system_protected_metadata_violation(path: &Path) { + eprintln!( + "sandbox blocked creation of protected workspace metadata path {}", + path.display() + ); +} + +/// Close an owned file descriptor and panic with context on failure. +/// +/// We use explicit close() checks here (instead of ignoring return codes) +/// because this code runs in low-level sandbox setup paths where fd leaks or +/// close errors can mask the root cause of later failures. +fn close_fd_or_panic(fd: libc::c_int, context: &str) { + let close_res = unsafe { libc::close(fd) }; + if close_res < 0 { + let err = std::io::Error::last_os_error(); + panic!("{context}: {err}"); + } +} + +#[cfg(test)] +#[path = "file_system_protected_metadata_runtime_tests.rs"] +mod tests; diff --git a/codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime_tests.rs b/codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime_tests.rs new file mode 100644 index 0000000000..9c95c168ce --- /dev/null +++ b/codex-rs/linux-sandbox/src/file_system_protected_metadata_runtime_tests.rs @@ -0,0 +1,46 @@ +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive() { + let supervisor_pid = unsafe { libc::fork() }; + assert!(supervisor_pid >= 0, "failed to fork supervisor"); + + if supervisor_pid == 0 { + run_bwrap_signal_forwarder_test_supervisor(); + } + + let status = wait_for_bwrap_child(supervisor_pid); + assert!(libc::WIFEXITED(status), "supervisor status: {status}"); + assert_eq!(libc::WEXITSTATUS(status), 0); +} + +#[cfg(test)] +fn run_bwrap_signal_forwarder_test_supervisor() -> ! { + let child_pid = unsafe { libc::fork() }; + if child_pid < 0 { + unsafe { + libc::_exit(2); + } + } + + if child_pid == 0 { + loop { + unsafe { + libc::pause(); + } + } + } + + install_bwrap_signal_forwarders(child_pid); + unsafe { + libc::raise(libc::SIGTERM); + } + + let status = wait_for_bwrap_child(child_pid); + let child_terminated_by_sigterm = + libc::WIFSIGNALED(status) && libc::WTERMSIG(status) == libc::SIGTERM; + unsafe { + libc::_exit(if child_terminated_by_sigterm { 0 } else { 1 }); + } +} diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs index 900287c99d..a46859ec3c 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/linux-sandbox/src/lib.rs @@ -6,6 +6,14 @@ #[cfg(target_os = "linux")] mod bwrap; #[cfg(target_os = "linux")] +mod file_system_protected_metadata; +#[cfg(target_os = "linux")] +mod file_system_protected_metadata_cleanup; +#[cfg(target_os = "linux")] +mod file_system_protected_metadata_monitor; +#[cfg(target_os = "linux")] +mod file_system_protected_metadata_runtime; +#[cfg(target_os = "linux")] mod landlock; #[cfg(target_os = "linux")] mod launcher; diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index c88a28b324..84520fbc3e 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -1,27 +1,15 @@ use clap::Parser; use std::ffi::CString; use std::fmt; -use std::fs; -use std::fs::File; -use std::fs::OpenOptions; -use std::io::Read; -use std::os::fd::AsRawFd; -use std::os::fd::FromRawFd; -use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::AtomicI32; -use std::sync::atomic::Ordering; -use std::thread; -use std::time::Duration; use crate::bwrap::BwrapNetworkMode; use crate::bwrap::BwrapOptions; use crate::bwrap::create_bwrap_command_args; +use crate::file_system_protected_metadata_runtime::run_bwrap_in_child_capture_stderr; +use crate::file_system_protected_metadata_runtime::run_or_exec_bwrap; use crate::landlock::apply_permission_profile_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; @@ -30,46 +18,6 @@ use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::NetworkSandboxPolicy; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; -static BWRAP_CHILD_PID: AtomicI32 = AtomicI32::new(0); -static PENDING_FORWARDED_SIGNAL: AtomicI32 = AtomicI32::new(0); - -const FORWARDED_SIGNALS: &[libc::c_int] = - &[libc::SIGHUP, libc::SIGINT, libc::SIGQUIT, libc::SIGTERM]; -const SYNTHETIC_MOUNT_MARKER_SYNTHETIC: &[u8] = b"synthetic\n"; -const SYNTHETIC_MOUNT_MARKER_EXISTING: &[u8] = b"existing\n"; -const PROTECTED_CREATE_MARKER: &[u8] = b"protected-create\n"; - -#[derive(Debug)] -struct SyntheticMountTargetRegistration { - target: crate::bwrap::SyntheticMountTarget, - marker_file: PathBuf, - marker_dir: PathBuf, -} - -#[derive(Debug)] -struct ProtectedCreateTargetRegistration { - target: crate::bwrap::ProtectedCreateTarget, - marker_file: PathBuf, - marker_dir: PathBuf, -} - -struct ProtectedCreateMonitor { - stop: Arc, - violation: Arc, - handle: thread::JoinHandle<()>, -} - -struct ProtectedCreateWatcher { - fd: libc::c_int, - _watches: Vec, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ProtectedCreateRemoval { - Directory, - Other, -} - #[derive(Debug, Parser)] /// CLI surface for the Linux sandbox helper. /// @@ -389,8 +337,7 @@ fn build_bwrap_argv( crate::bwrap::BwrapArgs { args: argv, preserved_files: bwrap_args.preserved_files, - synthetic_mount_targets: bwrap_args.synthetic_mount_targets, - protected_create_targets: bwrap_args.protected_create_targets, + file_system_permissions_enforcement: bwrap_args.file_system_permissions_enforcement, } } @@ -479,897 +426,6 @@ fn resolve_true_command() -> String { "true".to_string() } -fn run_or_exec_bwrap(bwrap_args: crate::bwrap::BwrapArgs) -> ! { - if bwrap_args.synthetic_mount_targets.is_empty() - && bwrap_args.protected_create_targets.is_empty() - { - exec_bwrap(bwrap_args.args, bwrap_args.preserved_files); - } - run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args); -} - -fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::BwrapArgs) -> ! { - let crate::bwrap::BwrapArgs { - args, - preserved_files, - synthetic_mount_targets, - protected_create_targets, - } = bwrap_args; - let setup_signal_mask = ForwardedSignalMask::block(); - let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets); - let protected_create_registrations = - register_protected_create_targets(&protected_create_targets); - let exec_start_pipe = create_exec_start_pipe(!protected_create_targets.is_empty()); - let parent_pid = unsafe { libc::getpid() }; - let pid = unsafe { libc::fork() }; - if pid < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to fork for bubblewrap: {err}"); - } - - if pid == 0 { - reset_forwarded_signal_handlers_to_default(); - setup_signal_mask.restore(); - let setpgid_res = unsafe { libc::setpgid(0, 0) }; - if setpgid_res < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to place bubblewrap child in its own process group: {err}"); - } - terminate_with_parent(parent_pid); - wait_for_parent_exec_start(exec_start_pipe[0], exec_start_pipe[1]); - exec_bwrap(args, preserved_files); - } - - close_child_exec_start_read(exec_start_pipe[0]); - let protected_create_monitor = ProtectedCreateMonitor::start(&protected_create_targets); - let signal_forwarders = install_bwrap_signal_forwarders(pid); - release_child_exec_start(exec_start_pipe[1]); - setup_signal_mask.restore(); - let status = wait_for_bwrap_child(pid); - let cleanup_signal_mask = ForwardedSignalMask::block(); - BWRAP_CHILD_PID.store(0, Ordering::SeqCst); - let protected_create_monitor_violation = protected_create_monitor - .map(ProtectedCreateMonitor::stop) - .unwrap_or(false); - cleanup_synthetic_mount_targets(&synthetic_mount_registrations); - let protected_create_violation = protected_create_monitor_violation - || cleanup_protected_create_targets(&protected_create_registrations); - signal_forwarders.restore(); - cleanup_signal_mask.restore(); - exit_with_wait_status_or_policy_violation(status, protected_create_violation); -} - -impl ProtectedCreateMonitor { - fn start(targets: &[crate::bwrap::ProtectedCreateTarget]) -> Option { - if targets.is_empty() { - return None; - } - - let targets = targets.to_vec(); - let stop = Arc::new(AtomicBool::new(false)); - let violation = Arc::new(AtomicBool::new(false)); - let monitor_stop = Arc::clone(&stop); - let monitor_violation = Arc::clone(&violation); - let handle = thread::spawn(move || { - let watcher = ProtectedCreateWatcher::new(&targets); - while !monitor_stop.load(Ordering::SeqCst) { - for target in &targets { - if remove_protected_create_target_best_effort(target).is_some() { - monitor_violation.store(true, Ordering::SeqCst); - } - } - if let Some(watcher) = &watcher { - watcher.wait_for_create_event(&monitor_stop); - } else { - thread::sleep(Duration::from_millis(1)); - } - } - }); - - Some(Self { - stop, - violation, - handle, - }) - } - - fn stop(self) -> bool { - self.stop.store(true, Ordering::SeqCst); - self.handle - .join() - .unwrap_or_else(|_| panic!("protected create monitor thread panicked")); - self.violation.load(Ordering::SeqCst) - } -} - -impl ProtectedCreateWatcher { - fn new(targets: &[crate::bwrap::ProtectedCreateTarget]) -> Option { - let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) }; - if fd < 0 { - return None; - } - - let mut watched_parents = Vec::::new(); - let mut watches = Vec::new(); - for target in targets { - let Some(parent) = target.path().parent() else { - continue; - }; - if watched_parents.iter().any(|watched| watched == parent) { - continue; - } - watched_parents.push(parent.to_path_buf()); - let Ok(parent_cstr) = CString::new(parent.as_os_str().as_bytes()) else { - continue; - }; - let mask = - libc::IN_CREATE | libc::IN_MOVED_TO | libc::IN_DELETE_SELF | libc::IN_MOVE_SELF; - let watch = unsafe { libc::inotify_add_watch(fd, parent_cstr.as_ptr(), mask) }; - if watch >= 0 { - watches.push(watch); - } - } - - if watches.is_empty() { - unsafe { - libc::close(fd); - } - return None; - } - - Some(Self { - fd, - _watches: watches, - }) - } - - fn wait_for_create_event(&self, stop: &AtomicBool) { - let mut poll_fd = libc::pollfd { - fd: self.fd, - events: libc::POLLIN, - revents: 0, - }; - while !stop.load(Ordering::SeqCst) { - let res = unsafe { libc::poll(&mut poll_fd, 1, 10) }; - if res > 0 { - self.drain_events(); - return; - } - if res == 0 { - return; - } - let err = std::io::Error::last_os_error(); - if err.kind() == std::io::ErrorKind::Interrupted { - continue; - } - return; - } - } - - fn drain_events(&self) { - let mut buf = [0_u8; 4096]; - loop { - let read = unsafe { libc::read(self.fd, buf.as_mut_ptr().cast(), buf.len()) }; - if read > 0 { - continue; - } - if read == 0 { - return; - } - let err = std::io::Error::last_os_error(); - if err.kind() == std::io::ErrorKind::Interrupted { - continue; - } - return; - } - } -} - -impl Drop for ProtectedCreateWatcher { - fn drop(&mut self) { - unsafe { - libc::close(self.fd); - } - } -} - -fn create_exec_start_pipe(enabled: bool) -> [libc::c_int; 2] { - if !enabled { - return [-1, -1]; - } - let mut pipe = [-1, -1]; - if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) } < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to create bubblewrap exec start pipe: {err}"); - } - pipe -} - -fn wait_for_parent_exec_start(read_fd: libc::c_int, write_fd: libc::c_int) { - if write_fd >= 0 { - unsafe { - libc::close(write_fd); - } - } - if read_fd < 0 { - return; - } - - let mut byte = [0_u8; 1]; - loop { - let read = unsafe { libc::read(read_fd, byte.as_mut_ptr().cast(), byte.len()) }; - if read >= 0 { - break; - } - let err = std::io::Error::last_os_error(); - if err.kind() != std::io::ErrorKind::Interrupted { - break; - } - } - unsafe { - libc::close(read_fd); - } -} - -fn close_child_exec_start_read(read_fd: libc::c_int) { - if read_fd >= 0 { - unsafe { - libc::close(read_fd); - } - } -} - -fn release_child_exec_start(write_fd: libc::c_int) { - if write_fd < 0 { - return; - } - let byte = [0_u8; 1]; - unsafe { - libc::write(write_fd, byte.as_ptr().cast(), byte.len()); - libc::close(write_fd); - } -} - -struct ForwardedSignalMask { - previous: libc::sigset_t, -} - -struct ForwardedSignalHandlers { - previous: Vec<(libc::c_int, libc::sigaction)>, -} - -impl ForwardedSignalMask { - fn block() -> Self { - let mut blocked: libc::sigset_t = unsafe { std::mem::zeroed() }; - let mut previous: libc::sigset_t = unsafe { std::mem::zeroed() }; - unsafe { - libc::sigemptyset(&mut blocked); - for signal in FORWARDED_SIGNALS { - libc::sigaddset(&mut blocked, *signal); - } - if libc::sigprocmask(libc::SIG_BLOCK, &blocked, &mut previous) < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to block bubblewrap forwarded signals: {err}"); - } - } - Self { previous } - } - - fn restore(&self) { - let mut restored = self.previous; - unsafe { - for signal in FORWARDED_SIGNALS { - libc::sigdelset(&mut restored, *signal); - } - if libc::sigprocmask(libc::SIG_SETMASK, &restored, std::ptr::null_mut()) < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to restore bubblewrap forwarded signals: {err}"); - } - } - } -} - -fn terminate_with_parent(parent_pid: libc::pid_t) { - let res = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) }; - if res < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to set bubblewrap child parent-death signal: {err}"); - } - if unsafe { libc::getppid() } != parent_pid { - unsafe { - libc::raise(libc::SIGTERM); - } - } -} - -impl ForwardedSignalHandlers { - fn restore(self) { - BWRAP_CHILD_PID.store(0, Ordering::SeqCst); - PENDING_FORWARDED_SIGNAL.store(0, Ordering::SeqCst); - for (signal, previous_action) in self.previous { - unsafe { - if libc::sigaction(signal, &previous_action, std::ptr::null_mut()) < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to restore bubblewrap signal handler for {signal}: {err}"); - } - } - } - } -} - -fn install_bwrap_signal_forwarders(pid: libc::pid_t) -> ForwardedSignalHandlers { - BWRAP_CHILD_PID.store(pid, Ordering::SeqCst); - let mut previous = Vec::with_capacity(FORWARDED_SIGNALS.len()); - for signal in FORWARDED_SIGNALS { - let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; - let mut previous_action: libc::sigaction = unsafe { std::mem::zeroed() }; - action.sa_sigaction = forward_signal_to_bwrap_child as *const () as libc::sighandler_t; - unsafe { - libc::sigemptyset(&mut action.sa_mask); - if libc::sigaction(*signal, &action, &mut previous_action) < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to install bubblewrap signal forwarder for {signal}: {err}"); - } - } - previous.push((*signal, previous_action)); - } - replay_pending_forwarded_signal(pid); - ForwardedSignalHandlers { previous } -} - -extern "C" fn forward_signal_to_bwrap_child(signal: libc::c_int) { - PENDING_FORWARDED_SIGNAL.store(signal, Ordering::SeqCst); - let pid = BWRAP_CHILD_PID.load(Ordering::SeqCst); - if pid > 0 { - send_signal_to_bwrap_child(pid, signal); - } -} - -fn replay_pending_forwarded_signal(pid: libc::pid_t) { - let signal = PENDING_FORWARDED_SIGNAL.swap(0, Ordering::SeqCst); - if signal > 0 { - send_signal_to_bwrap_child(pid, signal); - } -} - -fn send_signal_to_bwrap_child(pid: libc::pid_t, signal: libc::c_int) { - unsafe { - libc::kill(-pid, signal); - libc::kill(pid, signal); - } -} - -fn reset_forwarded_signal_handlers_to_default() { - for signal in FORWARDED_SIGNALS { - unsafe { - if libc::signal(*signal, libc::SIG_DFL) == libc::SIG_ERR { - let err = std::io::Error::last_os_error(); - panic!("failed to reset bubblewrap signal handler for {signal}: {err}"); - } - } - } -} - -fn wait_for_bwrap_child(pid: libc::pid_t) -> libc::c_int { - loop { - 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 { - return status; - } - 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 register_synthetic_mount_targets( - targets: &[crate::bwrap::SyntheticMountTarget], -) -> Vec { - with_synthetic_mount_registry_lock(|| { - targets - .iter() - .map(|target| { - let marker_dir = synthetic_mount_marker_dir(target.path()); - fs::create_dir_all(&marker_dir).unwrap_or_else(|err| { - panic!( - "failed to create synthetic bubblewrap mount marker directory {}: {err}", - marker_dir.display() - ) - }); - let target = if target.preserves_pre_existing_path() - && synthetic_mount_marker_dir_has_active_synthetic_owner(&marker_dir) - { - match target.kind() { - crate::bwrap::SyntheticMountTargetKind::EmptyFile => { - crate::bwrap::SyntheticMountTarget::missing(target.path()) - } - crate::bwrap::SyntheticMountTargetKind::EmptyDirectory => { - crate::bwrap::SyntheticMountTarget::missing_empty_directory( - target.path(), - ) - } - } - } else { - target.clone() - }; - let marker_file = marker_dir.join(std::process::id().to_string()); - fs::write(&marker_file, synthetic_mount_marker_contents(&target)).unwrap_or_else( - |err| { - panic!( - "failed to register synthetic bubblewrap mount target {}: {err}", - target.path().display() - ) - }, - ); - SyntheticMountTargetRegistration { - target, - marker_file, - marker_dir, - } - }) - .collect() - }) -} - -fn register_protected_create_targets( - targets: &[crate::bwrap::ProtectedCreateTarget], -) -> Vec { - with_synthetic_mount_registry_lock(|| { - targets - .iter() - .map(|target| { - let marker_dir = synthetic_mount_marker_dir(target.path()); - fs::create_dir_all(&marker_dir).unwrap_or_else(|err| { - panic!( - "failed to create protected create marker directory {}: {err}", - marker_dir.display() - ) - }); - let marker_file = marker_dir.join(std::process::id().to_string()); - fs::write(&marker_file, PROTECTED_CREATE_MARKER).unwrap_or_else(|err| { - panic!( - "failed to register protected create target {}: {err}", - target.path().display() - ) - }); - ProtectedCreateTargetRegistration { - target: target.clone(), - marker_file, - marker_dir, - } - }) - .collect() - }) -} - -fn synthetic_mount_marker_contents(target: &crate::bwrap::SyntheticMountTarget) -> &'static [u8] { - if target.preserves_pre_existing_path() { - SYNTHETIC_MOUNT_MARKER_EXISTING - } else { - SYNTHETIC_MOUNT_MARKER_SYNTHETIC - } -} - -fn synthetic_mount_marker_dir_has_active_synthetic_owner(marker_dir: &Path) -> bool { - synthetic_mount_marker_dir_has_active_process_matching(marker_dir, |path| { - match fs::read(path) { - Ok(contents) => contents == SYNTHETIC_MOUNT_MARKER_SYNTHETIC, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, - Err(err) => panic!( - "failed to read synthetic bubblewrap mount marker {}: {err}", - path.display() - ), - } - }) -} - -fn synthetic_mount_marker_dir_has_active_process(marker_dir: &Path) -> bool { - synthetic_mount_marker_dir_has_active_process_matching(marker_dir, |_| true) -} - -fn synthetic_mount_marker_dir_has_active_process_matching( - marker_dir: &Path, - matches_marker: impl Fn(&Path) -> bool, -) -> bool { - let entries = match fs::read_dir(marker_dir) { - Ok(entries) => entries, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return false, - Err(err) => panic!( - "failed to read synthetic bubblewrap mount marker directory {}: {err}", - marker_dir.display() - ), - }; - for entry in entries { - let entry = entry.unwrap_or_else(|err| { - panic!( - "failed to read synthetic bubblewrap mount marker in {}: {err}", - marker_dir.display() - ) - }); - let path = entry.path(); - let Some(pid) = path - .file_name() - .and_then(|name| name.to_str()) - .and_then(|name| name.parse::().ok()) - else { - continue; - }; - if !process_is_active(pid) { - match fs::remove_file(&path) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => panic!( - "failed to remove stale synthetic bubblewrap mount marker {}: {err}", - path.display() - ), - } - continue; - } - let matches_marker = matches_marker(&path); - if matches_marker { - return true; - } - } - false -} - -fn cleanup_synthetic_mount_targets(targets: &[SyntheticMountTargetRegistration]) { - with_synthetic_mount_registry_lock(|| { - for target in targets.iter().rev() { - match fs::remove_file(&target.marker_file) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => panic!( - "failed to unregister synthetic bubblewrap mount target {}: {err}", - target.target.path().display() - ), - } - } - - for target in targets.iter().rev() { - if synthetic_mount_marker_dir_has_active_process(&target.marker_dir) { - continue; - } - remove_synthetic_mount_target(&target.target); - match fs::remove_dir(&target.marker_dir) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {} - Err(err) => panic!( - "failed to remove synthetic bubblewrap mount marker directory {}: {err}", - target.marker_dir.display() - ), - } - } - }); -} - -fn cleanup_protected_create_targets(targets: &[ProtectedCreateTargetRegistration]) -> bool { - with_synthetic_mount_registry_lock(|| { - for target in targets.iter().rev() { - match fs::remove_file(&target.marker_file) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => panic!( - "failed to unregister protected create target {}: {err}", - target.target.path().display() - ), - } - } - - let mut violation = false; - for target in targets.iter().rev() { - if synthetic_mount_marker_dir_has_active_process(&target.marker_dir) { - if target.target.path().exists() { - violation = true; - } - continue; - } - violation |= remove_protected_create_target(&target.target); - match fs::remove_dir(&target.marker_dir) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {} - Err(err) => panic!( - "failed to remove protected create marker directory {}: {err}", - target.marker_dir.display() - ), - } - } - violation - }) -} - -fn remove_protected_create_target(target: &crate::bwrap::ProtectedCreateTarget) -> bool { - for attempt in 0..100 { - match try_remove_protected_create_target(target) { - Ok(removal) => return removal.is_some(), - Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty && attempt < 99 => { - thread::sleep(Duration::from_millis(1)); - } - Err(err) => { - panic!( - "failed to remove protected create target {}: {err}", - target.path().display() - ); - } - } - } - unreachable!("protected create removal retry loop should return or panic") -} - -fn remove_protected_create_target_best_effort( - target: &crate::bwrap::ProtectedCreateTarget, -) -> Option { - for _ in 0..100 { - match try_remove_protected_create_target(target) { - Ok(removal) => return removal, - Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => { - thread::sleep(Duration::from_millis(1)); - } - Err(_) => return Some(ProtectedCreateRemoval::Other), - } - } - Some(ProtectedCreateRemoval::Other) -} - -fn try_remove_protected_create_target( - target: &crate::bwrap::ProtectedCreateTarget, -) -> std::io::Result> { - let path = target.path(); - let metadata = match fs::symlink_metadata(path) { - Ok(metadata) => metadata, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(err), - }; - - let removal = if metadata.is_dir() { - ProtectedCreateRemoval::Directory - } else { - ProtectedCreateRemoval::Other - }; - let result = if removal == ProtectedCreateRemoval::Directory { - fs::remove_dir_all(path) - } else { - fs::remove_file(path) - }; - match result { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(err), - } - eprintln!( - "sandbox blocked creation of protected workspace metadata path {}", - path.display() - ); - Ok(Some(removal)) -} - -fn remove_synthetic_mount_target(target: &crate::bwrap::SyntheticMountTarget) { - let path = target.path(); - let metadata = match fs::symlink_metadata(path) { - Ok(metadata) => metadata, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return, - Err(err) => panic!( - "failed to inspect synthetic bubblewrap mount target {}: {err}", - path.display() - ), - }; - if !target.should_remove_after_bwrap(&metadata) { - return; - } - match target.kind() { - crate::bwrap::SyntheticMountTargetKind::EmptyFile => match fs::remove_file(path) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => panic!( - "failed to remove synthetic bubblewrap mount target {}: {err}", - path.display() - ), - }, - crate::bwrap::SyntheticMountTargetKind::EmptyDirectory => match fs::remove_dir(path) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {} - Err(err) => panic!( - "failed to remove synthetic bubblewrap mount target {}: {err}", - path.display() - ), - }, - } -} - -fn process_is_active(pid: libc::pid_t) -> bool { - let result = unsafe { libc::kill(pid, 0) }; - if result == 0 { - return true; - } - let err = std::io::Error::last_os_error(); - !matches!(err.raw_os_error(), Some(libc::ESRCH)) -} - -fn with_synthetic_mount_registry_lock(f: impl FnOnce() -> T) -> T { - let registry_root = synthetic_mount_registry_root(); - fs::create_dir_all(®istry_root).unwrap_or_else(|err| { - panic!( - "failed to create synthetic bubblewrap mount registry {}: {err}", - registry_root.display() - ) - }); - let lock_path = registry_root.join("lock"); - let lock_file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(&lock_path) - .unwrap_or_else(|err| { - panic!( - "failed to open synthetic bubblewrap mount registry lock {}: {err}", - lock_path.display() - ) - }); - if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) } < 0 { - let err = std::io::Error::last_os_error(); - panic!( - "failed to lock synthetic bubblewrap mount registry {}: {err}", - lock_path.display() - ); - } - let result = f(); - if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN) } < 0 { - let err = std::io::Error::last_os_error(); - panic!( - "failed to unlock synthetic bubblewrap mount registry {}: {err}", - lock_path.display() - ); - } - result -} - -fn synthetic_mount_marker_dir(path: &Path) -> PathBuf { - synthetic_mount_registry_root().join(format!("{:016x}", hash_path(path))) -} - -fn synthetic_mount_registry_root() -> PathBuf { - std::env::temp_dir().join("codex-bwrap-synthetic-mount-targets") -} - -fn hash_path(path: &Path) -> u64 { - let mut hash = 0xcbf29ce484222325u64; - for byte in path.as_os_str().as_bytes() { - hash ^= u64::from(*byte); - hash = hash.wrapping_mul(0x100000001b3); - } - hash -} - -fn exit_with_wait_status(status: libc::c_int) -> ! { - if libc::WIFEXITED(status) { - std::process::exit(libc::WEXITSTATUS(status)); - } - - if libc::WIFSIGNALED(status) { - let signal = libc::WTERMSIG(status); - unsafe { - libc::signal(signal, libc::SIG_DFL); - libc::kill(libc::getpid(), signal); - } - std::process::exit(128 + signal); - } - - std::process::exit(1); -} - -fn exit_with_wait_status_or_policy_violation( - status: libc::c_int, - protected_create_violation: bool, -) -> ! { - if protected_create_violation && libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 { - std::process::exit(1); - } - - exit_with_wait_status(status); -} - -/// Run a short-lived bubblewrap preflight in a child process and capture stderr. -/// -/// Strategy: -/// - This is used only by `preflight_proc_mount_support`, which runs `/bin/true` -/// under bubblewrap with `--proc /proc`. -/// - The goal is to detect environments where mounting `/proc` fails (for -/// example, restricted containers), so we can retry the real run with -/// `--no-proc`. -/// - We capture stderr from that preflight to match known mount-failure text. -/// We do not stream it because this is a one-shot probe with a trivial -/// 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 crate::bwrap::BwrapArgs { - args, - preserved_files, - synthetic_mount_targets, - protected_create_targets, - } = bwrap_args; - let setup_signal_mask = ForwardedSignalMask::block(); - let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets); - let protected_create_registrations = - register_protected_create_targets(&protected_create_targets); - - let mut pipe_fds = [0; 2]; - let pipe_res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) }; - if pipe_res < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to create stderr pipe for bubblewrap: {err}"); - } - 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}"); - } - - if pid == 0 { - reset_forwarded_signal_handlers_to_default(); - setup_signal_mask.restore(); - // Child: redirect stderr to the pipe, then run bubblewrap. - unsafe { - close_fd_or_panic(read_fd, "close read end in bubblewrap child"); - if libc::dup2(write_fd, libc::STDERR_FILENO) < 0 { - let err = std::io::Error::last_os_error(); - panic!("failed to redirect stderr for bubblewrap: {err}"); - } - close_fd_or_panic(write_fd, "close write end in bubblewrap child"); - } - - exec_bwrap(args, preserved_files); - } - - let signal_forwarders = install_bwrap_signal_forwarders(pid); - setup_signal_mask.restore(); - // Parent: close the write end and read stderr while the child runs. - close_fd_or_panic(write_fd, "close write end in bubblewrap parent"); - - // SAFETY: `read_fd` is a valid owned fd in the parent. - let mut read_file = unsafe { File::from_raw_fd(read_fd) }; - let mut stderr_bytes = Vec::new(); - let mut limited_reader = (&mut read_file).take(MAX_PREFLIGHT_STDERR_BYTES); - if let Err(err) = limited_reader.read_to_end(&mut stderr_bytes) { - panic!("failed to read bubblewrap stderr: {err}"); - } - - let status = wait_for_bwrap_child(pid); - let cleanup_signal_mask = ForwardedSignalMask::block(); - BWRAP_CHILD_PID.store(0, Ordering::SeqCst); - cleanup_synthetic_mount_targets(&synthetic_mount_registrations); - cleanup_protected_create_targets(&protected_create_registrations); - signal_forwarders.restore(); - cleanup_signal_mask.restore(); - if libc::WIFSIGNALED(status) { - exit_with_wait_status(status); - } - - String::from_utf8_lossy(&stderr_bytes).into_owned() -} - -/// Close an owned file descriptor and panic with context on failure. -/// -/// We use explicit close() checks here (instead of ignoring return codes) -/// because this code runs in low-level sandbox setup paths where fd leaks or -/// close errors can mask the root cause of later failures. -fn close_fd_or_panic(fd: libc::c_int, context: &str) { - let close_res = unsafe { libc::close(fd) }; - if close_res < 0 { - let err = std::io::Error::last_os_error(); - panic!("{context}: {err}"); - } -} - fn is_proc_mount_failure(stderr: &str) -> bool { stderr.contains("Can't mount proc") && stderr.contains("/newroot/proc") diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index 228cea6e5d..2b3f963557 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -1,10 +1,6 @@ #[cfg(test)] use super::*; #[cfg(test)] -use crate::linux_run_main::install_bwrap_signal_forwarders; -#[cfg(test)] -use crate::linux_run_main::wait_for_bwrap_child; -#[cfg(test)] use codex_protocol::models::PermissionProfile; #[cfg(test)] use codex_protocol::protocol::FileSystemSandboxPolicy; @@ -269,180 +265,6 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() { assert!(argv.iter().any(|arg| arg == "--")); } -#[test] -fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let empty_file = temp_dir.path().join(".git"); - let empty_dir = temp_dir.path().join(".agents"); - let non_empty_file = temp_dir.path().join("non-empty"); - let missing_file = temp_dir.path().join(".missing"); - std::fs::write(&empty_file, "").expect("write empty file"); - std::fs::create_dir(&empty_dir).expect("create empty dir"); - std::fs::write(&non_empty_file, "keep").expect("write nonempty file"); - - let registrations = register_synthetic_mount_targets(&[ - crate::bwrap::SyntheticMountTarget::missing(&empty_file), - crate::bwrap::SyntheticMountTarget::missing_empty_directory(&empty_dir), - crate::bwrap::SyntheticMountTarget::missing(&non_empty_file), - crate::bwrap::SyntheticMountTarget::missing(&missing_file), - ]); - cleanup_synthetic_mount_targets(®istrations); - - assert!(!empty_file.exists()); - assert!(!empty_dir.exists()); - assert_eq!( - std::fs::read_to_string(&non_empty_file).expect("read nonempty file"), - "keep" - ); - assert!(!missing_file.exists()); -} - -#[test] -fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let empty_file = temp_dir.path().join(".git"); - std::fs::write(&empty_file, "").expect("write empty file"); - let target = crate::bwrap::SyntheticMountTarget::missing(&empty_file); - - let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target)); - let active_marker = registrations[0].marker_dir.join("1"); - std::fs::write(&active_marker, "").expect("write active marker"); - - cleanup_synthetic_mount_targets(®istrations); - assert!(empty_file.exists()); - - std::fs::remove_file(active_marker).expect("remove active marker"); - let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target)); - cleanup_synthetic_mount_targets(®istrations); - - assert!(!empty_file.exists()); -} - -#[test] -fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let empty_file = temp_dir.path().join(".git"); - let first_target = crate::bwrap::SyntheticMountTarget::missing(&empty_file); - - let first_registrations = register_synthetic_mount_targets(&[first_target]); - std::fs::write(&empty_file, "").expect("write transient empty file"); - let active_marker = first_registrations[0].marker_dir.join("1"); - std::fs::write(&active_marker, SYNTHETIC_MOUNT_MARKER_SYNTHETIC).expect("write active marker"); - let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file"); - let second_target = - crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata); - let second_registrations = register_synthetic_mount_targets(&[second_target]); - - cleanup_synthetic_mount_targets(&first_registrations); - assert!(empty_file.exists()); - - std::fs::remove_file(active_marker).expect("remove active marker"); - cleanup_synthetic_mount_targets(&second_registrations); - - assert!(!empty_file.exists()); -} - -#[test] -fn cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let empty_file = temp_dir.path().join(".git"); - std::fs::write(&empty_file, "").expect("write pre-existing empty file"); - let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file"); - let first_target = - crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata); - let second_target = - crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata); - - let first_registrations = register_synthetic_mount_targets(&[first_target]); - let second_registrations = register_synthetic_mount_targets(&[second_target]); - - cleanup_synthetic_mount_targets(&first_registrations); - cleanup_synthetic_mount_targets(&second_registrations); - - assert!(empty_file.exists()); -} - -#[test] -fn cleanup_protected_create_targets_removes_created_path_and_reports_violation() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let dot_git = temp_dir.path().join(".git"); - let target = crate::bwrap::ProtectedCreateTarget::missing(&dot_git); - - let registrations = register_protected_create_targets(&[target]); - std::fs::create_dir(&dot_git).expect("create protected path"); - let violation = cleanup_protected_create_targets(®istrations); - - assert!(violation); - assert!(!dot_git.exists()); -} - -#[test] -fn cleanup_protected_create_targets_waits_for_other_active_registrations() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let dot_git = temp_dir.path().join(".git"); - let target = crate::bwrap::ProtectedCreateTarget::missing(&dot_git); - - let registrations = register_protected_create_targets(std::slice::from_ref(&target)); - let active_marker = registrations[0].marker_dir.join("1"); - std::fs::write(&active_marker, PROTECTED_CREATE_MARKER).expect("write active marker"); - std::fs::write(&dot_git, "").expect("create protected path"); - - let violation = cleanup_protected_create_targets(®istrations); - assert!(violation); - assert!(dot_git.exists()); - - std::fs::remove_file(active_marker).expect("remove active marker"); - let registrations = register_protected_create_targets(std::slice::from_ref(&target)); - let violation = cleanup_protected_create_targets(®istrations); - - assert!(violation); - assert!(!dot_git.exists()); -} - -#[test] -fn bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive() { - let supervisor_pid = unsafe { libc::fork() }; - assert!(supervisor_pid >= 0, "failed to fork supervisor"); - - if supervisor_pid == 0 { - run_bwrap_signal_forwarder_test_supervisor(); - } - - let status = wait_for_bwrap_child(supervisor_pid); - assert!(libc::WIFEXITED(status), "supervisor status: {status}"); - assert_eq!(libc::WEXITSTATUS(status), 0); -} - -#[cfg(test)] -fn run_bwrap_signal_forwarder_test_supervisor() -> ! { - let child_pid = unsafe { libc::fork() }; - if child_pid < 0 { - unsafe { - libc::_exit(2); - } - } - - if child_pid == 0 { - loop { - unsafe { - libc::pause(); - } - } - } - - install_bwrap_signal_forwarders(child_pid); - unsafe { - libc::raise(libc::SIGTERM); - } - - let status = wait_for_bwrap_child(child_pid); - let child_terminated_by_sigterm = - libc::WIFSIGNALED(status) && libc::WTERMSIG(status) == libc::SIGTERM; - unsafe { - libc::_exit(if child_terminated_by_sigterm { 0 } else { 1 }); - } -} - #[test] fn managed_proxy_inner_command_includes_route_spec() { let permission_profile = read_only_permission_profile();