Skip to content

Commit 35e30b8

Browse files
committed
Apply metadata preflight consistently
1 parent 74f29c7 commit 35e30b8

9 files changed

Lines changed: 280 additions & 128 deletions

File tree

codex-rs/cli/src/debug_sandbox.rs

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ 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-
4238
#[cfg(target_os = "macos")]
4339
pub async fn run_command_under_seatbelt(
4440
command: SeatbeltCommand,
@@ -149,6 +145,7 @@ async fn run_command_under_sandbox(
149145
if let Some(reason) = codex_shell_command::metadata_write_forbidden_reason(
150146
&command,
151147
cwd.as_path(),
148+
sandbox_policy_cwd.as_path(),
152149
&config.permissions.file_system_sandbox_policy(),
153150
) {
154151
anyhow::bail!("{reason}");
@@ -272,9 +269,6 @@ async fn run_command_under_sandbox(
272269
denial_logger.on_child_spawn(&child);
273270
}
274271

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

280274
#[cfg(target_os = "macos")]
@@ -452,96 +446,13 @@ async fn spawn_debug_sandbox_child(
452446
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
453447
}
454448

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-
476449
cmd.stdin(Stdio::inherit())
477450
.stdout(Stdio::inherit())
478451
.stderr(Stdio::inherit())
479452
.kill_on_drop(true)
480453
.spawn()
481454
}
482455

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-
545456
#[cfg(target_os = "windows")]
546457
mod windows_stdio_bridge {
547458
use std::io::Read;

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ use crate::tools::runtimes::shell::ShellRequest;
3535
use crate::tools::runtimes::shell::ShellRuntime;
3636
use crate::tools::runtimes::shell::ShellRuntimeBackend;
3737
use crate::tools::sandboxing::ToolCtx;
38+
use crate::tools::sandboxing::apply_protected_metadata_write_preflight;
3839
use codex_features::Feature;
3940
use codex_protocol::models::AdditionalPermissionProfile;
4041
use codex_protocol::protocol::ExecCommandSource;
42+
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
4143
use codex_shell_command::is_safe_command::is_known_safe_command;
4244
use codex_tools::ShellCommandBackendConfig;
4345

@@ -513,7 +515,11 @@ impl ShellHandler {
513515
);
514516
emitter.begin(event_ctx).await;
515517

516-
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
518+
let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();
519+
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
520+
&base_file_system_sandbox_policy,
521+
normalized_additional_permissions.as_ref(),
522+
);
517523
let exec_approval_requirement = session
518524
.services
519525
.exec_policy
@@ -531,14 +537,14 @@ impl ShellHandler {
531537
prefix_rule,
532538
})
533539
.await;
534-
let exec_approval_requirement = codex_shell_command::metadata_write_forbidden_reason(
540+
let exec_approval_requirement = apply_protected_metadata_write_preflight(
541+
exec_approval_requirement,
542+
effective_additional_permissions.sandbox_permissions,
535543
&exec_params.command,
536544
&exec_params.cwd,
545+
turn.cwd.as_path(),
537546
&file_system_sandbox_policy,
538-
)
539-
.map_or(exec_approval_requirement, |reason| {
540-
crate::tools::sandboxing::ExecApprovalRequirement::Forbidden { reason }
541-
});
547+
);
542548

543549
let req = ShellRequest {
544550
command: exec_params.command.clone(),

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::exec_policy::ExecApprovalRequest;
12
use crate::function_tool::FunctionCallError;
23
use crate::maybe_emit_implicit_skill_invocation;
34
use crate::sandboxing::SandboxPermissions;
@@ -19,6 +20,7 @@ use crate::tools::registry::PostToolUsePayload;
1920
use crate::tools::registry::PreToolUsePayload;
2021
use crate::tools::registry::ToolHandler;
2122
use crate::tools::registry::ToolKind;
23+
use crate::tools::sandboxing::apply_protected_metadata_write_preflight;
2224
use crate::unified_exec::ExecCommandRequest;
2325
use crate::unified_exec::UnifiedExecContext;
2426
use crate::unified_exec::UnifiedExecError;
@@ -32,6 +34,7 @@ use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
3234
use codex_protocol::models::AdditionalPermissionProfile;
3335
use codex_protocol::protocol::EventMsg;
3436
use codex_protocol::protocol::TerminalInteractionEvent;
37+
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
3538
use codex_shell_command::is_safe_command::is_known_safe_command;
3639
use codex_tools::UnifiedExecShellMode;
3740
use codex_utils_output_truncation::TruncationPolicy;
@@ -330,6 +333,40 @@ impl ToolHandler for UnifiedExecHandler {
330333
});
331334
}
332335

336+
let base_file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
337+
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
338+
&base_file_system_sandbox_policy,
339+
normalized_additional_permissions.as_ref(),
340+
);
341+
let exec_approval_requirement = context
342+
.session
343+
.services
344+
.exec_policy
345+
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
346+
command: &command,
347+
approval_policy: context.turn.approval_policy.value(),
348+
permission_profile: context.turn.permission_profile(),
349+
file_system_sandbox_policy: &file_system_sandbox_policy,
350+
sandbox_cwd: context.turn.cwd.as_path(),
351+
sandbox_permissions: if effective_additional_permissions
352+
.permissions_preapproved
353+
{
354+
SandboxPermissions::UseDefault
355+
} else {
356+
effective_additional_permissions.sandbox_permissions
357+
},
358+
prefix_rule: prefix_rule.clone(),
359+
})
360+
.await;
361+
let exec_approval_requirement = apply_protected_metadata_write_preflight(
362+
exec_approval_requirement,
363+
effective_additional_permissions.sandbox_permissions,
364+
&command,
365+
&cwd,
366+
context.turn.cwd.as_path(),
367+
&file_system_sandbox_policy,
368+
);
369+
333370
emit_unified_exec_tty_metric(&turn.session_telemetry, tty);
334371
match manager
335372
.exec_command(
@@ -345,10 +382,11 @@ impl ToolHandler for UnifiedExecHandler {
345382
sandbox_permissions: effective_additional_permissions
346383
.sandbox_permissions,
347384
additional_permissions: normalized_additional_permissions,
385+
#[cfg(unix)]
348386
additional_permissions_preapproved: effective_additional_permissions
349387
.permissions_preapproved,
350388
justification,
351-
prefix_rule,
389+
exec_approval_requirement,
352390
},
353391
&context,
354392
)

codex-rs/core/src/tools/sandboxing.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use serde::Serialize;
3434
use std::collections::HashMap;
3535
use std::fmt::Debug;
3636
use std::hash::Hash;
37+
use std::path::Path;
3738
use std::sync::Arc;
3839

3940
#[derive(Clone, Default, Debug)]
@@ -194,6 +195,50 @@ impl ExecApprovalRequirement {
194195
}
195196
}
196197

198+
/// Applies an early UX check for protected metadata writes only to commands
199+
/// that would otherwise run in the sandbox. Explicit sandbox-bypass approval
200+
/// paths keep their existing approval semantics.
201+
pub(crate) fn apply_protected_metadata_write_preflight(
202+
exec_approval_requirement: ExecApprovalRequirement,
203+
sandbox_permissions: SandboxPermissions,
204+
command: &[String],
205+
command_cwd: &Path,
206+
sandbox_policy_cwd: &Path,
207+
file_system_sandbox_policy: &FileSystemSandboxPolicy,
208+
) -> ExecApprovalRequirement {
209+
let bypasses_sandbox = matches!(
210+
&exec_approval_requirement,
211+
ExecApprovalRequirement::Skip {
212+
bypass_sandbox: true,
213+
..
214+
}
215+
);
216+
let approval_is_for_sandbox_override = sandbox_permissions.requests_sandbox_override()
217+
&& matches!(
218+
&exec_approval_requirement,
219+
ExecApprovalRequirement::NeedsApproval { .. }
220+
);
221+
if bypasses_sandbox
222+
|| approval_is_for_sandbox_override
223+
|| matches!(
224+
&exec_approval_requirement,
225+
ExecApprovalRequirement::Forbidden { .. }
226+
)
227+
{
228+
return exec_approval_requirement;
229+
}
230+
231+
codex_shell_command::metadata_write_forbidden_reason(
232+
command,
233+
command_cwd,
234+
sandbox_policy_cwd,
235+
file_system_sandbox_policy,
236+
)
237+
.map_or(exec_approval_requirement, |reason| {
238+
ExecApprovalRequirement::Forbidden { reason }
239+
})
240+
}
241+
197242
/// - Never, OnFailure: do not ask
198243
/// - OnRequest: ask unless filesystem access is unrestricted
199244
/// - Granular: ask unless filesystem access is unrestricted, but auto-reject

0 commit comments

Comments
 (0)