macOS沙箱执行流程
Seatbelt policy 生成只是“准备了一个 launch command”,还没有执行用户程序。macOS 本地流程要经过四个边界:SandboxManager::transform 生成 /usr/bin/sandbox-exec argv;Core 将 SandboxExecRequest 转为含环境、capture 和权限快照的 ExecRequest;spawn 层创建 child 并收集 stdout/stderr;退出后再依据 concrete sandbox type、exit code 和输出分类 denial。
0.150.0 还增加了 Unified Exec 与 executor-managed 路径的区分。local Seatbelt 进程的 SandboxType 是 MacosSeatbelt;remote 或 shell-snapshot request 在 Core 侧可能是 None,但 sandbox intent 仍通过 exec_server_sandbox 传给 executor。因而 denial 判断不能只看一个枚举值,重试也不能简单“把 sandbox 字段改成 None”。
本文承接macOS Seatbelt规则生成和跨平台Sandbox抽象。前文讲 SBPL 如何生成,本篇讲它如何进入进程生命周期、何时被识别为拒绝、为什么普通非零退出不应自动升级;不重复每条文件或网络 SBPL 规则。
1. Transform
1.1 本机包装
SandboxManager::transform 接收 SandboxType::MacosSeatbelt 时,先用 effective PermissionProfile 生成 Seatbelt 参数,再把固定路径 /usr/bin/sandbox-exec 放在 command 首位。-- 之后是原始 command;SBPL 文本和 -D 定义只属于 wrapper 参数。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::transform
let (argv, arg0_override, pending_sandboxed_request) = match sandbox {
SandboxType::None => (os_argv_to_strings(argv), None, None),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => {
use crate::seatbelt::CreateSeatbeltCommandArgsParams;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::seatbelt::SeatbeltPreparationError;
use crate::seatbelt::create_seatbelt_command_args_with_profile;
let pending = pending_sandboxed_request?;
let (file_system_sandbox_policy, network_sandbox_policy) = pending
.effective_permission_profile
.to_runtime_permissions();
let mut args = create_seatbelt_command_args_with_profile(
CreateSeatbeltCommandArgsParams {
command: os_argv_to_strings(argv),
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(),
enforce_managed_network,
managed_network,
environment_id,
network,
extra_allow_unix_sockets: &[],
},
self.seatbelt_profile,
)
.map_err(|err| match err {
SeatbeltPreparationError::FileSystem(message) => {
SandboxTransformError::SeatbeltPreparation(message)
}
SeatbeltPreparationError::EnvironmentNetworkProxy(message) => {
SandboxTransformError::EnvironmentNetworkProxy(message)
}
})?;
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
(full_command, None, Some(pending))
}如果同一代码在非 macOS 编译,显式请求 MacosSeatbelt 会返回 SeatbeltUnavailable;这不是把 Linux 或 Windows 当成 Seatbelt 的兼容实现。
1.2 Request字段
transform 的返回值仍使用 PathUri cwd、network environment ID、PermissionProfile、Windows compatibility 字段和 arg0。它是一次 launch snapshot,不是已创建的 child handle。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxExecRequest
#[derive(Debug)]
pub struct SandboxExecRequest {
pub command: Vec<String>,
pub cwd: PathUri,
pub sandbox_policy_cwd: PathUri,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub network_environment_id: Option<String>,
pub sandbox: SandboxType,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub permission_profile: PermissionProfile,
pub arg0: Option<String>,
}pending_sandboxed_request 在 Seatbelt 分支中已经完成 cwd 和 profile 的 native 准备;None 分支则保留 base effective profile,以支持 foreign cwd 或 executor-owned request。
2. Core ExecRequest
2.1 环境标记
Core 的 adapter 不会把环境变量当作 sandbox 本身。它在 network policy 未启用时写入 CODEX_SANDBOX_NETWORK_DISABLED=1,在 macOS Seatbelt concrete type 下写入 CODEX_SANDBOX=seatbelt。这些字段供子进程和诊断读取,真正的边界仍由 sandbox-exec 应用 SBPL。
源码位置:codex-rs/core/src/sandboxing/mod.rs :: ExecRequest::from_sandbox_exec_request
let network_sandbox_policy = permission_profile.network_sandbox_policy();
if !network_sandbox_policy.is_enabled() {
env.insert(
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(),
"1".to_string(),
);
}
#[cfg(target_os = "macos")]
if sandbox == SandboxType::MacosSeatbelt {
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
}2.2 adapter转换
from_sandbox_exec_request 还会根据 Windows concrete type 计算 filesystem overrides;macOS 则保留 transform 的 command/cwd/env,并把 capture expiration 复制到 ExecRequest。这一步没有启动进程。
源码位置:codex-rs/core/src/sandboxing/mod.rs :: ExecRequest::from_sandbox_exec_request
pub(crate) fn from_sandbox_exec_request(
request: SandboxExecRequest,
options: ExecOptions,
windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
) -> Result<Self, CodexErr> {
let SandboxExecRequest {
command,
cwd,
sandbox_policy_cwd: windows_sandbox_policy_cwd,
mut env,
network,
network_environment_id,
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
permission_profile,
arg0,
..
} = request;
let ExecOptions {
expiration,
capture_policy,
} = options;
let network_sandbox_policy = permission_profile.network_sandbox_policy();ExecRequest 同时携带 sandbox 和 permission_profile。后者用于后续 network/sandbox 逻辑,前者用于 denial 分类和平台指标;两者不能只保留其中一个。
3. Local spawn
3.1 普通执行入口
最窄的 local path 由 execute_env 调用 execute_exec_request。Unified Exec 则通过 SandboxAttempt::env_for 创建同样的 ExecRequest,再由 process manager 管理长生命周期 session。
源码位置:codex-rs/core/src/sandboxing/mod.rs :: execute_env;codex-rs/core/src/unified_exec/process_manager.rs :: open_session_with_exec_env
pub async fn execute_env(
exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,
) -> codex_protocol::error::Result<ExecToolCallOutput> {
execute_exec_request(exec_request, stdout_stream, /*after_spawn*/ None).await
}Unified Exec 的 manager path 会根据 environment 或 shell snapshot 决定是否调用 env_for_exec_server;local Seatbelt 请求则走本机 env_for,保留 concrete MacosSeatbelt。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: open_session_with_exec_env
let mut request = if environment.is_remote() || request.exec_server_shell_snapshot.is_some() {
attempt.env_for_exec_server(command, options)?
} else {
attempt.env_for(command, options, network, environment_id)?
};
request.exec_server_shell_snapshot = shell_snapshot;3.2 child与输出
local process manager 创建 child 后,把 stdout/stderr reader、expiration、capture policy 和 sandbox type 绑定到执行状态。child handle 的所有权属于 process manager,不属于 policy generator。
源码位置:codex-rs/core/src/exec.rs :: execute_exec_request
let params = ExecParams {
command,
cwd,
expiration,
capture_policy,
env,
network: network.clone(),
network_environment_id,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level,
windows_sandbox_private_desktop,
justification: None,
arg0,
};ExecParams 进入 spawn 层后才会产生实际 child。输出被拆成 stdout、stderr 和 aggregated output,后续 denial heuristic 会读取这些不同视图。
4. Denial识别
4.1 Seatbelt类型
is_likely_sandbox_denied 是 side-effect-free predicate。它先排除 SandboxType::None 和 exit 0,再识别 executor-managed keyword、Linux Seccomp 的 SIGSYS;在当前 macOS concrete type 上,常见的 operation not permitted、read-only file system 等输出只能说明“很像 sandbox denial”,不能说明唯一原因。
源码位置:codex-rs/sandboxing/src/denial.rs :: is_likely_sandbox_denied
pub fn is_likely_sandbox_denied(
sandbox_type: SandboxType,
exec_output: &ExecToolCallOutput,
) -> bool {
if sandbox_type == SandboxType::None || exec_output.exit_code == 0 {
return false;
}
if is_likely_executor_managed_sandbox_denied(exec_output) {
return true;
}
const QUICK_REJECT_EXIT_CODES: [i32; 3] = [2, 126, 127];
if QUICK_REJECT_EXIT_CODES.contains(&exec_output.exit_code) {
return false;
}
#[cfg(unix)]
{
const EXIT_CODE_SIGNAL_BASE: i32 = 128;
const SIGSYS_CODE: i32 = libc::SIGSYS;
if sandbox_type == SandboxType::LinuxSeccomp
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE
{
return true;
}
}
false
}这里的 quick reject 保护很重要:command not found(127)、permission/argument usage error(2)和不可执行文件(126)不能仅凭非零退出被升级为 Seatbelt denial。
4.2 Executor关键词
当具体 backend 不在 Core 进程中时,is_likely_executor_managed_sandbox_denied 扫描 stderr、stdout 和 aggregated output 的七类关键词。它同样只返回可能性,不修改状态。
源码位置:codex-rs/sandboxing/src/denial.rs :: is_likely_executor_managed_sandbox_denied
pub fn is_likely_executor_managed_sandbox_denied(exec_output: &ExecToolCallOutput) -> bool {
if exec_output.exit_code == 0 {
return false;
}
const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [
"operation not permitted",
"permission denied",
"read-only file system",
"seccomp",
"sandbox",
"landlock",
"failed to write file",
];
[
&exec_output.stderr.text,
&exec_output.stdout.text,
&exec_output.aggregated_output.text,
]
.into_iter()
.any(|section| {
let lower = section.to_lowercase();
SANDBOX_DENIED_KEYWORDS
.iter()
.any(|needle| lower.contains(needle))
})
}4.3 Unified Exec等待
长生命周期 Unified Exec 不会在 child 刚退出的瞬间读取一个可能尚未 drain 完的空 buffer。check_for_sandbox_denial 先等待最多 20ms 的 output notification,再快照聚合输出;若 executor 已报告 denial 或 concrete predicate 命中,才构造 UnifiedExecError::SandboxDenied。
源码位置:codex-rs/core/src/unified_exec/process.rs :: check_for_sandbox_denial, check_for_sandbox_denial_with_text
pub(super) async fn check_for_sandbox_denial(&self) -> Result<(), UnifiedExecError> {
let _ = tokio::time::timeout(
Duration::from_millis(20),
self.output.output_notify.notified(),
)
.await;
let aggregated = self.snapshot_output().await;
let aggregated_text = String::from_utf8_lossy(&aggregated);
self.check_for_sandbox_denial_with_text(aggregated_text.as_ref())
.await?;
Ok(())
}源码位置:codex-rs/core/src/unified_exec/process.rs :: check_for_sandbox_denial_with_text
let executor_reported_denial = self.state_rx.borrow().sandbox_denied;
let sandbox_type = self.sandbox_type();
if !self.has_exited() || (!executor_reported_denial && sandbox_type == SandboxType::None) {
return Ok(());
}
let exit_code = self.exit_code().unwrap_or(-1);
let exec_output = ExecToolCallOutput {
exit_code,
stderr: StreamOutput::new(text.to_string()),
aggregated_output: StreamOutput::new(text.to_string()),
..Default::default()
};
let likely_sandbox_denial = is_likely_sandbox_denied(sandbox_type, &exec_output);
if executor_reported_denial || likely_sandbox_denial {
return Err(UnifiedExecError::sandbox_denied(
text.to_string(),
exec_output,
));
}executor reported denial 是跨 owner 的显式信号;Core concrete predicate 则是本机的补充判断。两者合并后才进入统一错误模型。
5. 错误传播
5.1 普通execute
短命令路径在 finalize_exec_result 中先把 signal、timeout、exit code 和 output 组装成 ExecToolCallOutput。timeout 先返回 SandboxErr::Timeout;只有 likely denial 才返回 SandboxErr::Denied。
源码位置:codex-rs/core/src/exec.rs :: finalize_exec_result
if timed_out {
return Err(CodexErr::Sandbox(SandboxErr::Timeout {
output: Box::new(exec_output),
}));
}
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
record_filesystem_sandbox_violation(sandbox_type, &exec_output);
return Err(CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(exec_output),
network_policy_decision: None,
}));
}
Ok(RawExecToolCallOutput {
exit_status,
stdout,
stderr,
aggregated_output,
timed_out,
})普通 non-zero 但没有 denial 证据的 command 会保留为普通执行结果,让上层看到真实 exit code 和输出,而不是伪装成 sandbox 问题。
5.2 Unified Exec
Unified process 记录 sandbox_denied,退出事件携带该分类;调用方随后把它转换为 UnifiedExecError::SandboxDenied,保留截断后的 output snippet。这种状态可被 process manager、SSE 事件和 retry orchestration 分别消费。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: exit bookkeeping
process.sandbox_denied = is_likely_sandbox_denied(process.sandbox, &exec_output);
let _ = process.wake_tx.send(seq);
process.events.publish(ExecProcessEvent::Exited {
seq,
exit_code,
sandbox_denied: Some(process.sandbox_denied),
});
Some(ExecExitedNotification {
process_id: process_id.clone(),
seq,
exit_code,
sandbox_denied: Some(process.sandbox_denied),
})6. 审批与重试
6.1 首次attempt
Orchestrator 在首次执行前已经完成 approval requirement、sandbox override、managed network 和 owner 选择。Seatbelt denial 并不会由 is_likely_sandbox_denied 直接重跑;它只把结果交给 orchestrator。
源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run
let sandbox_override = if unsandboxed_allowed {
sandbox_override_for_first_attempt(
tool.sandbox_permissions(req),
&requirement,
&file_system_sandbox_policy,
)
} else {
SandboxOverride::NoOverride
};
let sandbox_requested = match sandbox_override {
SandboxOverride::BypassSandboxFirstAttempt => false,
SandboxOverride::NoOverride => self.sandbox.should_sandbox(
&permissions,
sandbox_preference,
managed_network_active,
),
};
let initial_sandbox = if sandbox_requested && !executor_managed_process_sandbox {
self.sandbox.select_initial(
&permissions,
sandbox_preference,
turn_ctx.windows_sandbox_level,
managed_network_active,
)
} else {
SandboxType::None
};6.2 denial后重算
首次 attempt 失败后,Orchestrator 重新计算 retry_sandbox_requested。local owner 重新选择 concrete backend;executor-managed owner 保持 host SandboxType::None,由 exec-server context 继续强制。deny-read 存在时,unsandboxed_allowed 为 false,不能用 retry 把唯一的文件读取边界去掉。
源码位置:codex-rs/core/src/tools/orchestrator.rs :: retry sandbox construction
let retry_sandbox_requested = !unsandboxed_allowed
&& self.sandbox.should_sandbox(
&permissions,
sandbox_preference,
managed_network_active,
);
let retry_sandbox = if retry_sandbox_requested && !executor_managed_process_sandbox {
self.sandbox.select_initial(
&permissions,
sandbox_preference,
turn_ctx.windows_sandbox_level,
managed_network_active,
)
} else {
SandboxType::None
};
let retry_attempt = SandboxAttempt {
sandbox: retry_sandbox,
sandbox_requested: retry_sandbox_requested,
permissions: &permissions,
exec_server_permissions: permission_profile,
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots,是否请求第二次审批取决于 escalation policy、additional permissions 和当前 turn 的 approval flow;denial heuristic 本身不授予 unsandboxed execution。
7. 测试边界
本篇针对执行和 denial 路径运行了以下测试:
codex-coreexec tests:8 项目标断言覆盖无关键词、stderr 关键词、quick reject exit、SandboxType::None、aggregated output 和 Linux SIGSYS。codex-sandboxingviolation tests:10 项目标断言覆盖 concrete/executor keyword、stream aggregation、network policy text 排除和 violation 映射。codex-coreUnified Exec process tests:5 项目标测试覆盖 process state、sandbox type、终态错误和远程 process 语义。codex-coreUnified Exec process-manager tests:15 项覆盖 environment overlay、exec-server 参数、输出 drain、network denial grace 和 process retention。codex-coresandboxing tests:11 项覆盖env_for、env_for_exec_server、deny-read、Windows fallback 和 executor context。codex-exec-serverprocess-sandbox tests:6 项覆盖 executor-native argv、sandbox wrapper、custom arg0、managed proxy 和 native launch。codex-exec-serverlocal-process tests:13 项覆盖 child 环境、退出收尾、network policy notification、process retention 和 native cwd 拒绝。
源码位置:
codex-rs/core/src/exec_tests.rs :: sandbox_detection_identifies_keyword_in_stderrcodex-rs/core/src/exec_tests.rs :: sandbox_detection_respects_quick_reject_exit_codescodex-rs/core/src/exec_tests.rs :: sandbox_detection_uses_aggregated_outputcodex-rs/sandboxing/src/violation_tests.rs :: classifies_filesystem_violation_from_aggregated_outputcodex-rs/core/src/unified_exec/process_manager_tests.rs :: late_network_denial_grace_observes_cancellation_after_exitcodex-rs/core/src/tools/sandboxing_tests.rs :: exec_server_env_keeps_command_native_and_carries_sandbox_contextcodex-rs/exec-server/src/process_sandbox_tests.rs :: sandbox_request_wraps_native_argv_on_executorcodex-rs/exec-server/src/local_process.rs :: exit_before_shutdown_records_success
cargo test -p codex-core --lib exec_tests -- --nocapture --test-threads=1
cargo test -p codex-sandboxing --lib violation::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib unified_exec::process_tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib unified_exec::process_manager::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib tools::sandboxing::tests:: -- --nocapture --test-threads=1
cargo test -p codex-exec-server --lib process_sandbox::tests:: -- --nocapture --test-threads=1
cargo test -p codex-exec-server --lib local_process::tests:: -- --nocapture --test-threads=1这些测试证明 transform/adapter 的字段传播、输出分类和 retry 输入准备;不能证明某个关键词必然由 Seatbelt 产生,也不能证明所有 macOS 版本都允许测试 fixture 调用 sandbox-exec。当前环境若在 sandbox_apply 阶段被宿主拒绝,应与用户 command 在已应用 policy 下返回的 denial 分开解释。
8. 继续阅读
建议按 SandboxManager::transform → ExecRequest::from_sandbox_exec_request → execute_exec_request 或 open_session_with_exec_env → output drain → is_likely_sandbox_denied → ToolOrchestrator retry 阅读。读完后应能区分:SBPL 生成、wrapper 启动、child 输出、denial heuristic、executor-reported denial 和第二次 attempt 各自属于哪一层。
下一篇Linux Landlock策略将把同一 execution contract 放到 Linux,分析 PermissionProfile 如何进入 Landlock/Bubblewrap 以及 legacy flag 的兼容边界。
