Compare commits

...

1 Commits

Author SHA1 Message Date
viyatb-oai
db1b5f04a2 refactor: split linux metadata guard runtime
Move protected metadata guard target modeling, runtime supervision, and cleanup/watch logic into focused Linux sandbox modules.

Co-authored-by: Codex <noreply@openai.com>
2026-04-29 23:06:10 -07:00
10 changed files with 1305 additions and 1266 deletions

View File

@@ -19,11 +19,14 @@ use std::fs::Metadata;
use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
pub(crate) use crate::metadata_paths::ProtectedCreateTarget;
pub(crate) use crate::metadata_paths::SyntheticMountTarget;
pub(crate) use crate::metadata_paths::SyntheticMountTargetKind;
use crate::metadata_paths::should_leave_missing_git_for_parent_repo_discovery;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use codex_protocol::permissions::is_protected_metadata_name;
@@ -111,119 +114,6 @@ pub(crate) struct BwrapArgs {
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,
}
}
}
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
/// with explicit writable roots and read-only subpaths layered afterward.
///
@@ -669,34 +559,6 @@ fn append_metadata_path_masks_for_writable_root(
}
}
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,

View File

@@ -0,0 +1,372 @@
use std::fs::File;
use std::io::Read;
use std::os::fd::FromRawFd;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use crate::bwrap::BwrapArgs;
use crate::launcher::exec_bwrap;
use crate::metadata_guard::MetadataGuardRegistrations;
use crate::metadata_guard::ProtectedCreateMonitor;
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) -> ! {
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_with_runtime_supervision(bwrap_args);
}
fn run_bwrap_with_runtime_supervision(bwrap_args: BwrapArgs) -> ! {
let BwrapArgs {
args,
preserved_files,
synthetic_mount_targets,
protected_create_targets,
} = bwrap_args;
let setup_signal_mask = ForwardedSignalMask::block();
let metadata_guard_registrations =
MetadataGuardRegistrations::register(&synthetic_mount_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);
let protected_create_violation =
metadata_guard_registrations.cleanup(protected_create_monitor_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);
}
pub(crate) fn capture_bwrap_preflight_stderr(bwrap_args: BwrapArgs) -> String {
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
let BwrapArgs {
args,
preserved_files,
synthetic_mount_targets,
protected_create_targets,
} = bwrap_args;
let setup_signal_mask = ForwardedSignalMask::block();
let metadata_guard_registrations =
MetadataGuardRegistrations::register(&synthetic_mount_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();
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();
close_fd_or_panic(write_fd, "close write end in bubblewrap 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);
metadata_guard_registrations.cleanup(/*protected_create_monitor_violation*/ false);
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 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 = "bwrap_runtime_tests.rs"]
mod tests;

View File

@@ -0,0 +1,45 @@
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);
}
fn run_bwrap_signal_forwarder_test_supervisor() -> ! {
let child_pid = unsafe { libc::fork() };
if child_pid < 0 {
unsafe {
libc::_exit(2);
}
}
if child_pid == 0 {
loop {
unsafe {
libc::pause();
}
}
}
install_bwrap_signal_forwarders(child_pid);
unsafe {
libc::raise(libc::SIGTERM);
}
let status = wait_for_bwrap_child(child_pid);
let child_terminated_by_sigterm =
libc::WIFSIGNALED(status) && libc::WTERMSIG(status) == libc::SIGTERM;
unsafe {
libc::_exit(if child_terminated_by_sigterm { 0 } else { 1 });
}
}

View File

@@ -6,12 +6,20 @@
#[cfg(target_os = "linux")]
mod bwrap;
#[cfg(target_os = "linux")]
mod bwrap_runtime;
#[cfg(target_os = "linux")]
mod landlock;
#[cfg(target_os = "linux")]
mod launcher;
#[cfg(target_os = "linux")]
mod linux_run_main;
#[cfg(target_os = "linux")]
mod metadata_guard;
#[cfg(target_os = "linux")]
mod metadata_guard_watcher;
#[cfg(target_os = "linux")]
mod metadata_paths;
#[cfg(target_os = "linux")]
mod proxy_routing;
#[cfg(target_os = "linux")]
mod vendored_bwrap;

View File

@@ -1,27 +1,15 @@
use clap::Parser;
use std::ffi::CString;
use std::fmt;
use std::fs;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Read;
use std::os::fd::AsRawFd;
use std::os::fd::FromRawFd;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use crate::bwrap::BwrapNetworkMode;
use crate::bwrap::BwrapOptions;
use crate::bwrap::create_bwrap_command_args;
use crate::bwrap_runtime::capture_bwrap_preflight_stderr;
use crate::bwrap_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.
///
@@ -446,7 +394,7 @@ fn preflight_proc_mount_support(
file_system_sandbox_policy,
network_mode,
);
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
let stderr = capture_bwrap_preflight_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
}
@@ -479,897 +427,6 @@ fn resolve_true_command() -> String {
"true".to_string()
}
fn run_or_exec_bwrap(bwrap_args: crate::bwrap::BwrapArgs) -> ! {
if bwrap_args.synthetic_mount_targets.is_empty()
&& bwrap_args.protected_create_targets.is_empty()
{
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
}
run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args);
}
fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::BwrapArgs) -> ! {
let crate::bwrap::BwrapArgs {
args,
preserved_files,
synthetic_mount_targets,
protected_create_targets,
} = bwrap_args;
let setup_signal_mask = ForwardedSignalMask::block();
let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets);
let protected_create_registrations =
register_protected_create_targets(&protected_create_targets);
let exec_start_pipe = create_exec_start_pipe(!protected_create_targets.is_empty());
let parent_pid = unsafe { libc::getpid() };
let pid = unsafe { libc::fork() };
if pid < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to fork for bubblewrap: {err}");
}
if pid == 0 {
reset_forwarded_signal_handlers_to_default();
setup_signal_mask.restore();
let setpgid_res = unsafe { libc::setpgid(0, 0) };
if setpgid_res < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to place bubblewrap child in its own process group: {err}");
}
terminate_with_parent(parent_pid);
wait_for_parent_exec_start(exec_start_pipe[0], exec_start_pipe[1]);
exec_bwrap(args, preserved_files);
}
close_child_exec_start_read(exec_start_pipe[0]);
let protected_create_monitor = ProtectedCreateMonitor::start(&protected_create_targets);
let signal_forwarders = install_bwrap_signal_forwarders(pid);
release_child_exec_start(exec_start_pipe[1]);
setup_signal_mask.restore();
let status = wait_for_bwrap_child(pid);
let cleanup_signal_mask = ForwardedSignalMask::block();
BWRAP_CHILD_PID.store(0, Ordering::SeqCst);
let protected_create_monitor_violation = protected_create_monitor
.map(ProtectedCreateMonitor::stop)
.unwrap_or(false);
cleanup_synthetic_mount_targets(&synthetic_mount_registrations);
let protected_create_violation = protected_create_monitor_violation
|| cleanup_protected_create_targets(&protected_create_registrations);
signal_forwarders.restore();
cleanup_signal_mask.restore();
exit_with_wait_status_or_policy_violation(status, protected_create_violation);
}
impl ProtectedCreateMonitor {
fn start(targets: &[crate::bwrap::ProtectedCreateTarget]) -> Option<Self> {
if targets.is_empty() {
return None;
}
let targets = targets.to_vec();
let stop = Arc::new(AtomicBool::new(false));
let violation = Arc::new(AtomicBool::new(false));
let monitor_stop = Arc::clone(&stop);
let monitor_violation = Arc::clone(&violation);
let handle = thread::spawn(move || {
let watcher = ProtectedCreateWatcher::new(&targets);
while !monitor_stop.load(Ordering::SeqCst) {
for target in &targets {
if remove_protected_create_target_best_effort(target).is_some() {
monitor_violation.store(true, Ordering::SeqCst);
}
}
if let Some(watcher) = &watcher {
watcher.wait_for_create_event(&monitor_stop);
} else {
thread::sleep(Duration::from_millis(1));
}
}
});
Some(Self {
stop,
violation,
handle,
})
}
fn stop(self) -> bool {
self.stop.store(true, Ordering::SeqCst);
self.handle
.join()
.unwrap_or_else(|_| panic!("protected create monitor thread panicked"));
self.violation.load(Ordering::SeqCst)
}
}
impl ProtectedCreateWatcher {
fn new(targets: &[crate::bwrap::ProtectedCreateTarget]) -> Option<Self> {
let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
if fd < 0 {
return None;
}
let mut watched_parents = Vec::<PathBuf>::new();
let mut watches = Vec::new();
for target in targets {
let Some(parent) = target.path().parent() else {
continue;
};
if watched_parents.iter().any(|watched| watched == parent) {
continue;
}
watched_parents.push(parent.to_path_buf());
let Ok(parent_cstr) = CString::new(parent.as_os_str().as_bytes()) else {
continue;
};
let mask =
libc::IN_CREATE | libc::IN_MOVED_TO | libc::IN_DELETE_SELF | libc::IN_MOVE_SELF;
let watch = unsafe { libc::inotify_add_watch(fd, parent_cstr.as_ptr(), mask) };
if watch >= 0 {
watches.push(watch);
}
}
if watches.is_empty() {
unsafe {
libc::close(fd);
}
return None;
}
Some(Self {
fd,
_watches: watches,
})
}
fn wait_for_create_event(&self, stop: &AtomicBool) {
let mut poll_fd = libc::pollfd {
fd: self.fd,
events: libc::POLLIN,
revents: 0,
};
while !stop.load(Ordering::SeqCst) {
let res = unsafe { libc::poll(&mut poll_fd, 1, 10) };
if res > 0 {
self.drain_events();
return;
}
if res == 0 {
return;
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return;
}
}
fn drain_events(&self) {
let mut buf = [0_u8; 4096];
loop {
let read = unsafe { libc::read(self.fd, buf.as_mut_ptr().cast(), buf.len()) };
if read > 0 {
continue;
}
if read == 0 {
return;
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return;
}
}
}
impl Drop for ProtectedCreateWatcher {
fn drop(&mut self) {
unsafe {
libc::close(self.fd);
}
}
}
fn create_exec_start_pipe(enabled: bool) -> [libc::c_int; 2] {
if !enabled {
return [-1, -1];
}
let mut pipe = [-1, -1];
if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to create bubblewrap exec start pipe: {err}");
}
pipe
}
fn wait_for_parent_exec_start(read_fd: libc::c_int, write_fd: libc::c_int) {
if write_fd >= 0 {
unsafe {
libc::close(write_fd);
}
}
if read_fd < 0 {
return;
}
let mut byte = [0_u8; 1];
loop {
let read = unsafe { libc::read(read_fd, byte.as_mut_ptr().cast(), byte.len()) };
if read >= 0 {
break;
}
let err = std::io::Error::last_os_error();
if err.kind() != std::io::ErrorKind::Interrupted {
break;
}
}
unsafe {
libc::close(read_fd);
}
}
fn close_child_exec_start_read(read_fd: libc::c_int) {
if read_fd >= 0 {
unsafe {
libc::close(read_fd);
}
}
}
fn release_child_exec_start(write_fd: libc::c_int) {
if write_fd < 0 {
return;
}
let byte = [0_u8; 1];
unsafe {
libc::write(write_fd, byte.as_ptr().cast(), byte.len());
libc::close(write_fd);
}
}
struct ForwardedSignalMask {
previous: libc::sigset_t,
}
struct ForwardedSignalHandlers {
previous: Vec<(libc::c_int, libc::sigaction)>,
}
impl ForwardedSignalMask {
fn block() -> Self {
let mut blocked: libc::sigset_t = unsafe { std::mem::zeroed() };
let mut previous: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe {
libc::sigemptyset(&mut blocked);
for signal in FORWARDED_SIGNALS {
libc::sigaddset(&mut blocked, *signal);
}
if libc::sigprocmask(libc::SIG_BLOCK, &blocked, &mut previous) < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to block bubblewrap forwarded signals: {err}");
}
}
Self { previous }
}
fn restore(&self) {
let mut restored = self.previous;
unsafe {
for signal in FORWARDED_SIGNALS {
libc::sigdelset(&mut restored, *signal);
}
if libc::sigprocmask(libc::SIG_SETMASK, &restored, std::ptr::null_mut()) < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to restore bubblewrap forwarded signals: {err}");
}
}
}
}
fn terminate_with_parent(parent_pid: libc::pid_t) {
let res = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) };
if res < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to set bubblewrap child parent-death signal: {err}");
}
if unsafe { libc::getppid() } != parent_pid {
unsafe {
libc::raise(libc::SIGTERM);
}
}
}
impl ForwardedSignalHandlers {
fn restore(self) {
BWRAP_CHILD_PID.store(0, Ordering::SeqCst);
PENDING_FORWARDED_SIGNAL.store(0, Ordering::SeqCst);
for (signal, previous_action) in self.previous {
unsafe {
if libc::sigaction(signal, &previous_action, std::ptr::null_mut()) < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to restore bubblewrap signal handler for {signal}: {err}");
}
}
}
}
}
fn install_bwrap_signal_forwarders(pid: libc::pid_t) -> ForwardedSignalHandlers {
BWRAP_CHILD_PID.store(pid, Ordering::SeqCst);
let mut previous = Vec::with_capacity(FORWARDED_SIGNALS.len());
for signal in FORWARDED_SIGNALS {
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
let mut previous_action: libc::sigaction = unsafe { std::mem::zeroed() };
action.sa_sigaction = forward_signal_to_bwrap_child as *const () as libc::sighandler_t;
unsafe {
libc::sigemptyset(&mut action.sa_mask);
if libc::sigaction(*signal, &action, &mut previous_action) < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to install bubblewrap signal forwarder for {signal}: {err}");
}
}
previous.push((*signal, previous_action));
}
replay_pending_forwarded_signal(pid);
ForwardedSignalHandlers { previous }
}
extern "C" fn forward_signal_to_bwrap_child(signal: libc::c_int) {
PENDING_FORWARDED_SIGNAL.store(signal, Ordering::SeqCst);
let pid = BWRAP_CHILD_PID.load(Ordering::SeqCst);
if pid > 0 {
send_signal_to_bwrap_child(pid, signal);
}
}
fn replay_pending_forwarded_signal(pid: libc::pid_t) {
let signal = PENDING_FORWARDED_SIGNAL.swap(0, Ordering::SeqCst);
if signal > 0 {
send_signal_to_bwrap_child(pid, signal);
}
}
fn send_signal_to_bwrap_child(pid: libc::pid_t, signal: libc::c_int) {
unsafe {
libc::kill(-pid, signal);
libc::kill(pid, signal);
}
}
fn reset_forwarded_signal_handlers_to_default() {
for signal in FORWARDED_SIGNALS {
unsafe {
if libc::signal(*signal, libc::SIG_DFL) == libc::SIG_ERR {
let err = std::io::Error::last_os_error();
panic!("failed to reset bubblewrap signal handler for {signal}: {err}");
}
}
}
}
fn wait_for_bwrap_child(pid: libc::pid_t) -> libc::c_int {
loop {
let mut status: libc::c_int = 0;
let wait_res = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, 0) };
if wait_res >= 0 {
return status;
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
panic!("waitpid failed for bubblewrap child: {err}");
}
}
fn register_synthetic_mount_targets(
targets: &[crate::bwrap::SyntheticMountTarget],
) -> Vec<SyntheticMountTargetRegistration> {
with_synthetic_mount_registry_lock(|| {
targets
.iter()
.map(|target| {
let marker_dir = synthetic_mount_marker_dir(target.path());
fs::create_dir_all(&marker_dir).unwrap_or_else(|err| {
panic!(
"failed to create synthetic bubblewrap mount marker directory {}: {err}",
marker_dir.display()
)
});
let target = if target.preserves_pre_existing_path()
&& synthetic_mount_marker_dir_has_active_synthetic_owner(&marker_dir)
{
match target.kind() {
crate::bwrap::SyntheticMountTargetKind::EmptyFile => {
crate::bwrap::SyntheticMountTarget::missing(target.path())
}
crate::bwrap::SyntheticMountTargetKind::EmptyDirectory => {
crate::bwrap::SyntheticMountTarget::missing_empty_directory(
target.path(),
)
}
}
} else {
target.clone()
};
let marker_file = marker_dir.join(std::process::id().to_string());
fs::write(&marker_file, synthetic_mount_marker_contents(&target)).unwrap_or_else(
|err| {
panic!(
"failed to register synthetic bubblewrap mount target {}: {err}",
target.path().display()
)
},
);
SyntheticMountTargetRegistration {
target,
marker_file,
marker_dir,
}
})
.collect()
})
}
fn register_protected_create_targets(
targets: &[crate::bwrap::ProtectedCreateTarget],
) -> Vec<ProtectedCreateTargetRegistration> {
with_synthetic_mount_registry_lock(|| {
targets
.iter()
.map(|target| {
let marker_dir = synthetic_mount_marker_dir(target.path());
fs::create_dir_all(&marker_dir).unwrap_or_else(|err| {
panic!(
"failed to create protected create marker directory {}: {err}",
marker_dir.display()
)
});
let marker_file = marker_dir.join(std::process::id().to_string());
fs::write(&marker_file, PROTECTED_CREATE_MARKER).unwrap_or_else(|err| {
panic!(
"failed to register protected create target {}: {err}",
target.path().display()
)
});
ProtectedCreateTargetRegistration {
target: target.clone(),
marker_file,
marker_dir,
}
})
.collect()
})
}
fn synthetic_mount_marker_contents(target: &crate::bwrap::SyntheticMountTarget) -> &'static [u8] {
if target.preserves_pre_existing_path() {
SYNTHETIC_MOUNT_MARKER_EXISTING
} else {
SYNTHETIC_MOUNT_MARKER_SYNTHETIC
}
}
fn synthetic_mount_marker_dir_has_active_synthetic_owner(marker_dir: &Path) -> bool {
synthetic_mount_marker_dir_has_active_process_matching(marker_dir, |path| {
match fs::read(path) {
Ok(contents) => contents == SYNTHETIC_MOUNT_MARKER_SYNTHETIC,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
Err(err) => panic!(
"failed to read synthetic bubblewrap mount marker {}: {err}",
path.display()
),
}
})
}
fn synthetic_mount_marker_dir_has_active_process(marker_dir: &Path) -> bool {
synthetic_mount_marker_dir_has_active_process_matching(marker_dir, |_| true)
}
fn synthetic_mount_marker_dir_has_active_process_matching(
marker_dir: &Path,
matches_marker: impl Fn(&Path) -> bool,
) -> bool {
let entries = match fs::read_dir(marker_dir) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return false,
Err(err) => panic!(
"failed to read synthetic bubblewrap mount marker directory {}: {err}",
marker_dir.display()
),
};
for entry in entries {
let entry = entry.unwrap_or_else(|err| {
panic!(
"failed to read synthetic bubblewrap mount marker in {}: {err}",
marker_dir.display()
)
});
let path = entry.path();
let Some(pid) = path
.file_name()
.and_then(|name| name.to_str())
.and_then(|name| name.parse::<libc::pid_t>().ok())
else {
continue;
};
if !process_is_active(pid) {
match fs::remove_file(&path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => panic!(
"failed to remove stale synthetic bubblewrap mount marker {}: {err}",
path.display()
),
}
continue;
}
let matches_marker = matches_marker(&path);
if matches_marker {
return true;
}
}
false
}
fn cleanup_synthetic_mount_targets(targets: &[SyntheticMountTargetRegistration]) {
with_synthetic_mount_registry_lock(|| {
for target in targets.iter().rev() {
match fs::remove_file(&target.marker_file) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => panic!(
"failed to unregister synthetic bubblewrap mount target {}: {err}",
target.target.path().display()
),
}
}
for target in targets.iter().rev() {
if synthetic_mount_marker_dir_has_active_process(&target.marker_dir) {
continue;
}
remove_synthetic_mount_target(&target.target);
match fs::remove_dir(&target.marker_dir) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {}
Err(err) => panic!(
"failed to remove synthetic bubblewrap mount marker directory {}: {err}",
target.marker_dir.display()
),
}
}
});
}
fn cleanup_protected_create_targets(targets: &[ProtectedCreateTargetRegistration]) -> bool {
with_synthetic_mount_registry_lock(|| {
for target in targets.iter().rev() {
match fs::remove_file(&target.marker_file) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => panic!(
"failed to unregister protected create target {}: {err}",
target.target.path().display()
),
}
}
let mut violation = false;
for target in targets.iter().rev() {
if synthetic_mount_marker_dir_has_active_process(&target.marker_dir) {
if target.target.path().exists() {
violation = true;
}
continue;
}
violation |= remove_protected_create_target(&target.target);
match fs::remove_dir(&target.marker_dir) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {}
Err(err) => panic!(
"failed to remove protected create marker directory {}: {err}",
target.marker_dir.display()
),
}
}
violation
})
}
fn remove_protected_create_target(target: &crate::bwrap::ProtectedCreateTarget) -> bool {
for attempt in 0..100 {
match try_remove_protected_create_target(target) {
Ok(removal) => return removal.is_some(),
Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty && attempt < 99 => {
thread::sleep(Duration::from_millis(1));
}
Err(err) => {
panic!(
"failed to remove protected create target {}: {err}",
target.path().display()
);
}
}
}
unreachable!("protected create removal retry loop should return or panic")
}
fn remove_protected_create_target_best_effort(
target: &crate::bwrap::ProtectedCreateTarget,
) -> Option<ProtectedCreateRemoval> {
for _ in 0..100 {
match try_remove_protected_create_target(target) {
Ok(removal) => return removal,
Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {
thread::sleep(Duration::from_millis(1));
}
Err(_) => return Some(ProtectedCreateRemoval::Other),
}
}
Some(ProtectedCreateRemoval::Other)
}
fn try_remove_protected_create_target(
target: &crate::bwrap::ProtectedCreateTarget,
) -> std::io::Result<Option<ProtectedCreateRemoval>> {
let path = target.path();
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err),
};
let removal = if metadata.is_dir() {
ProtectedCreateRemoval::Directory
} else {
ProtectedCreateRemoval::Other
};
let result = if removal == ProtectedCreateRemoval::Directory {
fs::remove_dir_all(path)
} else {
fs::remove_file(path)
};
match result {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err),
}
eprintln!(
"sandbox blocked creation of protected workspace metadata path {}",
path.display()
);
Ok(Some(removal))
}
fn remove_synthetic_mount_target(target: &crate::bwrap::SyntheticMountTarget) {
let path = target.path();
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return,
Err(err) => panic!(
"failed to inspect synthetic bubblewrap mount target {}: {err}",
path.display()
),
};
if !target.should_remove_after_bwrap(&metadata) {
return;
}
match target.kind() {
crate::bwrap::SyntheticMountTargetKind::EmptyFile => match fs::remove_file(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => panic!(
"failed to remove synthetic bubblewrap mount target {}: {err}",
path.display()
),
},
crate::bwrap::SyntheticMountTargetKind::EmptyDirectory => match fs::remove_dir(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {}
Err(err) => panic!(
"failed to remove synthetic bubblewrap mount target {}: {err}",
path.display()
),
},
}
}
fn process_is_active(pid: libc::pid_t) -> bool {
let result = unsafe { libc::kill(pid, 0) };
if result == 0 {
return true;
}
let err = std::io::Error::last_os_error();
!matches!(err.raw_os_error(), Some(libc::ESRCH))
}
fn with_synthetic_mount_registry_lock<T>(f: impl FnOnce() -> T) -> T {
let registry_root = synthetic_mount_registry_root();
fs::create_dir_all(&registry_root).unwrap_or_else(|err| {
panic!(
"failed to create synthetic bubblewrap mount registry {}: {err}",
registry_root.display()
)
});
let lock_path = registry_root.join("lock");
let lock_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.unwrap_or_else(|err| {
panic!(
"failed to open synthetic bubblewrap mount registry lock {}: {err}",
lock_path.display()
)
});
if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) } < 0 {
let err = std::io::Error::last_os_error();
panic!(
"failed to lock synthetic bubblewrap mount registry {}: {err}",
lock_path.display()
);
}
let result = f();
if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN) } < 0 {
let err = std::io::Error::last_os_error();
panic!(
"failed to unlock synthetic bubblewrap mount registry {}: {err}",
lock_path.display()
);
}
result
}
fn synthetic_mount_marker_dir(path: &Path) -> PathBuf {
synthetic_mount_registry_root().join(format!("{:016x}", hash_path(path)))
}
fn synthetic_mount_registry_root() -> PathBuf {
std::env::temp_dir().join("codex-bwrap-synthetic-mount-targets")
}
fn hash_path(path: &Path) -> u64 {
let mut hash = 0xcbf29ce484222325u64;
for byte in path.as_os_str().as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
fn exit_with_wait_status(status: libc::c_int) -> ! {
if libc::WIFEXITED(status) {
std::process::exit(libc::WEXITSTATUS(status));
}
if libc::WIFSIGNALED(status) {
let signal = libc::WTERMSIG(status);
unsafe {
libc::signal(signal, libc::SIG_DFL);
libc::kill(libc::getpid(), signal);
}
std::process::exit(128 + signal);
}
std::process::exit(1);
}
fn exit_with_wait_status_or_policy_violation(
status: libc::c_int,
protected_create_violation: bool,
) -> ! {
if protected_create_violation && libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
std::process::exit(1);
}
exit_with_wait_status(status);
}
/// Run a short-lived bubblewrap preflight in a child process and capture stderr.
///
/// Strategy:
/// - This is used only by `preflight_proc_mount_support`, which runs `/bin/true`
/// under bubblewrap with `--proc /proc`.
/// - The goal is to detect environments where mounting `/proc` fails (for
/// example, restricted containers), so we can retry the real run with
/// `--no-proc`.
/// - We capture stderr from that preflight to match known mount-failure text.
/// We do not stream it because this is a one-shot probe with a trivial
/// command, and reads are bounded to a fixed max size.
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String {
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
let crate::bwrap::BwrapArgs {
args,
preserved_files,
synthetic_mount_targets,
protected_create_targets,
} = bwrap_args;
let setup_signal_mask = ForwardedSignalMask::block();
let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets);
let protected_create_registrations =
register_protected_create_targets(&protected_create_targets);
let mut pipe_fds = [0; 2];
let pipe_res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
if pipe_res < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to create stderr pipe for bubblewrap: {err}");
}
let read_fd = pipe_fds[0];
let write_fd = pipe_fds[1];
let pid = unsafe { libc::fork() };
if pid < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to fork for bubblewrap: {err}");
}
if pid == 0 {
reset_forwarded_signal_handlers_to_default();
setup_signal_mask.restore();
// Child: redirect stderr to the pipe, then run bubblewrap.
unsafe {
close_fd_or_panic(read_fd, "close read end in bubblewrap child");
if libc::dup2(write_fd, libc::STDERR_FILENO) < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to redirect stderr for bubblewrap: {err}");
}
close_fd_or_panic(write_fd, "close write end in bubblewrap child");
}
exec_bwrap(args, preserved_files);
}
let signal_forwarders = install_bwrap_signal_forwarders(pid);
setup_signal_mask.restore();
// Parent: close the write end and read stderr while the child runs.
close_fd_or_panic(write_fd, "close write end in bubblewrap parent");
// SAFETY: `read_fd` is a valid owned fd in the parent.
let mut read_file = unsafe { File::from_raw_fd(read_fd) };
let mut stderr_bytes = Vec::new();
let mut limited_reader = (&mut read_file).take(MAX_PREFLIGHT_STDERR_BYTES);
if let Err(err) = limited_reader.read_to_end(&mut stderr_bytes) {
panic!("failed to read bubblewrap stderr: {err}");
}
let status = wait_for_bwrap_child(pid);
let cleanup_signal_mask = ForwardedSignalMask::block();
BWRAP_CHILD_PID.store(0, Ordering::SeqCst);
cleanup_synthetic_mount_targets(&synthetic_mount_registrations);
cleanup_protected_create_targets(&protected_create_registrations);
signal_forwarders.restore();
cleanup_signal_mask.restore();
if libc::WIFSIGNALED(status) {
exit_with_wait_status(status);
}
String::from_utf8_lossy(&stderr_bytes).into_owned()
}
/// Close an owned file descriptor and panic with context on failure.
///
/// We use explicit close() checks here (instead of ignoring return codes)
/// because this code runs in low-level sandbox setup paths where fd leaks or
/// close errors can mask the root cause of later failures.
fn close_fd_or_panic(fd: libc::c_int, context: &str) {
let close_res = unsafe { libc::close(fd) };
if close_res < 0 {
let err = std::io::Error::last_os_error();
panic!("{context}: {err}");
}
}
fn is_proc_mount_failure(stderr: &str) -> bool {
stderr.contains("Can't mount proc")
&& stderr.contains("/newroot/proc")

View File

@@ -1,10 +1,6 @@
#[cfg(test)]
use super::*;
#[cfg(test)]
use crate::linux_run_main::install_bwrap_signal_forwarders;
#[cfg(test)]
use crate::linux_run_main::wait_for_bwrap_child;
#[cfg(test)]
use codex_protocol::models::PermissionProfile;
#[cfg(test)]
use codex_protocol::protocol::FileSystemSandboxPolicy;
@@ -269,180 +265,6 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
assert!(argv.iter().any(|arg| arg == "--"));
}
#[test]
fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
let empty_dir = temp_dir.path().join(".agents");
let non_empty_file = temp_dir.path().join("non-empty");
let missing_file = temp_dir.path().join(".missing");
std::fs::write(&empty_file, "").expect("write empty file");
std::fs::create_dir(&empty_dir).expect("create empty dir");
std::fs::write(&non_empty_file, "keep").expect("write nonempty file");
let registrations = register_synthetic_mount_targets(&[
crate::bwrap::SyntheticMountTarget::missing(&empty_file),
crate::bwrap::SyntheticMountTarget::missing_empty_directory(&empty_dir),
crate::bwrap::SyntheticMountTarget::missing(&non_empty_file),
crate::bwrap::SyntheticMountTarget::missing(&missing_file),
]);
cleanup_synthetic_mount_targets(&registrations);
assert!(!empty_file.exists());
assert!(!empty_dir.exists());
assert_eq!(
std::fs::read_to_string(&non_empty_file).expect("read nonempty file"),
"keep"
);
assert!(!missing_file.exists());
}
#[test]
fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
std::fs::write(&empty_file, "").expect("write empty file");
let target = crate::bwrap::SyntheticMountTarget::missing(&empty_file);
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
let active_marker = registrations[0].marker_dir.join("1");
std::fs::write(&active_marker, "").expect("write active marker");
cleanup_synthetic_mount_targets(&registrations);
assert!(empty_file.exists());
std::fs::remove_file(active_marker).expect("remove active marker");
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
cleanup_synthetic_mount_targets(&registrations);
assert!(!empty_file.exists());
}
#[test]
fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
let first_target = crate::bwrap::SyntheticMountTarget::missing(&empty_file);
let first_registrations = register_synthetic_mount_targets(&[first_target]);
std::fs::write(&empty_file, "").expect("write transient empty file");
let active_marker = first_registrations[0].marker_dir.join("1");
std::fs::write(&active_marker, SYNTHETIC_MOUNT_MARKER_SYNTHETIC).expect("write active marker");
let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file");
let second_target =
crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
let second_registrations = register_synthetic_mount_targets(&[second_target]);
cleanup_synthetic_mount_targets(&first_registrations);
assert!(empty_file.exists());
std::fs::remove_file(active_marker).expect("remove active marker");
cleanup_synthetic_mount_targets(&second_registrations);
assert!(!empty_file.exists());
}
#[test]
fn cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
std::fs::write(&empty_file, "").expect("write pre-existing empty file");
let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file");
let first_target =
crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
let second_target =
crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
let first_registrations = register_synthetic_mount_targets(&[first_target]);
let second_registrations = register_synthetic_mount_targets(&[second_target]);
cleanup_synthetic_mount_targets(&first_registrations);
cleanup_synthetic_mount_targets(&second_registrations);
assert!(empty_file.exists());
}
#[test]
fn cleanup_protected_create_targets_removes_created_path_and_reports_violation() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let dot_git = temp_dir.path().join(".git");
let target = crate::bwrap::ProtectedCreateTarget::missing(&dot_git);
let registrations = register_protected_create_targets(&[target]);
std::fs::create_dir(&dot_git).expect("create protected path");
let violation = cleanup_protected_create_targets(&registrations);
assert!(violation);
assert!(!dot_git.exists());
}
#[test]
fn cleanup_protected_create_targets_waits_for_other_active_registrations() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let dot_git = temp_dir.path().join(".git");
let target = crate::bwrap::ProtectedCreateTarget::missing(&dot_git);
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
let active_marker = registrations[0].marker_dir.join("1");
std::fs::write(&active_marker, PROTECTED_CREATE_MARKER).expect("write active marker");
std::fs::write(&dot_git, "").expect("create protected path");
let violation = cleanup_protected_create_targets(&registrations);
assert!(violation);
assert!(dot_git.exists());
std::fs::remove_file(active_marker).expect("remove active marker");
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
let violation = cleanup_protected_create_targets(&registrations);
assert!(violation);
assert!(!dot_git.exists());
}
#[test]
fn bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive() {
let supervisor_pid = unsafe { libc::fork() };
assert!(supervisor_pid >= 0, "failed to fork supervisor");
if supervisor_pid == 0 {
run_bwrap_signal_forwarder_test_supervisor();
}
let status = wait_for_bwrap_child(supervisor_pid);
assert!(libc::WIFEXITED(status), "supervisor status: {status}");
assert_eq!(libc::WEXITSTATUS(status), 0);
}
#[cfg(test)]
fn run_bwrap_signal_forwarder_test_supervisor() -> ! {
let child_pid = unsafe { libc::fork() };
if child_pid < 0 {
unsafe {
libc::_exit(2);
}
}
if child_pid == 0 {
loop {
unsafe {
libc::pause();
}
}
}
install_bwrap_signal_forwarders(child_pid);
unsafe {
libc::raise(libc::SIGTERM);
}
let status = wait_for_bwrap_child(child_pid);
let child_terminated_by_sigterm =
libc::WIFSIGNALED(status) && libc::WTERMSIG(status) == libc::SIGTERM;
unsafe {
libc::_exit(if child_terminated_by_sigterm { 0 } else { 1 });
}
}
#[test]
fn managed_proxy_inner_command_includes_route_spec() {
let permission_profile = read_only_permission_profile();

View File

@@ -0,0 +1,489 @@
use std::fs;
use std::fs::OpenOptions;
use std::os::fd::AsRawFd;
use std::path::Path;
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::metadata_guard_watcher::ProtectedCreateWatcher;
use crate::metadata_paths::ProtectedCreateTarget;
use crate::metadata_paths::SyntheticMountTarget;
use crate::metadata_paths::SyntheticMountTargetKind;
const METADATA_GUARD_MARKER_SYNTHETIC: &[u8] = b"synthetic\n";
const METADATA_GUARD_MARKER_EXISTING: &[u8] = b"existing\n";
const PROTECTED_CREATE_MARKER: &[u8] = b"protected-create\n";
#[derive(Debug)]
struct SyntheticMountTargetRegistration {
target: SyntheticMountTarget,
marker_file: PathBuf,
marker_dir: PathBuf,
}
#[derive(Debug)]
struct ProtectedCreateTargetRegistration {
target: ProtectedCreateTarget,
marker_file: PathBuf,
marker_dir: PathBuf,
}
pub(crate) struct MetadataGuardRegistrations {
synthetic_mounts: Vec<SyntheticMountTargetRegistration>,
protected_creates: Vec<ProtectedCreateTargetRegistration>,
}
pub(crate) struct ProtectedCreateMonitor {
stop: Arc<AtomicBool>,
violation: Arc<AtomicBool>,
handle: thread::JoinHandle<()>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ProtectedCreateRemoval {
Directory,
Other,
}
impl MetadataGuardRegistrations {
pub(crate) fn register(
synthetic_mounts: &[SyntheticMountTarget],
protected_creates: &[ProtectedCreateTarget],
) -> Self {
Self {
synthetic_mounts: register_synthetic_mount_targets(synthetic_mounts),
protected_creates: register_protected_create_targets(protected_creates),
}
}
pub(crate) fn cleanup(self, protected_create_monitor_violation: bool) -> bool {
cleanup_synthetic_mount_targets(&self.synthetic_mounts);
protected_create_monitor_violation
|| cleanup_protected_create_targets(&self.protected_creates)
}
}
impl ProtectedCreateMonitor {
pub(crate) fn start(targets: &[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,
})
}
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)
}
}
fn register_synthetic_mount_targets(
targets: &[SyntheticMountTarget],
) -> Vec<SyntheticMountTargetRegistration> {
with_metadata_guard_registry_lock(|| {
targets
.iter()
.map(|target| {
let marker_dir = metadata_guard_marker_dir(target.path());
fs::create_dir_all(&marker_dir).unwrap_or_else(|err| {
panic!(
"failed to create metadata guard marker directory {}: {err}",
marker_dir.display()
)
});
let target = if target.preserves_pre_existing_path()
&& metadata_guard_marker_dir_has_active_synthetic_owner(&marker_dir)
{
match target.kind() {
SyntheticMountTargetKind::EmptyFile => {
SyntheticMountTarget::missing(target.path())
}
SyntheticMountTargetKind::EmptyDirectory => {
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: &[ProtectedCreateTarget],
) -> Vec<ProtectedCreateTargetRegistration> {
with_metadata_guard_registry_lock(|| {
targets
.iter()
.map(|target| {
let marker_dir = metadata_guard_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: &SyntheticMountTarget) -> &'static [u8] {
if target.preserves_pre_existing_path() {
METADATA_GUARD_MARKER_EXISTING
} else {
METADATA_GUARD_MARKER_SYNTHETIC
}
}
fn metadata_guard_marker_dir_has_active_synthetic_owner(marker_dir: &Path) -> bool {
metadata_guard_marker_dir_has_active_process_matching(marker_dir, |path| match fs::read(path) {
Ok(contents) => contents == METADATA_GUARD_MARKER_SYNTHETIC,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
Err(err) => panic!(
"failed to read metadata guard marker {}: {err}",
path.display()
),
})
}
fn metadata_guard_marker_dir_has_active_process(marker_dir: &Path) -> bool {
metadata_guard_marker_dir_has_active_process_matching(marker_dir, |_| true)
}
fn metadata_guard_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 guard marker directory {}: {err}",
marker_dir.display()
),
};
for entry in entries {
let entry = entry.unwrap_or_else(|err| {
panic!(
"failed to read metadata guard 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 guard 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_metadata_guard_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_guard_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 guard marker directory {}: {err}",
target.marker_dir.display()
),
}
}
});
}
fn cleanup_protected_create_targets(targets: &[ProtectedCreateTargetRegistration]) -> bool {
with_metadata_guard_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_guard_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: &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: &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: &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),
}
Ok(Some(removal))
}
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;
}
match target.kind() {
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()
),
},
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_metadata_guard_registry_lock<T>(f: impl FnOnce() -> T) -> T {
let registry_root = metadata_guard_registry_root();
fs::create_dir_all(&registry_root).unwrap_or_else(|err| {
panic!(
"failed to create metadata guard 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 metadata guard 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 metadata guard 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 metadata guard registry {}: {err}",
lock_path.display()
);
}
result
}
fn metadata_guard_marker_dir(path: &Path) -> PathBuf {
metadata_guard_registry_root().join(format!("{:016x}", hash_path(path)))
}
fn metadata_guard_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 = "metadata_guard_tests.rs"]
mod tests;

View File

@@ -0,0 +1,131 @@
use super::*;
use crate::metadata_paths::ProtectedCreateTarget;
use crate::metadata_paths::SyntheticMountTarget;
use pretty_assertions::assert_eq;
#[test]
fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
let empty_dir = temp_dir.path().join(".agents");
let non_empty_file = temp_dir.path().join("non-empty");
let missing_file = temp_dir.path().join(".missing");
std::fs::write(&empty_file, "").expect("write empty file");
std::fs::create_dir(&empty_dir).expect("create empty dir");
std::fs::write(&non_empty_file, "keep").expect("write nonempty file");
let registrations = register_synthetic_mount_targets(&[
SyntheticMountTarget::missing(&empty_file),
SyntheticMountTarget::missing_empty_directory(&empty_dir),
SyntheticMountTarget::missing(&non_empty_file),
SyntheticMountTarget::missing(&missing_file),
]);
cleanup_synthetic_mount_targets(&registrations);
assert!(!empty_file.exists());
assert!(!empty_dir.exists());
assert_eq!(
std::fs::read_to_string(&non_empty_file).expect("read nonempty file"),
"keep"
);
assert!(!missing_file.exists());
}
#[test]
fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
std::fs::write(&empty_file, "").expect("write empty file");
let target = SyntheticMountTarget::missing(&empty_file);
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
let active_marker = registrations[0].marker_dir.join("1");
std::fs::write(&active_marker, "").expect("write active marker");
cleanup_synthetic_mount_targets(&registrations);
assert!(empty_file.exists());
std::fs::remove_file(active_marker).expect("remove active marker");
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
cleanup_synthetic_mount_targets(&registrations);
assert!(!empty_file.exists());
}
#[test]
fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
let first_target = SyntheticMountTarget::missing(&empty_file);
let first_registrations = register_synthetic_mount_targets(&[first_target]);
std::fs::write(&empty_file, "").expect("write transient empty file");
let active_marker = first_registrations[0].marker_dir.join("1");
std::fs::write(&active_marker, METADATA_GUARD_MARKER_SYNTHETIC).expect("write active marker");
let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file");
let second_target = SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
let second_registrations = register_synthetic_mount_targets(&[second_target]);
cleanup_synthetic_mount_targets(&first_registrations);
assert!(empty_file.exists());
std::fs::remove_file(active_marker).expect("remove active marker");
cleanup_synthetic_mount_targets(&second_registrations);
assert!(!empty_file.exists());
}
#[test]
fn cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let empty_file = temp_dir.path().join(".git");
std::fs::write(&empty_file, "").expect("write pre-existing empty file");
let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file");
let first_target = SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
let second_target = SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
let first_registrations = register_synthetic_mount_targets(&[first_target]);
let second_registrations = register_synthetic_mount_targets(&[second_target]);
cleanup_synthetic_mount_targets(&first_registrations);
cleanup_synthetic_mount_targets(&second_registrations);
assert!(empty_file.exists());
}
#[test]
fn cleanup_protected_create_targets_removes_created_path_and_reports_violation() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let dot_git = temp_dir.path().join(".git");
let target = ProtectedCreateTarget::missing(&dot_git);
let registrations = register_protected_create_targets(&[target]);
std::fs::create_dir(&dot_git).expect("create protected path");
let violation = cleanup_protected_create_targets(&registrations);
assert!(violation);
assert!(!dot_git.exists());
}
#[test]
fn cleanup_protected_create_targets_waits_for_other_active_registrations() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let dot_git = temp_dir.path().join(".git");
let target = ProtectedCreateTarget::missing(&dot_git);
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
let active_marker = registrations[0].marker_dir.join("1");
std::fs::write(&active_marker, PROTECTED_CREATE_MARKER).expect("write active marker");
std::fs::write(&dot_git, "").expect("create protected path");
let violation = cleanup_protected_create_targets(&registrations);
assert!(violation);
assert!(dot_git.exists());
std::fs::remove_file(active_marker).expect("remove active marker");
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
let violation = cleanup_protected_create_targets(&registrations);
assert!(violation);
assert!(!dot_git.exists());
}

View File

@@ -0,0 +1,103 @@
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use crate::metadata_paths::ProtectedCreateTarget;
pub(crate) struct ProtectedCreateWatcher {
fd: libc::c_int,
_watches: Vec<libc::c_int>,
}
impl ProtectedCreateWatcher {
pub(crate) 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,
})
}
pub(crate) 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);
}
}
}

View File

@@ -0,0 +1,150 @@
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, 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)),
}
}
pub(crate) 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(crate) 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
}