mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
13dbcda28f
commit
74f06dcdfb
@@ -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(®istry_root).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to create synthetic bubblewrap mount registry {}: {err}",
|
||||
registry_root.display()
|
||||
)
|
||||
});
|
||||
let lock_path = registry_root.join("lock");
|
||||
let lock_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to open synthetic bubblewrap mount registry lock {}: {err}",
|
||||
lock_path.display()
|
||||
)
|
||||
});
|
||||
if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!(
|
||||
"failed to lock synthetic bubblewrap mount registry {}: {err}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
let result = f();
|
||||
if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!(
|
||||
"failed to unlock synthetic bubblewrap mount registry {}: {err}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn synthetic_mount_marker_dir(path: &Path) -> PathBuf {
|
||||
synthetic_mount_registry_root().join(format!("{:016x}", hash_path(path)))
|
||||
}
|
||||
|
||||
fn synthetic_mount_registry_root() -> PathBuf {
|
||||
std::env::temp_dir().join("codex-bwrap-synthetic-mount-targets")
|
||||
}
|
||||
|
||||
fn hash_path(path: &Path) -> u64 {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for byte in path.as_os_str().as_bytes() {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn exit_with_wait_status(status: libc::c_int) -> ! {
|
||||
if libc::WIFEXITED(status) {
|
||||
std::process::exit(libc::WEXITSTATUS(status));
|
||||
}
|
||||
|
||||
if libc::WIFSIGNALED(status) {
|
||||
let signal = libc::WTERMSIG(status);
|
||||
unsafe {
|
||||
libc::signal(signal, libc::SIG_DFL);
|
||||
libc::kill(libc::getpid(), signal);
|
||||
}
|
||||
std::process::exit(128 + signal);
|
||||
}
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn exit_with_wait_status_or_policy_violation(
|
||||
status: libc::c_int,
|
||||
protected_create_violation: bool,
|
||||
) -> ! {
|
||||
if protected_create_violation && libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
exit_with_wait_status(status);
|
||||
}
|
||||
|
||||
/// Run a short-lived bubblewrap preflight in a child process and capture stderr.
|
||||
///
|
||||
/// Strategy:
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user