mirror of
https://github.com/openai/codex.git
synced 2026-05-01 09:56:37 +00:00
Compare commits
1 Commits
owen/perfo
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db1b5f04a2 |
@@ -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,
|
||||
|
||||
372
codex-rs/linux-sandbox/src/bwrap_runtime.rs
Normal file
372
codex-rs/linux-sandbox/src/bwrap_runtime.rs
Normal 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;
|
||||
45
codex-rs/linux-sandbox/src/bwrap_runtime_tests.rs
Normal file
45
codex-rs/linux-sandbox/src/bwrap_runtime_tests.rs
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(®istry_root).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to create synthetic bubblewrap mount registry {}: {err}",
|
||||
registry_root.display()
|
||||
)
|
||||
});
|
||||
let lock_path = registry_root.join("lock");
|
||||
let lock_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to open synthetic bubblewrap mount registry lock {}: {err}",
|
||||
lock_path.display()
|
||||
)
|
||||
});
|
||||
if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!(
|
||||
"failed to lock synthetic bubblewrap mount registry {}: {err}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
let result = f();
|
||||
if unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_UN) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!(
|
||||
"failed to unlock synthetic bubblewrap mount registry {}: {err}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn synthetic_mount_marker_dir(path: &Path) -> PathBuf {
|
||||
synthetic_mount_registry_root().join(format!("{:016x}", hash_path(path)))
|
||||
}
|
||||
|
||||
fn synthetic_mount_registry_root() -> PathBuf {
|
||||
std::env::temp_dir().join("codex-bwrap-synthetic-mount-targets")
|
||||
}
|
||||
|
||||
fn hash_path(path: &Path) -> u64 {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for byte in path.as_os_str().as_bytes() {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn exit_with_wait_status(status: libc::c_int) -> ! {
|
||||
if libc::WIFEXITED(status) {
|
||||
std::process::exit(libc::WEXITSTATUS(status));
|
||||
}
|
||||
|
||||
if libc::WIFSIGNALED(status) {
|
||||
let signal = libc::WTERMSIG(status);
|
||||
unsafe {
|
||||
libc::signal(signal, libc::SIG_DFL);
|
||||
libc::kill(libc::getpid(), signal);
|
||||
}
|
||||
std::process::exit(128 + signal);
|
||||
}
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn exit_with_wait_status_or_policy_violation(
|
||||
status: libc::c_int,
|
||||
protected_create_violation: bool,
|
||||
) -> ! {
|
||||
if protected_create_violation && libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
exit_with_wait_status(status);
|
||||
}
|
||||
|
||||
/// Run a short-lived bubblewrap preflight in a child process and capture stderr.
|
||||
///
|
||||
/// Strategy:
|
||||
/// - This is used only by `preflight_proc_mount_support`, which runs `/bin/true`
|
||||
/// under bubblewrap with `--proc /proc`.
|
||||
/// - The goal is to detect environments where mounting `/proc` fails (for
|
||||
/// example, restricted containers), so we can retry the real run with
|
||||
/// `--no-proc`.
|
||||
/// - We capture stderr from that preflight to match known mount-failure text.
|
||||
/// We do not stream it because this is a one-shot probe with a trivial
|
||||
/// command, and reads are bounded to a fixed max size.
|
||||
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String {
|
||||
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
|
||||
let crate::bwrap::BwrapArgs {
|
||||
args,
|
||||
preserved_files,
|
||||
synthetic_mount_targets,
|
||||
protected_create_targets,
|
||||
} = bwrap_args;
|
||||
let setup_signal_mask = ForwardedSignalMask::block();
|
||||
let synthetic_mount_registrations = register_synthetic_mount_targets(&synthetic_mount_targets);
|
||||
let protected_create_registrations =
|
||||
register_protected_create_targets(&protected_create_targets);
|
||||
|
||||
let mut pipe_fds = [0; 2];
|
||||
let pipe_res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
|
||||
if pipe_res < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to create stderr pipe for bubblewrap: {err}");
|
||||
}
|
||||
let read_fd = pipe_fds[0];
|
||||
let write_fd = pipe_fds[1];
|
||||
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to fork for bubblewrap: {err}");
|
||||
}
|
||||
|
||||
if pid == 0 {
|
||||
reset_forwarded_signal_handlers_to_default();
|
||||
setup_signal_mask.restore();
|
||||
// Child: redirect stderr to the pipe, then run bubblewrap.
|
||||
unsafe {
|
||||
close_fd_or_panic(read_fd, "close read end in bubblewrap child");
|
||||
if libc::dup2(write_fd, libc::STDERR_FILENO) < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to redirect stderr for bubblewrap: {err}");
|
||||
}
|
||||
close_fd_or_panic(write_fd, "close write end in bubblewrap child");
|
||||
}
|
||||
|
||||
exec_bwrap(args, preserved_files);
|
||||
}
|
||||
|
||||
let signal_forwarders = install_bwrap_signal_forwarders(pid);
|
||||
setup_signal_mask.restore();
|
||||
// Parent: close the write end and read stderr while the child runs.
|
||||
close_fd_or_panic(write_fd, "close write end in bubblewrap parent");
|
||||
|
||||
// SAFETY: `read_fd` is a valid owned fd in the parent.
|
||||
let mut read_file = unsafe { File::from_raw_fd(read_fd) };
|
||||
let mut stderr_bytes = Vec::new();
|
||||
let mut limited_reader = (&mut read_file).take(MAX_PREFLIGHT_STDERR_BYTES);
|
||||
if let Err(err) = limited_reader.read_to_end(&mut stderr_bytes) {
|
||||
panic!("failed to read bubblewrap stderr: {err}");
|
||||
}
|
||||
|
||||
let status = wait_for_bwrap_child(pid);
|
||||
let cleanup_signal_mask = ForwardedSignalMask::block();
|
||||
BWRAP_CHILD_PID.store(0, Ordering::SeqCst);
|
||||
cleanup_synthetic_mount_targets(&synthetic_mount_registrations);
|
||||
cleanup_protected_create_targets(&protected_create_registrations);
|
||||
signal_forwarders.restore();
|
||||
cleanup_signal_mask.restore();
|
||||
if libc::WIFSIGNALED(status) {
|
||||
exit_with_wait_status(status);
|
||||
}
|
||||
|
||||
String::from_utf8_lossy(&stderr_bytes).into_owned()
|
||||
}
|
||||
|
||||
/// Close an owned file descriptor and panic with context on failure.
|
||||
///
|
||||
/// We use explicit close() checks here (instead of ignoring return codes)
|
||||
/// because this code runs in low-level sandbox setup paths where fd leaks or
|
||||
/// close errors can mask the root cause of later failures.
|
||||
fn close_fd_or_panic(fd: libc::c_int, context: &str) {
|
||||
let close_res = unsafe { libc::close(fd) };
|
||||
if close_res < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("{context}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_proc_mount_failure(stderr: &str) -> bool {
|
||||
stderr.contains("Can't mount proc")
|
||||
&& stderr.contains("/newroot/proc")
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#[cfg(test)]
|
||||
use super::*;
|
||||
#[cfg(test)]
|
||||
use crate::linux_run_main::install_bwrap_signal_forwarders;
|
||||
#[cfg(test)]
|
||||
use crate::linux_run_main::wait_for_bwrap_child;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
@@ -269,180 +265,6 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
|
||||
assert!(argv.iter().any(|arg| arg == "--"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join(".git");
|
||||
let empty_dir = temp_dir.path().join(".agents");
|
||||
let non_empty_file = temp_dir.path().join("non-empty");
|
||||
let missing_file = temp_dir.path().join(".missing");
|
||||
std::fs::write(&empty_file, "").expect("write empty file");
|
||||
std::fs::create_dir(&empty_dir).expect("create empty dir");
|
||||
std::fs::write(&non_empty_file, "keep").expect("write nonempty file");
|
||||
|
||||
let registrations = register_synthetic_mount_targets(&[
|
||||
crate::bwrap::SyntheticMountTarget::missing(&empty_file),
|
||||
crate::bwrap::SyntheticMountTarget::missing_empty_directory(&empty_dir),
|
||||
crate::bwrap::SyntheticMountTarget::missing(&non_empty_file),
|
||||
crate::bwrap::SyntheticMountTarget::missing(&missing_file),
|
||||
]);
|
||||
cleanup_synthetic_mount_targets(®istrations);
|
||||
|
||||
assert!(!empty_file.exists());
|
||||
assert!(!empty_dir.exists());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&non_empty_file).expect("read nonempty file"),
|
||||
"keep"
|
||||
);
|
||||
assert!(!missing_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join(".git");
|
||||
std::fs::write(&empty_file, "").expect("write empty file");
|
||||
let target = crate::bwrap::SyntheticMountTarget::missing(&empty_file);
|
||||
|
||||
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
|
||||
let active_marker = registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, "").expect("write active marker");
|
||||
|
||||
cleanup_synthetic_mount_targets(®istrations);
|
||||
assert!(empty_file.exists());
|
||||
|
||||
std::fs::remove_file(active_marker).expect("remove active marker");
|
||||
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
|
||||
cleanup_synthetic_mount_targets(®istrations);
|
||||
|
||||
assert!(!empty_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join(".git");
|
||||
let first_target = crate::bwrap::SyntheticMountTarget::missing(&empty_file);
|
||||
|
||||
let first_registrations = register_synthetic_mount_targets(&[first_target]);
|
||||
std::fs::write(&empty_file, "").expect("write transient empty file");
|
||||
let active_marker = first_registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, SYNTHETIC_MOUNT_MARKER_SYNTHETIC).expect("write active marker");
|
||||
let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file");
|
||||
let second_target =
|
||||
crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
|
||||
let second_registrations = register_synthetic_mount_targets(&[second_target]);
|
||||
|
||||
cleanup_synthetic_mount_targets(&first_registrations);
|
||||
assert!(empty_file.exists());
|
||||
|
||||
std::fs::remove_file(active_marker).expect("remove active marker");
|
||||
cleanup_synthetic_mount_targets(&second_registrations);
|
||||
|
||||
assert!(!empty_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join(".git");
|
||||
std::fs::write(&empty_file, "").expect("write pre-existing empty file");
|
||||
let metadata = std::fs::symlink_metadata(&empty_file).expect("stat empty file");
|
||||
let first_target =
|
||||
crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
|
||||
let second_target =
|
||||
crate::bwrap::SyntheticMountTarget::existing_empty_file(&empty_file, &metadata);
|
||||
|
||||
let first_registrations = register_synthetic_mount_targets(&[first_target]);
|
||||
let second_registrations = register_synthetic_mount_targets(&[second_target]);
|
||||
|
||||
cleanup_synthetic_mount_targets(&first_registrations);
|
||||
cleanup_synthetic_mount_targets(&second_registrations);
|
||||
|
||||
assert!(empty_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_protected_create_targets_removes_created_path_and_reports_violation() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let dot_git = temp_dir.path().join(".git");
|
||||
let target = crate::bwrap::ProtectedCreateTarget::missing(&dot_git);
|
||||
|
||||
let registrations = register_protected_create_targets(&[target]);
|
||||
std::fs::create_dir(&dot_git).expect("create protected path");
|
||||
let violation = cleanup_protected_create_targets(®istrations);
|
||||
|
||||
assert!(violation);
|
||||
assert!(!dot_git.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_protected_create_targets_waits_for_other_active_registrations() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let dot_git = temp_dir.path().join(".git");
|
||||
let target = crate::bwrap::ProtectedCreateTarget::missing(&dot_git);
|
||||
|
||||
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
|
||||
let active_marker = registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, PROTECTED_CREATE_MARKER).expect("write active marker");
|
||||
std::fs::write(&dot_git, "").expect("create protected path");
|
||||
|
||||
let violation = cleanup_protected_create_targets(®istrations);
|
||||
assert!(violation);
|
||||
assert!(dot_git.exists());
|
||||
|
||||
std::fs::remove_file(active_marker).expect("remove active marker");
|
||||
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
|
||||
let violation = cleanup_protected_create_targets(®istrations);
|
||||
|
||||
assert!(violation);
|
||||
assert!(!dot_git.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive() {
|
||||
let supervisor_pid = unsafe { libc::fork() };
|
||||
assert!(supervisor_pid >= 0, "failed to fork supervisor");
|
||||
|
||||
if supervisor_pid == 0 {
|
||||
run_bwrap_signal_forwarder_test_supervisor();
|
||||
}
|
||||
|
||||
let status = wait_for_bwrap_child(supervisor_pid);
|
||||
assert!(libc::WIFEXITED(status), "supervisor status: {status}");
|
||||
assert_eq!(libc::WEXITSTATUS(status), 0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn run_bwrap_signal_forwarder_test_supervisor() -> ! {
|
||||
let child_pid = unsafe { libc::fork() };
|
||||
if child_pid < 0 {
|
||||
unsafe {
|
||||
libc::_exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
if child_pid == 0 {
|
||||
loop {
|
||||
unsafe {
|
||||
libc::pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install_bwrap_signal_forwarders(child_pid);
|
||||
unsafe {
|
||||
libc::raise(libc::SIGTERM);
|
||||
}
|
||||
|
||||
let status = wait_for_bwrap_child(child_pid);
|
||||
let child_terminated_by_sigterm =
|
||||
libc::WIFSIGNALED(status) && libc::WTERMSIG(status) == libc::SIGTERM;
|
||||
unsafe {
|
||||
libc::_exit(if child_terminated_by_sigterm { 0 } else { 1 });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_inner_command_includes_route_spec() {
|
||||
let permission_profile = read_only_permission_profile();
|
||||
|
||||
489
codex-rs/linux-sandbox/src/metadata_guard.rs
Normal file
489
codex-rs/linux-sandbox/src/metadata_guard.rs
Normal 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(®istry_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;
|
||||
131
codex-rs/linux-sandbox/src/metadata_guard_tests.rs
Normal file
131
codex-rs/linux-sandbox/src/metadata_guard_tests.rs
Normal 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(®istrations);
|
||||
|
||||
assert!(!empty_file.exists());
|
||||
assert!(!empty_dir.exists());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&non_empty_file).expect("read nonempty file"),
|
||||
"keep"
|
||||
);
|
||||
assert!(!missing_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join(".git");
|
||||
std::fs::write(&empty_file, "").expect("write empty file");
|
||||
let target = SyntheticMountTarget::missing(&empty_file);
|
||||
|
||||
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
|
||||
let active_marker = registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, "").expect("write active marker");
|
||||
|
||||
cleanup_synthetic_mount_targets(®istrations);
|
||||
assert!(empty_file.exists());
|
||||
|
||||
std::fs::remove_file(active_marker).expect("remove active marker");
|
||||
let registrations = register_synthetic_mount_targets(std::slice::from_ref(&target));
|
||||
cleanup_synthetic_mount_targets(®istrations);
|
||||
|
||||
assert!(!empty_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let empty_file = temp_dir.path().join(".git");
|
||||
let first_target = SyntheticMountTarget::missing(&empty_file);
|
||||
|
||||
let first_registrations = register_synthetic_mount_targets(&[first_target]);
|
||||
std::fs::write(&empty_file, "").expect("write transient empty file");
|
||||
let active_marker = first_registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, 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(®istrations);
|
||||
|
||||
assert!(violation);
|
||||
assert!(!dot_git.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_protected_create_targets_waits_for_other_active_registrations() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let dot_git = temp_dir.path().join(".git");
|
||||
let target = ProtectedCreateTarget::missing(&dot_git);
|
||||
|
||||
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
|
||||
let active_marker = registrations[0].marker_dir.join("1");
|
||||
std::fs::write(&active_marker, PROTECTED_CREATE_MARKER).expect("write active marker");
|
||||
std::fs::write(&dot_git, "").expect("create protected path");
|
||||
|
||||
let violation = cleanup_protected_create_targets(®istrations);
|
||||
assert!(violation);
|
||||
assert!(dot_git.exists());
|
||||
|
||||
std::fs::remove_file(active_marker).expect("remove active marker");
|
||||
let registrations = register_protected_create_targets(std::slice::from_ref(&target));
|
||||
let violation = cleanup_protected_create_targets(®istrations);
|
||||
|
||||
assert!(violation);
|
||||
assert!(!dot_git.exists());
|
||||
}
|
||||
103
codex-rs/linux-sandbox/src/metadata_guard_watcher.rs
Normal file
103
codex-rs/linux-sandbox/src/metadata_guard_watcher.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
150
codex-rs/linux-sandbox/src/metadata_paths.rs
Normal file
150
codex-rs/linux-sandbox/src/metadata_paths.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user