Skip to content

Commit b0df7b1

Browse files
committed
Add preserved path shell preflight
1 parent 613fe13 commit b0df7b1

7 files changed

Lines changed: 471 additions & 0 deletions

File tree

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ codex-responses-api-proxy = { workspace = true }
4343
codex-rmcp-client = { workspace = true }
4444
codex-rollout-trace = { workspace = true }
4545
codex-sandboxing = { workspace = true }
46+
codex-shell-command = { workspace = true }
4647
codex-state = { workspace = true }
4748
codex-stdio-to-uds = { workspace = true }
4849
codex-terminal-detection = { workspace = true }

codex-rs/cli/src/debug_sandbox.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ use crate::exit_status::handle_exit_status;
3535
#[cfg(target_os = "macos")]
3636
use seatbelt::DenialLogger;
3737

38+
#[cfg(target_os = "linux")]
39+
const LINUX_SANDBOX_FORWARDED_SIGNALS: &[libc::c_int] =
40+
&[libc::SIGHUP, libc::SIGINT, libc::SIGQUIT, libc::SIGTERM];
41+
3842
#[cfg(target_os = "macos")]
3943
pub async fn run_command_under_seatbelt(
4044
command: SeatbeltCommand,
@@ -142,6 +146,13 @@ async fn run_command_under_sandbox(
142146
// sandbox policy. In the future, we could add a CLI option to set them
143147
// separately.
144148
let sandbox_policy_cwd = cwd.clone();
149+
if let Some(reason) = codex_shell_command::preserved_path_write_forbidden_reason(
150+
&command,
151+
cwd.as_path(),
152+
&config.permissions.file_system_sandbox_policy(),
153+
) {
154+
anyhow::bail!("{reason}");
155+
}
145156

146157
let env = create_env(
147158
&config.permissions.shell_environment_policy,
@@ -261,6 +272,9 @@ async fn run_command_under_sandbox(
261272
denial_logger.on_child_spawn(&child);
262273
}
263274

275+
#[cfg(target_os = "linux")]
276+
let status = wait_for_debug_sandbox_child(&mut child).await?;
277+
#[cfg(not(target_os = "linux"))]
264278
let status = child.wait().await?;
265279

266280
#[cfg(target_os = "macos")]
@@ -438,13 +452,96 @@ async fn spawn_debug_sandbox_child(
438452
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
439453
}
440454

455+
#[cfg(target_os = "linux")]
456+
{
457+
let parent_pid = unsafe { libc::getpid() };
458+
// SAFETY: `pre_exec` runs in the child immediately before exec. The
459+
// closure only adjusts the child signal mask, installs a parent-death
460+
// signal, and checks the inherited parent pid to close the fork/exec
461+
// race.
462+
unsafe {
463+
cmd.pre_exec(move || {
464+
block_linux_sandbox_forwarded_signals()?;
465+
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) == -1 {
466+
return Err(std::io::Error::last_os_error());
467+
}
468+
if libc::getppid() != parent_pid {
469+
libc::raise(libc::SIGTERM);
470+
}
471+
Ok(())
472+
});
473+
}
474+
}
475+
441476
cmd.stdin(Stdio::inherit())
442477
.stdout(Stdio::inherit())
443478
.stderr(Stdio::inherit())
444479
.kill_on_drop(true)
445480
.spawn()
446481
}
447482

483+
#[cfg(target_os = "linux")]
484+
async fn wait_for_debug_sandbox_child(
485+
child: &mut Child,
486+
) -> std::io::Result<std::process::ExitStatus> {
487+
let child_pid = child.id().map(|pid| pid as libc::pid_t);
488+
tokio::select! {
489+
status = child.wait() => status,
490+
signal = recv_linux_sandbox_forwarded_signal() => {
491+
let signal = signal?;
492+
if let Some(child_pid) = child_pid {
493+
signal_debug_sandbox_child(child_pid, signal)?;
494+
}
495+
child.wait().await
496+
}
497+
}
498+
}
499+
500+
#[cfg(target_os = "linux")]
501+
async fn recv_linux_sandbox_forwarded_signal() -> std::io::Result<libc::c_int> {
502+
use tokio::signal::unix::SignalKind;
503+
use tokio::signal::unix::signal;
504+
505+
let mut sighup = signal(SignalKind::hangup())?;
506+
let mut sigint = signal(SignalKind::interrupt())?;
507+
let mut sigquit = signal(SignalKind::quit())?;
508+
let mut sigterm = signal(SignalKind::terminate())?;
509+
510+
let signal = tokio::select! {
511+
_ = sighup.recv() => libc::SIGHUP,
512+
_ = sigint.recv() => libc::SIGINT,
513+
_ = sigquit.recv() => libc::SIGQUIT,
514+
_ = sigterm.recv() => libc::SIGTERM,
515+
};
516+
Ok(signal)
517+
}
518+
519+
#[cfg(target_os = "linux")]
520+
fn signal_debug_sandbox_child(pid: libc::pid_t, signal: libc::c_int) -> std::io::Result<()> {
521+
if unsafe { libc::kill(pid, signal) } < 0 {
522+
let err = std::io::Error::last_os_error();
523+
if err.raw_os_error() != Some(libc::ESRCH) {
524+
return Err(err);
525+
}
526+
}
527+
Ok(())
528+
}
529+
530+
#[cfg(target_os = "linux")]
531+
fn block_linux_sandbox_forwarded_signals() -> std::io::Result<()> {
532+
let mut blocked: libc::sigset_t = unsafe { std::mem::zeroed() };
533+
unsafe {
534+
libc::sigemptyset(&mut blocked);
535+
for signal in LINUX_SANDBOX_FORWARDED_SIGNALS {
536+
libc::sigaddset(&mut blocked, *signal);
537+
}
538+
if libc::sigprocmask(libc::SIG_BLOCK, &blocked, std::ptr::null_mut()) < 0 {
539+
return Err(std::io::Error::last_os_error());
540+
}
541+
}
542+
Ok(())
543+
}
544+
448545
#[cfg(target_os = "windows")]
449546
mod windows_stdio_bridge {
450547
use std::io::Read;

codex-rs/core/src/tools/handlers/shell.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,14 @@ impl ShellHandler {
531531
prefix_rule,
532532
})
533533
.await;
534+
let exec_approval_requirement = codex_shell_command::preserved_path_write_forbidden_reason(
535+
&exec_params.command,
536+
&exec_params.cwd,
537+
&file_system_sandbox_policy,
538+
)
539+
.map_or(exec_approval_requirement, |reason| {
540+
crate::tools::sandboxing::ExecApprovalRequirement::Forbidden { reason }
541+
});
534542

535543
let req = ShellRequest {
536544
command: exec_params.command.clone(),

0 commit comments

Comments
 (0)