Enforce workspace metadata protections in Linux sandbox (#19852)

## Summary

Enforce FileSystemSandboxPolicy protected metadata names in the Linux
bubblewrap adapter so `.git`, `.agents`, and `.codex` remain read only
inside writable workspace roots unless the policy grants an explicit
write carveout.

## Scope

1. Translate protected metadata names from FileSystemSandboxPolicy into
bubblewrap masks for existing metadata paths.
2. Represent missing protected metadata paths as guarded mount targets
so agents cannot create `.git`, `.agents`, or `.codex` under writable
roots.
3. Preserve normal git discovery for existing repos, worktrees, and
parent repos.
4. Keep explicit user write grants working when policy allows a
protected metadata path directly.

## Not in scope

1. No shell preflight UX.
2. No TUI runtime profile propagation.
3. No macOS Seatbelt changes in this PR.

## Reviewer focus

1. This should be reviewed as the Linux enforcement adapter for the
policy primitive from PR 19846.
2. macOS enforcement already landed in PR 19847.
3. The important invariant is that `FileSystemSandboxPolicy` is the
source of truth for `.git`, `.agents`, and `.codex`.

## Validation

1. `git diff` whitespace check passed.
2. `cargo fmt` check passed with the existing stable rustfmt warning
about `imports_granularity`.
3. Full Linux sandbox Cargo test suite passed on the devbox.
4. Devbox forty six case suite passed at head
`012accb703c13bd28df5b40079a9bf183036336a`.
5. Devbox summary: pass 46, fail 0.
6. The devbox suite was run through `just c sandbox linux`.
7. Focused repo test for Viyat parent repo case passed on the devbox.
This commit is contained in:
evawong-oai
2026-04-29 16:14:14 -07:00
committed by GitHub
Unverified
parent 13dbcda28f
commit 74f06dcdfb
4 changed files with 1911 additions and 113 deletions
+873 -7
View File
@@ -1,11 +1,21 @@
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;
@@ -20,6 +30,46 @@ 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.
///
@@ -302,7 +352,7 @@ fn run_bwrap_with_proc_fallback(
options,
);
apply_inner_command_argv0(&mut bwrap_args.args);
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
run_or_exec_bwrap(bwrap_args);
}
fn bwrap_network_mode(
@@ -339,6 +389,8 @@ 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,
}
}
@@ -427,6 +479,802 @@ 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:
@@ -440,6 +1288,16 @@ fn resolve_true_command() -> String {
/// command, and reads are bounded to a fixed max size.
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String {
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
let 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) };
@@ -457,6 +1315,8 @@ fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> Str
}
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");
@@ -467,9 +1327,11 @@ fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> Str
close_fd_or_panic(write_fd, "close write end in bubblewrap child");
}
exec_bwrap(bwrap_args.args, bwrap_args.preserved_files);
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");
@@ -481,11 +1343,15 @@ fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> Str
panic!("failed to read bubblewrap stderr: {err}");
}
let mut status: libc::c_int = 0;
let wait_res = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, 0) };
if wait_res < 0 {
let err = std::io::Error::last_os_error();
panic!("waitpid failed for bubblewrap child: {err}");
let 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()