mirror of
https://github.com/openai/codex.git
synced 2026-05-03 10:56:37 +00:00
Compare commits
1 Commits
etraut/sid
...
codex/linu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eab33bf0f |
@@ -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()
|
||||
|
||||
190
codex-rs/linux-sandbox/src/file_system_protected_metadata.rs
Normal file
190
codex-rs/linux-sandbox/src/file_system_protected_metadata.rs
Normal 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
|
||||
}
|
||||
@@ -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(®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;
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(®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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user