mirror of
https://github.com/openai/codex.git
synced 2026-06-02 03:11:59 +00:00
Compare commits
4 Commits
fix/window
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81bc6ca867 | ||
|
|
e9c66b523a | ||
|
|
15b3795166 | ||
|
|
913d8afeb4 |
@@ -320,6 +320,7 @@ impl CommandExecRequestProcessor {
|
||||
&sandbox_cwd,
|
||||
&codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
codex_core::exec::SandboxProcessLifetime::TerminateWithParent,
|
||||
)
|
||||
.map_err(|err| internal_error(format!("exec failed: {err}")))?;
|
||||
self.command_exec_manager
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::SandboxProcessLifetime;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -301,6 +302,7 @@ async fn run_command_under_sandbox(
|
||||
permission_profile_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(managed_network_requirements_enabled),
|
||||
SandboxProcessLifetime::TerminateWithParent,
|
||||
);
|
||||
spawn_debug_sandbox_child(
|
||||
codex_linux_sandbox_exe,
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_protocol::protocol::ExecOutputStream;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
pub use codex_sandboxing::SandboxProcessLifetime;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
@@ -307,6 +308,7 @@ pub async fn process_exec_tool_call(
|
||||
sandbox_cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
SandboxProcessLifetime::AllowDetachedChildren,
|
||||
)?;
|
||||
|
||||
// Route through the sandboxing module for a single, unified execution path.
|
||||
@@ -321,6 +323,7 @@ pub fn build_exec_request(
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_legacy_landlock: bool,
|
||||
process_lifetime: SandboxProcessLifetime,
|
||||
) -> Result<ExecRequest> {
|
||||
let ExecParams {
|
||||
command,
|
||||
@@ -382,6 +385,7 @@ pub fn build_exec_request(
|
||||
sandbox_policy_cwd: sandbox_cwd,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock,
|
||||
process_lifetime,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxProcessLifetime;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
@@ -41,6 +42,7 @@ where
|
||||
sandbox_policy_cwd,
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(/*enforce_managed_network*/ false),
|
||||
SandboxProcessLifetime::AllowDetachedChildren,
|
||||
);
|
||||
let codex_linux_sandbox_exe = codex_linux_sandbox_exe.as_ref();
|
||||
// Preserve the helper alias when we already have it; otherwise force argv0
|
||||
|
||||
@@ -43,6 +43,7 @@ use codex_protocol::protocol::NetworkPolicyRuleAction;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxProcessLifetime;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
@@ -925,6 +926,7 @@ impl CoreShellCommandExecutor {
|
||||
sandbox_policy_cwd: &self.sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
process_lifetime: SandboxProcessLifetime::AllowDetachedChildren,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})?;
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxProcessLifetime;
|
||||
use codex_sandboxing::SandboxTransformError;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
@@ -409,6 +410,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
.codex_linux_sandbox_exe
|
||||
.map(std::path::PathBuf::as_path),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
process_lifetime: SandboxProcessLifetime::AllowDetachedChildren,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxProcessLifetime;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -111,6 +112,7 @@ impl FileSystemSandboxRunner {
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
process_lifetime: SandboxProcessLifetime::TerminateWithParent,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
||||
})
|
||||
|
||||
@@ -22,6 +22,7 @@ async fn spawn_command_under_sandbox(
|
||||
) -> std::io::Result<Child> {
|
||||
use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecParams;
|
||||
use codex_core::exec::SandboxProcessLifetime;
|
||||
use codex_core::exec::build_exec_request;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -46,6 +47,7 @@ async fn spawn_command_under_sandbox(
|
||||
sandbox_cwd,
|
||||
&codex_linux_sandbox_exe,
|
||||
/*use_legacy_landlock*/ false,
|
||||
SandboxProcessLifetime::AllowDetachedChildren,
|
||||
)
|
||||
.map_err(|err| io::Error::other(err.to_string()))?;
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ pub(crate) struct BwrapOptions {
|
||||
pub mount_proc: bool,
|
||||
/// How networking should be configured inside the bubblewrap sandbox.
|
||||
pub network_mode: BwrapNetworkMode,
|
||||
/// How the sandbox lifetime should relate to the initial command process.
|
||||
pub process_lifetime: BwrapProcessLifetime,
|
||||
/// Optional maximum depth for expanding unreadable glob patterns with ripgrep.
|
||||
///
|
||||
/// Keep this uncapped by default so existing nested deny-read matches are
|
||||
@@ -77,11 +79,22 @@ impl Default for BwrapOptions {
|
||||
Self {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
process_lifetime: BwrapProcessLifetime::TerminateWithParent,
|
||||
glob_scan_max_depth: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifetime behavior for descendants running inside bubblewrap.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub(crate) enum BwrapProcessLifetime {
|
||||
/// Ask bubblewrap to kill the sandbox when its parent exits.
|
||||
#[default]
|
||||
TerminateWithParent,
|
||||
/// Allow intentionally detached children to keep running in the sandbox.
|
||||
AllowDetachedChildren,
|
||||
}
|
||||
|
||||
/// Network policy modes for bubblewrap.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub(crate) enum BwrapNetworkMode {
|
||||
@@ -265,9 +278,11 @@ pub(crate) fn create_bwrap_command_args(
|
||||
}
|
||||
|
||||
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> BwrapArgs {
|
||||
let mut args = vec![
|
||||
"--new-session".to_string(),
|
||||
"--die-with-parent".to_string(),
|
||||
let mut args = vec!["--new-session".to_string()];
|
||||
if options.process_lifetime == BwrapProcessLifetime::TerminateWithParent {
|
||||
args.push("--die-with-parent".to_string());
|
||||
}
|
||||
args.extend([
|
||||
"--bind".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
@@ -275,7 +290,7 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
|
||||
// not need ambient CAP_SYS_ADMIN to create the remaining namespaces.
|
||||
"--unshare-user".to_string(),
|
||||
"--unshare-pid".to_string(),
|
||||
];
|
||||
]);
|
||||
if options.network_mode.should_unshare_network() {
|
||||
args.push("--unshare-net".to_string());
|
||||
}
|
||||
@@ -316,7 +331,9 @@ fn create_bwrap_flags(
|
||||
let normalized_command_cwd = normalize_command_cwd_for_bwrap(command_cwd);
|
||||
let mut args = Vec::new();
|
||||
args.push("--new-session".to_string());
|
||||
args.push("--die-with-parent".to_string());
|
||||
if options.process_lifetime == BwrapProcessLifetime::TerminateWithParent {
|
||||
args.push("--die-with-parent".to_string());
|
||||
}
|
||||
args.extend(filesystem_args);
|
||||
// Request a user namespace explicitly rather than relying on bubblewrap's
|
||||
// auto-enable behavior, which is skipped when the caller runs as uid 0.
|
||||
@@ -1411,6 +1428,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_detached_children_omits_die_with_parent_but_keeps_pid_namespace() {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}]);
|
||||
let args = create_bwrap_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
process_lifetime: BwrapProcessLifetime::AllowDetachedChildren,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("create bwrap args");
|
||||
|
||||
assert!(!args.args.contains(&"--die-with-parent".to_string()));
|
||||
assert!(args.args.contains(&"--unshare-pid".to_string()));
|
||||
assert!(args.args.contains(&"--unshare-user".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_disk_write_with_unreadable_glob_still_wraps_and_masks_match() {
|
||||
if !ripgrep_available() {
|
||||
|
||||
167
codex-rs/linux-sandbox/src/bwrap_output_relay.rs
Normal file
167
codex-rs/linux-sandbox/src/bwrap_output_relay.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::os::fd::OwnedFd;
|
||||
|
||||
/// Relays output from a bubblewrap process without waiting for orphaned
|
||||
/// descendants that keep bubblewrap's PID-namespace reaper alive.
|
||||
pub(crate) struct BwrapOutputRelay {
|
||||
stdout_read: OwnedFd,
|
||||
stdout_write: OwnedFd,
|
||||
stderr_read: OwnedFd,
|
||||
stderr_write: OwnedFd,
|
||||
}
|
||||
|
||||
impl BwrapOutputRelay {
|
||||
pub(crate) fn new() -> Self {
|
||||
let (stdout_read, stdout_write) = create_pipe("stdout");
|
||||
let (stderr_read, stderr_write) = create_pipe("stderr");
|
||||
Self {
|
||||
stdout_read,
|
||||
stdout_write,
|
||||
stderr_read,
|
||||
stderr_write,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn redirect_child_output(self) {
|
||||
let Self {
|
||||
stdout_read,
|
||||
stdout_write,
|
||||
stderr_read,
|
||||
stderr_write,
|
||||
} = self;
|
||||
drop(stdout_read);
|
||||
drop(stderr_read);
|
||||
dup2_or_panic(stdout_write.as_raw_fd(), libc::STDOUT_FILENO, "stdout");
|
||||
dup2_or_panic(stderr_write.as_raw_fd(), libc::STDERR_FILENO, "stderr");
|
||||
}
|
||||
|
||||
pub(crate) fn forward_until_child_exit(self, pid: libc::pid_t) -> libc::c_int {
|
||||
let Self {
|
||||
stdout_read,
|
||||
stdout_write,
|
||||
stderr_read,
|
||||
stderr_write,
|
||||
} = self;
|
||||
drop(stdout_write);
|
||||
drop(stderr_write);
|
||||
set_nonblocking(stdout_read.as_raw_fd(), "stdout");
|
||||
set_nonblocking(stderr_read.as_raw_fd(), "stderr");
|
||||
|
||||
loop {
|
||||
forward_available(stdout_read.as_raw_fd(), libc::STDOUT_FILENO, "stdout");
|
||||
forward_available(stderr_read.as_raw_fd(), libc::STDERR_FILENO, "stderr");
|
||||
if let Some(status) = try_wait_for_child(pid) {
|
||||
forward_available(stdout_read.as_raw_fd(), libc::STDOUT_FILENO, "stdout");
|
||||
forward_available(stderr_read.as_raw_fd(), libc::STDERR_FILENO, "stderr");
|
||||
return status;
|
||||
}
|
||||
poll_for_output(&stdout_read, &stderr_read);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pipe(stream: &str) -> (OwnedFd, OwnedFd) {
|
||||
let mut pipe_fds = [0; 2];
|
||||
if unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to create bubblewrap {stream} relay pipe: {err}");
|
||||
}
|
||||
|
||||
// SAFETY: pipe2 initialized both returned file descriptors and transfers
|
||||
// their ownership to this function.
|
||||
unsafe {
|
||||
(
|
||||
OwnedFd::from_raw_fd(pipe_fds[0]),
|
||||
OwnedFd::from_raw_fd(pipe_fds[1]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn dup2_or_panic(source: libc::c_int, target: libc::c_int, stream: &str) {
|
||||
if unsafe { libc::dup2(source, target) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to redirect bubblewrap {stream}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn set_nonblocking(fd: libc::c_int, stream: &str) {
|
||||
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
||||
if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("failed to configure bubblewrap {stream} relay pipe: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn forward_available(source: libc::c_int, destination: libc::c_int, stream: &str) {
|
||||
let mut buffer = [0_u8; 8192];
|
||||
loop {
|
||||
let bytes_read = unsafe { libc::read(source, buffer.as_mut_ptr().cast(), buffer.len()) };
|
||||
if bytes_read > 0 {
|
||||
write_all(destination, &buffer[..bytes_read as usize], stream);
|
||||
continue;
|
||||
}
|
||||
if bytes_read == 0 {
|
||||
return;
|
||||
}
|
||||
let err = std::io::Error::last_os_error();
|
||||
match err.kind() {
|
||||
std::io::ErrorKind::Interrupted => continue,
|
||||
std::io::ErrorKind::WouldBlock => return,
|
||||
_ => panic!("failed to read bubblewrap {stream} output: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_all(fd: libc::c_int, mut bytes: &[u8], stream: &str) {
|
||||
while !bytes.is_empty() {
|
||||
let bytes_written = unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) };
|
||||
if bytes_written > 0 {
|
||||
bytes = &bytes[bytes_written as usize..];
|
||||
continue;
|
||||
}
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() == std::io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
panic!("failed to forward bubblewrap {stream} output: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn try_wait_for_child(pid: libc::pid_t) -> Option<libc::c_int> {
|
||||
let mut status = 0;
|
||||
let result = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
|
||||
if result == pid {
|
||||
return Some(status);
|
||||
}
|
||||
if result == 0 {
|
||||
return None;
|
||||
}
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() == std::io::ErrorKind::Interrupted {
|
||||
return None;
|
||||
}
|
||||
panic!("waitpid failed for bubblewrap child: {err}");
|
||||
}
|
||||
|
||||
fn poll_for_output(stdout_read: &OwnedFd, stderr_read: &OwnedFd) {
|
||||
let mut poll_fds = [
|
||||
libc::pollfd {
|
||||
fd: stdout_read.as_raw_fd(),
|
||||
events: libc::POLLIN | libc::POLLHUP,
|
||||
revents: 0,
|
||||
},
|
||||
libc::pollfd {
|
||||
fd: stderr_read.as_raw_fd(),
|
||||
events: libc::POLLIN | libc::POLLHUP,
|
||||
revents: 0,
|
||||
},
|
||||
];
|
||||
let result = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as _, 10) };
|
||||
if result < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() != std::io::ErrorKind::Interrupted {
|
||||
panic!("failed to poll bubblewrap output relay pipes: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ mod bundled_bwrap;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap_output_relay;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod exec_util;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod landlock;
|
||||
|
||||
@@ -19,7 +19,9 @@ use std::time::Duration;
|
||||
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::BwrapProcessLifetime;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::bwrap_output_relay::BwrapOutputRelay;
|
||||
use crate::landlock::apply_permission_profile_to_current_thread;
|
||||
use crate::launcher::exec_bwrap;
|
||||
use crate::launcher::preferred_bwrap_supports_argv0;
|
||||
@@ -122,6 +124,11 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)]
|
||||
pub allow_network_for_proxy: bool,
|
||||
|
||||
/// Internal: permit intentionally detached command descendants to outlive
|
||||
/// the one-shot sandbox helper invocation.
|
||||
#[arg(long = "allow-detached-children", hide = true, default_value_t = false)]
|
||||
pub allow_detached_children: bool,
|
||||
|
||||
/// Internal route spec used for managed proxy routing in bwrap mode.
|
||||
#[arg(long = "proxy-route-spec", hide = true)]
|
||||
pub proxy_route_spec: Option<String>,
|
||||
@@ -152,6 +159,7 @@ pub fn run_main() -> ! {
|
||||
use_legacy_landlock,
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
allow_detached_children,
|
||||
proxy_route_spec,
|
||||
no_proc,
|
||||
command,
|
||||
@@ -238,6 +246,11 @@ pub fn run_main() -> ! {
|
||||
inner,
|
||||
!no_proc,
|
||||
allow_network_for_proxy,
|
||||
if allow_detached_children {
|
||||
BwrapProcessLifetime::AllowDetachedChildren
|
||||
} else {
|
||||
BwrapProcessLifetime::TerminateWithParent
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -314,6 +327,7 @@ fn ensure_legacy_landlock_mode_supports_policy(
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: Option<&Path>,
|
||||
@@ -322,6 +336,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
inner: Vec<String>,
|
||||
mount_proc: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
process_lifetime: BwrapProcessLifetime,
|
||||
) -> ! {
|
||||
let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy);
|
||||
let mut mount_proc = mount_proc;
|
||||
@@ -344,6 +359,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
let options = BwrapOptions {
|
||||
mount_proc,
|
||||
network_mode,
|
||||
process_lifetime,
|
||||
..Default::default()
|
||||
};
|
||||
let mut bwrap_args = build_bwrap_argv(
|
||||
@@ -355,7 +371,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
)
|
||||
.unwrap_or_else(|err| exit_with_bwrap_build_error(err));
|
||||
apply_inner_command_argv0(&mut bwrap_args.args);
|
||||
run_or_exec_bwrap(bwrap_args);
|
||||
run_or_exec_bwrap(bwrap_args, process_lifetime);
|
||||
}
|
||||
|
||||
fn bwrap_network_mode(
|
||||
@@ -486,16 +502,23 @@ 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()
|
||||
fn run_or_exec_bwrap(
|
||||
bwrap_args: crate::bwrap::BwrapArgs,
|
||||
process_lifetime: BwrapProcessLifetime,
|
||||
) -> ! {
|
||||
if process_lifetime == BwrapProcessLifetime::TerminateWithParent
|
||||
&& 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);
|
||||
run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args, process_lifetime);
|
||||
}
|
||||
|
||||
fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::BwrapArgs) -> ! {
|
||||
fn run_bwrap_in_child_with_synthetic_mount_cleanup(
|
||||
bwrap_args: crate::bwrap::BwrapArgs,
|
||||
process_lifetime: BwrapProcessLifetime,
|
||||
) -> ! {
|
||||
let crate::bwrap::BwrapArgs {
|
||||
args,
|
||||
preserved_files,
|
||||
@@ -507,6 +530,8 @@ fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::Bwr
|
||||
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 output_relay = (process_lifetime == BwrapProcessLifetime::AllowDetachedChildren)
|
||||
.then(BwrapOutputRelay::new);
|
||||
let parent_pid = unsafe { libc::getpid() };
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
@@ -517,6 +542,9 @@ fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::Bwr
|
||||
if pid == 0 {
|
||||
reset_forwarded_signal_handlers_to_default();
|
||||
setup_signal_mask.restore();
|
||||
if let Some(output_relay) = output_relay {
|
||||
output_relay.redirect_child_output();
|
||||
}
|
||||
let setpgid_res = unsafe { libc::setpgid(0, 0) };
|
||||
if setpgid_res < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
@@ -532,7 +560,10 @@ fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::Bwr
|
||||
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 status = match output_relay {
|
||||
Some(output_relay) => output_relay.forward_until_child_exit(pid),
|
||||
None => 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
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
// At least on GitHub CI, the arm64 tests appear to need longer timeouts.
|
||||
@@ -393,6 +394,47 @@ async fn sandbox_ignores_missing_writable_roots_under_bwrap() {
|
||||
assert_eq!(output.stdout.text, "sandbox-ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn detached_children_survive_parent_exit_under_bwrap() {
|
||||
if should_skip_bwrap_tests().await {
|
||||
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let log_path = tempdir.path().join("detached.log");
|
||||
let command = format!(
|
||||
"nohup sh -c 'for i in 1 2 3 4 5 6 7 8 9 10; do printf x >> \"{log}\"; sleep 0.2; done' >/dev/null 2>&1 </dev/null &",
|
||||
log = log_path.to_string_lossy(),
|
||||
);
|
||||
|
||||
let output = run_cmd_result_with_writable_roots(
|
||||
&["bash", "-lc", &command],
|
||||
&[tempdir.path().to_path_buf()],
|
||||
LONG_TIMEOUT_MS,
|
||||
/*use_legacy_landlock*/ false,
|
||||
/*network_access*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("sandboxed command should execute");
|
||||
|
||||
assert_eq!(output.exit_code, 0);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
let first_len = std::fs::metadata(&log_path)
|
||||
.expect("detached child should create log file")
|
||||
.len();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
let second_len = std::fs::metadata(&log_path)
|
||||
.expect("detached child log file should still exist")
|
||||
.len();
|
||||
assert!(
|
||||
second_len > first_len,
|
||||
"detached child should keep writing after the parent exits",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_new_privs_is_enabled() {
|
||||
let output = run_cmd_output(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::SandboxProcessLifetime;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -27,6 +28,7 @@ pub fn create_linux_sandbox_command_args_for_permission_profile(
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_legacy_landlock: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
process_lifetime: SandboxProcessLifetime,
|
||||
) -> Vec<String> {
|
||||
let permission_profile_json = serde_json::to_string(permission_profile)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize permission profile: {err}"));
|
||||
@@ -53,6 +55,9 @@ pub fn create_linux_sandbox_command_args_for_permission_profile(
|
||||
if allow_network_for_proxy {
|
||||
linux_cmd.push("--allow-network-for-proxy".to_string());
|
||||
}
|
||||
if process_lifetime == SandboxProcessLifetime::AllowDetachedChildren {
|
||||
linux_cmd.push("--allow-detached-children".to_string());
|
||||
}
|
||||
linux_cmd.push("--".to_string());
|
||||
linux_cmd.extend(command);
|
||||
linux_cmd
|
||||
|
||||
@@ -65,6 +65,7 @@ fn permission_profile_flag_is_included() {
|
||||
cwd,
|
||||
/*use_legacy_landlock*/ true,
|
||||
/*allow_network_for_proxy*/ false,
|
||||
SandboxProcessLifetime::TerminateWithParent,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -77,6 +78,18 @@ fn permission_profile_flag_is_included() {
|
||||
.any(|window| window[0] == "--command-cwd" && window[1] == "/tmp/link"),
|
||||
true
|
||||
);
|
||||
assert!(!args.contains(&"--allow-detached-children".to_string()));
|
||||
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
vec!["/bin/true".to_string()],
|
||||
command_cwd,
|
||||
&permission_profile,
|
||||
cwd,
|
||||
/*use_legacy_landlock*/ true,
|
||||
/*allow_network_for_proxy*/ false,
|
||||
SandboxProcessLifetime::AllowDetachedChildren,
|
||||
);
|
||||
assert!(args.contains(&"--allow-detached-children".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,6 +13,7 @@ pub use bwrap::system_bwrap_warning;
|
||||
pub use manager::SandboxCommand;
|
||||
pub use manager::SandboxExecRequest;
|
||||
pub use manager::SandboxManager;
|
||||
pub use manager::SandboxProcessLifetime;
|
||||
pub use manager::SandboxTransformError;
|
||||
pub use manager::SandboxTransformRequest;
|
||||
pub use manager::SandboxType;
|
||||
|
||||
@@ -85,6 +85,13 @@ pub struct SandboxExecRequest {
|
||||
pub arg0: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum SandboxProcessLifetime {
|
||||
#[default]
|
||||
TerminateWithParent,
|
||||
AllowDetachedChildren,
|
||||
}
|
||||
|
||||
/// Bundled arguments for sandbox transformation.
|
||||
///
|
||||
/// This keeps call sites self-documenting when several fields are optional.
|
||||
@@ -99,6 +106,7 @@ pub struct SandboxTransformRequest<'a> {
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
pub codex_linux_sandbox_exe: Option<&'a Path>,
|
||||
pub use_legacy_landlock: bool,
|
||||
pub process_lifetime: SandboxProcessLifetime,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
}
|
||||
@@ -178,6 +186,7 @@ impl SandboxManager {
|
||||
sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
process_lifetime,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
} = request;
|
||||
@@ -232,6 +241,7 @@ impl SandboxManager {
|
||||
sandbox_policy_cwd,
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
process_lifetime,
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::SandboxCommand;
|
||||
use super::SandboxManager;
|
||||
use super::SandboxProcessLifetime;
|
||||
use super::SandboxTransformRequest;
|
||||
use super::SandboxType;
|
||||
use super::SandboxablePreference;
|
||||
@@ -93,6 +94,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
process_lifetime: SandboxProcessLifetime::TerminateWithParent,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -144,6 +146,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
process_lifetime: SandboxProcessLifetime::TerminateWithParent,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -212,6 +215,7 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
process_lifetime: SandboxProcessLifetime::TerminateWithParent,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
@@ -265,6 +269,7 @@ fn transform_linux_seccomp_request(
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe),
|
||||
use_legacy_landlock: false,
|
||||
process_lifetime: SandboxProcessLifetime::TerminateWithParent,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user