Compare commits

...

1 Commits

Author SHA1 Message Date
Eva Wong
3eab33bf0f Split Linux metadata runtime modules 2026-05-01 08:48:56 -07:00
10 changed files with 1393 additions and 1324 deletions

View File

@@ -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<String>,
pub preserved_files: Vec<File>,
pub synthetic_mount_targets: Vec<SyntheticMountTarget>,
pub protected_create_targets: Vec<ProtectedCreateTarget>,
}
#[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<FileIdentity>,
}
#[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<String>, 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<PathBuf>,
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<PathBuf> {
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<PathBuf> {
args.protected_create_targets
args.file_system_permissions_enforcement
.protected_create_targets
.iter()
.map(|target| target.path().to_path_buf())
.collect()

View File

@@ -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<FileIdentity>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ProtectedCreateTarget {
path: PathBuf,
}
#[derive(Debug, Default)]
pub(crate) struct FileSystemPermissionsEnforcement {
pub(crate) synthetic_mount_targets: Vec<SyntheticMountTarget>,
pub(crate) protected_create_targets: Vec<ProtectedCreateTarget>,
}
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<PathBuf>,
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
}

View File

@@ -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<T> {
target: T,
marker_file: PathBuf,
marker_dir: PathBuf,
}
pub(crate) fn register_synthetic_mount_targets(
targets: &[SyntheticMountTarget],
) -> Vec<TargetRegistration<SyntheticMountTarget>> {
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<TargetRegistration<ProtectedCreateTarget>> {
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::<libc::pid_t>().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<SyntheticMountTarget>],
) {
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<ProtectedCreateTarget>],
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<bool> {
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<T>(f: impl FnOnce() -> T) -> T {
let registry_root = metadata_runtime_registry_root();
fs::create_dir_all(&registry_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;

View File

@@ -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(&registrations);
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(&registrations);
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(&registrations);
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(&registrations, 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(&registrations, 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(&registrations, ignore_metadata_violation);
assert!(violation);
assert!(!dot_git.exists());
}

View File

@@ -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<AtomicBool>,
violation: Arc<AtomicBool>,
handle: thread::JoinHandle<()>,
}
struct CreateWatcher {
fd: libc::c_int,
_watches: Vec<libc::c_int>,
}
impl CreateMonitor {
pub(crate) fn start(
targets: &[ProtectedCreateTarget],
report_violation: ViolationReporter,
) -> Option<Self> {
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<Self> {
let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
if fd < 0 {
return None;
}
let mut watched_parents = Vec::<PathBuf>::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);
}
}
}

View File

@@ -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;

View File

@@ -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 });
}
}

View File

@@ -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;

View File

@@ -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<AtomicBool>,
violation: Arc<AtomicBool>,
handle: thread::JoinHandle<()>,
}
struct ProtectedCreateWatcher {
fd: libc::c_int,
_watches: Vec<libc::c_int>,
}
#[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<Self> {
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<Self> {
let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
if fd < 0 {
return None;
}
let mut watched_parents = Vec::<PathBuf>::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<SyntheticMountTargetRegistration> {
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<ProtectedCreateTargetRegistration> {
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::<libc::pid_t>().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<ProtectedCreateRemoval> {
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<Option<ProtectedCreateRemoval>> {
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<T>(f: impl FnOnce() -> T) -> T {
let registry_root = synthetic_mount_registry_root();
fs::create_dir_all(&registry_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")

View File

@@ -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(&registrations);
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(&registrations);
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(&registrations);
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(&registrations);
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(&registrations);
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(&registrations);
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();