Codex 系统手册

Windows 沙盒预配与进程启动内部机制

stage-14.2.627 个文件

这一层是 Windows 上“把外部命令关进小房间再启动”的幕后支撑。lib、unified_exec 和 TUI 帮手接住上层请求,决定开不开沙箱、走普通还是提权通道。setup、identity 和错误报告准备专用账号、目录、运行文件和失败记录。ACL、token、审计、防火墙像门锁和保安,限制文件和网络。最后 spawn、process、runner、管道、假终端和 stdio_bridge 开进程、接输入输出,并负责等待、超时或杀掉它。

本阶段的文件27

公共执行入口

这些文件定义面向 crate 和 TUI 的入口点,用于选择沙盒执行模式并暴露通用 API 接口。

windows-sandbox-rs/src/lib.rs源码 ↗
orchestrationcross-cutting;在启动沙箱任务、等待任务结束、预检查权限时活跃

这个文件像一个总服务台,把 Windows 沙箱相关的小模块统一摆出来给别人用。真正运行在 Windows 上时,它会导入权限、进程、日志、管道、ACL(Windows 文件权限规则)等模块,并公开一组好用的函数。它自己也放了旧版沙箱执行流程:先根据权限配置准备环境和安全身份,再设置文件访问规则,接着用管道接住子进程的输入输出,等待它结束、超时或被取消,最后返回退出码和 stdout/stderr。它还定义了一个取消令牌,外部可以用它告诉正在等待的沙箱任务“别等了”。如果程序不是跑在 Windows 上,这里会换成 stub(占位实现),调用时直接说明“Windows 沙箱只能在 Windows 上用”,避免假装成功。

函数细节16
WindowsSandboxCancellationToken::new19–23 ↗
fn new(is_cancelled: impl Fn() -> bool + Send + Sync + 'static) -> Self

作用:创建一个“取消令牌”。外部把一个能回答“现在要不要取消”的小函数交进来,后面等待沙箱进程时就能随时询问它。

数据流:进去的是一个返回 true/false 的判断函数 → 它把这个函数包进 Arc(一种可以安全共享的引用)里,方便复制给不同地方用 → 出来的是 WindowsSandboxCancellationToken,里面保存了这条取消判断规则。

调用关系:它是取消机制的入口。测试 legacy_capture_cancellation_is_not_reported_as_timeout 会创建这个令牌来模拟取消;真正等待进程时,等待逻辑会通过令牌询问是否该停下。

调用图:被 1 处调用(legacy_capture_cancellation_is_not_reported_as_timeout);外部调用 1 个(new)。

WindowsSandboxCancellationToken::is_cancelled26–28 ↗
fn is_cancelled(&self) -> bool

作用:问一句:“调用方现在是不是要求取消了?”它让等待中的沙箱任务能及时停下来,而不是一直傻等。

数据流:进去的是这个令牌本身,里面已经保存了取消判断函数 → 它直接调用这个函数 → 出来是 true 或 false,表示是否取消。

调用关系:它被等待进程的逻辑拿来反复检查。可以把它理解成工作人员隔一会儿看一下墙上的“停止施工”牌子。

WindowsSandboxCancellationToken::fmt32–35 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:让取消令牌在调试打印时有个安全、简短的样子。它不会把里面的函数细节打印出来,因为函数本身也没法有意义地展示。

数据流:进去的是调试格式化器 → 它写入结构名 WindowsSandboxCancellationToken,并标记还有内部内容没有展开 → 输出给调试日志或调试显示使用。

调用关系:这是 Rust 的 Debug 打印支持。它调用标准的 debug_struct 来生成可读的调试文本,主要在排查问题时有用。

调用图:外部调用 1 个(debug_struct)。

windows_impl::wait_for_process379–415 ↗
fn wait_for_process(
        process: HANDLE,
        timeout_ms: Option<u64>,
        cancellation: Option<&WindowsSandboxCancellationToken>,
    ) -> WaitOutcome

作用:等待一个 Windows 子进程结束,同时支持超时和取消。没有它,沙箱命令可能卡住不退出,或者用户点了取消却还要继续等。

数据流:进去的是进程句柄、可选超时时间、可选取消令牌 → 如果没有取消令牌,就直接等到进程结束或超时;如果有,就每最多 50 毫秒醒来一次,检查是否取消、是否超时、进程是否结束 → 出来是 Exited、TimedOut 或 Cancelled 三种结果之一。

调用关系:它服务于 windows_impl::run_windows_sandbox_capture_with_filesystem_overrides。后者启动沙箱进程后,把等待这件事交给它;它内部调用 Windows 的 WaitForSingleObject,并用当前时间和毫秒时长来判断截止时间。

调用图:外部调用 3 个(from_millis, now, WaitForSingleObject)。

windows_impl::setup_stdio_pipes417–443 ↗
fn setup_stdio_pipes() -> io::Result<PipeHandles>

作用:给即将启动的沙箱子进程准备三根“管子”:stdin、stdout、stderr。这样父进程能喂输入、收标准输出、收错误输出。

数据流:进去没有业务参数 → 它调用 Windows 的 CreatePipe 创建三对读写句柄,再把子进程需要继承的那几端标记为可继承 → 成功时出来三组管道句柄;失败时出来一个带 Windows 错误码的 io::Error。

调用关系:它被 windows_impl::run_windows_sandbox_capture_with_filesystem_overrides 调用,发生在真正创建子进程之前。后续 create_process_as_user 会拿这些句柄接到子进程的标准输入输出上。

调用图:外部调用 5 个(from_raw_os_error, null_mut, GetLastError, SetHandleInformation, CreatePipe)。

windows_impl::run_windows_sandbox_capture453–477 ↗
fn run_windows_sandbox_capture(
        permission_profile: &PermissionProfile,
        workspace_roots: &[AbsolutePathBuf],
        codex_home: &Path,
        command: Vec<String>,
        cwd: &Path

作用:这是旧版 Windows 沙箱执行的简化入口:给它权限配置、命令、工作目录、环境变量等,它会运行命令并收集结果。

数据流:进去的是权限配置、工作区目录、Codex 主目录、命令、当前目录、环境变量、超时时间、取消令牌和是否使用私有桌面 → 它不自己做复杂工作,而是补上“没有额外读写禁止路径”这两个默认值 → 出来是 CaptureResult,里面有退出码、标准输出、错误输出和是否超时。

调用关系:它是对 windows_impl::run_windows_sandbox_capture_with_filesystem_overrides 的薄包装。普通调用者不需要额外指定文件系统禁止项时,会走这个更简单的入口。

调用图:外部调用 1 个(run_windows_sandbox_capture_with_filesystem_overrides)。

windows_impl::run_windows_sandbox_capture_with_filesystem_overrides480–678 ↗
fn run_windows_sandbox_capture_with_filesystem_overrides(
        permission_profile: &PermissionProfile,
        workspace_roots: &[AbsolutePathBuf],
        codex_home: &Path,
        command: Vec<S

作用:这是旧版 Windows 沙箱真正干活的执行器。它按权限配置启动一个受限制的 Windows 进程,收集输出,并处理成功、失败、超时和取消。

数据流:进去的是权限配置、工作区、Codex 主目录、命令、目录、环境变量、超时/取消设置、额外禁止读写路径、是否私有桌面 → 它先准备启动上下文和安全规则,拒绝旧后端做不了的受限读权限;再设置 ACL(文件访问控制规则)和能力 SID(Windows 用来表示某种权限身份的标识);接着建管道、用受限令牌启动进程、用两个线程读取 stdout/stderr;最后等待进程结束,必要时杀掉它,关闭句柄并写成功/失败日志 → 出来是 CaptureResult,同时可能改动磁盘上的 ACL 并写日志。

调用关系:它是本文件 Windows 旧后端的核心流程。它把准备工作交给 prepare_legacy_spawn_context、prepare_legacy_session_security、legacy_session_capability_roots、apply_legacy_session_acl_rules 等函数,把创建进程交给 create_process_as_user,把等待交给 windows_impl::wait_for_process,把日志交给 log_success 或 log_failure。

调用图:调用 8 个内部函数(log_failure, log_success, create_process_as_user, allow_null_device_for_workspace_write, apply_legacy_session_acl_rules, legacy_session_capability_roots, prepare_legacy_session_security, prepare_legacy_spawn_context);外部调用 11 个(bail!, format!, matches!, spawn, is_empty, iter, setup_stdio_pipes, wait_for_process, CloseHandle, GetExitCodeProcess (+1 more))。

windows_impl::run_windows_sandbox_legacy_preflight680–717 ↗
fn run_windows_sandbox_legacy_preflight(
        permission_profile: &PermissionProfile,
        workspace_roots: &[AbsolutePathBuf],
        codex_home: &Path,
        cwd: &Path,
        env_map: &H

作用:在真正启动旧版沙箱前,提前检查并铺好必要的文件权限。这样后面子进程需要写工作区时,不会因为权限规则还没准备好而失败。

数据流:进去的是权限配置、工作区目录、Codex 主目录、当前目录和环境变量 → 它先尝试把配置解析成 Windows 沙箱权限;如果这种配置不用管理文件权限,就直接结束;如果需要写能力,就确保 Codex 主目录存在,算出能力根目录和对应 SID,再应用旧版 ACL 规则 → 成功时没有返回数据,只表示预处理完成;出错时返回错误。

调用关系:它是旧版沙箱启动前的预备动作。它调用权限解析、目录确保、能力 SID 计算和 ACL 应用函数,目的不是启动进程,而是先把地基打好。

调用图:调用 5 个内部函数(try_from_permission_profile_for_workspace_roots, ensure_codex_home_exists, apply_legacy_session_acl_rules, legacy_session_capability_roots, root_capability_sids);外部调用 1 个(to_path_buf)。

windows_impl::tests::workspace_profile727–734 ↗
fn workspace_profile(network_policy: NetworkSandboxPolicy) -> PermissionProfile

作用:测试用的小帮手,用指定的网络策略快速造一个“工作区可写”的权限配置。这样每个测试不用重复写一长串配置参数。

数据流:进去的是网络沙箱策略,比如允许网络或限制网络 → 它调用 workspace_write_with 生成对应的 PermissionProfile → 出来是测试可直接使用的权限配置。

调用关系:它只在本文件的测试里使用,配合 should_apply_network_block 检查不同网络策略下是否应该启用网络封锁。

调用图:调用 1 个内部函数(workspace_write_with)。

windows_impl::tests::should_apply_network_block736–743 ↗
fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool

作用:测试用的小帮手,用来回答某个权限配置最终会不会启用网络阻断。它把底层解析步骤藏起来,让断言更直观。

数据流:进去的是一个 PermissionProfile → 它把配置解析成 ResolvedWindowsSandboxPermissions,然后读取 should_apply_network_block 的判断结果 → 出来是 true 或 false。

调用关系:多个网络相关测试都会用它。它调用 try_from_permission_profile_for_workspace_roots,模拟真实代码把用户配置变成 Windows 沙箱可执行规则的过程。

调用图:调用 1 个内部函数(try_from_permission_profile_for_workspace_roots)。

windows_impl::tests::applies_network_block_when_access_is_disabled746–750 ↗
fn applies_network_block_when_access_is_disabled()

作用:确认当网络策略是受限时,沙箱确实会启用网络阻断。这个测试防止以后有人改代码时把安全限制漏掉。

数据流:进去没有外部输入 → 它创建一个网络受限的工作区写权限配置,再判断是否应该阻断网络 → 结果必须是 true,否则测试失败。

调用关系:它调用测试帮手 workspace_profile 和 should_apply_network_block,最后用 assert! 做检查。它覆盖的是“禁止网络时必须真的禁止”的安全预期。

调用图:外部调用 1 个(assert!)。

windows_impl::tests::skips_network_block_when_access_is_allowed753–757 ↗
fn skips_network_block_when_access_is_allowed()

作用:确认当网络策略明确允许时,沙箱不会错误地启用网络阻断。这个测试防止功能被限制得过头。

数据流:进去没有外部输入 → 它创建一个网络启用的工作区写权限配置,再判断是否会阻断网络 → 结果必须是 false,否则测试失败。

调用关系:它和上一个测试是一对:一个验证该拦时拦,一个验证不该拦时别拦。它同样通过测试帮手完成配置和判断。

调用图:外部调用 1 个(assert!)。

windows_impl::tests::applies_network_block_for_read_only760–762 ↗
fn applies_network_block_for_read_only()

作用:确认只读权限配置下也会启用网络阻断。也就是说,文件只读并不代表网络可以随便用。

数据流:进去没有外部输入 → 它创建只读 PermissionProfile,检查解析后的规则是否要求网络阻断 → 结果必须是 true。

调用关系:它使用 should_apply_network_block 检查真实权限解析结果。这个测试补上了只读场景,避免只测工作区写权限。

调用图:外部调用 1 个(assert!)。

windows_impl::tests::legacy_preflight_skips_profiles_without_managed_filesystem_permissions765–781 ↗
fn legacy_preflight_skips_profiles_without_managed_filesystem_permissions()

作用:确认旧版预检查遇到“不由 Windows 沙箱管理文件权限”的配置时,会安静跳过而不是报错。这样外部或禁用类配置不会被错误处理。

数据流:进去没有外部输入 → 它构造 Disabled 和 External 两种权限配置,并传入空工作区、当前目录和空环境变量 → 调用 run_windows_sandbox_legacy_preflight 后应当成功返回,不应该要求做 ACL 预处理。

调用关系:它直接覆盖 windows_impl::run_windows_sandbox_legacy_preflight 的早退路径。测试里用 new 创建空映射或路径相关值,并确认这些不适用的配置不会触发旧版权限铺设流程。

调用图:外部调用 3 个(new, new, run_windows_sandbox_legacy_preflight)。

stub::run_windows_sandbox_capture804–816 ↗
fn run_windows_sandbox_capture(
        _permission_profile: &PermissionProfile,
        _workspace_roots: &[AbsolutePathBuf],
        _codex_home: &Path,
        _command: Vec<String>,
        _cwd:

作用:这是非 Windows 系统上的占位版本。它的作用不是运行沙箱,而是清楚地告诉调用者:这个功能只能在 Windows 上用。

数据流:进去的参数和 Windows 版本一样,但全部被忽略 → 它立刻生成一个错误,错误信息说明 Windows sandbox is only available on Windows → 出来是失败结果,不会启动进程,也不会产生输出。

调用关系:当编译目标不是 Windows 时,公开的 run_windows_sandbox_capture 会指向这个函数。它用 bail! 直接返回错误,防止 Linux 或 macOS 上误以为沙箱已经执行。

调用图:外部调用 1 个(bail!)。

stub::run_windows_sandbox_legacy_preflight818–826 ↗
fn run_windows_sandbox_legacy_preflight(
        _permission_profile: &PermissionProfile,
        _workspace_roots: &[AbsolutePathBuf],
        _codex_home: &Path,
        _cwd: &Path,
        _env_ma

作用:这是非 Windows 系统上的旧版预检查占位版本。因为 ACL 和 Windows 身份这些东西在非 Windows 上不存在,所以它只能报错。

数据流:进去的是权限配置、目录和环境变量等参数,但都不会使用 → 它立刻返回“Windows 沙箱只能在 Windows 上使用”的错误 → 出来是失败结果,不会检查目录,也不会改权限。

调用关系:当编译目标不是 Windows 时,公开的 run_windows_sandbox_legacy_preflight 会指向它。它和 stub::run_windows_sandbox_capture 一起组成非 Windows 平台的安全护栏。

调用图:外部调用 1 个(bail!)。

windows-sandbox-rs/src/unified_exec/mod.rs源码 ↗
orchestrationrequest handling

在 Windows 上把一个命令放进沙箱运行,不只有一种办法。有时可以直接用受限制的身份启动;有时因为要强制代理、或用户选择了更高等级的沙箱,就必须走“提权 runner”(可以理解成一个更有权限的帮手进程)这条路。这个文件不亲自做复杂的 Windows 操作,它像前台分诊台:先接收完整的启动申请,包括要运行的命令、工作目录、环境变量、哪些目录能读写、哪些路径禁止访问、是否需要终端等;然后根据沙箱等级和代理要求,把请求转交给 legacy 后端或 elevated 后端。这样上层代码不用关心底层有几套实现,只要拿到同一种结果:一个已经启动好的 SpawnedProcess,也就是可继续交互和等待结束的子进程对象。

函数细节3
spawn_windows_sandbox_session_for_level45–87 ↗
async fn spawn_windows_sandbox_session_for_level(
    request: WindowsSandboxSessionRequest<'_>,
) -> Result<SpawnedProcess>

作用:这是最主要的分流函数。调用者把完整的 Windows 沙箱启动请求交给它,它会判断该用普通方式启动,还是用提权方式启动。

数据流:输入是一整包 WindowsSandboxSessionRequest,里面有权限配置、工作区目录、命令、环境变量、读写限制、终端设置等信息。它先看是否强制代理,或沙箱等级是否是 Elevated;如果是,就把这些信息拆开交给提权启动函数;否则交给 legacy 启动函数。输出是启动成功后的 SpawnedProcess;如果中途失败,就返回错误。

调用关系:它站在统一入口的位置,上层只需要调用它,不必自己判断后端。它内部用 matches! 判断沙箱等级,然后把工作转给 spawn_windows_sandbox_session_elevated_for_permission_profile 或 spawn_windows_sandbox_session_legacy。

调用图:调用 2 个内部函数(spawn_windows_sandbox_session_elevated_for_permission_profile, spawn_windows_sandbox_session_legacy);外部调用 1 个(matches!)。

spawn_windows_sandbox_session_legacy90–119 ↗
async fn spawn_windows_sandbox_session_legacy(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    command: Vec<String>,
    cwd: &Path,

作用:这个函数把“普通受限方式启动 Windows 沙箱命令”的请求转交给 legacy 后端。legacy 可以理解成直接用较传统的受限身份方式来启动进程。

数据流:输入包括权限配置、工作区目录、Codex 主目录、要执行的命令、当前目录、环境变量、超时时间、额外禁止读写的路径,以及终端和标准输入设置。它自己不展开实现,而是原样转给 backends::legacy 里的同名启动函数。输出是一个 SpawnedProcess,或者返回启动失败的错误。

调用关系:它通常由 spawn_windows_sandbox_session_for_level 在不需要提权通道时调用。它是统一接口和 legacy 后端之间的薄薄一层转接,让外部代码不用直接依赖后端模块。

调用图:调用 1 个内部函数(spawn_windows_sandbox_session_legacy);被 1 处调用(spawn_windows_sandbox_session_for_level)。

spawn_windows_sandbox_session_elevated_for_permission_profile122–159 ↗
async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    command: Vec<Str

作用:这个函数把“通过提权通道启动 Windows 沙箱命令”的请求转交给 elevated 后端。它用于需要更强控制的情况,比如强制代理或明确选择 Elevated 沙箱等级。

数据流:输入除了命令、目录、环境变量和权限配置外,还包括代理是否强制、读目录覆盖、写目录覆盖、是否包含平台默认读目录、禁止读写路径、终端和标准输入设置等。它把这些参数传给 backends::elevated 里的同名函数。结果是启动好的 SpawnedProcess;如果提权 runner 或通信过程失败,就返回错误。

调用关系:它由 spawn_windows_sandbox_session_for_level 在需要 elevated 路径时调用。它本身不做提权细节,而是把任务交给 elevated 后端,保持这个文件只负责统一入口和路线选择。

调用图:调用 1 个内部函数(spawn_windows_sandbox_session_elevated_for_permission_profile);被 1 处调用(spawn_windows_sandbox_session_for_level)。

windows-sandbox-rs/src/unified_exec/backends/elevated.rs源码 ↗
orchestrationprocess spawn / request handling

这个文件像一个“沙箱启动总控台”。调用方给它一个要执行的命令、工作目录、环境变量和权限配置,它先把权限配置翻译成 Windows 沙箱真正能用的读写规则,再准备提权运行所需的身份凭据和目录。接着它和沙箱里的 runner 建立一条通信管道。runner 可以理解成沙箱里的“小助手进程”,真正负责启动目标命令。通信成功后,这个文件会把标准输入、标准输出、标准错误、退出码、终止信号都接成统一的 ProcessDriver。这样上层看起来只是启动了一个普通进程,但背后其实经过了权限解析、身份准备、跨进程通信和输入输出转发。一个重要细节是:如果发现沙箱登录凭据过期,它会自动刷新凭据再试一次,减少因为旧身份信息导致的启动失败。

函数细节2
spawn_runner_transport_task30–48 ↗
async fn spawn_runner_transport_task(
    codex_home: PathBuf,
    cwd: PathBuf,
    sandbox_creds: SandboxCreds,
    logs_base_dir: Option<PathBuf>,
    spawn_request: SpawnRequest,
) -> Result<Runne

作用:这个函数专门负责和沙箱里的 runner 建立通信通道。因为建立通道的过程可能会卡住当前异步任务,所以它把这件事丢到后台阻塞线程里做,避免拖慢主运行循环。

数据流:进去的是 Codex 主目录、当前工作目录、沙箱身份凭据、日志目录和启动请求。它把这些信息交给底层的 spawn_runner_transport 去完成 runner 握手,也就是双方先确认能正常通信。成功后出来的是 RunnerTransport,表示已经连上的通信通道;如果后台任务本身崩了,就把错误包装成“runner 握手任务失败”的错误返回。

调用关系:它是 spawn_windows_sandbox_session_elevated_for_permission_profile 里的一个小帮手。主函数准备好权限和启动请求后,会调用它去真正建立沙箱 runner 的连接;它内部把重活交给 spawn_blocking 里的 spawn_runner_transport,自己负责把异步世界和可能阻塞的 Windows 启动过程隔开。

调用图:被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 1 个(spawn_blocking)。

spawn_windows_sandbox_session_elevated_for_permission_profile51–196 ↗
async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    command: Vec<Str

作用:这个函数是本文件的核心:根据一份权限档案启动一个提权的 Windows 沙箱会话,并返回一个可以像普通子进程一样操作的 SpawnedProcess。调用方用它来运行命令,不需要自己关心沙箱身份、权限目录、IPC 通信和输入输出转发。

数据流:进去的是权限档案、工作区目录、Codex 主目录、要执行的命令、工作目录、环境变量、读写目录覆盖规则、是否开终端、是否保留标准输入、是否使用私有桌面等信息。它先把禁止读写路径转换成普通路径,再把权限档案解析成 Windows 沙箱权限;随后准备提权启动上下文,这一步会补齐沙箱身份、沙箱目录、能力标识和日志目录。然后它组装 SpawnRequest,去连接 runner。连接成功后,它从 transport 里拆出读写管道,建立输入写入通道、输出广播通道、退出码通道和终止函数。最后把这些零件装进 ProcessDriver,再交给 finish_driver_spawn 包成 SpawnedProcess 返回。过程中如果发现沙箱凭据过期,会刷新凭据并重试一次。

调用关系:它串起了整个提权沙箱启动流程:先调用 try_from_permission_profile_for_workspace_roots 解析权限,再调用 prepare_elevated_spawn_context_for_permissions 准备提权环境,然后通过 spawn_runner_transport_task 连接沙箱 runner。如果连接报的是过期凭据错误,它会用 is_stale_sandbox_creds_error 判断,再调用 refresh_logon_sandbox_creds 刷新身份后重试。通信建立后,它调用 start_runner_pipe_writer、start_runner_stdin_writer、start_runner_stdout_reader 把输入输出接起来;如果是终端模式,还用 make_runner_resizer 支持窗口大小变化;最后用 finish_driver_spawn 完成统一的进程包装。调用图里把它标成由同名流程触发,实际代码里它扮演的是提权沙箱启动入口。

调用图:调用 10 个内部函数(is_stale_sandbox_creds_error, refresh_logon_sandbox_creds, try_from_permission_profile_for_workspace_roots, prepare_elevated_spawn_context_for_permissions, spawn_runner_transport_task, finish_driver_spawn, make_runner_resizer, start_runner_pipe_writer, start_runner_stdin_writer, start_runner_stdout_reader);被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 6 个(new, clone, to_path_buf, clone, iter, to_vec)。

windows-sandbox-rs/src/unified_exec/backends/legacy.rs源码 ↗
orchestrationrequest handling 和 session runtime

这个文件解决的是:在 Windows 上启动一个相对受限的子进程,同时还能像普通程序一样给它喂输入、读输出、等它结束。它有两种启动方式:如果需要像终端一样交互,就用 ConPTY(Windows 的伪终端,像给程序接了一个假的命令行窗口);否则就用普通管道分别接标准输出和标准错误。启动前它会准备权限和 ACL(访问控制列表,决定哪些文件能读写),但旧版后端能力有限:它不能可靠实现“禁止读取某些路径”,这种情况会直接拒绝运行。运行中,它用通道传输输入输出,用后台线程等待进程结束、处理超时杀进程、关闭 Windows 句柄,并写成功或失败日志。

函数细节7
spawn_legacy_process59–139 ↗
fn spawn_legacy_process(
    h_token: HANDLE,
    command: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    use_private_desktop: bool,
    tty: bool,
    stdin_open: bool,
    std

作用:这个函数真正把子进程启动起来。它会根据是否需要终端交互,选择“伪终端模式”或“普通管道模式”,并把输入输出通道都接好。

数据流:进去的是用户令牌、命令、工作目录、环境变量、是否使用私有桌面、是否启用 TTY、标准输入是否打开,以及输出和输入通道。它先判断 TTY:TTY 模式下创建 ConPTY 并接上一个输出读取线程和一个输入写入任务;非 TTY 模式下创建普通 stdin/stdout/stderr 管道,并分别读取 stdout 和 stderr。出来的是 LegacyProcessHandles,里面装着进程句柄、输出线程、输入任务、伪终端句柄、用户令牌和桌面对象。

调用关系:它是 spawn_windows_sandbox_session_legacy 在完成权限准备之后调用的启动步骤。它把底层启动工作交给 spawn_conpty_process_as_user 或 spawn_process_with_pipes,再把读输出交给 spawn_output_reader,把写输入交给 spawn_input_writer。

调用图:调用 4 个内部函数(spawn_conpty_process_as_user, spawn_process_with_pipes, spawn_input_writer, spawn_output_reader);被 1 处调用(spawn_windows_sandbox_session_legacy);外部调用 2 个(bail!, spawn)。

spawn_output_reader141–148 ↗
fn spawn_output_reader(
    output_read: HANDLE,
    output_tx: broadcast::Sender<Vec<u8>>,
) -> std::thread::JoinHandle<()>

作用:这个函数负责持续读取子进程的输出。它把 Windows 句柄里读到的字节块转发到广播通道,让外面的代码可以收到程序输出。

数据流:进去的是一个可读的 Windows 句柄和一个输出广播发送器。它启动 read_handle_loop,不断从句柄里读数据;每读到一块,就复制成字节数组发出去。出来的是一个线程句柄,表示这个后台读取线程正在运行。

调用关系:它由 spawn_legacy_process 调用。TTY 模式下它读伪终端输出;普通管道模式下它分别读 stdout 和 stderr。它不解释输出内容,只负责像传送带一样把字节送出去。

调用图:调用 1 个内部函数(read_handle_loop);被 1 处调用(spawn_legacy_process)。

spawn_input_writer150–176 ↗
fn spawn_input_writer(
    input_write: Option<HANDLE>,
    mut writer_rx: mpsc::Receiver<Vec<u8>>,
    normalize_newlines: bool,
) -> tokio::task::JoinHandle<()>

作用:这个函数负责把外部发来的输入写进子进程。比如用户在终端里敲字,这里就把这些字节送到子进程的 stdin 或伪终端输入端。

数据流:进去的是可选的写入句柄、一个接收输入字节的通道,以及是否要转换换行符。它在阻塞任务里循环收输入;如果有写入句柄,就按需要把换行格式改成 Windows 终端更合适的形式,然后写入句柄。通道结束或写入失败后,它会关闭写入句柄。出来的是一个后台任务句柄。

调用关系:它由 spawn_legacy_process 创建。TTY 模式下它会配合 normalize_windows_tty_input 处理换行;非 TTY 模式下直接写。实际写入动作由 write_all_handle 完成。

调用图:被 1 处调用(spawn_legacy_process);外部调用 1 个(spawn_blocking)。

write_all_handle178–200 ↗
fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()>

作用:这个函数保证一整段字节都写进 Windows 句柄里。它处理一种常见情况:系统调用一次可能只写进去一部分,所以要循环写完。

数据流:进去的是 Windows 写入句柄和一段字节。它反复调用 WriteFile,每次把还没写完的部分继续写;如果系统报告失败,就读取 Windows 错误码并返回错误;如果系统说成功但一个字节没写,也认为不正常。全部写完后返回成功,不额外产出数据。

调用关系:它是输入写入流程里的底层小工具,主要服务于 spawn_input_writer。上层只关心“把输入送进去”,它负责和 Windows 的 WriteFile 这种底层接口打交道。

调用图:外部调用 5 个(anyhow!, bail!, null_mut, GetLastError, WriteFile)。

finalize_exit203–243 ↗
fn finalize_exit(
    exit_tx: oneshot::Sender<i32>,
    process_handle: Arc<StdMutex<Option<HANDLE>>>,
    thread_handle: HANDLE,
    output_join: std::thread::JoinHandle<()>,
    logs_base_dir: Opti

作用:这个函数负责给子进程收尾。它等待进程结束,取退出码,等输出线程收完尾,通知外部“程序已经结束”,然后关闭句柄并写日志。

数据流:进去的是退出码发送器、进程句柄、线程句柄、输出线程、日志目录和命令内容。它等待进程结束并读取退出码,再等待输出读取线程结束,把退出码通过一次性通道发出去,随后关闭线程和进程句柄。最后根据退出码是 0 还是非 0 写成功或失败日志。

调用关系:它由 spawn_windows_sandbox_session_legacy 创建的等待线程在最后调用。前面负责启动和运行,到了这里就像“关店清账”:确认结果、通知调用方、释放 Windows 资源、记录日志。

调用图:调用 2 个内部函数(log_failure, log_success);外部调用 6 个(join, send, format!, CloseHandle, GetExitCodeProcess, WaitForSingleObject)。

resize_conpty_handle245–269 ↗
fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()>

作用:这个函数用来调整伪终端窗口大小。比如外部终端从 80 列变成 120 列时,它把新尺寸告诉 Windows 的 ConPTY。

数据流:进去的是被互斥锁保护的 ConPTY 句柄和新的行列数。它先加锁取出句柄;如果进程没有接伪终端,就返回错误;否则调用 ResizePseudoConsole,把列数和行数传给 Windows。成功返回空结果,失败返回带错误信息的结果。

调用关系:它会被 spawn_windows_sandbox_session_legacy 放进 ProcessDriver 的 resizer 里。也就是说,外部有人要求改变终端大小时,驱动器会回调它来完成真正的 Windows 调整动作。

调用图:外部调用 2 个(anyhow!, ResizePseudoConsole)。

spawn_windows_sandbox_session_legacy272–443 ↗
async fn spawn_windows_sandbox_session_legacy(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    command: Vec<String>,
    cwd: &Path,

作用:这是这个文件的主入口:用旧版 Windows 沙箱方式启动一次会话。它负责准备权限、启动进程、接好输入输出、设置超时和终止按钮,最后交出一个可控制的 SpawnedProcess。

数据流:进去的是权限配置、工作区路径、Codex 主目录、命令、工作目录、环境变量、超时、额外禁止读写路径、是否 TTY、stdin 是否打开、是否使用私有桌面。它先准备启动上下文和旧版安全令牌;如果请求了旧版做不到的受限读取或禁止读取,就直接报错。然后它设置 ACL 写权限规则,创建输入输出和退出通道,调用 spawn_legacy_process 启动程序。启动后,它开一个等待线程处理超时、杀进程、释放令牌和收尾;同时做出 ProcessDriver,里面包含写输入、读输出、等退出、终止进程和调整终端大小的能力。出来的是 SpawnedProcess,外部可以用它和子进程交互。

调用关系:这是旧版后端被外部统一执行层使用的核心函数。它先调用 prepare_legacy_spawn_context、legacy_session_capability_roots、prepare_legacy_session_security、apply_legacy_session_acl_rules 等函数做安全准备,再调用 spawn_legacy_process 真正启动,最后用 finish_driver_spawn 把底层驱动包装成统一的进程对象。

调用图:调用 7 个内部函数(allow_null_device_for_workspace_write, apply_legacy_session_acl_rules, legacy_session_capability_roots, prepare_legacy_session_security, prepare_legacy_spawn_context, spawn_legacy_process, finish_driver_spawn);被 1 处调用(spawn_windows_sandbox_session_legacy);外部调用 9 个(clone, new, new, new, bail!, spawn, is_empty, iter, CloseHandle)。

tui/src/windows_sandbox.rs源码 ↗
orchestration启动、权限判断、Windows 沙箱设置和失败上报时活跃

Windows 沙箱可以理解成给命令运行套一个“安全围栏”,限制它能碰哪些文件、用什么权限运行。这个文件主要解决两件事:先根据配置判断围栏开不开、开多严;然后在 Windows 上调用底层沙箱工具完成准备工作。它还会把设置失败的原因整理成适合统计上报的名字和内容,避免日志里塞进不干净或过长的信息。要注意,这里明确说了是 TUI 本地进程里的临时帮手:它检查和修改的是当前这台运行 TUI 的机器,所以不适合远程服务器模式。以后这些准备工作应该搬到远端 RPC(远程过程调用,也就是让另一端执行函数)里去。

函数细节6
level_from_config23–35 ↗
fn level_from_config(config: &Config) -> WindowsSandboxLevel

作用:根据用户配置和功能开关,决定 Windows 沙箱应该处在什么级别:关闭、普通受限、还是提升权限模式。它的用处是让界面和后续执行流程对“当前安全围栏有多强”有一个统一答案。

数据流:输入是一份 Config 配置。函数先看用户有没有明确写 windows_sandbox_mode;如果写了,就按用户指定来。没写的话,再看功能开关是否打开了提升版沙箱或普通沙箱。最后输出一个 WindowsSandboxLevel,表示沙箱级别;它不改配置,只做判断。

调用关系:它是很多流程判断沙箱状态的共同入口。运行主流程、权限弹窗、命令分发、内置命令参数生成、沙箱设置是否需要提升权限等地方都会问它一句:现在应该用哪种 Windows 沙箱级别。

调用图:被 11 处调用(run, propagate_windows_sandbox_turn_context, handle_event, open_permissions_popup, permission_mode_actions, builtin_command_flags, dispatch_command, elevated_windows_sandbox_setup_required, maybe_prompt_windows_sandbox_enable, new (+1 more))。

sandbox_setup_is_complete41–43 ↗
fn sandbox_setup_is_complete(_codex_home: &Path) -> bool

作用:检查 Windows 沙箱的准备工作是不是已经完成。这样程序就能避免每次都重复做安装或配置。

数据流:在非 Windows 系统上,输入的 codex_home 路径会被忽略,函数直接返回 false,意思是这里没有可用的 Windows 沙箱设置状态。在 Windows 上,这个名字会转接到底层 windows_sandbox 库里的真实检查函数。

调用关系:它是“要不要先做沙箱准备”的前置检查。上层流程可以先调用它,如果已经准备好,就继续运行;如果没准备好,再进入设置流程。

run_elevated_setup46–67 ↗
fn run_elevated_setup(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
) -> a

作用:在 Windows 上执行需要提升权限的沙箱设置。可以把它理解成:当安全围栏需要管理员级别的准备动作时,由这个函数把材料整理好并交给底层工具去办。

数据流:输入包括权限档案、工作区根目录、命令当前目录、环境变量表和 codex_home。函数先把“用户允许访问什么”转换成 Windows 沙箱真正能用的权限清单;然后组装一个设置请求,带上目录、环境变量和 home 路径;最后调用底层 run_elevated_setup。成功时没有额外输出,只表示设置完成;失败时返回错误。

调用关系:它自己不直接操作系统权限,而是做中间翻译和派发。它先调用 try_from_permission_profile_for_workspace_roots 生成权限,再把请求交给外部的 run_elevated_setup,并使用默认的根目录覆盖设置。

调用图:调用 1 个内部函数(try_from_permission_profile_for_workspace_roots);外部调用 2 个(run_elevated_setup, default)。

elevated_setup_failure_details70–76 ↗
fn elevated_setup_failure_details(err: &anyhow::Error) -> Option<(String, String)>

作用:把提升权限设置失败的错误,整理成一组更适合上报和展示的简短信息。它会拿到错误代码,并清理错误消息,避免统计标签里出现不合适的内容。

数据流:输入是一个 anyhow::Error,也就是一包可能来自很多地方的错误。函数先尝试从里面提取沙箱设置失败信息;如果提取不到,就返回 None。提取到了,就输出一对字符串:失败代码,以及清理过的失败消息。

调用关系:它通常在 run_elevated_setup 失败之后被使用。它把底层错误交给 extract_setup_failure 识别,再交给 sanitize_setup_metric_tag_value 清洗消息,供指标统计或诊断信息使用。

调用图:外部调用 2 个(extract_setup_failure, sanitize_setup_metric_tag_value)。

elevated_setup_failure_metric_name79–90 ↗
fn elevated_setup_failure_metric_name(err: &anyhow::Error) -> &'static str

作用:给提升权限设置失败选择一个统计指标名字。特别地,如果用户取消了辅助程序启动,它会归到“取消”指标,而不是普通失败指标。

数据流:输入是一个错误。函数尝试从错误里提取沙箱设置失败信息,并检查失败代码是不是 OrchestratorHelperLaunchCanceled。是的话输出 canceled 专用指标名;否则输出通用 failure 指标名。它不修改错误本身。

调用关系:它和 elevated_setup_failure_details 一样,属于失败上报的小工具。底层的 extract_setup_failure 负责认出具体失败类型,这个函数负责把失败类型变成统计系统能用的固定名字。

调用图:外部调用 1 个(extract_setup_failure)。

grant_read_root_non_elevated93–122 ↗
fn grant_read_root_non_elevated(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Pa

作用:在不走完整提升权限设置的情况下,给 Windows 沙箱额外增加一个只读目录。比如用户临时允许程序读取某个文件夹,就会需要这种补充授权。

数据流:输入包括当前权限档案、工作区根目录、命令目录、环境变量、codex_home,以及想新增的 read_root 路径。函数先检查这个路径必须是绝对路径、必须存在、必须是目录;不满足就直接报错。然后把路径规范化成系统认可的真实路径,再调用底层刷新设置,把这个目录作为额外只读根目录加入。成功后返回规范化后的目录路径。

调用关系:它处在“用户额外授予读取某个目录”这个流程的后半段。自己负责把关路径是否合法,并用 canonicalize 统一路径形式;真正刷新沙箱权限的动作交给 run_setup_refresh_with_extra_read_roots。

调用图:外部调用 7 个(exists, is_absolute, is_dir, bail!, run_setup_refresh_with_extra_read_roots, canonicalize, vec!)。

预配状态与安全原语

这些文件建立设置阶段的状态、身份、错误,以及用于预配沙盒凭据和权限的底层 ACL 与 token 机制。

windows-sandbox-rs/src/setup_error.rs源码 ↗
utilsetup error reporting / cross-cutting

沙箱安装会分两边跑:普通权限的主程序负责发起,提权后的 helper 程序负责真正改系统设置。两边一旦失败,如果只丢出一段杂乱文字,主程序就很难判断哪里坏了,也不好做统计。这个文件就像一张标准故障单:SetupErrorCode 是固定故障编号,SetupErrorReport 是写到磁盘上的报告,SetupFailure 是程序内部传来传去的失败对象。helper 可以把失败写到 codex_home/.sandbox/setup_error.json,主程序再读回来,给用户和指标系统一个清楚结果。它还会把错误消息里的用户名路径替换成 <user>,再做指标标签清洗,防止把本机用户名带到遥测里。一个重要细节是:删除或读取报告时,如果文件本来不存在,不算错误;这让重复运行和清理更安全。

函数细节17
SetupErrorCode::as_str78–111 ↗
fn as_str(self) -> &'static str

作用:把一个固定的安装错误代码变成稳定的英文小写字符串。这个字符串主要给日志和指标系统用,方便机器统计“哪类故障发生了多少次”。

数据流:进去的是一个 SetupErrorCode 枚举值 → 函数按分支找到对应的固定文本 → 出来的是一段不会临时变化的字符串,比如 helper_firewall_rule_verify_failed。

调用关系:它是错误代码对外展示和打标签的出口。SetupFailure::fmt 在把错误打印成文字时会用它,所以用户或日志看到的错误前缀来自这里。

SetupFailure::new127–132 ↗
fn new(code: SetupErrorCode, message: impl Into<String>) -> Self

作用:创建一个标准的安装失败对象。调用方只要给出“哪类错误”和“具体说明”,它就把两者打包成统一格式。

数据流:进去的是 SetupErrorCode 和一段可转成字符串的消息 → 函数把消息转成真正的 String → 出来的是 SetupFailure,里面同时有机器能识别的 code 和人能读懂的 message。

调用关系:这是很多安装步骤报错时的基础入口。网络配置、防火墙规则、用户创建、本地组检查、主流程 real_main 等地方发现问题时,都会用它生成统一失败对象;failure 和 from_report 也会间接依赖它。

调用图:被 11 处调用(configure_offline_sandbox_network, configure_rule, ensure_offline_outbound_block, ensure_offline_proxy_allowlist, validate_local_policy_modify_result, provision_and_hide_sandbox_users, real_main, ensure_local_group, ensure_local_user, prepare_setup_marker (+1 more));外部调用 1 个(into)。

SetupFailure::from_report134–136 ↗
fn from_report(report: SetupErrorReport) -> Self

作用:把磁盘上读到的错误报告转换成程序内部使用的失败对象。这样 helper 写下来的报告,主程序可以继续按统一错误来处理。

数据流:进去的是 SetupErrorReport,里面有 code 和 message → 函数把这两个字段交给 SetupFailure::new → 出来的是 SetupFailure。

调用关系:它主要服务于 report_helper_failure。主程序发现 helper 失败后,会读 setup_error.json,再用这个函数把报告还原成内部错误。

调用图:被 1 处调用(report_helper_failure);外部调用 1 个(new)。

SetupFailure::metric_message138–140 ↗
fn metric_message(&self) -> String

作用:把失败消息变成适合放进指标系统的安全文本。它会尽量保留有用信息,同时去掉或规范化不适合上传的内容。

数据流:进去的是当前 SetupFailure 的 message → 函数调用 sanitize_setup_metric_tag_value 清洗消息 → 出来的是可以作为指标标签的字符串。

调用关系:它是 SetupFailure 面向遥测统计的一层包装。真正的清洗工作交给 sanitize_setup_metric_tag_value。

调用图:调用 1 个内部函数(sanitize_setup_metric_tag_value)。

SetupFailure::fmt144–146 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:定义 SetupFailure 被打印时长什么样。它让错误显示为“错误代码: 具体消息”,人和机器都比较容易读。

数据流:进去的是 SetupFailure 和一个格式化输出目标 → 函数先把 code 转成字符串,再拼上 message → 输出目标里写入完整错误文字。

调用关系:这是 Rust 标准显示接口的一部分。任何地方把 SetupFailure 当错误打印、记录日志或转成字符串时,都会走到这里;它依赖 SetupErrorCode::as_str 提供稳定错误名前缀。

调用图:外部调用 1 个(write!)。

failure151–153 ↗
fn failure(code: SetupErrorCode, message: impl Into<String>) -> anyhow::Error

作用:把安装失败快速包装成 anyhow::Error。anyhow::Error 可以理解成项目里常用的“通用错误盒子”,方便不同函数用同一种方式返回错误。

数据流:进去的是错误代码和错误消息 → 函数先创建 SetupFailure,再放进 anyhow::Error → 出来的是可向上传递的通用错误。

调用关系:这是安装流程里常用的报错快捷方式。report_helper_failure、run_elevated_provisioning_setup、run_setup_exe、verify_setup_completed 等流程发现安装失败时,会用它把具体失败塞进统一错误通道。

调用图:调用 1 个内部函数(new);被 4 处调用(report_helper_failure, run_elevated_provisioning_setup, run_setup_exe, verify_setup_completed);外部调用 1 个(new)。

extract_failure155–157 ↗
fn extract_failure(err: &anyhow::Error) -> Option<&SetupFailure>

作用:从一个通用错误盒子里尝试取回 SetupFailure。也就是说,它判断这个错误是不是本文件定义的那种标准安装失败。

数据流:进去的是 anyhow::Error 的引用 → 函数尝试按 SetupFailure 类型向下取出 → 如果里面确实是 SetupFailure,就返回它;否则返回 None。

调用关系:它用于需要识别错误真实类型的地方,尤其是测试里会检查 report_helper_failure 在不同清理结果下是否保留了正确的安装失败信息。

调用图:被 2 处调用(report_helper_failure_ignores_setup_error_report_when_clear_failed, report_helper_failure_uses_setup_error_report_when_clear_succeeded)。

setup_error_path159–161 ↗
fn setup_error_path(codex_home: &Path) -> PathBuf

作用:算出安装错误报告文件应该放在哪里。它统一规定报告路径是 codex_home/.sandbox/setup_error.json。

数据流:进去的是 codex_home 路径 → 函数依次拼上 .sandbox 和 setup_error.json → 出来的是完整文件路径 PathBuf。

调用关系:它避免各处手写路径导致不一致。清理、写入、读取错误报告的函数都会先调用它拿到同一个位置。

调用图:被 3 处调用(clear_setup_error_report, read_setup_error_report, write_setup_error_report);外部调用 1 个(join)。

clear_setup_error_report163–170 ↗
fn clear_setup_error_report(codex_home: &Path) -> Result<()>

作用:删除旧的安装错误报告。这样下一次安装开始前,不会把上一次失败留下的报告误当成这一次的结果。

数据流:进去的是 codex_home → 函数先算出 setup_error.json 路径,再尝试删除 → 如果删成功或文件本来不存在,就返回成功;如果是别的删除错误,就带着路径说明返回失败。

调用关系:run_setup_exe 和 run_setup_refresh_inner 会在启动或刷新安装流程时调用它。它把路径计算交给 setup_error_path,把真正删除交给系统文件接口。

调用图:调用 1 个内部函数(setup_error_path);被 2 处调用(run_setup_exe, run_setup_refresh_inner);外部调用 1 个(remove_file)。

write_setup_error_report172–180 ↗
fn write_setup_error_report(codex_home: &Path, report: &SetupErrorReport) -> Result<()>

作用:把安装失败报告写到磁盘上。提权 helper 失败时,可以用这个文件把失败原因留给外面的主程序读取。

数据流:进去的是 codex_home 和 SetupErrorReport → 函数确保 .sandbox 目录存在,再把报告转成漂亮排版的 JSON 字节 → 写入 setup_error.json;成功时返回空结果,失败时带上下文说明。

调用关系:它通常由 helper 失败汇报路径使用,测试也会用它造出报告文件。它调用 setup_error_path 保持路径统一,并用 JSON 序列化让另一个进程能读懂。

调用图:调用 1 个内部函数(setup_error_path);被 2 处调用(report_helper_failure_ignores_setup_error_report_when_clear_failed, report_helper_failure_uses_setup_error_report_when_clear_succeeded);外部调用 4 个(join, create_dir_all, write, to_vec_pretty)。

read_setup_error_report182–192 ↗
fn read_setup_error_report(codex_home: &Path) -> Result<Option<SetupErrorReport>>

作用:从磁盘读取 helper 留下的安装错误报告。主程序可以借此知道提权进程到底在哪里失败。

数据流:进去的是 codex_home → 函数找到 setup_error.json 并读取字节 → 如果文件不存在,返回 Ok(None);如果存在,就把 JSON 解析成 SetupErrorReport 并返回 Some(report)。

调用关系:report_helper_failure 会在 helper 退出失败后调用它。这个函数只负责读和解析,后续把报告变成 SetupFailure 的工作由 SetupFailure::from_report 完成。

调用图:调用 1 个内部函数(setup_error_path);被 1 处调用(report_helper_failure);外部调用 1 个(read)。

sanitize_setup_metric_tag_value195–197 ↗
fn sanitize_setup_metric_tag_value(value: &str) -> String

作用:清洗安装错误消息,让它适合当统计指标的标签。它特别会先遮掉路径里的用户名,避免把个人信息带出去。

数据流:进去的是原始错误文本 → 函数先调用 redact_home_paths 替换用户名路径片段,再调用通用的 sanitize_metric_tag_value 做指标标签规范化 → 出来的是更安全、更规整的字符串。

调用关系:SetupFailure::metric_message 和 emit_wfp_setup_metric 会用它生成遥测字段。它自己把隐私处理交给 redact_home_paths,把通用标签清洗交给外部工具函数。

调用图:调用 1 个内部函数(redact_home_paths);被 2 处调用(metric_message, emit_wfp_setup_metric);外部调用 1 个(sanitize_metric_tag_value)。

redact_home_paths199–214 ↗
fn redact_home_paths(value: &str) -> String

作用:找出当前系统用户名,并把错误文本路径中等于用户名的那一段替换掉。它主要是保护隐私。

数据流:进去的是一段错误文本 → 函数读取环境变量 USERNAME 和 USER,收集当前用户可能的名字 → 再调用 redact_username_segments 做实际替换 → 出来的是用户名片段被改成 <user> 的文本。

调用关系:它只被 sanitize_setup_metric_tag_value 调用,是清洗指标消息前的隐私保护步骤。真正按路径分段和替换的细活交给 redact_username_segments。

调用图:调用 1 个内部函数(redact_username_segments);被 1 处调用(sanitize_setup_metric_tag_value);外部调用 2 个(new, var)。

redact_username_segments216–256 ↗
fn redact_username_segments(value: &str, usernames: &[String]) -> String

作用:把文本中像路径一样用斜杠分开的片段逐个检查,如果某一段正好是用户名,就替换成 <user>。它不会胡乱替换普通单词,只替换完整路径片段。

数据流:进去的是原始文本和用户名列表 → 函数按 \ 或 / 把文本切成片段,同时记住分隔符 → 每个片段如果等于用户名,就改成 <user>;Windows 下比较时忽略大小写 → 最后按原分隔符拼回字符串。

调用关系:这是隐私遮盖的核心小工具。redact_home_paths 会把当前用户名传给它,测试函数也直接调用它验证各种路径替换场景。

调用图:被 4 处调用(redact_home_paths, sanitize_tag_value_leaves_unknown_segments, sanitize_tag_value_redacts_multiple_occurrences, sanitize_tag_value_redacts_username_segments);外部调用 4 个(new, new, cfg!, take)。

tests::sanitize_tag_value_redacts_username_segments264–272 ↗
fn sanitize_tag_value_redacts_username_segments()

作用:验证路径里出现的用户名会被遮掉。它用 Alice 和 Bob 两个例子,确保不同盘符、不同路径都能处理。

数据流:进去的是测试里准备的用户名列表和包含这些用户名的错误消息 → 调用 redact_username_segments → 检查输出中 Alice、Bob 都变成了 <user>。

调用关系:这是 redact_username_segments 的测试用例之一。它不参与正式运行,只在测试时保证隐私替换功能不会被改坏。

调用图:调用 1 个内部函数(redact_username_segments);外部调用 2 个(assert_eq!, vec!)。

tests::sanitize_tag_value_leaves_unknown_segments275–280 ↗
fn sanitize_tag_value_leaves_unknown_segments()

作用:验证不属于用户名的路径片段不会被误伤。这样错误消息里的普通目录名能保留下来。

数据流:进去的是用户名 Alice 和一段不包含 Alice 的路径消息 → 调用 redact_username_segments → 检查输出和原文完全一样。

调用关系:这是对 redact_username_segments 的保护性测试。它确保清洗逻辑不会为了隐私而把无关路径改乱。

调用图:调用 1 个内部函数(redact_username_segments);外部调用 2 个(assert_eq!, vec!)。

tests::sanitize_tag_value_redacts_multiple_occurrences283–288 ↗
fn sanitize_tag_value_redacts_multiple_occurrences()

作用:验证同一个用户名出现多次时,每一次都会被遮掉。它防止只替换第一个位置、漏掉后面位置的错误。

数据流:进去的是用户名 Alice 和一段包含两个 Alice 路径的消息 → 调用 redact_username_segments → 检查两个 Alice 都被替换成 <user>。

调用关系:这是 redact_username_segments 的重复出现测试。它帮助保证指标清洗在长错误消息里也可靠。

调用图:调用 1 个内部函数(redact_username_segments);外部调用 2 个(assert_eq!, vec!)。

windows-sandbox-rs/src/setup.rs源码 ↗
orchestrationstartup / setup refresh / provisioning

Windows 上要安全地跑沙箱,不能只启动一个进程就完事。系统里要有专用用户,要给这些用户设置哪些目录能读、哪些目录能写,还要处理网络是否可用、代理端口是否放行。这个文件就像一张施工单生成器:先根据权限配置和当前工作目录算出读写范围,再避开敏感目录,比如 CODEX_HOME、.ssh、云账号配置等;然后把这些信息做成 payload(给 helper 程序的任务清单),必要时用管理员权限启动 codex-windows-sandbox-setup.exe。它还会检查安装是否完成、读取 helper 写下的错误报告,并提供一批测试来防止权限范围算错。没有它,沙箱可能启动不了,或者更危险:把本不该暴露的密钥、配置、控制文件暴露给沙箱。

函数细节77
sandbox_dir71–73 ↗
fn sandbox_dir(codex_home: &Path) -> PathBuf

作用:算出沙箱状态目录的位置,也就是 CODEX_HOME 下面的 .sandbox。其他地方用它来放安装标记、日志和控制文件。

数据流:输入一个 codex_home 路径 → 在后面拼上 .sandbox → 输出完整目录路径,不会自己读写磁盘。

调用关系:它是很多流程的基础路径工具;安装、刷新、日志、敏感目录过滤、标记文件路径都会先问它沙箱目录在哪里。

调用图:被 8 处调用(sync_persistent_deny_read_acls, require_logon_sandbox_creds, filter_sensitive_write_roots, run_elevated_provisioning_setup, run_elevated_setup, run_setup_exe, run_setup_refresh_inner, setup_marker_path);外部调用 1 个(join)。

sandbox_bin_dir75–77 ↗
fn sandbox_bin_dir(codex_home: &Path) -> PathBuf

作用:算出沙箱 helper 二进制文件目录,也就是 CODEX_HOME 下的 .sandbox-bin。这个目录需要被特别保护,不能随便给沙箱写权限。

数据流:输入 codex_home → 拼出 .sandbox-bin 路径 → 返回这个路径。

调用关系:主要被 filter_sensitive_write_roots 使用,用来把 helper 程序目录从可写范围里剔除。

调用图:被 1 处调用(filter_sensitive_write_roots);外部调用 1 个(join)。

sandbox_secrets_dir79–81 ↗
fn sandbox_secrets_dir(codex_home: &Path) -> PathBuf

作用:算出沙箱秘密文件目录,也就是 CODEX_HOME 下的 .sandbox-secrets。这里会放沙箱用户凭据一类敏感东西。

数据流:输入 codex_home → 拼出 .sandbox-secrets 路径 → 返回这个路径。

调用关系:sandbox_users_path 用它定位用户密码文件;filter_sensitive_write_roots 用它避免把秘密目录交给沙箱写。

调用图:被 2 处调用(filter_sensitive_write_roots, sandbox_users_path);外部调用 1 个(join)。

setup_marker_path83–85 ↗
fn setup_marker_path(codex_home: &Path) -> PathBuf

作用:给出安装完成标记文件 setup_marker.json 的位置。这个标记用来判断当前沙箱设置是不是已经按指定版本准备好了。

数据流:输入 codex_home → 先算 .sandbox 目录 → 再拼 setup_marker.json → 输出标记文件路径。

调用关系:load_marker 会用它读取安装状态;它依赖 sandbox_dir 保持路径规则统一。

调用图:调用 1 个内部函数(sandbox_dir);被 1 处调用(load_marker)。

sandbox_users_path87–89 ↗
fn sandbox_users_path(codex_home: &Path) -> PathBuf

作用:给出保存沙箱用户信息的 sandbox_users.json 文件位置。这个文件属于敏感数据,所以放在 secrets 目录下。

数据流:输入 codex_home → 先算 .sandbox-secrets 目录 → 拼 sandbox_users.json → 输出路径。

调用关系:加载、删除沙箱用户文件的代码都会用它,确保大家找的是同一个秘密文件。

调用图:调用 1 个内部函数(sandbox_secrets_dir);被 4 处调用(load_users, remove_sandbox_users_file, remove_sandbox_users_file_deletes_existing_file, remove_sandbox_users_file_ignores_missing_file)。

run_setup_refresh108–134 ↗
fn run_setup_refresh(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
    pro

作用:按普通权限配置刷新沙箱设置。它适合已有沙箱需要更新目录权限或网络规则时调用。

数据流:输入权限档案、工作区、当前目录、环境变量、codex_home 和代理开关 → 先把通用权限档案转换成 Windows 沙箱能理解的权限 → 如果这个档案不适用就直接结束 → 否则交给内部刷新函数执行。

调用关系:它是刷新流程的公开入口;真正打包 payload、启动 helper 的活儿交给 run_setup_refresh_inner。

调用图:调用 2 个内部函数(try_from_permission_profile_for_workspace_roots, run_setup_refresh_inner);外部调用 1 个(default)。

run_setup_refresh_with_overrides136–141 ↗
fn run_setup_refresh_with_overrides(
    request: SandboxSetupRequest<'_>,
    overrides: SetupRootOverrides,
) -> Result<()>

作用:用调用者指定的读写根目录覆盖默认计算结果,再刷新沙箱设置。适合上层已经算好更精确权限范围的情况。

数据流:输入完整的沙箱 setup 请求和覆盖项 → 不重新解释权限档案 → 直接交给内部刷新函数 → 返回刷新结果。

调用关系:require_logon_sandbox_creds 会用它;它只是薄薄的一层,把特殊参数送进 run_setup_refresh_inner。

调用图:调用 1 个内部函数(run_setup_refresh_inner);被 1 处调用(require_logon_sandbox_creds)。

run_setup_refresh_with_extra_read_roots143–178 ↗
fn run_setup_refresh_with_extra_read_roots(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    code

作用:在正常可读目录之外,临时多加一些只读目录来刷新沙箱。常用于 helper 或运行时额外需要读某些文件的场景。

数据流:输入权限档案、工作区、当前目录、环境变量、codex_home、额外可读目录和代理开关 → 解析权限 → 收集默认可读目录 → 加上额外目录 → 用覆盖模式调用内部刷新。

调用关系:它先调用 gather_read_roots 算基础列表,再把结果交给 run_setup_refresh_inner;不支持的权限档案会直接跳过。

调用图:调用 3 个内部函数(try_from_permission_profile_for_workspace_roots, gather_read_roots, run_setup_refresh_inner);外部调用 1 个(new)。

run_setup_refresh_inner180–266 ↗
fn run_setup_refresh_inner(
    request: SandboxSetupRequest<'_>,
    overrides: SetupRootOverrides,
) -> Result<()>

作用:执行刷新沙箱设置的核心流程。它把目录权限、拒绝列表、网络代理规则打包,然后启动 setup helper,但明确不弹管理员授权。

数据流:输入 setup 请求和覆盖项 → 检查权限是否能由 Windows 沙箱执行 → 计算读写目录和禁止目录 → 判断使用离线还是在线用户 → 读取代理端口 → 生成 JSON 后转成 base64 → 启动 helper → 根据退出码和错误报告返回成功或失败。

调用关系:run_setup_refresh、run_setup_refresh_with_overrides、run_setup_refresh_with_extra_read_roots 都会进入这里;它会调用 build_payload_roots、build_payload_deny_write_paths、offline_proxy_settings_from_env、find_setup_exe 和 report_helper_failure 等函数串起整个刷新动作。

调用图:调用 11 个内部函数(current_log_file_path, log_note, from_permissions, build_payload_deny_read_paths, build_payload_deny_write_paths, build_payload_roots, find_setup_exe, offline_proxy_settings_from_env, report_helper_failure, sandbox_dir (+1 more));被 3 处调用(run_setup_refresh, run_setup_refresh_with_extra_read_roots, run_setup_refresh_with_overrides);外部调用 7 个(null, bail!, new, format!, to_vec, current_dir, var)。

SetupMarker::version_matches282–284 ↗
fn version_matches(&self) -> bool

作用:判断磁盘上的安装标记版本是不是当前代码认识的版本。版本不一致通常表示需要重新安装或刷新。

数据流:读取 SetupMarker 里的 version → 和 SETUP_VERSION 比较 → 输出 true 或 false。

调用关系:它是安装状态检查的一部分,帮助上层判断旧设置还能不能继续用。

SetupMarker::request_mismatch_reason286–306 ↗
fn request_mismatch_reason(
        &self,
        network_identity: SandboxNetworkIdentity,
        offline_proxy_settings: &OfflineProxySettings,
    ) -> Option<String>

作用:判断当前请求需要的离线网络设置,和上次安装留下的标记是否不一致。如果不一致,会给出一段人能看懂的原因。

数据流:输入网络身份和期望的离线代理设置 → 如果不是离线身份就忽略代理差异 → 如果端口和本地绑定设置一样就返回无差异 → 否则返回描述差异的文字。

调用关系:它调用 SandboxNetworkIdentity::uses_offline_identity;上层可用它决定是否因为防火墙或代理变化而重新 setup。

调用图:调用 1 个内部函数(uses_offline_identity);外部调用 1 个(format!)。

SandboxUsersFile::version_matches324–326 ↗
fn version_matches(&self) -> bool

作用:判断保存沙箱用户信息的文件版本是否匹配当前 setup 版本。这样可以避免用旧格式或旧账号资料。

数据流:读取 SandboxUsersFile 的 version → 和 SETUP_VERSION 比较 → 输出是否匹配。

调用关系:它服务于用户凭据加载流程,和 SetupMarker::version_matches 的作用类似,都是防止旧状态误用。

is_elevated329–359 ↗
fn is_elevated() -> Result<bool>

作用:检查当前进程是不是已经有管理员权限。因为创建用户、改防火墙、改权限这类事在 Windows 上常常需要管理员身份。

数据流:不接收业务输入 → 调用 Windows 系统接口创建“管理员组”的身份标识并检查当前令牌是否属于该组 → 返回 true、false,或系统调用错误。

调用关系:run_elevated_setup 和 run_elevated_provisioning_setup 会先调用它,决定是直接运行 helper,还是要求管理员权限。

调用图:被 2 处调用(run_elevated_provisioning_setup, run_elevated_setup);外部调用 5 个(anyhow!, null_mut, AllocateAndInitializeSid, CheckTokenMembership, FreeSid)。

canonical_existing361–371 ↗
fn canonical_existing(paths: &[PathBuf]) -> Vec<PathBuf>

作用:把一批路径整理成真实存在的标准路径,并丢掉不存在的路径。这样后面比较路径时不容易因为写法不同而出错。

数据流:输入路径列表 → 对每个存在的路径尝试规范化,比如解析 .. 和链接 → 不存在的跳过 → 输出整理后的路径列表。

调用关系:读写根目录计算都会用它;build_payload_roots、gather_read_roots、gather_write_roots_for_permissions 等函数靠它清理路径。

调用图:被 5 处调用(build_payload_roots, effective_write_roots_for_permissions, gather_full_read_roots_for_permissions, gather_read_roots, gather_write_roots_for_permissions);外部调用 1 个(iter)。

profile_read_roots373–390 ↗
fn profile_read_roots(user_profile: &Path) -> Vec<PathBuf>

作用:列出用户主目录下可以作为可读根的第一层项目,但跳过 .ssh、.aws、.docker 这类敏感配置目录。

数据流:输入用户主目录 → 尝试读取目录内容 → 过滤掉配置好的敏感名字 → 输出剩下的子路径;如果读目录失败,就退回只返回用户主目录本身。

调用关系:全盘读策略和用户主目录展开都会用它;测试会确认敏感目录确实被跳过。

调用图:被 4 处调用(expand_user_profile_root_for, gather_full_read_roots_for_permissions, profile_read_roots_excludes_configured_top_level_entries, profile_read_roots_falls_back_to_profile_root_when_enumeration_fails);外部调用 2 个(read_dir, vec!)。

gather_helper_read_roots392–396 ↗
fn gather_helper_read_roots(codex_home: &Path) -> Vec<PathBuf>

作用:确保 helper 程序目录存在,并把它加入沙箱必须能读的目录。没有这个目录,沙箱里的辅助程序可能找不到。

数据流:输入 codex_home → 计算 helper_bin_dir → 尝试创建目录 → 返回只包含这个目录的列表。

调用关系:gather_read_roots、gather_full_read_roots_for_permissions、build_payload_roots 都会调用它,保证 helper 文件始终可读。

调用图:调用 1 个内部函数(helper_bin_dir);被 3 处调用(build_payload_roots, gather_full_read_roots_for_permissions, gather_read_roots);外部调用 2 个(create_dir_all, vec!)。

gather_full_read_roots_for_permissions398–421 ↗
fn gather_full_read_roots_for_permissions(
    command_cwd: &Path,
    permissions: &ResolvedWindowsSandboxPermissions,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
) -> Vec<PathBuf>

作用:为“允许较大范围读取”的情况收集可读目录。它会保留 Windows 系统目录、用户主目录的安全部分、当前目录和可写目录。

数据流:输入当前目录、权限、环境变量和 codex_home → 加入 helper 目录和 Windows 默认目录 → 加入用户目录中非敏感项目 → 加入当前目录和可写根 → 标准化存在的路径 → 输出可读根列表。

调用关系:gather_read_roots 在发现权限允许全盘读时会转到这里;测试会检查它保留老版本需要的平台默认目录。

调用图:调用 4 个内部函数(writable_roots_for_cwd, canonical_existing, gather_helper_read_roots, profile_read_roots);被 2 处调用(gather_read_roots, full_read_roots_preserve_legacy_platform_defaults);外部调用 3 个(new, to_path_buf, var)。

gather_read_roots423–448 ↗
fn gather_read_roots(
    command_cwd: &Path,
    permissions: &ResolvedWindowsSandboxPermissions,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
) -> Vec<PathBuf>

作用:按权限配置算出沙箱可以读取哪些目录。它既要让程序正常运行,又要避免把不该读的地方放进去。

数据流:输入当前目录、权限、环境变量和 codex_home → 如果是全盘读策略就走完整收集 → 否则加入 helper 目录、可选 Windows 默认目录和权限声明的可读根 → 标准化路径 → 输出列表。

调用关系:run_setup_refresh_with_extra_read_roots 和 build_payload_roots 都会调用它;它依赖 gather_helper_read_roots、gather_full_read_roots_for_permissions 和权限对象的方法。

调用图:调用 6 个内部函数(has_full_disk_read_access, include_platform_defaults, readable_roots_for_cwd, canonical_existing, gather_full_read_roots_for_permissions, gather_helper_read_roots);被 4 处调用(build_payload_roots, run_setup_refresh_with_extra_read_roots, gather_read_roots_includes_helper_bin_dir, workspace_write_roots_remain_readable)。

gather_write_roots_for_permissions450–468 ↗
fn gather_write_roots_for_permissions(
    permissions: &ResolvedWindowsSandboxPermissions,
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
) -> Vec<PathBuf>

作用:按权限配置算出沙箱可以写哪些目录,并去掉重复项。写权限更危险,所以这里要更谨慎。

数据流:输入权限、当前目录和环境变量 → 从权限对象拿可写根 → 只保留真实存在并规范化的路径 → 用集合去重但保持输出顺序 → 返回可写目录列表。

调用关系:effective_write_roots_for_permissions 会先调用它拿基础写目录,然后再做一系列安全过滤。

调用图:调用 2 个内部函数(writable_roots_for_cwd, canonical_existing);被 1 处调用(effective_write_roots_for_permissions);外部调用 2 个(new, new)。

effective_write_roots_for_setup470–484 ↗
fn effective_write_roots_for_setup(
    permissions: &ResolvedWindowsSandboxPermissions,
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
    write_roots_override:

作用:给 setup payload 准备最终可写目录。它是一个转接函数,让 setup 代码用统一名字调用更通用的写目录计算。

数据流:输入权限、当前目录、环境变量、codex_home 和可选覆盖目录 → 原样传给 effective_write_roots_for_permissions → 返回过滤后的可写根。

调用关系:build_payload_roots 调用它;真正的过滤工作在 effective_write_roots_for_permissions。

调用图:调用 1 个内部函数(effective_write_roots_for_permissions);被 1 处调用(build_payload_roots)。

effective_write_roots_for_permissions486–503 ↗
fn effective_write_roots_for_permissions(
    permissions: &ResolvedWindowsSandboxPermissions,
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
    write_roots_ove

作用:算出真正可以授予沙箱写权限的目录。它会把用户主目录、敏感配置、SSH 依赖、Codex 控制目录等危险位置排除掉。

数据流:输入权限、当前目录、环境变量、codex_home 和可选覆盖目录 → 先取覆盖目录或按权限计算目录 → 展开用户主目录 → 删除用户主目录根和敏感子目录 → 删除 SSH 配置依赖 → 删除 CODEX_HOME 及沙箱控制目录 → 输出安全后的写目录。

调用关系:setup、运行时权限应用、旧会话能力目录、提升启动上下文都会用它;它把 gather_write_roots_for_permissions 和多个 filter 函数串起来。

调用图:调用 7 个内部函数(canonical_existing, expand_user_profile_root, filter_sensitive_write_roots, filter_ssh_config_dependency_roots, filter_user_profile_root, filter_user_profile_root_exclusions, gather_write_roots_for_permissions);被 5 处调用(apply_capability_denies_for_world_writable_for_permissions, run_windows_sandbox_capture_for_permission_profile, effective_write_roots_for_setup, legacy_session_capability_roots, prepare_elevated_spawn_context_for_permissions)。

SandboxNetworkIdentity::from_permissions548–557 ↗
fn from_permissions(
        permissions: &ResolvedWindowsSandboxPermissions,
        proxy_enforced: bool,
    ) -> Self

作用:决定沙箱该用“离线用户”还是“在线用户”。如果代理被强制,或权限本身不允许网络,就选离线用户。

数据流:输入权限对象和 proxy_enforced 开关 → 查看网络策略是否启用 → 根据规则返回 Offline 或 Online。

调用关系:run_elevated_setup、run_setup_refresh_inner 和 require_logon_sandbox_creds 会用它决定后续网络、防火墙和账号选择。

调用图:调用 1 个内部函数(network_policy);被 3 处调用(require_logon_sandbox_creds, run_elevated_setup, run_setup_refresh_inner)。

SandboxNetworkIdentity::uses_offline_identity559–561 ↗
fn uses_offline_identity(self) -> bool

作用:简单判断当前网络身份是不是离线用户。离线用户通常需要额外处理代理端口和防火墙。

数据流:输入一个 SandboxNetworkIdentity 值 → 判断是否等于 Offline → 输出 true 或 false。

调用关系:offline_proxy_settings_from_env 和 SetupMarker::request_mismatch_reason 都靠它决定要不要关注离线代理设置。

调用图:被 2 处调用(request_mismatch_reason, offline_proxy_settings_from_env);外部调用 1 个(matches!)。

offline_proxy_settings_from_env578–594 ↗
fn offline_proxy_settings_from_env(
    env_map: &HashMap<String, String>,
    network_identity: SandboxNetworkIdentity,
) -> OfflineProxySettings

作用:从环境变量里提取离线沙箱需要放行的本机代理端口。只有离线身份才需要这样做。

数据流:输入环境变量表和网络身份 → 如果不是离线身份,返回空端口且不允许本地绑定 → 如果是离线身份,扫描代理变量并读取 CODEX_NETWORK_ALLOW_LOCAL_BINDING → 输出代理端口列表和本地绑定开关。

调用关系:run_elevated_setup、run_setup_refresh_inner 和 require_logon_sandbox_creds 会用它生成 setup payload 里的防火墙需求。

调用图:调用 2 个内部函数(uses_offline_identity, proxy_ports_from_env);被 3 处调用(require_logon_sandbox_creds, run_elevated_setup, run_setup_refresh_inner);外部调用 1 个(vec!)。

proxy_ports_from_env596–606 ↗
fn proxy_ports_from_env(env_map: &HashMap<String, String>) -> Vec<u16>

作用:从 HTTP_PROXY、HTTPS_PROXY 等环境变量里找本机代理端口,并去重排序。

数据流:输入环境变量表 → 遍历一组代理变量名 → 对每个值尝试解析 localhost、127.0.0.1 或 ::1 的端口 → 放进有序集合 → 输出端口列表。

调用关系:offline_proxy_settings_from_env 调用它;它把具体 URL 解析交给 loopback_proxy_port_from_url。

调用图:调用 1 个内部函数(loopback_proxy_port_from_url);被 1 处调用(offline_proxy_settings_from_env);外部调用 1 个(new)。

loopback_proxy_port_from_url608–627 ↗
fn loopback_proxy_port_from_url(url: &str) -> Option<u16>

作用:从一个代理 URL 里识别“本机回环地址”的端口。回环地址就是只指向本机的地址,比如 localhost 或 127.0.0.1。

数据流:输入 URL 字符串 → 拆出协议后的主机和端口,支持带用户名密码和 IPv6 方括号写法 → 如果主机不是本机或端口无效就返回空 → 否则返回端口号。

调用关系:proxy_ports_from_env 对每个代理环境变量调用它;相关测试覆盖常见 URL 和拒绝非本机地址。

调用图:被 1 处调用(proxy_ports_from_env)。

quote_arg629–663 ↗
fn quote_arg(arg: &str) -> String

作用:把命令行参数按 Windows 规则加引号和转义。这样 base64 payload 传给提升权限的 helper 时不会被命令行拆坏。

数据流:输入一个参数字符串 → 判断是否含空格、引号等需要保护的字符 → 必要时加双引号并正确处理反斜杠和引号 → 输出安全的命令行参数文本。

调用关系:run_setup_exe 在用 ShellExecuteExW 提权启动 helper 时调用它。

调用图:被 1 处调用(run_setup_exe);外部调用 1 个(from)。

find_setup_exe665–672 ↗
fn find_setup_exe() -> PathBuf

作用:寻找 setup helper 程序 codex-windows-sandbox-setup.exe 的路径。找不到打包资源时,就退回使用当前目录或 PATH 下的文件名。

数据流:尝试读取当前可执行文件路径 → 根据当前 exe 查找随包资源里的 setup exe → 找到就返回完整路径 → 找不到就返回默认文件名。

调用关系:run_setup_exe 和 run_setup_refresh_inner 都要靠它知道该启动哪个 helper。

调用图:调用 1 个内部函数(find_setup_exe_for_current_exe);被 2 处调用(run_setup_exe, run_setup_refresh_inner);外部调用 2 个(from, current_exe)。

find_setup_exe_for_current_exe674–676 ↗
fn find_setup_exe_for_current_exe(exe: &Path) -> Option<PathBuf>

作用:根据当前主程序的位置,查找同一安装包里的 setup helper。它把实际查找规则委托给 helper_materialization 模块。

数据流:输入当前 exe 路径 → 调用 bundled_executable_path_for_exe 查找指定文件名 → 返回找到的路径或空。

调用关系:find_setup_exe 调用它;测试会验证在 package/bin 和 resources 布局下能找到 helper。

调用图:调用 1 个内部函数(bundled_executable_path_for_exe);被 2 处调用(find_setup_exe, setup_exe_lookup_checks_package_resource_dir_for_bin_exe)。

report_helper_failure678–695 ↗
fn report_helper_failure(
    codex_home: &Path,
    cleared_report: bool,
    exit_code: Option<i32>,
) -> anyhow::Error

作用:把 setup helper 失败变成更有用的错误。如果 helper 写了结构化错误报告,就优先采用那份报告。

数据流:输入 codex_home、之前是否成功清掉旧报告、helper 退出码 → 如果旧报告没清掉,就只报告退出码 → 否则尝试读 setup_error.json → 读到就转换成 SetupFailure → 没读到或读取失败就生成对应错误。

调用关系:run_setup_exe 和 run_setup_refresh_inner 在 helper 非零退出时调用它,让用户看到更准确的失败原因。

调用图:调用 3 个内部函数(from_report, failure, read_setup_error_report);被 2 处调用(run_setup_exe, run_setup_refresh_inner);外部调用 2 个(new, format!)。

verify_setup_completed697–706 ↗
fn verify_setup_completed(codex_home: &Path) -> Result<()>

作用:确认 helper 虽然退出成功,但确实把沙箱设置完整做好了。退出码成功不代表文件和账号都已经到位。

数据流:输入 codex_home → 调用 sandbox_setup_is_complete 检查必要产物 → 完成则返回成功 → 不完整则返回 OrchestratorHelperIncomplete 错误。

调用关系:run_setup_exe 在 helper 成功退出后调用它;测试会确认缺少产物时会报错。

调用图:调用 2 个内部函数(sandbox_setup_is_complete, failure);被 2 处调用(run_setup_exe, setup_completion_requires_ready_artifacts)。

run_setup_exe708–819 ↗
fn run_setup_exe(
    payload: &ElevationPayload,
    needs_elevation: bool,
    codex_home: &Path,
) -> Result<()>

作用:真正启动 setup helper,并处理是否需要管理员授权。它负责传 payload、等待 helper 结束、处理退出码和清理错误报告。

数据流:输入 payload、是否需要提权、codex_home → 序列化 payload 并 base64 编码 → 清掉旧错误报告 → 非提权时普通启动,提权时用 Windows ShellExecute 的 runas 动词启动 → 等待结束 → 失败时读取错误报告 → 成功后确认 setup 完成并清理报告。

调用关系:run_elevated_setup 和 run_elevated_provisioning_setup 都把最终任务交给它;它调用 find_setup_exe、quote_arg、report_helper_failure、verify_setup_completed 和日志函数。

调用图:调用 9 个内部函数(log_note, find_setup_exe, quote_arg, report_helper_failure, sandbox_dir, verify_setup_completed, clear_setup_error_report, failure, to_wide);被 2 处调用(run_elevated_provisioning_setup, run_elevated_setup);外部调用 7 个(null, new, format!, to_string, zeroed, CloseHandle, GetLastError)。

run_elevated_setup821–866 ↗
fn run_elevated_setup(
    request: SandboxSetupRequest<'_>,
    overrides: SetupRootOverrides,
) -> Result<()>

作用:执行完整的沙箱安装或重装流程。它会在需要时触发 Windows 管理员授权窗口。

数据流:输入 setup 请求和覆盖项 → 检查权限能否用 Windows 沙箱执行 → 创建 .sandbox 目录 → 计算读写根和拒绝路径 → 计算网络身份和代理设置 → 生成完整 payload → 检查当前是否管理员 → 调用 run_setup_exe。

调用关系:require_logon_sandbox_creds 会调用它;它是完整安装的上层调度,底层启动 helper 交给 run_setup_exe。

调用图:调用 8 个内部函数(from_permissions, build_payload_deny_read_paths, build_payload_deny_write_paths, build_payload_roots, is_elevated, offline_proxy_settings_from_env, run_setup_exe, sandbox_dir);被 1 处调用(require_logon_sandbox_creds);外部调用 4 个(bail!, global_statsig_metrics_settings, var, create_dir_all)。

run_elevated_provisioning_setup868–905 ↗
fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> Result<()>

作用:只做预置账号和基础状态的 setup,不带具体工作区权限。它要求当前进程已经是管理员。

数据流:输入 codex_home 和真实用户名 → 创建 .sandbox 目录 → 检查管理员权限,不是管理员就报错 → 构造 ProvisionOnly 模式的空权限 payload → 非提权方式调用 run_setup_exe。

调用关系:它适合预安装或管理员主动准备环境;因为自己要求已经 elevated,所以不会再弹 UAC。

调用图:调用 4 个内部函数(is_elevated, run_setup_exe, sandbox_dir, failure);外部调用 4 个(to_path_buf, new, global_statsig_metrics_settings, create_dir_all)。

build_payload_roots907–946 ↗
fn build_payload_roots(
    request: &SandboxSetupRequest<'_>,
    overrides: &SetupRootOverrides,
) -> (Vec<PathBuf>, Vec<PathBuf>)

作用:生成传给 helper 的最终可读目录和可写目录。它会处理覆盖项、默认项、用户主目录展开,以及读写重叠去重。

数据流:输入 setup 请求和覆盖项 → 先算安全后的写目录 → 再用覆盖读目录或默认读目录 → 展开和过滤用户主目录敏感项 → 去掉 SSH 依赖 → 如果某目录已经可写,就不再作为只读目录重复传 → 输出读目录和写目录两组列表。

调用关系:run_elevated_setup 和 run_setup_refresh_inner 都调用它;它串起 effective_write_roots_for_setup、gather_read_roots、gather_helper_read_roots 和多个过滤函数。

调用图:调用 8 个内部函数(canonical_existing, effective_write_roots_for_setup, expand_user_profile_root, filter_ssh_config_dependency_roots, filter_user_profile_root, filter_user_profile_root_exclusions, gather_helper_read_roots, gather_read_roots);被 5 处调用(run_elevated_setup, run_setup_refresh_inner, build_payload_roots_preserves_helper_roots_when_read_override_is_provided, build_payload_roots_replaces_full_read_policy_when_read_override_is_provided, effective_write_roots_match_payload_filtering_for_overrides)。

build_payload_deny_write_paths948–964 ↗
fn build_payload_deny_write_paths(
    request: &SandboxSetupRequest<'_>,
    explicit_deny_write_paths: Option<Vec<PathBuf>>,
) -> Vec<PathBuf>

作用:生成明确禁止写入的路径列表。除了调用者指定的禁止项,还会加入权限规则自动保护的目录,比如 .git 或 .codex。

数据流:输入 setup 请求和可选显式禁止写路径 → 根据权限计算 allow/deny 路径 → 把显式路径规范化 → 合并自动 deny 列表 → 输出最终禁止写路径。

调用关系:run_elevated_setup 和 run_setup_refresh_inner 调用它,把结果放进 helper payload。

调用图:调用 1 个内部函数(compute_allow_paths_for_permissions);被 2 处调用(run_elevated_setup, run_setup_refresh_inner)。

build_payload_deny_read_paths966–970 ↗
fn build_payload_deny_read_paths(explicit_deny_read_paths: Option<Vec<PathBuf>>) -> Vec<PathBuf>

作用:生成明确禁止读取的路径列表。它故意保留调用者写的原始路径形式,方便底层权限层同时处理字面路径和真实目标。

数据流:输入可选显式禁止读路径 → 没有就返回空列表 → 有就原样返回这些路径。

调用关系:run_elevated_setup 和 run_setup_refresh_inner 调用它;后续 ACL 层会根据这些路径做实际拒绝规则。

调用图:被 2 处调用(run_elevated_setup, run_setup_refresh_inner)。

expand_user_profile_root972–977 ↗
fn expand_user_profile_root(roots: Vec<PathBuf>) -> Vec<PathBuf>

作用:如果可读或可写根直接是整个用户主目录,就把它拆成主目录下的一层子项。这样后面可以更细地排除 .ssh 等敏感目录。

数据流:输入目录列表 → 从 USERPROFILE 环境变量拿当前用户主目录 → 拿不到就原样返回 → 拿到就交给 expand_user_profile_root_for 展开。

调用关系:build_payload_roots 和 effective_write_roots_for_permissions 都会调用它,避免粗暴授权整个用户目录。

调用图:调用 1 个内部函数(expand_user_profile_root_for);被 2 处调用(build_payload_roots, effective_write_roots_for_permissions);外部调用 2 个(new, var)。

expand_user_profile_root_for979–993 ↗
fn expand_user_profile_root_for(roots: Vec<PathBuf>, user_profile: &Path) -> Vec<PathBuf>

作用:按指定用户主目录执行展开,方便正式代码和测试复用。遇到主目录本身时,替换成它下面的一层项目。

数据流:输入目录列表和用户主目录 → 用规范化 key 比较路径 → 如果某项就是用户主目录,就加入 profile_read_roots 的结果 → 其他路径保留 → 排序去重 → 输出展开后的列表。

调用关系:expand_user_profile_root 调用它;测试直接用它验证展开行为。

调用图:调用 2 个内部函数(canonical_path_key, profile_read_roots);被 1 处调用(expand_user_profile_root);外部调用 1 个(new)。

filter_user_profile_root995–1002 ↗
fn filter_user_profile_root(mut roots: Vec<PathBuf>) -> Vec<PathBuf>

作用:从目录列表中删掉用户主目录本身。这样不会把整个个人目录一口气授权给沙箱。

数据流:输入目录列表 → 读取 USERPROFILE → 用规范化路径比较 → 删除等于用户主目录的项 → 输出过滤后的列表;没有 USERPROFILE 时原样返回。

调用关系:build_payload_roots 和 effective_write_roots_for_permissions 都会在展开后调用它作为保险。

调用图:调用 1 个内部函数(canonical_path_key);被 2 处调用(build_payload_roots, effective_write_roots_for_permissions);外部调用 2 个(new, var)。

filter_user_profile_root_exclusions1004–1011 ↗
fn filter_user_profile_root_exclusions(mut roots: Vec<PathBuf>) -> Vec<PathBuf>

作用:删除用户主目录下明确列入黑名单的敏感子目录,比如 .ssh、.aws、.kube、.docker。

数据流:输入目录列表 → 读取 USERPROFILE → 对每个目录判断是否属于被排除的主目录子项 → 删除命中的项 → 输出剩余目录。

调用关系:读目录和写目录最终生成时都会调用它;具体判断逻辑在 is_user_profile_root_exclusion。

调用图:被 2 处调用(build_payload_roots, effective_write_roots_for_permissions);外部调用 2 个(new, var)。

is_user_profile_root_exclusion1013–1031 ↗
fn is_user_profile_root_exclusion(root: &Path, user_profile: &Path) -> bool

作用:判断某个路径是不是用户主目录下被禁止暴露的敏感顶层目录或其子路径。

数据流:输入待检查路径和用户主目录 → 转成统一比较用的路径 key → 取出相对用户主目录的第一层名字 → 和排除名单忽略大小写比较 → 输出是否应排除。

调用关系:filter_user_profile_root_exclusions 依赖它;测试会检查 .ssh、.tsh 等目录会被挡住。

调用图:调用 1 个内部函数(canonical_path_key);外部调用 1 个(format!)。

filter_ssh_config_dependency_roots1033–1041 ↗
fn filter_ssh_config_dependency_roots(mut roots: Vec<PathBuf>) -> Vec<PathBuf>

作用:删除 SSH 配置间接引用到的敏感路径,比如 Include 文件或 IdentityFile 私钥所在目录。这样即使 .ssh 本身被挡住,也不会从旁路暴露密钥。

数据流:输入目录列表 → 读取 USERPROFILE → 分析 SSH 配置依赖路径 → 删除和这些依赖同属用户主目录顶层子项的路径 → 输出过滤后的列表。

调用关系:build_payload_roots 和 effective_write_roots_for_permissions 调用它;它把具体判断交给 is_ssh_config_dependency_root。

调用图:调用 1 个内部函数(ssh_config_dependency_paths);被 2 处调用(build_payload_roots, effective_write_roots_for_permissions);外部调用 2 个(new, var)。

is_ssh_config_dependency_root1043–1056 ↗
fn is_ssh_config_dependency_root(
    root: &Path,
    user_profile: &Path,
    dependency_paths: &[PathBuf],
) -> bool

作用:判断某个目录是否覆盖了 SSH 配置依赖的顶层位置。命中时就不该作为沙箱根目录开放。

数据流:输入候选路径、用户主目录和 SSH 依赖路径列表 → 取候选路径在用户主目录下的第一层名字 → 与每个依赖路径的第一层名字比较 → 输出是否命中。

调用关系:filter_ssh_config_dependency_roots 用它逐个筛目录;它依赖 user_profile_child_name 拆出第一层名字。

调用图:调用 1 个内部函数(user_profile_child_name);外部调用 1 个(iter)。

user_profile_child_name1058–1068 ↗
fn user_profile_child_name(path: &Path, user_profile: &Path) -> Option<String>

作用:从一个路径里取出它位于用户主目录下的第一层名字。比如 C:\Users\me\Documents\a.txt 会得到 Documents。

数据流:输入路径和用户主目录 → 转成统一路径 key → 判断是否在用户主目录下面 → 取相对路径的第一段 → 返回名字或空。

调用关系:is_ssh_config_dependency_root 用它比较候选目录和 SSH 依赖目录是否在同一个顶层位置。

调用图:调用 1 个内部函数(canonical_path_key);被 1 处调用(is_ssh_config_dependency_root);外部调用 1 个(format!)。

filter_sensitive_write_roots1070–1093 ↗
fn filter_sensitive_write_roots(mut roots: Vec<PathBuf>, codex_home: &Path) -> Vec<PathBuf>

作用:从可写目录里删掉 Codex 自己的控制区和秘密区。沙箱绝不能改这些地方,否则可能篡改 helper、状态或凭据。

数据流:输入可写目录列表和 codex_home → 算出 CODEX_HOME、.sandbox、.sandbox-bin、.sandbox-secrets 及其子路径的规范 key → 删除等于或位于这些区域下的写目录 → 输出安全写目录。

调用关系:effective_write_roots_for_permissions 在最后调用它;它使用 sandbox_dir、sandbox_bin_dir、sandbox_secrets_dir 得到受保护路径。

调用图:调用 4 个内部函数(canonical_path_key, sandbox_bin_dir, sandbox_dir, sandbox_secrets_dir);被 1 处调用(effective_write_roots_for_permissions);外部调用 1 个(format!)。

tests::canonical_windows_platform_default_roots1126–1131 ↗
fn canonical_windows_platform_default_roots() -> Vec<PathBuf>

作用:测试辅助函数,把 Windows 默认可读目录转成测试可比较的规范路径。

数据流:读取 WINDOWS_PLATFORM_DEFAULT_READ_ROOTS 常量 → 尝试规范化每个路径 → 失败则保留原路径 → 输出列表。

调用关系:多个测试用它确认默认 Windows 目录是否被加入或排除。

tests::setup_completion_requires_ready_artifacts1134–1143 ↗
fn setup_completion_requires_ready_artifacts()

作用:测试 helper 成功退出但缺少安装产物时,系统会报“不完整”错误。

数据流:创建临时 codex_home → 调用 verify_setup_completed → 预期得到错误 → 检查错误码是否是 OrchestratorHelperIncomplete。

调用关系:它直接覆盖 verify_setup_completed 的防呆逻辑。

调用图:调用 1 个内部函数(verify_setup_completed);外部调用 2 个(new, assert_eq!)。

tests::permissions_for1145–1154 ↗
fn permissions_for(
        permission_profile: &PermissionProfile,
        workspace_roots: &[AbsolutePathBuf],
    ) -> ResolvedWindowsSandboxPermissions

作用:测试辅助函数,把权限档案转换成 ResolvedWindowsSandboxPermissions,省得每个测试重复写转换代码。

数据流:输入 PermissionProfile 和工作区根 → 调用转换函数 → 成功时返回解析后的权限,失败则让测试失败。

调用关系:许多权限相关测试先用它准备权限对象。

调用图:调用 1 个内部函数(try_from_permission_profile_for_workspace_roots)。

tests::workspace_roots_for1156–1158 ↗
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf>

作用:测试辅助函数,把一个绝对路径包装成工作区根列表。

数据流:输入路径 → 转成 AbsolutePathBuf → 放进单元素数组 → 返回。

调用关系:权限构造测试常用它给 permissions_for 提供工作区。

调用图:外部调用 1 个(vec!)。

tests::workspace_write_profile1160–1171 ↗
fn workspace_write_profile(
        writable_roots: &[AbsolutePathBuf],
        exclude_tmpdir_env_var: bool,
        exclude_slash_tmp: bool,
    ) -> PermissionProfile

作用:测试辅助函数,快速创建“工作区可写、网络受限”的权限档案。

数据流:输入额外可写根和排除临时目录的开关 → 调用 PermissionProfile::workspace_write_with → 返回权限档案。

调用关系:写权限相关测试用它构造一致的测试权限。

调用图:调用 1 个内部函数(workspace_write_with)。

tests::report_helper_failure_uses_setup_error_report_when_clear_succeeded1174–1200 ↗
fn report_helper_failure_uses_setup_error_report_when_clear_succeeded()

作用:测试当旧错误报告已成功清掉时,helper 新写的错误报告会被采用。

数据流:创建临时 codex_home → 写入 setup_error.json → 调用 report_helper_failure 并声明清理成功 → 提取错误 → 检查错误码和消息来自报告。

调用关系:它验证 report_helper_failure 会优先给出 helper 的真实原因。

调用图:调用 2 个内部函数(extract_failure, write_setup_error_report);外部调用 3 个(new, assert_eq!, report_helper_failure)。

tests::report_helper_failure_ignores_setup_error_report_when_clear_failed1203–1229 ↗
fn report_helper_failure_ignores_setup_error_report_when_clear_failed()

作用:测试如果启动前没能清掉旧错误报告,就不会误信磁盘上的陈旧报告。

数据流:写入一份可能过期的错误报告 → 调用 report_helper_failure 并声明清理失败 → 提取错误 → 检查结果只反映 helper 非零退出。

调用关系:它保护 report_helper_failure 的安全判断,避免把旧错误当新错误。

调用图:调用 2 个内部函数(extract_failure, write_setup_error_report);外部调用 3 个(new, assert_eq!, report_helper_failure)。

tests::setup_refresh_skips_profiles_without_managed_filesystem_permissions1232–1266 ↗
fn setup_refresh_skips_profiles_without_managed_filesystem_permissions()

作用:测试不由 Windows 沙箱管理文件权限的档案,刷新 setup 时会安静跳过。

数据流:创建临时工作区和 codex_home → 对 Disabled 和 External 权限分别调用 run_setup_refresh 与 run_setup_refresh_with_extra_read_roots → 预期都成功返回且不启动真实 setup。

调用关系:它覆盖 run_setup_refresh 系列入口的早退逻辑。

调用图:外部调用 7 个(new, new, create_dir_all, run_setup_refresh, run_setup_refresh_with_extra_read_roots, vec!, workspace_roots_for)。

tests::loopback_proxy_url_parsing_supports_common_forms1269–1282 ↗
fn loopback_proxy_url_parsing_supports_common_forms()

作用:测试本机代理 URL 的常见写法都能解析出端口。

数据流:输入 localhost、127.0.0.1、[::1] 三类 URL → 调用 loopback_proxy_port_from_url → 检查返回端口正确。

调用关系:它验证 proxy_ports_from_env 底层解析器的正向场景。

调用图:外部调用 1 个(assert_eq!)。

tests::setup_exe_lookup_checks_package_resource_dir_for_bin_exe1285–1300 ↗
fn setup_exe_lookup_checks_package_resource_dir_for_bin_exe()

作用:测试从主程序位置能找到安装包 resources 目录里的 setup helper。

数据流:创建假的 package/bin 和 resources 目录 → 写入 codex.exe 和 setup exe → 调用 find_setup_exe_for_current_exe → 检查返回 resources 下的 helper 路径。

调用关系:它覆盖 find_setup_exe_for_current_exe 的打包查找规则。

调用图:调用 1 个内部函数(find_setup_exe_for_current_exe);外部调用 4 个(new, assert_eq!, create_dir_all, write)。

tests::loopback_proxy_url_parsing_rejects_non_loopback_and_zero_port1303–1310 ↗
fn loopback_proxy_url_parsing_rejects_non_loopback_and_zero_port()

作用:测试代理 URL 解析会拒绝非本机地址、端口 0 和缺少协议的字符串。

数据流:输入 example.com、127.0.0.1:0、没有 scheme 的 localhost 字符串 → 调用解析函数 → 检查都返回空。

调用关系:它防止 loopback_proxy_port_from_url 错把外部代理或无效端口放进防火墙规则。

调用图:外部调用 1 个(assert_eq!)。

tests::proxy_ports_from_env_dedupes_and_sorts1313–1330 ↗
fn proxy_ports_from_env_dedupes_and_sorts()

作用:测试从多个代理环境变量读取端口时,会去重并排序,同时忽略非本机代理。

数据流:构造含重复 8080、本机 1081、外部 9999 的环境变量表 → 调用 proxy_ports_from_env → 检查结果是排序后的 1081 和 8080。

调用关系:它覆盖 proxy_ports_from_env 对集合和过滤的处理。

调用图:外部调用 2 个(new, assert_eq!)。

tests::offline_proxy_settings_ignore_proxy_env_when_online_identity_selected1333–1351 ↗
fn offline_proxy_settings_ignore_proxy_env_when_online_identity_selected()

作用:测试选择在线身份时,即使环境变量里有代理,也不会生成离线代理设置。

数据流:构造包含 HTTP_PROXY 和允许本地绑定开关的环境变量 → 用 Online 身份调用 offline_proxy_settings_from_env → 检查端口为空且本地绑定为 false。

调用关系:它验证 offline_proxy_settings_from_env 会先看网络身份。

调用图:外部调用 2 个(new, assert_eq!)。

tests::offline_proxy_settings_capture_proxy_ports_and_local_binding_for_offline_identity1354–1376 ↗
fn offline_proxy_settings_capture_proxy_ports_and_local_binding_for_offline_identity()

作用:测试离线身份会正确收集本机代理端口,并识别允许本地绑定开关。

数据流:构造两个本机代理和 CODEX_NETWORK_ALLOW_LOCAL_BINDING=1 → 用 Offline 身份调用 → 检查端口列表和 allow_local_binding 为 true。

调用关系:它覆盖 offline_proxy_settings_from_env 的离线正向场景。

调用图:外部调用 2 个(new, assert_eq!)。

tests::setup_marker_request_mismatch_reason_ignores_proxy_drift_for_online_identity1379–1397 ↗
fn setup_marker_request_mismatch_reason_ignores_proxy_drift_for_online_identity()

作用:测试在线身份下,即使标记里的代理设置和当前期望不同,也不会认为需要重装。

数据流:构造带旧代理端口的 SetupMarker 和新的期望代理设置 → 用 Online 身份调用 request_mismatch_reason → 检查返回空。

调用关系:它验证 SetupMarker::request_mismatch_reason 对在线身份的忽略逻辑。

调用图:外部调用 2 个(assert_eq!, vec!)。

tests::setup_marker_request_mismatch_reason_reports_offline_firewall_drift1400–1421 ↗
fn setup_marker_request_mismatch_reason_reports_offline_firewall_drift()

作用:测试离线身份下,代理端口或本地绑定设置变化会给出明确的不匹配原因。

数据流:构造旧标记和新期望设置 → 用 Offline 身份调用 request_mismatch_reason → 检查返回的文字包含旧值和新值。

调用关系:它覆盖 SetupMarker::request_mismatch_reason 的重装触发说明。

调用图:外部调用 2 个(assert_eq!, vec!)。

tests::profile_read_roots_excludes_configured_top_level_entries1424–1444 ↗
fn profile_read_roots_excludes_configured_top_level_entries()

作用:测试列用户主目录可读根时,会跳过 .ssh、.tsh、.AWS 等敏感顶层项。

数据流:创建临时用户目录,里面有普通文件夹和敏感文件夹 → 调用 profile_read_roots → 比较结果只包含普通项。

调用关系:它直接验证 profile_read_roots 的敏感目录过滤。

调用图:调用 1 个内部函数(profile_read_roots);外部调用 4 个(new, assert_eq!, create_dir_all, write)。

tests::profile_read_roots_falls_back_to_profile_root_when_enumeration_fails1447–1454 ↗
fn profile_read_roots_falls_back_to_profile_root_when_enumeration_fails()

作用:测试用户主目录无法枚举时,函数会退回返回主目录本身,而不是崩溃。

数据流:传入不存在的用户目录 → 调用 profile_read_roots → 检查返回列表只含这个路径。

调用关系:它覆盖 profile_read_roots 的容错分支。

调用图:调用 1 个内部函数(profile_read_roots);外部调用 2 个(new, assert_eq!)。

tests::is_user_profile_root_exclusion_blocks_configured_children1457–1491 ↗
fn is_user_profile_root_exclusion_blocks_configured_children()

作用:测试用户主目录敏感排除规则能挡住配置名单里的子目录及其下级路径。

数据流:创建 Documents、AppData、.ssh/config、.tsh/keys 等路径 → 调用 is_user_profile_root_exclusion → 检查普通目录不挡、敏感目录挡。

调用关系:它验证 is_user_profile_root_exclusion 的路径归属和大小写处理。

调用图:外部调用 3 个(new, assert!, create_dir_all)。

tests::is_ssh_config_dependency_root_blocks_config_dependencies1494–1537 ↗
fn is_ssh_config_dependency_root_blocks_config_dependencies()

作用:测试 SSH 配置里引用的密钥目录和 include 文件目录会被识别为需要屏蔽。

数据流:创建 .ssh/config,里面引用 ~/.keys 和 ~/.included → 计算 ssh_config_dependency_paths → 调用 is_ssh_config_dependency_root 检查多个候选目录 → 确认依赖目录被挡。

调用关系:它覆盖 SSH 依赖过滤的核心判断。

调用图:外部调用 5 个(new, assert!, create_dir_all, write, ssh_config_dependency_paths)。

tests::expand_user_profile_root_for_replaces_profile_root_with_children1540–1558 ↗
fn expand_user_profile_root_for_replaces_profile_root_with_children()

作用:测试当根目录列表里出现整个用户主目录时,会被替换成主目录下的子项。

数据流:创建用户主目录、Documents、.local 和其他目录 → 调用 expand_user_profile_root_for → 检查结果包含子项和其他目录,不再是主目录整体。

调用关系:它验证 expand_user_profile_root_for 的展开行为。

调用图:外部调用 5 个(new, assert_eq!, create_dir_all, expand_user_profile_root_for, vec!)。

tests::expanded_write_roots_still_drop_protected_codex_home1561–1577 ↗
fn expanded_write_roots_still_drop_protected_codex_home()

作用:测试即使写目录来自用户主目录展开,CODEX_HOME 这种受保护目录仍会被删掉。

数据流:创建用户主目录、CodexHome 和 Documents → 展开用户主目录 → 手动过滤主目录和排除项 → 调用 filter_sensitive_write_roots → 检查只剩 Documents。

调用关系:它验证展开逻辑不会绕过 filter_sensitive_write_roots 的保护。

调用图:外部调用 7 个(new, assert_eq!, create_dir_all, canonical_path_key, expand_user_profile_root_for, filter_sensitive_write_roots, vec!)。

tests::gather_read_roots_includes_helper_bin_dir1580–1594 ↗
fn gather_read_roots_includes_helper_bin_dir()

作用:测试普通可读目录收集一定包含 helper 二进制目录。

数据流:创建临时 codex_home 和工作区 → 构造只读权限 → 调用 gather_read_roots → 检查结果包含 helper_bin_dir 的规范路径。

调用关系:它保护 gather_read_roots 对 helper 目录的基本要求。

调用图:调用 3 个内部函数(read_only, helper_bin_dir, gather_read_roots);外部调用 7 个(new, new, assert!, canonicalize, create_dir_all, permissions_for, workspace_roots_for)。

tests::workspace_write_roots_remain_readable1597–1620 ↗
fn workspace_write_roots_remain_readable()

作用:测试被授权可写的工作区额外目录,同时也会保持可读。因为能写却不能读会让很多程序行为异常。

数据流:创建工作区和额外可写根 → 构造 workspace_write 权限 → 调用 gather_read_roots → 检查可写根也在可读列表里。

调用关系:它验证 gather_full_read_roots_for_permissions 或相关权限逻辑不会漏掉可写目录的读取能力。

调用图:调用 1 个内部函数(gather_read_roots);外部调用 9 个(new, new, assert!, canonicalize, create_dir_all, vec!, permissions_for, workspace_roots_for, workspace_write_profile)。

tests::build_payload_roots_preserves_helper_roots_when_read_override_is_provided1623–1667 ↗
fn build_payload_roots_preserves_helper_roots_when_read_override_is_provided()

作用:测试即使调用者提供了完整读目录覆盖,helper 目录和指定的平台默认目录仍会保留。

数据流:创建工作区、codex_home 和额外可读目录 → 用 read_roots 覆盖并要求包含平台默认目录 → 调用 build_payload_roots → 检查 helper 和可读目录存在、当前目录不被偷偷加回、平台目录存在。

调用关系:它覆盖 build_payload_roots 的覆盖模式细节。

调用图:调用 3 个内部函数(read_only, helper_bin_dir, build_payload_roots);外部调用 9 个(new, new, assert!, assert_eq!, canonicalize, create_dir_all, vec!, permissions_for, workspace_roots_for)。

tests::build_payload_roots_replaces_full_read_policy_when_read_override_is_provided1670–1714 ↗
fn build_payload_roots_replaces_full_read_policy_when_read_override_is_provided()

作用:测试提供读目录覆盖时,不会再把全量默认读策略偷偷混进来。

数据流:创建测试目录 → 用 read_roots 覆盖且不包含平台默认目录 → 调用 build_payload_roots → 检查 helper 和指定可读目录存在,但当前目录和平台默认目录不存在。

调用关系:它防止 build_payload_roots 在覆盖模式下扩大权限。

调用图:调用 3 个内部函数(read_only, helper_bin_dir, build_payload_roots);外部调用 9 个(new, new, assert!, assert_eq!, canonicalize, create_dir_all, vec!, permissions_for, workspace_roots_for)。

tests::effective_write_roots_match_payload_filtering_for_overrides1717–1773 ↗
fn effective_write_roots_match_payload_filtering_for_overrides()

作用:测试直接计算的有效写目录,和 payload 里最终写目录使用同一套过滤规则。

数据流:创建工作区、额外目录、codex_home 和 .sandbox → 提供写目录覆盖,其中包含受保护目录 → 分别调用 effective_write_roots_for_setup 和 build_payload_roots → 检查两者一致,且受保护目录被删除。

调用关系:它保证 effective_write_roots_for_setup 与 build_payload_roots 不会算出两套不同结果。

调用图:调用 1 个内部函数(build_payload_roots);外部调用 12 个(new, new, assert!, assert_eq!, canonicalize, create_dir_all, effective_write_roots_for_setup, sandbox_dir, vec!, permissions_for (+2 more))。

tests::effective_write_roots_use_runtime_workspace_roots_for_workspace_root1776–1804 ↗
fn effective_write_roots_use_runtime_workspace_roots_for_workspace_root()

作用:测试工作区写权限会使用运行时解析出的工作区根,而不是当前子目录。

数据流:创建 workspace/subdir 作为当前目录 → 构造 workspace_write 权限 → 调用 effective_write_roots_for_setup → 检查结果是 workspace 根目录。

调用关系:它验证权限对象和 gather_write_roots_for_permissions 的工作区根处理。

调用图:外部调用 8 个(new, new, assert_eq!, create_dir_all, effective_write_roots_for_setup, permissions_for, workspace_roots_for, workspace_write_profile)。

tests::payload_deny_write_paths_merge_explicit_and_protected_children1807–1848 ↗
fn payload_deny_write_paths_merge_explicit_and_protected_children()

作用:测试 payload 的禁止写路径会合并显式禁止项和自动保护项。

数据流:创建 .git、.codex 和显式禁止路径 → 构造带额外可写根的权限 → 调用 build_payload_deny_write_paths → 检查结果包含三类路径。

调用关系:它覆盖 build_payload_deny_write_paths 与 compute_allow_paths_for_permissions 的合并行为。

调用图:外部调用 9 个(new, new, assert_eq!, create_dir_all, build_payload_deny_write_paths, vec!, permissions_for, workspace_roots_for, workspace_write_profile)。

tests::full_read_roots_preserve_legacy_platform_defaults1851–1872 ↗
fn full_read_roots_preserve_legacy_platform_defaults()

作用:测试全读策略仍包含旧版依赖的 Windows 默认可读目录。

数据流:创建临时工作区和 codex_home → 构造只读权限 → 调用 gather_full_read_roots_for_permissions → 检查 Windows 默认目录都在结果中。

调用关系:它保护 gather_full_read_roots_for_permissions 的兼容行为。

调用图:调用 2 个内部函数(read_only, gather_full_read_roots_for_permissions);外部调用 6 个(new, new, assert!, create_dir_all, permissions_for, workspace_roots_for)。

tests::build_payload_deny_read_paths_preserves_explicit_paths1875–1885 ↗
fn build_payload_deny_read_paths_preserves_explicit_paths()

作用:测试显式禁止读取的路径会原样保留,包括存在和不存在的路径。

数据流:创建一个已存在文件和一个未来路径 → 调用 build_payload_deny_read_paths → 检查输出和输入路径顺序一致、没有规范化或丢弃。

调用关系:它验证 build_payload_deny_read_paths 为 ACL 层保留原始路径拼写。

调用图:外部调用 3 个(new, assert_eq!, write)。

windows-sandbox-rs/src/identity.rs源码 ↗
orchestrationsandbox startup / before spawning sandbox process

Windows 沙盒不是随便拿当前用户就能跑的,它需要提前创建好的受限账号,还要配好哪些目录能读、哪些目录能写、是否能联网等。这个文件就像“门卫和值班管理员”:先看磁盘上的 setup marker(初始化标记,说明初始化版本和网络要求)和 users 文件(保存沙盒账号信息)是不是齐全、版本是不是对;再按当前权限选择 offline 或 online 账号;密码不是明文放着,而是先用 Base64 解码,再用 Windows 的 DPAPI(系统提供的加密保护机制,只能在合适机器和用户环境里解开)解密。如果资料缺失、过期,或者离线代理设置变了,它会调用需要提权的 setup 程序重新准备账号和权限。即使账号看起来没问题,它也会再跑一次非提权刷新,把当前目录读写规则同步过去。这样沙盒每次启动前都能拿到正确凭据,同时尽量不把过期或坏掉的账号继续拿来用。

函数细节10
sandbox_setup_is_complete42–48 ↗
fn sandbox_setup_is_complete(codex_home: &Path) -> bool

作用:快速判断沙盒初始化文件是不是已经准备好,而且版本是不是当前代码能接受的版本。它适合做一个粗略的“能不能继续”的检查。

数据流:输入是 codex_home,也就是项目自己的家目录位置。函数会从这里尝试读取初始化标记和用户文件,检查两者是否存在并且版本匹配。输出是 true 或 false;它不修改文件,只给出一个简单结论。

调用关系:它被 verify_setup_completed 用来做前置检查。更细的检查,比如网络身份和离线代理是否匹配,不在这里做,而是交给 require_logon_sandbox_creds。

调用图:被 1 处调用(verify_setup_completed);外部调用 1 个(matches!)。

load_marker50–73 ↗
fn load_marker(codex_home: &Path) -> Result<Option<SetupMarker>>

作用:读取沙盒初始化标记文件。这个标记像一张“验收单”,说明沙盒曾经按哪个版本、哪些网络要求设置过。

数据流:输入是 codex_home。函数先用 setup_marker_path 算出标记文件路径,再读文本并解析成 SetupMarker。读不到文件、解析失败或读取出错时,它会写 debug 日志,然后返回 None;成功时返回 Some(marker)。它本身不修复文件。

调用关系:require_logon_sandbox_creds 用它决定是否需要重新 setup;select_identity 也用它确认用户文件可以被信任。它把路径计算交给 setup_marker_path,把失败原因交给 debug_log 记录。

调用图:调用 2 个内部函数(debug_log, setup_marker_path);被 2 处调用(require_logon_sandbox_creds, select_identity);外部调用 2 个(format!, read_to_string)。

load_users75–98 ↗
fn load_users(codex_home: &Path) -> Result<Option<SandboxUsersFile>>

作用:读取保存沙盒账号信息的 users 文件。这个文件里有 online/offline 两套账号记录,但密码仍是加密后的。

数据流:输入是 codex_home。函数用 sandbox_users_path 找到用户文件,读取 JSON 内容并解析成 SandboxUsersFile。文件不存在、读取失败或解析失败都会返回 None,并在合适的时候写 debug 日志;成功则返回 Some(users)。

调用关系:它主要被 select_identity 调用。select_identity 先确认初始化标记有效,再靠它拿到可选账号,然后继续解密密码。

调用图:调用 2 个内部函数(debug_log, sandbox_users_path);被 1 处调用(select_identity);外部调用 2 个(format!, read_to_string)。

remove_sandbox_users_file100–111 ↗
fn remove_sandbox_users_file(codex_home: &Path, reason: &str) -> Result<()>

作用:删除本地保存的沙盒用户文件,通常用于怀疑账号密码已经坏掉或过期时。它相当于把旧钥匙扔掉,逼系统下次重新配一把。

数据流:输入是 codex_home 和 reason。函数算出 users 文件路径,记录一条说明为什么删除的 debug 日志,然后尝试删除文件。删除成功或文件本来就不存在都算成功;如果是其他删除错误,就返回错误并带上文件路径说明。

调用关系:refresh_logon_sandbox_creds 在沙盒登录失败后会先调用它,再重新申请凭据。测试函数也直接调用它,确认它既能删已有文件,也不会因为文件不存在而报错。

调用图:调用 2 个内部函数(debug_log, sandbox_users_path);被 3 处调用(refresh_logon_sandbox_creds, remove_sandbox_users_file_deletes_existing_file, remove_sandbox_users_file_ignores_missing_file);外部调用 2 个(format!, remove_file)。

decode_password113–120 ↗
fn decode_password(record: &SandboxUserRecord) -> Result<String>

作用:把用户记录里保存的加密密码还原成真正能登录的密码。它把“保险箱里的密文”变成程序能用的明文。

数据流:输入是一个 SandboxUserRecord,其中 password 是 Base64 字符串。函数先做 Base64 解码得到原始加密字节,再调用 dpapi::unprotect 用 Windows DPAPI 解密,最后把字节转成 UTF-8 字符串。输出是明文密码;如果任一步失败就返回错误。

调用关系:它只被 select_identity 调用。select_identity 选好 online 或 offline 用户记录后,把解密密码这一步交给它完成。

调用图:调用 1 个内部函数(unprotect);被 1 处调用(select_identity);外部调用 1 个(from_utf8)。

select_identity122–143 ↗
fn select_identity(
    network_identity: SandboxNetworkIdentity,
    codex_home: &Path,
) -> Result<Option<SandboxIdentity>>

作用:根据当前需要联网还是离线,选出对应的沙盒账号,并解出密码。它不会创建账号,只从已有初始化结果里挑一个可用身份。

数据流:输入是 network_identity 和 codex_home。函数先读取并验证 marker,再读取并验证 users 文件;任一缺失或版本不对就返回 None。都正常时,它按 Offline 或 Online 选择对应用户记录,解密密码,最后返回包含用户名和明文密码的 SandboxIdentity。

调用关系:require_logon_sandbox_creds 在判断 setup 可能有效时会调用它取凭据;如果它返回 None,require_logon_sandbox_creds 会认为需要重新初始化。它内部依次依赖 load_marker、load_users 和 decode_password。

调用图:调用 3 个内部函数(decode_password, load_marker, load_users);被 1 处调用(require_logon_sandbox_creds)。

require_logon_sandbox_creds146–248 ↗
fn require_logon_sandbox_creds(
    permissions: &ResolvedWindowsSandboxPermissions,
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
    read_roots_override: Opti

作用:这是本文件的核心入口:确保现在有一套能登录 Windows 沙盒的账号密码。如果现有配置不够新、不匹配或缺失,它会自动触发 setup,再返回可用凭据。

数据流:输入包括当前权限要求、命令工作目录、环境变量、codex_home、读写目录覆盖规则、拒绝访问规则,以及是否强制代理。函数先算出沙盒需要读哪些目录、写哪些目录,再根据权限决定用 online 还是 offline 身份,并计算离线代理设置。接着检查 marker 和 users 是否可用;不可用时记录原因,调用 run_elevated_setup 做需要管理员权限的初始化。之后无论如何都会调用 run_setup_refresh_with_overrides 刷新当前目录访问规则。最后输出 SandboxCreds,也就是用户名和密码;如果仍拿不到身份,就返回明确错误。

调用关系:它被真正启动沙盒或准备提权启动上下文的流程调用,例如 run_windows_sandbox_capture_for_permission_profile、prepare_elevated_spawn_context_for_permissions,也被 refresh_logon_sandbox_creds 复用。它负责串起 load_marker、select_identity、offline_proxy_settings_from_env、run_elevated_setup 和 run_setup_refresh_with_overrides,是“启动前把凭据和权限都兜底检查一遍”的调度点。

调用图:调用 8 个内部函数(load_marker, select_identity, log_note, from_permissions, offline_proxy_settings_from_env, run_elevated_setup, run_setup_refresh_with_overrides, sandbox_dir);被 3 处调用(run_windows_sandbox_capture_for_permission_profile, refresh_logon_sandbox_creds, prepare_elevated_spawn_context_for_permissions);外部调用 2 个(to_vec, format!)。

refresh_logon_sandbox_creds251–276 ↗
fn refresh_logon_sandbox_creds(
    permissions: &ResolvedWindowsSandboxPermissions,
    command_cwd: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
    read_roots_override: Opti

作用:当沙盒登录失败时,用它强制刷新凭据。它先删掉可能已经失效的用户文件,再走正常的凭据获取流程。

数据流:输入和 require_logon_sandbox_creds 基本一样,都是当前权限、目录和环境信息。函数先调用 remove_sandbox_users_file 删除旧 users 文件,然后把同一批输入交给 require_logon_sandbox_creds。输出是一套新的或重新确认过的 SandboxCreds;过程中会改动磁盘上的 users 文件状态。

调用关系:它被 spawn_windows_sandbox_session_elevated_for_permission_profile 在登录失败后的恢复路径使用。它自己不懂如何重新 setup,而是先清掉旧记录,再把复杂工作交回 require_logon_sandbox_creds。

调用图:调用 2 个内部函数(remove_sandbox_users_file, require_logon_sandbox_creds);被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile)。

tests::remove_sandbox_users_file_deletes_existing_file286–295 ↗
fn remove_sandbox_users_file_deletes_existing_file()

作用:这个测试确认 remove_sandbox_users_file 确实能删除已经存在的 users 文件。

数据流:测试先创建一个临时 codex_home,算出 users 文件路径,创建目录并写入一个假文件。然后调用 remove_sandbox_users_file。最后检查该文件已经不存在。

调用关系:它直接测试 remove_sandbox_users_file 的正常删除场景,防止以后有人改代码时不小心让清理旧凭据的功能失效。

调用图:调用 2 个内部函数(remove_sandbox_users_file, sandbox_users_path);外部调用 4 个(new, assert!, create_dir_all, write)。

tests::remove_sandbox_users_file_ignores_missing_file298–304 ↗
fn remove_sandbox_users_file_ignores_missing_file()

作用:这个测试确认 users 文件不存在时,remove_sandbox_users_file 不会把这当成错误。

数据流:测试创建一个临时 codex_home,但不创建 users 文件。它调用 remove_sandbox_users_file 后,确认函数成功返回,并且文件仍然不存在。

调用关系:它覆盖 remove_sandbox_users_file 的容错场景。这个行为很重要,因为刷新凭据时旧文件可能早就被删了,程序不应该因此中断。

调用图:调用 2 个内部函数(remove_sandbox_users_file, sandbox_users_path);外部调用 2 个(new, assert!)。

windows-sandbox-rs/src/acl.rs源码 ↗
domain_logicsandbox setup 和权限应用阶段,也会在运行中同步或撤销权限时使用

Windows 的文件权限像门禁表:DACL(自主访问控制列表,就是一张“谁能做什么”的表)里有很多 ACE(访问控制项,就是一条门禁规则),每条规则对应一个 SID(安全标识符,可以理解成某个用户或用户组的身份证)。这个文件就是读、查、改这张门禁表的工具箱。它会先拿到文件或目录当前的 DACL,检查某个 SID 是否已经有读写权限或已经被拒绝读写;如果缺少规则,就用 Windows 系统 API 加上允许或拒绝规则。它还特别注意“继承”规则,让目录下新建的文件也能跟着受控。重要的是,它会跳过只用于继承、不作用于当前对象的规则,并在用完 Windows 返回的内存后释放,防止权限判断错或资源泄漏。

函数细节18
fetch_dacl_handle62–97 ↗
fn fetch_dacl_handle(path: &Path) -> Result<(*mut ACL, *mut c_void)>

作用:打开一个已有文件或目录,并取出它当前的 DACL,也就是 Windows 保存的权限规则表。别人要检查或修改权限时,通常先靠它把原始权限表拿出来。

数据流:输入是一个路径。它把路径转成 Windows 需要的宽字符格式,打开这个文件或目录,再向系统询问它的安全信息。输出是 DACL 指针和安全描述符指针;调用者之后必须释放安全描述符占用的系统内存。

调用关系:这是底层取权限表的入口之一。path_mask_allows 用它来做只读检查,ensure_allow_mask_aces_with_inheritance_impl 用它来决定是否需要补权限;它自己把路径转换交给 to_wide,把真正读取交给 Windows 的 CreateFileW 和 GetSecurityInfo。

调用图:调用 1 个内部函数(to_wide);被 2 处调用(ensure_allow_mask_aces_with_inheritance_impl, path_mask_allows);外部调用 5 个(anyhow!, null_mut, CloseHandle, GetSecurityInfo, CreateFileW)。

dacl_mask_allows101–161 ↗
fn dacl_mask_allows(
    p_dacl: *mut ACL,
    psids: &[*mut c_void],
    desired_mask: u32,
    require_all_bits: bool,
) -> bool

作用:在一张已经拿到的 DACL 里,快速判断指定 SID 们是否被允许某些权限。比如看某个用户组有没有写权限。

数据流:输入是 DACL、一个或多个 SID、想要的权限位,以及是否要求全部权限都具备。它逐条查看允许类型的 ACE,跳过只继承不生效的规则,比较 SID,并把 Windows 的通用权限换算成文件权限。输出是 true 或 false,不修改 DACL。

调用关系:它是权限检查的核心小筛子。path_mask_allows 拿到 DACL 后会调用它;ensure_allow_mask_aces_with_inheritance_impl 也用它判断某个 SID 是否已经够权限,够了就不重复添加。

调用图:被 2 处调用(ensure_allow_mask_aces_with_inheritance_impl, path_mask_allows);外部调用 7 个(is_null, zeroed, null_mut, EqualSid, GetAce, GetAclInformation, MapGenericMask)。

path_mask_allows164–178 ↗
fn path_mask_allows(
    path: &Path,
    psids: &[*mut c_void],
    desired_mask: u32,
    require_all_bits: bool,
) -> Result<bool>

作用:给外部提供一个按路径检查权限的方便入口。调用者不用自己先取 DACL,也不用自己释放系统内存。

数据流:输入是路径、SID 列表、目标权限和匹配方式。它先调用 fetch_dacl_handle 拿权限表,再调用 dacl_mask_allows 判断,最后释放 Windows 返回的安全描述符。输出是检查结果,或读取失败时返回错误。

调用关系:它把“取权限表”和“查权限位”这两步包成一个函数。上层的 path_has_world_write_allow 会用它来判断某个路径是否对所有人可写。

调用图:调用 2 个内部函数(dacl_mask_allows, fetch_dacl_handle);被 1 处调用(path_has_world_write_allow);外部调用 1 个(LocalFree)。

dacl_has_write_allow_for_sid180–219 ↗
fn dacl_has_write_allow_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -> bool

作用:检查某个 SID 在当前 DACL 里是否已经有写入允许规则。这样可以避免反复添加重复的允许规则。

数据流:输入是 DACL 和一个 SID。它遍历 DACL 里的允许 ACE,跳过只继承的规则,找到同一个 SID 后看权限位里有没有写权限。输出是 true 或 false,不改任何东西。

调用关系:add_allow_ace 在真正改权限前先调用它。如果已经能写,add_allow_ace 就直接返回,不再重写 DACL。

调用图:被 1 处调用(add_allow_ace);外部调用 6 个(is_null, zeroed, null_mut, EqualSid, GetAce, GetAclInformation)。

dacl_has_write_deny_for_sid221–264 ↗
fn dacl_has_write_deny_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -> bool

作用:检查某个 SID 是否已经被明确禁止写入、追加或删除。它用来防止重复加同样的拒绝写规则。

数据流:输入是 DACL 和 SID。它遍历拒绝类型的 ACE,跳过只继承的规则,确认 SID 相同后检查是否命中写入、追加、删除等危险权限位。输出 true 或 false,不修改权限表。

调用关系:DenyAceKind::already_present 在处理“拒绝写”时会调用它;add_deny_ace 通过这个判断决定是否真的需要新增拒绝 ACE。

调用图:被 1 处调用(already_present);外部调用 6 个(is_null, zeroed, null_mut, EqualSid, GetAce, GetAclInformation)。

dacl_has_read_deny_for_sid266–302 ↗
fn dacl_has_read_deny_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -> bool

作用:检查某个 SID 是否已经被明确禁止读取。它主要用于避免重复写入拒绝读规则。

数据流:输入是 DACL 和 SID。它逐条查看拒绝 ACE,排除只继承的规则,比较 SID,再看权限位是否包含读取相关权限。输出 true 或 false,不改 DACL。

调用关系:DenyAceKind::already_present 在处理“拒绝读”时会调用它;add_deny_ace 借它确认现有权限表里是不是已经有相同禁令。

调用图:被 1 处调用(already_present);外部调用 6 个(is_null, zeroed, null_mut, EqualSid, GetAce, GetAclInformation)。

ensure_allow_mask_aces_with_inheritance_impl307–376 ↗
fn ensure_allow_mask_aces_with_inheritance_impl(
    path: &Path,
    sids: &[*mut c_void],
    allow_mask: u32,
    inheritance: u32,
) -> Result<bool>

作用:确保一批 SID 都拥有指定权限,并按传入的继承方式写入规则。它是批量补“允许权限”的真正执行者。

数据流:输入是路径、SID 列表、要允许的权限位和继承标志。它先取当前 DACL,对每个 SID 检查权限是否已满足;缺的就组织成新的允许 ACE,合并进 DACL,再写回文件或目录。输出表示是否真的新增过规则,同时会释放中间分配的系统内存。

调用关系:ensure_allow_mask_aces_with_inheritance 是它的安全包装入口。它内部依赖 fetch_dacl_handle 取旧表,依赖 dacl_mask_allows 做去重检查,再把合并和写回交给 Windows 的 SetEntriesInAclW 和 SetNamedSecurityInfoW。

调用图:调用 3 个内部函数(dacl_mask_allows, fetch_dacl_handle, to_wide);被 1 处调用(ensure_allow_mask_aces_with_inheritance);外部调用 6 个(new, anyhow!, null_mut, LocalFree, SetEntriesInAclW, SetNamedSecurityInfoW)。

ensure_allow_mask_aces_with_inheritance383–390 ↗
fn ensure_allow_mask_aces_with_inheritance(
    path: &Path,
    sids: &[*mut c_void],
    allow_mask: u32,
    inheritance: u32,
) -> Result<bool>

作用:这是公开的包装函数,用来确保一批 SID 有指定权限,并允许调用者自己决定这些权限怎样继承到子文件或子目录。

数据流:输入是路径、SID 列表、权限位和继承标志。它不自己做复杂工作,而是把这些参数原样交给 ensure_allow_mask_aces_with_inheritance_impl。输出是是否新增了权限规则,或错误。

调用关系:它位于公开接口和内部实现之间。ensure_allow_mask_aces 会调用它,并使用默认的文件和目录继承方式。

调用图:调用 1 个内部函数(ensure_allow_mask_aces_with_inheritance_impl);被 1 处调用(ensure_allow_mask_aces)。

ensure_allow_mask_aces397–408 ↗
fn ensure_allow_mask_aces(
    path: &Path,
    sids: &[*mut c_void],
    allow_mask: u32,
) -> Result<bool>

作用:用默认继承方式给一批 SID 补指定权限。默认意思是:这个目录下面以后新建的文件和子目录也会继承这条规则。

数据流:输入是路径、SID 列表和权限位。它自动加上“容器继承”和“对象继承”两个标志,然后调用 ensure_allow_mask_aces_with_inheritance。输出是是否新增了 ACE,或错误。

调用关系:它是更省事的允许权限入口。ensure_allow_write_aces 会调用它来补一组固定的写相关权限。

调用图:调用 1 个内部函数(ensure_allow_mask_aces_with_inheritance);被 1 处调用(ensure_allow_write_aces)。

ensure_allow_write_aces415–417 ↗
fn ensure_allow_write_aces(path: &Path, sids: &[*mut c_void]) -> Result<bool>

作用:确保一批 SID 对某个路径有写入所需的权限。沙箱准备工作常需要用它让特定账号能操作工作目录。

数据流:输入是路径和 SID 列表。它使用预设的写入权限组合,包括读、写、执行、删除等与正常写工作区有关的权限,然后调用 ensure_allow_mask_aces。输出是是否添加了权限规则,或错误。

调用关系:这是面向“给工作区放行写权限”的简化入口。它把具体权限位交给 ensure_allow_mask_aces,由后者继续走批量补 ACE 的流程。

调用图:调用 1 个内部函数(ensure_allow_mask_aces)。

add_allow_ace423–483 ↗
fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result<bool>

作用:给单个 SID 添加读、写、执行的允许规则。它适合旧流程中需要直接给某个路径放行的场景。

数据流:输入是路径和 SID。它读取当前 DACL,先检查这个 SID 是否已经有写允许;如果没有,就创建一条可继承的允许 ACE,合并到 DACL 并写回。输出 true 表示加过规则,false 表示原本就够或没有实际新增;失败时返回错误。

调用关系:apply_legacy_session_acl_rules 会调用它。它自己先用 dacl_has_write_allow_for_sid 去重,再把创建新 DACL 和写回交给 Windows API。

调用图:调用 2 个内部函数(dacl_has_write_allow_for_sid, to_wide);被 1 处调用(apply_legacy_session_acl_rules);外部调用 7 个(anyhow!, zeroed, null_mut, LocalFree, GetNamedSecurityInfoW, SetEntriesInAclW, SetNamedSecurityInfoW)。

add_deny_write_ace489–491 ↗
fn add_deny_write_ace(path: &Path, psid: *mut c_void) -> Result<bool>

作用:给某个 SID 添加“禁止写”的规则。它是外部更容易理解的入口,不需要调用者关心内部的拒绝规则种类。

数据流:输入是路径和 SID。它把操作类型固定为 Write,然后交给 add_deny_ace 完成读取、检查、合并和写回。输出表示是否新增了拒绝规则,或错误。

调用关系:多个上层流程会用它保护目录,比如按能力权限拒绝世界可写、旧会话规则、保护工作区子目录。它本身只是把“拒绝写”这个意图转交给通用的 add_deny_ace。

调用图:调用 1 个内部函数(add_deny_ace);被 3 处调用(apply_capability_denies_for_world_writable_for_permissions, apply_legacy_session_acl_rules, protect_workspace_subdir)。

DenyAceKind::mask500–514 ↗
fn mask(self) -> u32

作用:把“拒绝读”或“拒绝写”这种人能看懂的种类,翻译成 Windows 权限位。权限位可以理解成一组开关,告诉系统具体禁止哪些动作。

数据流:输入是 DenyAceKind 的一个值:Read 或 Write。它根据种类返回对应的权限位组合;读就是读相关权限,写则包括写数据、追加、改属性、删除等。它不读取也不修改外部状态。

调用关系:add_deny_ace 在创建拒绝 ACE 时调用它,用返回的权限位填进 Windows 需要的结构里。

调用图:被 1 处调用(add_deny_ace)。

DenyAceKind::already_present516–521 ↗
fn already_present(self, p_dacl: *mut ACL, psid: *mut c_void) -> bool

作用:判断某种拒绝规则是否已经存在。这样添加拒绝权限时不会把同一条禁令反复塞进 DACL。

数据流:输入是拒绝种类、DACL 和 SID。它如果是 Read,就调用 dacl_has_read_deny_for_sid;如果是 Write,就调用 dacl_has_write_deny_for_sid。输出 true 或 false,不改权限表。

调用关系:add_deny_ace 在准备新增拒绝 ACE 前会先调用它。它是“通用拒绝添加逻辑”和“具体读写检查函数”之间的分路器。

调用图:调用 2 个内部函数(dacl_has_read_deny_for_sid, dacl_has_write_deny_for_sid);被 1 处调用(add_deny_ace)。

add_deny_ace524–578 ↗
fn add_deny_ace(path: &Path, psid: *mut c_void, kind: DenyAceKind) -> Result<bool>

作用:这是添加拒绝规则的通用实现。无论是拒绝读还是拒绝写,最后都由它把规则写进 Windows 的 DACL。

数据流:输入是路径、SID 和拒绝种类。它读取当前 DACL,先问 kind.already_present 是否已有同类规则;没有的话,就用 kind.mask 得到要禁止的权限位,创建可继承的拒绝 ACE,合并并写回。输出是否新增了规则,或错误。

调用关系:add_deny_write_ace 和 add_deny_read_ace 都只是它的薄包装。它负责调用 DenyAceKind::already_present 做去重,调用 DenyAceKind::mask 生成权限位,再交给 Windows API 修改 DACL。

调用图:调用 3 个内部函数(already_present, mask, to_wide);被 2 处调用(add_deny_read_ace, add_deny_write_ace);外部调用 7 个(anyhow!, zeroed, null_mut, LocalFree, GetNamedSecurityInfoW, SetEntriesInAclW, SetNamedSecurityInfoW)。

add_deny_read_ace589–591 ↗
fn add_deny_read_ace(path: &Path, psid: *mut c_void) -> Result<bool>

作用:给某个 SID 添加“禁止读取”的规则。它用于让沙箱里的特定身份看不到某些路径。

数据流:输入是路径和 SID。它把拒绝类型固定为 Read,然后调用 add_deny_ace。输出表示是否真的新增了拒绝读规则,或错误。

调用关系:它是拒绝读的公开入口。真正的去重、权限位选择和写回 DACL 都在 add_deny_ace 里完成。

调用图:调用 1 个内部函数(add_deny_ace)。

revoke_ace593–643 ↗
fn revoke_ace(path: &Path, psid: *mut c_void)

作用:撤销某个 SID 在指定路径上的显式权限规则。可以理解成从门禁表里把这个人的专门规则删掉。

数据流:输入是路径和 SID。它读取当前 DACL,创建一条 REVOKE_ACCESS 操作,告诉 Windows 从 ACL 中移除这个 SID 的相关显式权限,再写回新 DACL。它没有返回值,失败时基本选择安静退出,并会释放拿到的系统内存。

调用关系:apply_deny_read_acls 和 sync_persistent_deny_read_acls 会调用它,用来在同步拒绝读权限时清理旧规则或撤掉不再需要的权限。

调用图:调用 1 个内部函数(to_wide);被 2 处调用(apply_deny_read_acls, sync_persistent_deny_read_acls);外部调用 6 个(zeroed, null_mut, LocalFree, GetNamedSecurityInfoW, SetEntriesInAclW, SetNamedSecurityInfoW)。

allow_null_device649–710 ↗
fn allow_null_device(psid: *mut c_void)

作用:给某个 SID 放行 Windows 的 NUL 设备。NUL 类似一个“黑洞文件”,程序把输出重定向过去时需要能打开它,否则 stdout/stderr 重定向可能失败。

数据流:输入是 SID。它打开 \\.\NUL 这个系统设备,读取它的安全信息,添加一条不继承的读、写、执行允许 ACE,然后把新权限写回设备对象。过程中如果打不开或失败就直接返回,最后释放内存并关闭句柄。

调用关系:运行沙箱并捕获输出、准备提升权限的启动上下文、旧 ACL 规则、工作区写权限相关流程都会调用它。它是少数不针对普通文件路径,而是直接修改 Windows 内核对象权限的函数。

调用图:调用 1 个内部函数(to_wide);被 4 处调用(run_windows_sandbox_capture_for_permission_profile, allow_null_device_for_workspace_write, apply_legacy_session_acl_rules, prepare_elevated_spawn_context_for_permissions);外部调用 8 个(zeroed, null_mut, CloseHandle, LocalFree, GetSecurityInfo, SetEntriesInAclW, SetSecurityInfo, CreateFileW)。

windows-sandbox-rs/src/token.rs源码 ↗
domain_logicsandbox process preparation

沙箱要安全,关键是不能让里面运行的程序拿着正常用户的全部权限乱跑。这个文件做的事,就是从当前进程的 Windows 权限令牌出发,造出一个更受限制的新令牌。它会加入一些 SID(安全标识符,可以理解成 Windows 用来认人、认组、认权限标签的号码),比如能力 SID、登录 SID、Everyone SID。然后它用 Windows API 创建受限令牌,关掉大部分高权限,只保留必要的小权限。它还会设置默认 DACL(默认访问控制表,像新建管道、临时对象时自动贴上的权限清单),避免沙箱进程之间需要通信时莫名其妙被拒绝。这里有不少 unsafe,是因为它直接调用 Windows 底层接口、操作裸指针和系统句柄,所以谁创建的句柄,谁也必须按规矩关闭。

函数细节17
set_default_dacl56–107 ↗
fn set_default_dacl(h_token: HANDLE, sids: &[*mut c_void]) -> Result<()>

作用:给一个令牌设置比较宽松的默认 DACL。这样沙箱里的程序创建管道、进程间通信对象时,不容易因为权限清单太紧而失败。

数据流:进去的是一个令牌句柄和一组 SID 指针。它把这些 SID 做成“允许全部访问”的访问规则,交给 Windows 生成新的 ACL,再把这个 ACL 写进令牌的默认 DACL。出来时没有返回新对象,只返回成功或错误;中途申请到的系统内存会被释放。

调用关系:它是 create_token_with_caps_from 的收尾步骤之一。新受限令牌造好后,还不能直接放心用,因为后续 PowerShell 管道等 IPC 对象可能会被默认权限挡住,所以 create_token_with_caps_from 会调用它补上合适的默认权限。

调用图:被 1 处调用(create_token_with_caps_from);外部调用 8 个(anyhow!, is_empty, iter, null_mut, GetLastError, LocalFree, SetEntriesInAclW, SetTokenInformation)。

world_sid109–128 ↗
fn world_sid() -> Result<Vec<u8>>

作用:生成 Windows 里的 Everyone SID,也就是“所有人”这个通用身份标识。很多权限判断需要把这个身份放进去,表示普通的全局可见访问规则。

数据流:进去不需要业务参数。它先问 Windows 这个 SID 需要多大内存,再分配字节数组,让 Windows 把 Everyone SID 写进去。出来的是一段保存 SID 内容的字节数组;如果系统调用失败,就返回错误。

调用关系:它会被 create_token_with_caps_from 用来把 Everyone 加进受限令牌的限制列表和默认 DACL。另一个路径检查函数 path_has_world_write_allow 也会用它判断文件路径是否允许 Everyone 写入。

调用图:被 2 处调用(path_has_world_write_allow, create_token_with_caps_from);外部调用 4 个(anyhow!, null_mut, vec!, CreateWellKnownSid)。

convert_string_sid_to_sid132–140 ↗
fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void>

作用:把文本形式的 SID 转成 Windows 能直接使用的 SID 指针。比如把一串像“S-...”这样的身份编号,变成系统 API 能读懂的内存对象。

数据流:进去的是 SID 字符串。它先把普通字符串转成 Windows 常用的宽字符格式,再调用 Windows 的 ConvertStringSidToSidW。出来是一个 SID 指针;如果字符串不合法或转换失败,就返回空值。

调用关系:它是 LocalSid::from_string 的底层帮手。外部代码通常不会直接拿这个裸指针,而是通过 LocalSid 包一层,避免忘记释放系统分配的内存。

调用图:调用 1 个内部函数(to_wide);被 1 处调用(from_string);外部调用 1 个(null_mut)。

LocalSid::from_string148–152 ↗
fn from_string(sid: &str) -> Result<Self>

作用:从 SID 字符串创建一个会自动释放内存的 LocalSid。它让调用者不用自己记住什么时候调用 LocalFree,少踩内存泄漏的坑。

数据流:进去的是一个 SID 字符串。它调用 convert_string_sid_to_sid 得到系统分配的 SID 指针,然后把指针装进 LocalSid。出来的是 LocalSid 对象;如果字符串无效,就返回错误。

调用关系:这是许多准备沙箱权限的地方会用的入口,例如 spawn_ipc_process、run_windows_sandbox_capture_for_permission_profile、prepare_elevated_spawn_context_for_permissions、prepare_legacy_session_security 和 root_capability_sids。它把外部传来的权限编号安全地变成后续令牌创建能用的指针。

调用图:调用 1 个内部函数(convert_string_sid_to_sid);被 5 处调用(spawn_ipc_process, run_windows_sandbox_capture_for_permission_profile, prepare_elevated_spawn_context_for_permissions, prepare_legacy_session_security, root_capability_sids)。

LocalSid::as_ptr154–156 ↗
fn as_ptr(&self) -> *mut c_void

作用:取出 LocalSid 里面保存的原始 SID 指针。需要把 SID 交给 Windows API 时,就用这个函数拿地址。

数据流:进去的是一个已经存在的 LocalSid。它不复制、不修改,只把内部的 SID 指针拿出来。出来的是裸指针,真正的内存仍然由 LocalSid 持有。

调用关系:它通常夹在“把字符串 SID 变成 LocalSid”和“把 SID 传给创建令牌或设置权限的函数”之间。它不负责释放内存,释放工作留给 LocalSid::drop。

LocalSid::drop160–166 ↗
fn drop(&mut self)

作用:在 LocalSid 不再使用时自动释放 Windows 分配的 SID 内存。它相当于给这块系统内存安排了一个自动清洁工。

数据流:进去的是即将销毁的 LocalSid。它检查内部指针是不是空;如果不是,就调用 LocalFree 释放。出来没有返回值,但系统内存被归还了。

调用关系:它由 Rust 在 LocalSid 生命周期结束时自动调用。LocalSid::from_string 创建的 SID,最后靠它收尾,避免调用者手动管理 LocalFree。

调用图:外部调用 2 个(is_null, LocalFree)。

get_current_token_for_restriction171–192 ↗
fn get_current_token_for_restriction() -> Result<HANDLE>

作用:打开当前进程的权限令牌,并要求拿到后续“复制、查询、调整、作为主令牌使用”等必要权限。创建沙箱受限令牌之前,必须先拿到这张原始通行证。

数据流:进去不需要业务参数。它向 Windows 请求当前进程的令牌句柄,并声明需要哪些访问权限。出来是一个令牌句柄;如果打开失败,就返回包含 Windows 错误码的错误。

调用关系:grant_desktop_access、allow_null_device_for_workspace_write、prepare_legacy_session_security 和 create_readonly_token_with_cap 都会用它拿当前令牌。拿到的句柄需要调用者之后关闭,否则会泄漏系统资源。

调用图:被 4 处调用(grant_desktop_access, allow_null_device_for_workspace_write, prepare_legacy_session_security, create_readonly_token_with_cap);外部调用 2 个(anyhow!, GetCurrentProcess)。

get_logon_sid_bytes194–277 ↗
fn get_logon_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>>

作用:从令牌里找出登录 SID,也就是这次登录会话的身份标识。沙箱需要保留这个标识,才能继续访问某些和当前登录会话有关的资源。

数据流:进去的是一个令牌句柄。它先读取令牌里的组列表,扫描带有“登录 ID”标记的 SID,复制成字节数组;如果当前令牌没找到,还会尝试读取关联令牌再找一次。出来是登录 SID 的字节数组;找不到就返回错误。过程中如果打开了关联令牌,会及时关闭。

调用关系:grant_desktop_access、allow_null_device_for_workspace_write 和 create_token_with_caps_from 都依赖它。特别是在创建受限令牌时,create_token_with_caps_from 会把登录 SID 放进限制列表和默认 DACL,让沙箱既受限又不至于失去必要的会话访问能力。

调用图:被 3 处调用(grant_desktop_access, allow_null_device_for_workspace_write, create_token_with_caps_from);外部调用 6 个(anyhow!, null_mut, read_unaligned, vec!, CloseHandle, GetTokenInformation)。

get_user_sid_bytes279–317 ↗
fn get_user_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>>

作用:从令牌中取出“这个令牌属于哪个用户”的 SID。对于某些提升权限的沙箱后端,需要把专用沙箱账号本身也放进限制列表。

数据流:进去的是一个令牌句柄。它先问 Windows 用户信息需要多大缓冲区,再读取 TokenUser 信息,取出里面的用户 SID,复制到自己的字节数组里。出来是用户 SID 的字节数组;如果读取、长度判断或复制失败,就返回错误。

调用关系:create_readonly_token_with_caps_and_user_from 和 create_workspace_write_token_with_caps_and_user_from 会调用它。它们先取到用户 SID,再交给 create_token_with_caps_from 一起生成受限令牌。

调用图:被 2 处调用(create_readonly_token_with_caps_and_user_from, create_workspace_write_token_with_caps_and_user_from);外部调用 7 个(anyhow!, null_mut, read_unaligned, vec!, CopySid, GetLengthSid, GetTokenInformation)。

enable_single_privilege319–348 ↗
fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()>

作用:在令牌里启用一个指定的小权限。这里主要用于启用 SeChangeNotifyPrivilege,也就是允许进程正常穿过目录进行路径访问检查的常见基础权限。

数据流:进去的是令牌句柄和权限名称。它先把权限名称转换成 Windows 能识别的 LUID(本机里的权限编号),再调用 AdjustTokenPrivileges 启用它。出来没有新对象,只返回成功或错误。

调用关系:create_token_with_caps_from 在创建完受限令牌后调用它。因为新令牌被削弱得很厉害,但仍需要保留某些 Windows 正常运行所需的小权限,所以这里把必要权限重新打开。

调用图:调用 1 个内部函数(to_wide);被 1 处调用(create_token_with_caps_from);外部调用 7 个(anyhow!, zeroed, null, null_mut, GetLastError, AdjustTokenPrivileges, LookupPrivilegeValueW)。

create_readonly_token_with_cap352–359 ↗
fn create_readonly_token_with_cap(
    psid_capability: *mut c_void,
) -> Result<(HANDLE, *mut c_void)>

作用:基于当前进程创建一个带单个能力 SID 的只读风格受限令牌。调用者不用自己先打开当前令牌,这个函数会代劳。

数据流:进去的是一个能力 SID 指针。它先打开当前进程令牌,再把这个令牌和能力 SID 交给 create_readonly_token_with_cap_from,最后关闭原始令牌。出来是新受限令牌句柄和原来的能力 SID 指针;新句柄需要调用者关闭。

调用关系:prepare_legacy_session_security 会用它准备旧式会话沙箱的安全令牌。它本身是便捷包装,把“打开当前令牌”和“创建受限令牌”串起来。

调用图:调用 2 个内部函数(create_readonly_token_with_cap_from, get_current_token_for_restriction);被 1 处调用(prepare_legacy_session_security);外部调用 1 个(CloseHandle)。

create_readonly_token_with_cap_from365–371 ↗
fn create_readonly_token_with_cap_from(
    base_token: HANDLE,
    psid_capability: *mut c_void,
) -> Result<(HANDLE, *mut c_void)>

作用:基于一个已有的基础令牌,创建一个带单个能力 SID 的受限令牌。适合调用者已经自己拿到了基础令牌的场景。

数据流:进去的是基础令牌句柄和一个能力 SID 指针。它把这个单个 SID 放进数组,交给 create_token_with_caps_from 统一创建受限令牌。出来是新令牌句柄和同一个能力 SID 指针。

调用关系:它被 create_readonly_token_with_cap 调用,是单能力版本的薄包装。真正复杂的令牌创建、限制 SID 拼装、默认 DACL 设置,都交给 create_token_with_caps_from。

调用图:调用 1 个内部函数(create_token_with_caps_from);被 1 处调用(create_readonly_token_with_cap)。

create_workspace_write_token_with_caps_from377–382 ↗
fn create_workspace_write_token_with_caps_from(
    base_token: HANDLE,
    psid_capabilities: &[*mut c_void],
) -> Result<HANDLE>

作用:用已有基础令牌创建一个带多个能力 SID 的受限令牌,给“工作区可写”这类沙箱场景使用。

数据流:进去的是基础令牌句柄和一组能力 SID 指针。它不额外加入用户 SID,只把这些能力 SID 交给 create_token_with_caps_from。出来是新受限令牌句柄。

调用关系:prepare_legacy_session_security 会调用它来准备需要写工作区的沙箱令牌。它是面向具体场景的外层入口,核心创建工作仍由 create_token_with_caps_from 完成。

调用图:调用 1 个内部函数(create_token_with_caps_from);被 1 处调用(prepare_legacy_session_security)。

create_workspace_write_token_with_caps_and_user_from391–398 ↗
fn create_workspace_write_token_with_caps_and_user_from(
    base_token: HANDLE,
    psid_capabilities: &[*mut c_void],
) -> Result<HANDLE>

作用:创建一个带多个能力 SID、并额外包含令牌用户 SID 的工作区可写受限令牌。它主要给提升权限的沙箱后端使用,因为那里令牌用户可能是专用沙箱账号。

数据流:进去的是基础令牌句柄和能力 SID 列表。它先从基础令牌读出用户 SID,把用户 SID 指针作为额外限制 SID,再连同能力 SID 一起交给 create_token_with_caps_from。出来是新受限令牌句柄。

调用关系:它先调用 get_user_sid_bytes 补齐用户身份,再调用 create_token_with_caps_from 做统一令牌创建。和不带 user 的版本相比,它多保留了专用沙箱账号这个身份约束。

调用图:调用 2 个内部函数(create_token_with_caps_from, get_user_sid_bytes)。

create_readonly_token_with_caps_from404–409 ↗
fn create_readonly_token_with_caps_from(
    base_token: HANDLE,
    psid_capabilities: &[*mut c_void],
) -> Result<HANDLE>

作用:用已有基础令牌创建一个带多个能力 SID 的只读风格受限令牌。它适合需要多个权限标签共同限定访问范围的情况。

数据流:进去的是基础令牌句柄和能力 SID 列表。它不添加额外用户 SID,直接交给 create_token_with_caps_from。出来是新受限令牌句柄。

调用关系:它是多能力、只读场景的简单入口。底层仍由 create_token_with_caps_from 负责拼限制列表、调用 Windows 创建令牌并设置默认权限。

调用图:调用 1 个内部函数(create_token_with_caps_from)。

create_readonly_token_with_caps_and_user_from418–425 ↗
fn create_readonly_token_with_caps_and_user_from(
    base_token: HANDLE,
    psid_capabilities: &[*mut c_void],
) -> Result<HANDLE>

作用:创建一个带多个能力 SID、并额外包含令牌用户 SID 的只读受限令牌。它同样主要服务于使用专用沙箱账号的提升权限后端。

数据流:进去的是基础令牌句柄和能力 SID 列表。它先读取基础令牌的用户 SID,再把用户 SID 加入额外限制列表,最后调用 create_token_with_caps_from。出来是新受限令牌句柄。

调用关系:它和 create_workspace_write_token_with_caps_and_user_from 的结构很像:先让 get_user_sid_bytes 拿用户身份,再把所有材料交给 create_token_with_caps_from。区别主要在调用场景,而不是底层机制。

调用图:调用 2 个内部函数(create_token_with_caps_from, get_user_sid_bytes)。

create_token_with_caps_from427–483 ↗
fn create_token_with_caps_from(
    base_token: HANDLE,
    psid_capabilities: &[*mut c_void],
    extra_restricting_sids: &[*mut c_void],
) -> Result<HANDLE>

作用:这是本文件的核心工厂:用一个基础令牌和一组能力 SID,真正创建 Windows 受限令牌。它决定沙箱进程最终拿到的是一张“被削弱、被限制、但还能正常通信和运行”的通行证。

数据流:进去的是基础令牌句柄、能力 SID 列表,以及可选的额外限制 SID。它先拒绝空能力列表,再取登录 SID 和 Everyone SID,把能力 SID、额外 SID、登录 SID、Everyone SID 按固定顺序组成限制项。然后调用 CreateRestrictedToken 生成新令牌,给新令牌设置默认 DACL,最后启用必要的小权限。出来是新令牌句柄;调用者之后必须关闭它。

调用关系:多个外层函数都会把不同场景包装成参数后交给它,例如只读令牌、工作区可写令牌、是否额外加入用户 SID 等。它内部会调用 get_logon_sid_bytes、world_sid、set_default_dacl 和 enable_single_privilege,等于是把“取身份材料、创建受限令牌、修好默认访问权限、补上必要权限”这一整条流水线串起来。

调用图:调用 4 个内部函数(enable_single_privilege, get_logon_sid_bytes, set_default_dacl, world_sid);被 5 处调用(create_readonly_token_with_cap_from, create_readonly_token_with_caps_and_user_from, create_readonly_token_with_caps_from, create_workspace_write_token_with_caps_and_user_from, create_workspace_write_token_with_caps_from);外部调用 8 个(with_capacity, anyhow!, is_empty, iter, len, null, vec!, CreateRestrictedToken)。

windows-sandbox-rs/src/deny_read_acl.rs源码 ↗
domain_logicsandbox setup / policy sync

在 Windows 里,ACL(访问控制列表,可以理解成门口的权限名单)决定谁能读、写、进某个文件或目录。这个文件做的事,就是给沙箱用户加一条 deny-read ACE(拒绝读取的权限条目)。它先规划到底要给哪些路径加限制:用户写在配置里的原始路径要保留;如果这个路径已经存在,还要把它解析成真实路径一起处理,避免通过快捷入口、链接或重定向路径绕过去。真正应用时,如果路径还不存在,它会先创建成目录。这样沙箱程序不能先创建一个本来被禁止的路径,再马上读取它。还有一个重要细节:如果中途某个路径加权限失败,本次已经加上的权限会被撤掉,避免留下半截状态。整体上,它像是在开工前先列清单,开工时逐个上锁,出错就把刚上的锁都拆回去。

函数细节6
plan_deny_read_acl_paths18–28 ↗
fn plan_deny_read_acl_paths(paths: &[PathBuf]) -> Vec<PathBuf>

作用:这个函数先算出哪些路径应该被加上“禁止读取”的规则。它不只看用户配置的字面路径,也会在路径已经存在时加入它的真实规范路径,防止同一个文件通过另一条路被读到。

数据流:进去的是一组路径。函数逐个检查:先把原始路径放进计划清单;如果这个路径现在已经存在,就调用 canonicalize_path 把它变成系统眼里的真实路径,再也放进清单。每次加入前都会通过 push_planned_path 去重。出来的是一个不重复的路径列表,供后面真正加权限使用。

调用关系:apply_deny_read_acls 会先调用它来准备施工清单,然后才开始改 Windows 权限。测试 tests::plan_includes_existing_canonical_targets 也会调用它,确认已有路径确实会同时保留原路径和真实路径。

调用图:调用 2 个内部函数(push_planned_path, canonicalize_path);被 2 处调用(apply_deny_read_acls, plan_includes_existing_canonical_targets);外部调用 2 个(new, new)。

push_planned_path30–34 ↗
fn push_planned_path(planned: &mut Vec<PathBuf>, seen: &mut HashSet<String>, path: PathBuf)

作用:这个小函数负责把路径安全地放进清单,同时避免同一个路径重复出现。它把路径先转换成统一的比较用写法,再判断以前有没有见过。

数据流:进去的是一个待加入的路径、一个结果列表,以及一个记录“已经见过哪些路径”的集合。它调用 lexical_path_key 把路径转成统一的小写、斜杠格式的 key;如果这个 key 没出现过,就把原始 PathBuf 放进列表。出来没有单独返回值,但会修改传入的列表和集合。

调用关系:plan_deny_read_acl_paths 用它给规划清单去重,apply_deny_read_acls 也用它给最终返回的已处理路径去重。它把具体的“路径怎么比较才算同一个”交给 lexical_path_key。

调用图:调用 1 个内部函数(lexical_path_key);被 2 处调用(apply_deny_read_acls, plan_deny_read_acl_paths)。

lexical_path_key36–41 ↗
fn lexical_path_key(path: &Path) -> String

作用:这个函数把一个路径变成适合比较的文字 key,用来判断两个写法不同的路径是不是应该当成同一个。比如反斜杠和正斜杠、小写和大写、末尾斜杠差异都会被抹平。

数据流:进去的是一个 Path。它先把路径转成可显示的字符串,再把 Windows 常见的反斜杠换成正斜杠,去掉末尾多余的斜杠,最后统一变成小写。出来的是这个标准化后的字符串 key。

调用关系:push_planned_path 用它来做去重判断。sync_persistent_deny_read_acls 也会用它,说明别处在同步长期保存的拒读规则时,也需要用同一套路径比较办法,避免重复或漏判。

调用图:被 2 处调用(push_planned_path, sync_persistent_deny_read_acls);外部调用 1 个(to_string_lossy)。

apply_deny_read_acls51–80 ↗
fn apply_deny_read_acls(paths: &[PathBuf], psid: *mut c_void) -> Result<Vec<PathBuf>>

作用:这个函数真正把“禁止读取”的权限规则写到 Windows 文件系统上。它还负责兜底:路径不存在就先建出来;如果中途失败,就撤销这次已经加过的规则。

数据流:进去的是要禁止读取的一组路径,以及一个 psid 指针。SID 是 Windows 用来代表某个用户或身份的标识;这里的 psid 指向要被禁止读取的沙箱身份。函数先调用 plan_deny_read_acl_paths 得到完整清单。然后逐个路径处理:不存在就 create_dir_all 创建目录,存在或创建成功后调用 add_deny_read_ace 加拒读权限。如果某一步失败,它会对本次已经成功加过权限的路径调用 revoke_ace 撤回,然后返回错误。成功时返回实际处理过、去重后的路径列表。

调用关系:sync_persistent_deny_read_acls 会调用它,把持久化的拒读策略落实到文件系统。它内部先请 plan_deny_read_acl_paths 列清单,再用 add_deny_read_ace 改权限;出错时把清理工作交给 revoke_ace。因为它接收的是裸指针 psid,所以被标成 unsafe,意思是调用者必须保证这个指针真的有效。

调用图:调用 3 个内部函数(revoke_ace, plan_deny_read_acl_paths, push_planned_path);被 1 处调用(sync_persistent_deny_read_acls);外部调用 2 个(new, new)。

tests::plan_preserves_missing_paths91–99 ↗
fn plan_preserves_missing_paths()

作用:这个测试确认:即使一个被禁止读取的路径现在还不存在,规划函数也不能把它丢掉。否则沙箱程序可能之后创建这个路径并读取它。

数据流:测试先创建一个临时目录,再拼出一个还不存在的 future-secret.env 路径。它把这个路径交给 plan_deny_read_acl_paths,期待出来的列表仍然只包含这个缺失路径。测试不会改真实项目文件,只用临时目录做验证。

调用关系:它专门验证 plan_deny_read_acl_paths 对“未来可能出现的敏感路径”的处理。这个行为支撑 apply_deny_read_acls 后面“路径不存在就先创建再加权限”的安全策略。

调用图:外部调用 2 个(new, assert_eq!)。

tests::plan_includes_existing_canonical_targets102–118 ↗
fn plan_includes_existing_canonical_targets()

作用:这个测试确认:如果被禁止读取的路径已经存在,规划函数会同时考虑原始路径和系统解析后的真实路径。这样可以减少通过别名路径绕过限制的风险。

数据流:测试先创建临时目录和一个真实文件 secret.env,并写入一点内容。然后调用 plan_deny_read_acl_paths 得到规划结果,再用系统的 canonicalize 算出期望的真实路径。最后把两边都变成集合来比较,确认结果里既有原始路径,也有规范路径。

调用关系:它直接测试 plan_deny_read_acl_paths 的关键安全行为。这个测试和 tests::plan_preserves_missing_paths 一起覆盖了两类重要情况:路径不存在时不能漏掉,路径存在时要连真实目标一起保护。

调用图:调用 1 个内部函数(plan_deny_read_acl_paths);外部调用 5 个(new, assert_eq!, canonicalize, write, from_ref)。

windows-sandbox-rs/src/audit.rs源码 ↗
domain_logicstartup / preflight security audit

这个文件像进门前的安检员,专门检查 Windows 文件夹权限里的一个危险情况:Everyone,也就是“所有用户”,是否被允许写入某些目录。如果某个目录人人可写,沙箱里的程序可能借这个洞改文件,绕过原本的限制。它先收集一批最值得检查的位置,比如当前工作目录、临时目录、用户目录、PATH 里的工具目录,以及 C 盘和 Windows 目录。然后它只做快速扫描:限制每个目录最多看多少项,也限制总耗时,避免启动时卡很久。发现可疑目录后,它会记录日志。更进一步,如果当前权限模式适合用 Windows 沙箱强制执行,它会给这些危险目录加一条拒绝写入规则。这里的 ACE 是访问控制项,可以理解成权限表里的一行;deny ACE 就是明确写上“这个身份不准写”。有个重要细节:如果某个可疑目录本来就是允许写入的工作区,它不会乱加拒绝规则,避免把合法写入也封掉。

函数细节7
unique_push38–44 ↗
fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf)

作用:把一个路径加入候选列表,但只在它能变成真实绝对路径、并且之前没加过时才加入。这样可以避免同一个目录因为写法不同被重复检查。

数据流:进去的是一个“已见过路径”的集合、一个输出列表,以及一个待加入的路径。它先把路径正规化成系统认可的真实路径;如果成功,并且集合里还没有这个路径,就同时放进集合和列表。出来的是被更新的集合和列表;如果路径不存在或重复,就什么也不加。

调用关系:它是 gather_candidates 的小帮手。gather_candidates 每找到一个可能需要检查的目录,就交给 unique_push 过滤和去重,保证后面的审计扫描少走重复路。

调用图:被 1 处调用(gather_candidates);外部调用 1 个(canonicalize)。

gather_candidates46–81 ↗
fn gather_candidates(cwd: &Path, env: &std::collections::HashMap<String, String>) -> Vec<PathBuf>

作用:整理出一份“最值得检查权限”的目录清单。它不扫描整个硬盘,而是挑当前目录、临时目录、用户目录、PATH 目录和系统根目录这些高风险或常用位置。

数据流:进去的是当前工作目录和一份环境变量表。它按顺序读取当前目录、TEMP/TMP、USERPROFILE、PUBLIC、PATH,以及固定的 C:/ 和 C:/Windows;每个路径都交给 unique_push 做真实路径转换和去重。出来的是一个不重复的目录列表,供后续安全扫描使用。

调用关系:它被 audit_everyone_writable 调用,用来决定要查哪些地方。测试函数 tests::gathers_path_entries_by_list_separator 也会调用它,确认 PATH 里用分号分隔的多个目录能被正确拆出来。

调用图:调用 1 个内部函数(unique_push);被 2 处调用(audit_everyone_writable, gathers_path_entries_by_list_separator);外部调用 7 个(new, new, to_path_buf, from, new, split_paths, var_os)。

path_has_world_write_allow83–93 ↗
fn path_has_world_write_allow(path: &Path) -> Result<bool>

作用:检查某个路径是否允许“所有人”写入。这里的“所有人”指 Windows 里的 Everyone 身份,通常是权限过宽的信号。

数据流:进去的是一个文件夹路径。它先拿到 Everyone 的安全身份 SID,SID 可以理解成 Windows 用来识别用户或用户组的身份证号;然后组合出几种写入权限,比如写数据、追加数据、改属性;最后询问 ACL 权限表是否允许这些写操作。出来的是 true 或 false,表示这个路径是否有人人可写的权限。

调用关系:它是 audit_everyone_writable 内部实际做权限判断的核心。audit_everyone_writable 每看到一个候选目录,就通过它判断是否要把目录标记为危险。它把底层权限查询交给 path_mask_allows,把 Everyone 身份的生成交给 world_sid。

调用图:调用 2 个内部函数(path_mask_allows, world_sid)。

audit_everyone_writable95–218 ↗
fn audit_everyone_writable(
    cwd: &Path,
    env: &std::collections::HashMap<String, String>,
    logs_base_dir: Option<&Path>,
) -> Result<Vec<PathBuf>>

作用:执行一次快速审计,找出当前环境里“Everyone 可写”的目录,并把结果记到日志里。它的目标是在不拖慢启动的前提下,尽早发现会破坏沙箱隔离的权限漏洞。

数据流:进去的是当前工作目录、环境变量表,以及可选的日志目录。它先优先检查当前目录下面的一层子目录,因为工作区问题最常见;再调用 gather_candidates 拿到更广的候选目录;每个目录用 path_has_world_write_allow 检查权限;发现问题后用 canonical_path_key 做去重,并加入 flagged 列表。出来的是危险目录列表,同时会写一条成功或失败的审计日志;如果某些目录权限读不了,它会当作不可疑处理并写调试日志。

调用关系:它是本文件的主要扫描入口,被 apply_world_writable_scan_and_denies_for_permissions 调用。它自己负责挑目录、限时限量扫描、去重和记日志;真正的权限判断交给 path_has_world_write_allow,候选目录整理交给 gather_candidates。

调用图:调用 3 个内部函数(gather_candidates, log_note, canonical_path_key);被 1 处调用(apply_world_writable_scan_and_denies_for_permissions);外部调用 7 个(from_secs, new, now, new, new, format!, read_dir)。

apply_world_writable_scan_and_denies_for_permissions220–245 ↗
fn apply_world_writable_scan_and_denies_for_permissions(
    codex_home: &Path,
    cwd: &Path,
    env_map: &std::collections::HashMap<String, String>,
    permissions: &ResolvedWindowsSandboxPermiss

作用:把“扫描危险目录”和“必要时加拒绝写入规则”串起来。调用方只要用它,就能先查问题,再尽量自动补上防护。

数据流:进去的是 Codex 的主目录、当前工作目录、环境变量、已经解析好的沙箱权限设置,以及日志目录。它先调用 audit_everyone_writable 得到危险目录;如果没有危险目录,就直接结束;如果有,就调用 apply_capability_denies_for_world_writable_for_permissions 尝试给这些目录加拒绝写入规则。出来通常是成功返回;即使加规则失败,它也会记录日志而不是让整个流程崩掉。

调用关系:它是外部最可能调用的门面函数,负责把两段工作接起来:先审计,再修补。它调用 audit_everyone_writable 做检查,随后把危险路径交给 apply_capability_denies_for_world_writable_for_permissions 做权限加固。

调用图:调用 3 个内部函数(apply_capability_denies_for_world_writable_for_permissions, audit_everyone_writable, log_note);外部调用 1 个(format!)。

apply_capability_denies_for_world_writable_for_permissions247–312 ↗
fn apply_capability_denies_for_world_writable_for_permissions(
    codex_home: &Path,
    flagged: &[PathBuf],
    permissions: &ResolvedWindowsSandboxPermissions,
    cwd: &Path,
    env_map: &std::c

作用:对已经发现的人人可写目录,按当前沙箱权限策略加上“能力身份不准写”的规则。这里的能力身份可以理解成沙箱里某类权限的专用身份证。

数据流:进去的是 Codex 主目录、危险路径列表、权限设置、当前目录、环境变量和日志目录。它先确保 Codex 主目录存在,并加载或创建能力 SID 文件;如果当前权限不能靠 Windows 沙箱强制执行,就只保存 SID 信息后退出。否则它根据权限模式决定要限制哪些 SID:如果当前工作区使用写入能力,就计算实际允许写的根目录并生成对应 SID;否则使用只读 SID。接着它遍历每个危险目录:如果这个目录属于合法写入工作区,就跳过;否则调用 add_deny_write_ace 添加拒绝写入规则,并把成功或失败写入日志。

调用关系:它被 apply_world_writable_scan_and_denies_for_permissions 在发现危险目录后调用。它连接了多个子系统:cap 模块提供能力 SID,setup 模块计算可写根目录,acl 模块真正修改 Windows 权限表,logging 模块记录发生了什么。

调用图:调用 7 个内部函数(add_deny_write_ace, cap_sid_file, load_or_create_cap_sids, log_note, is_enforceable_by_windows_sandbox, uses_write_capabilities_for_cwd, effective_write_roots_for_permissions);被 1 处调用(apply_world_writable_scan_and_denies_for_permissions);外部调用 7 个(is_empty, new, format!, to_string, create_dir_all, write, vec!)。

tests::gathers_path_entries_by_list_separator321–349 ↗
fn gathers_path_entries_by_list_separator()

作用:这是一个测试,确认 gather_candidates 能把 PATH 环境变量里的多个目录正确拆开,尤其是包含空格的目录也不能漏掉。

数据流:进去的是测试临时创建的一组目录和一份假的环境变量表。它创建 Tools、Bin、Program Files 三个目录,把它们用 Windows 常见的分号拼成 PATH,然后调用 gather_candidates。出来的是断言结果:候选目录里必须包含这三个真实路径,否则测试失败。

调用关系:它只在测试时运行,用来保护 gather_candidates 的行为不被以后改坏。它不参与真实沙箱启动流程,但能保证路径收集这一步在 Windows 风格 PATH 下可靠。

调用图:调用 1 个内部函数(gather_candidates);外部调用 5 个(new, assert!, format!, create_dir_all, tempdir)。

windows-sandbox-rs/src/hide_users.rs源码 ↗
domain_logic用户创建后,以及沙盒用户首次运行命令并生成用户目录时

沙盒运行时可能会创建一些专用 Windows 用户。如果不处理,这些用户可能出现在 Windows 登录界面,用户目录也会像普通账号一样显示在磁盘里,看起来很乱。这个文件做两件事:第一,把用户名写进 Windows 的 Winlogon UserList 注册表位置,并设成 0,意思是“不要在登录界面列出这个账号”。注册表可以理解成 Windows 的系统设置库。第二,在当前沙盒用户真正登录、用户目录被 Windows 创建出来以后,把这个目录加上“隐藏”和“系统”属性,让资源管理器默认不显示它。这里很多操作都是“尽力而为”:失败了会写日志,但不会让整个沙盒流程崩掉,因为隐藏用户只是清理体验,不是核心运行条件。

函数细节5
hide_newly_created_users26–36 ↗
fn hide_newly_created_users(usernames: &[String], log_base: &Path)

作用:这个函数在沙盒创建完新用户后调用,用来尝试把这些用户从 Windows 登录界面藏起来。如果没有用户要处理,它会直接什么都不做。

数据流:进去的是一组用户名和日志目录 → 它先检查用户名列表是不是空的,不空就交给 hide_users_in_winlogon 去改 Windows 注册表 → 如果修改过程整体失败,它不会抛出错误,而是把失败原因写进日志,流程继续往下走。

调用关系:它是隐藏新用户这条流程的外层入口。真正改注册表的活儿交给 hide_users_in_winlogon;如果那边失败,它用 log_note 留下一条说明,方便之后排查。

调用图:调用 2 个内部函数(hide_users_in_winlogon, log_note);外部调用 1 个(format!)。

hide_current_user_profile_dir43–74 ↗
fn hide_current_user_profile_dir(log_base: &Path)

作用:这个函数用来隐藏当前沙盒用户的个人目录,也就是类似 C:\Users\某个用户 这样的文件夹。它必须等用户真的登录过以后才有用,因为 Windows 通常是在第一次登录时才创建这个目录。

数据流:进去的是日志目录 → 它从环境变量 USERPROFILE 里读取当前用户目录位置 → 如果读不到或目录还不存在,就直接结束 → 如果目录存在,就调用 hide_directory 给目录加隐藏属性和系统属性 → 成功改动时写一条日志,失败时也写日志说明原因。

调用关系:它通常运行在命令执行器里,也就是已经以沙盒用户身份登录后。它把具体的文件属性修改交给 hide_directory,自己负责判断当前用户目录在哪里、是否存在,以及是否需要记录日志。

调用图:调用 2 个内部函数(hide_directory, log_note);外部调用 3 个(from, format!, var_os)。

hide_users_in_winlogon76–105 ↗
fn hide_users_in_winlogon(usernames: &[String], log_base: &Path) -> anyhow::Result<()>

作用:这个函数真正去改 Windows 登录相关的注册表,让指定用户名不要显示在登录界面。可以把它理解成把一串名字写进 Windows 的“隐藏名单”。

数据流:进去的是用户名列表和日志目录 → 它先调用 create_userlist_key 打开或创建 Winlogon 的 UserList 注册表项 → 然后逐个用户名写入一个数值 0,表示这个账号不在登录界面显示 → 某个用户名写入失败时,它记录日志但继续处理后面的用户名 → 最后关闭注册表句柄,返回成功或打开注册表时的错误。

调用关系:它由 hide_newly_created_users 调用,是这条隐藏登录用户流程的核心步骤。它自己不负责决定要不要执行,只负责拿到用户名后逐个写注册表;打开注册表的细节交给 create_userlist_key,失败记录交给 log_note。

调用图:调用 3 个内部函数(create_userlist_key, log_note, to_wide);被 1 处调用(hide_newly_created_users);外部调用 5 个(new, format!, size_of_val, RegCloseKey, RegSetValueExW)。

create_userlist_key107–130 ↗
fn create_userlist_key() -> anyhow::Result<HKEY>

作用:这个函数负责打开或创建 Windows 里专门控制登录界面用户显示的注册表位置。没有这个位置,后面就没地方写“隐藏这个用户”的设置。

数据流:进去不需要业务参数 → 它把固定的注册表路径转换成 Windows API 需要的宽字符格式 → 调用 RegCreateKeyExW 在 HKEY_LOCAL_MACHINE 下打开或创建这个键,并要求写入权限 → 成功时返回一个注册表句柄,失败时返回带有 Windows 错误说明的错误。

调用关系:它只被 hide_users_in_winlogon 使用,属于底层准备工作。hide_users_in_winlogon 需要先拿到它返回的句柄,才能继续给每个用户名写入隐藏标记。

调用图:调用 1 个内部函数(to_wide);被 1 处调用(hide_users_in_winlogon);外部调用 3 个(anyhow!, null_mut, RegCreateKeyExW)。

hide_directory133–158 ↗
fn hide_directory(path: &Path) -> anyhow::Result<bool>

作用:这个函数给指定文件夹加上 Windows 的“隐藏”和“系统”属性,让它在普通资源管理器设置下不容易被看到。它会判断是否真的改过,避免重复记录无意义的操作。

数据流:进去的是一个目录路径 → 它先把路径转换成 Windows API 需要的宽字符格式 → 读取当前文件属性 → 如果读取失败就返回错误 → 如果已经有隐藏和系统属性,就返回 false 表示没改动 → 否则写回新的属性 → 写成功返回 true,写失败返回带错误说明的错误。

调用关系:它被 hide_current_user_profile_dir 调用,专门负责实际修改目录属性。外层函数负责找到当前用户目录和写日志;这个函数只关心一个路径能不能被成功隐藏。

调用图:调用 1 个内部函数(to_wide);被 1 处调用(hide_current_user_profile_dir);外部调用 4 个(anyhow!, GetLastError, GetFileAttributesW, SetFileAttributesW)。

windows-sandbox-rs/src/bin/setup_main/win/setup_runtime_bin.rs源码 ↗
orchestrationstartup/setup

Codex 桌面版会把一些随软件带来的 Windows 可执行文件,从系统安装位置复制到用户的 LocalAppData 目录下,比如 OpenAI/Codex/bin。沙盒里的进程要用这些文件时,不能只看文件在不在,还要看 Windows 权限允不允许它读和执行。这个文件做的事就像给仓库门禁补一张通行卡:先找到这个缓存目录;目录不存在就什么也不做;存在的话,先检查沙盒用户组是否已经有“读取”和“执行”的权限。如果检查权限本身出错,它会把错误记下来并写日志,但不会立刻中断整个安装流程。若发现权限不够,它会给这个目录加一条 ACE(访问控制项,也就是 Windows 权限清单里的一条规则),并让这条规则继承到里面的文件和子目录。这样后续沙盒运行 Codex 相关二进制文件时,就不容易因为权限被拒绝而失败。

函数细节1
ensure_codex_app_runtime_bin_readable13–92 ↗
fn ensure_codex_app_runtime_bin_readable(
    sandbox_group_psid: *mut c_void,
    refresh_errors: &mut Vec<String>,
    log: &mut dyn Write,
) -> Result<()>

作用:这个函数确保沙盒用户组能读取并执行 Codex 本地缓存里的 bin 目录。有人会在完整安装或刷新沙盒权限时调用它,避免之后运行程序时被 Windows 权限挡住。

数据流:输入是一份沙盒用户组的 Windows 安全标识指针、一个用来收集错误文字的列表、以及一个日志输出口。它先从环境变量里找 LocalAppData,找不到就退回到 USERPROFILE/AppData/Local;再拼出 OpenAI/Codex/bin 这个目录。目录不存在时直接结束。目录存在时,它检查这个目录对沙盒用户组是否同时允许读取和执行;如果已经允许,就不改任何东西。若不允许,它写一条日志,然后尝试给目录加上读取和执行权限,并让权限传给子文件和子目录。最后无论授权失败与否,函数本身都会正常返回;失败信息会被写进错误列表和日志。

调用关系:它是在 run_setup_full 这个完整设置流程中被调用的一个步骤。它自己不负责整个设置流程,只负责 runtime bin 目录这一块权限。检查权限时它把活交给 path_mask_allows;需要补权限时交给 ensure_allow_mask_aces_with_inheritance;所有重要情况会通过上层的 log_line 写入日志,方便之后排查。

调用图:被 1 处调用(run_setup_full);外部调用 5 个(ensure_allow_mask_aces_with_inheritance, path_mask_allows, format!, var_os, log_line)。

windows-sandbox-rs/src/bin/setup_main/win/firewall.rs源码 ↗
domain_logicstartup / sandbox setup

这个文件做的是 Windows 防火墙里的“关门”和“留小门”。它通过 Windows 的 COM 接口(Component Object Model,Windows 提供给程序控制系统功能的一套老式接口)创建或更新防火墙规则。规则只套在指定的离线用户 SID(Windows 用来唯一标识用户的编号)上,而不是影响整台电脑。主要有两类规则:一类挡住非本机地址的外连;另一类挡住本机回环地址,也就是 127.0.0.1、::1 这类“自己连自己”的地址,但可以把代理端口排除掉。它还会检查本机防火墙策略是否真的生效,避免组策略之类的设置让规则写了也没用。创建规则时会先配好再加入,并且每次运行都重新写一遍,像修门锁时顺手检查螺丝,保证重复安装不会留下旧规则或半成品规则。

函数细节16
ensure_offline_proxy_allowlist55–154 ↗
fn ensure_offline_proxy_allowlist(
    offline_sid: &str,
    proxy_ports: &[u16],
    allow_local_binding: bool,
    log: &mut dyn Write,
) -> Result<()>

作用:给离线沙箱配置“只能走指定本机代理端口”的防火墙例外和阻断规则。它用来防止沙箱绕过代理访问本机其他服务,同时允许必要的代理通道存在。

数据流:输入是离线用户的 SID、允许的代理端口列表、是否允许本地绑定的开关,以及日志输出口。它先把 SID 包成 Windows 防火墙能理解的用户范围,初始化 COM,拿到防火墙策略和规则列表;如果允许本地绑定,就删除旧的代理相关阻断规则;否则就添加 UDP 回环阻断、TCP 回环阻断,再把 TCP 规则收窄成“除了代理端口以外都挡住”。最后关闭 COM,并把成功或失败返回给调用者。

调用关系:它由 configure_offline_sandbox_network 在配置离线沙箱网络时调用,是网络隔离设置里的“本机代理小门”部分。它会借助删除旧规则、创建阻断规则、计算端口补集、写日志等内部零件,把一堆 Windows 防火墙细节包装成一个高层动作。

调用图:调用 1 个内部函数(new);被 1 处调用(configure_offline_sandbox_network);外部调用 4 个(new, format!, CoInitializeEx, CoUninitialize)。

ensure_offline_outbound_block156–206 ↗
fn ensure_offline_outbound_block(offline_sid: &str, log: &mut dyn Write) -> Result<()>

作用:给离线沙箱用户安装一条外连阻断规则,挡住发往非本机地址的出站网络流量。它是“离线”这个承诺真正落地的关键保护。

数据流:输入是离线用户 SID 和日志输出口。它把 SID 转成防火墙用户范围,初始化 COM,打开 Windows 防火墙策略,确认本地规则会生效,然后取得规则集合,创建或更新一条阻断所有出站 IP 协议、目标地址为非回环地址的规则。做完后关闭 COM,返回成功或带错误码的失败。

调用关系:它由 configure_offline_sandbox_network 调用,负责离线网络配置中的“大门上锁”。具体写规则的活儿交给 ensure_block_rule,策略是否有效则交给 ensure_local_policy_rules_take_effect 检查。

调用图:调用 1 个内部函数(new);被 1 处调用(configure_offline_sandbox_network);外部调用 4 个(new, format!, CoInitializeEx, CoUninitialize)。

remove_rule_if_present208–224 ↗
fn remove_rule_if_present(
    rules: &INetFwRules,
    internal_name: &str,
    log: &mut dyn Write,
) -> Result<()>

作用:如果某条指定名字的防火墙规则存在,就把它删掉。它用来清理旧版本或旧模式留下的规则,避免新旧规则互相打架。

数据流:输入是防火墙规则集合、规则内部名字和日志输出口。它先按名字查规则;查到了就调用 Windows 防火墙删除它,并写一行日志;没查到就什么也不改。输出是成功,或者删除失败时返回带说明的错误。

调用关系:它被代理白名单配置流程用来切换模式时清场。删除成功后会把记录交给 log_line 写进日志,让后续排查知道发生过什么。

调用图:调用 1 个内部函数(log_line);外部调用 4 个(from, Item, Remove, format!)。

ensure_local_policy_rules_take_effect226–235 ↗
fn ensure_local_policy_rules_take_effect(policy: &INetFwPolicy2) -> Result<()>

作用:确认当前电脑允许本地程序改防火墙规则,并且这些规则真的会生效。它防止出现“代码写了规则,但 Windows 根本不执行”的假安全。

数据流:输入是 Windows 防火墙策略对象。它通过底层接口询问 Windows:本地防火墙修改现在是否有效;拿到返回码和状态后,交给 validate_local_policy_modify_result 判断。输出是成功,或者返回“策略访问失败/策略无效”的错误。

调用关系:它位于创建规则之前,是一道安全检查门。上层设置出站阻断或代理阻断时,应该先过它,再继续拿规则列表和写规则。

调用图:调用 1 个内部函数(validate_local_policy_modify_result);外部调用 3 个(default, as_raw, vtable)。

validate_local_policy_modify_result237–270 ↗
fn validate_local_policy_modify_result(
    result: windows::core::HRESULT,
    modify_state: NET_FW_MODIFY_STATE,
) -> Result<()>

作用:把 Windows 关于“本地防火墙规则是否生效”的回答翻译成项目自己的成功或失败。它把晦涩的 HRESULT 和状态值变成明确的安装错误。

数据流:输入是 Windows 返回的 HRESULT(一次系统调用的成败码)和修改状态。它先看系统调用本身有没有失败;再看结果是否覆盖所有当前网络配置;最后看状态是否明确表示可以修改。只有全部满足才返回成功,否则返回带 SetupErrorCode 的失败。

调用关系:ensure_local_policy_rules_take_effect 会把 Windows 查询结果交给它判断。测试函数 local_policy_modify_state_rejects_ineffective_policy 和 local_policy_modify_state_rejects_partial_profile_coverage 也直接调用它,确保它不会把危险状态误判为安全。

调用图:调用 1 个内部函数(new);被 3 处调用(ensure_local_policy_rules_take_effect, local_policy_modify_state_rejects_ineffective_policy, local_policy_modify_state_rejects_partial_profile_coverage);外部调用 3 个(is_err, new, format!)。

ensure_block_rule272–327 ↗
fn ensure_block_rule(
    rules: &INetFwRules,
    spec: &BlockRuleSpec<'_>,
    log: &mut dyn Write,
) -> Result<()>

作用:确保某条阻断型防火墙规则存在,而且内容是项目想要的样子。它让安装过程可以重复执行,不会因为规则已经存在就出错或留下旧配置。

数据流:输入是防火墙规则集合、一份规则规格 BlockRuleSpec,以及日志输出口。它按内部名字查找规则;有就复用并转成新版规则接口,没有就新建一条,先配置好再加入防火墙。随后无论新旧都会重新配置一遍,最后把规则名、协议、地址、端口、用户范围写入日志。输出是成功或创建、添加、转换失败的错误。

调用关系:它是本文件写防火墙规则的核心工人。上层的离线外连阻断和代理回环阻断都会把具体需求交给它;它再调用 configure_rule 设置规则内容,并调用 log_line 记录结果。

调用图:调用 2 个内部函数(configure_rule, log_line);外部调用 5 个(from, Add, Item, format!, CoCreateInstance)。

configure_rule329–390 ↗
fn configure_rule(rule: &INetFwRule3, spec: &BlockRuleSpec<'_>) -> Result<()>

作用:把一条防火墙规则设置成“启用的、出站的、阻断的、适用于所有网络配置,并且只作用于指定用户”。它负责把规则从空壳变成真正能保护沙箱的规则。

数据流:输入是一条 Windows 防火墙规则对象和规则规格。它设置说明文字、方向、动作、启用状态、适用配置,再让 configure_rule_network_scope 设置协议、远端地址和端口;然后写入用户范围。最后它会读回用户范围,确认里面真的包含离线用户 SID。输出是成功,或者任何设置/校验失败的错误。

调用关系:它由 ensure_block_rule 调用,是规则配置的主要步骤。它把网络范围细节下放给 configure_rule_network_scope,自己额外负责规则的通用属性和用户范围校验。

调用图:调用 2 个内部函数(configure_rule_network_scope, new);被 1 处调用(ensure_block_rule);外部调用 10 个(from, LocalUserAuthorizedList, SetAction, SetDescription, SetDirection, SetEnabled, SetLocalUserAuthorizedList, SetProfiles, new, format!)。

configure_rule_network_scope392–420 ↗
fn configure_rule_network_scope(rule: &INetFwRule3, spec: &BlockRuleSpec<'_>) -> Result<()>

作用:设置防火墙规则要挡哪种网络流量:协议、远端地址、远端端口。简单说,它决定这条规则堵的是哪条路。

数据流:输入是防火墙规则对象和规则规格。它把协议写进规则,把远端地址写成指定范围;如果规格里有端口范围,也把远端端口写进去。输出是成功,或者 Windows 拒绝这些网络范围时返回错误。

调用关系:configure_rule 会调用它来完成网络范围部分。测试 production_firewall_rule_network_scopes_are_accepted_by_firewall_com 也会间接验证这些范围字符串是 Windows 防火墙能接受的。

调用图:被 1 处调用(configure_rule);外部调用 4 个(from, SetProtocol, SetRemoteAddresses, SetRemotePorts)。

blocked_loopback_tcp_remote_ports422–453 ↗
fn blocked_loopback_tcp_remote_ports(proxy_ports: &[u16]) -> Option<String>

作用:根据允许的代理端口,算出所有应该被阻断的 TCP 回环端口范围。它的作用是把“只放行这几个端口”翻译成防火墙需要的“挡住其他所有端口”。

数据流:输入是一组代理端口。它去掉 0 端口,排序,去重,然后从 1 到 65535 扫一遍,把不在允许列表里的连续端口合并成范围字符串。若没有需要阻断的端口就返回 None;否则返回类似“1-8079,8081-65535”的字符串。

调用关系:代理只允许指定端口时,上层配置会用它生成 TCP 阻断规则的端口范围。测试 production_firewall_rule_network_scopes_are_accepted_by_firewall_com 也调用它,确认生成的端口范围能被 Windows 防火墙接受。

调用图:调用 1 个内部函数(port_range_string);被 1 处调用(production_firewall_rule_network_scopes_are_accepted_by_firewall_com);外部调用 2 个(new, from)。

port_range_string455–461 ↗
fn port_range_string(start: u32, end: u32) -> String

作用:把一个端口或一段连续端口变成防火墙能读的文字。它是端口范围格式化的小工具。

数据流:输入是起始端口和结束端口。如果两者相同,就输出单个数字;如果不同,就输出“起点-终点”这种范围。它不改动外部状态,只返回字符串。

调用关系:blocked_loopback_tcp_remote_ports 在拼接阻断端口范围时调用它。它像给端口清单做排版的小助手。

调用图:被 1 处调用(blocked_loopback_tcp_remote_ports);外部调用 1 个(format!)。

log_line463–467 ↗
fn log_line(log: &mut dyn Write, msg: &str) -> Result<()>

作用:给日志写一行带时间戳的消息。它让安装防火墙规则时发生的关键动作有迹可查。

数据流:输入是一个可写日志的对象和消息文字。它取当前 UTC 时间,转成标准时间字符串,再写成“[时间] 消息”的一行。输出是成功,或者写日志失败的错误。

调用关系:ensure_block_rule 配好规则后会用它记录规则内容,remove_rule_if_present 删除旧规则后也会用它记录清理动作。

调用图:被 2 处调用(ensure_block_rule, remove_rule_if_present);外部调用 2 个(now, writeln!)。

tests::configured_remote_address_literals_are_accepted_by_firewall_com478–507 ↗
fn configured_remote_address_literals_are_accepted_by_firewall_com()

作用:测试文件里写死的远端地址范围,Windows 防火墙 COM 接口是否真的接受。它防止开发者写出看起来合理、实际被 Windows 拒绝的地址表达式。

数据流:测试启动 COM,准备几个候选地址字符串,包括回环地址、非回环地址和通配符。它逐个创建临时防火墙规则,把地址写进去再读出来,看是否成功。最后关闭 COM,并用断言报告失败。

调用关系:这是面向真实 Windows 防火墙接口的测试,保护常量 LOOPBACK_REMOTE_ADDRESSES 和 NON_LOOPBACK_REMOTE_ADDRESSES 不被改坏。它不参与生产运行,只在测试时给配置字符串把关。

调用图:外部调用 3 个(assert!, CoInitializeEx, CoUninitialize)。

tests::production_firewall_rule_network_scopes_are_accepted_by_firewall_com510–571 ↗
fn production_firewall_rule_network_scopes_are_accepted_by_firewall_com()

作用:测试生产环境会用到的几种防火墙网络范围配置,Windows 是否全部接受。它比只测地址更完整,还覆盖协议和端口。

数据流:测试启动 COM,构造三种规则规格:UDP 回环阻断、TCP 回环阻断但排除代理端口、非回环全部阻断。它为每个规格创建临时规则,并调用 configure_rule_network_scope 写入协议、地址和端口。最后关闭 COM,并断言每个规格都没有被 Windows 拒绝。

调用关系:它调用 blocked_loopback_tcp_remote_ports 生成端口补集,并复用生产代码 configure_rule_network_scope。这样测试的不是另一套模拟逻辑,而是真正将来会跑的配置代码。

调用图:调用 1 个内部函数(blocked_loopback_tcp_remote_ports);外部调用 3 个(assert!, CoInitializeEx, CoUninitialize)。

tests::local_policy_modify_state_accepts_effective_policy574–576 ↗
fn local_policy_modify_state_accepts_effective_policy()

作用:测试当 Windows 明确说本地防火墙规则会生效时,校验函数应该放行。它验证正常路径不会被误拦。

数据流:输入是测试里构造的成功返回码 S_OK 和状态 NET_FW_MODIFY_STATE_OK。它调用校验逻辑并断言结果是成功。它不改动系统状态。

调用关系:这是 validate_local_policy_modify_result 的正向测试。它保证防火墙策略有效时,安装流程可以继续往下写规则。

调用图:外部调用 1 个(assert!)。

tests::local_policy_modify_state_rejects_ineffective_policy579–590 ↗
fn local_policy_modify_state_rejects_ineffective_policy()

作用:测试当 Windows 表示本地规则被组策略覆盖、不会生效时,校验函数必须报错。它防止沙箱在规则失效时还误以为自己安全。

数据流:输入是成功的系统调用返回码 S_OK,但状态是 NET_FW_MODIFY_STATE_GP_OVERRIDE,意思是组策略覆盖。测试调用 validate_local_policy_modify_result,期望得到错误,并检查错误码是 HelperFirewallPolicyIneffective。

调用关系:它直接验证 validate_local_policy_modify_result 的危险路径。这个测试对应真实场景:公司或系统策略可能让本地防火墙修改无效。

调用图:调用 1 个内部函数(validate_local_policy_modify_result);外部调用 1 个(assert_eq!)。

tests::local_policy_modify_state_rejects_partial_profile_coverage593–604 ↗
fn local_policy_modify_state_rejects_partial_profile_coverage()

作用:测试当 Windows 的回答只覆盖部分当前网络配置时,校验函数也要拒绝。它避免某些网络配置下规则生效、另一些不生效的半安全状态。

数据流:输入是 S_FALSE 和 NET_FW_MODIFY_STATE_OK。虽然状态看起来是 OK,但 S_FALSE 表示不是所有当前配置都确认有效;测试期望函数返回错误,并检查错误码是 HelperFirewallPolicyIneffective。

调用关系:它同样直接测试 validate_local_policy_modify_result。它确保安装流程不会接受“只在部分网络环境里安全”的防火墙设置。

调用图:调用 1 个内部函数(validate_local_policy_modify_result);外部调用 1 个(assert_eq!)。

windows-sandbox-rs/src/wfp/filter_specs.rs源码 ↗
configstartup / sandbox network policy setup

这个文件不执行操作,而是把要安装到 Windows Filtering Platform(简称 WFP,Windows 自带的底层防火墙/网络过滤系统)里的规则提前写成固定清单。可以把它理解成一张门卫值班表:每条 FilterSpec 都说明“这条规则叫什么、唯一编号是什么、放在哪个网络检查点上、满足什么条件就拦”。ConditionSpec 表示规则条件,比如只针对沙箱用户、只针对某种协议,或者只针对远端端口。清单里覆盖了 IPv4 和 IPv6 两套网络,拦截 ICMP(常见于 ping)、DNS 的 53 端口、DNS-over-TLS 的 853 端口,以及 SMB 的 445/139 端口。每条规则都有固定 GUID(全局唯一编号),这样程序以后添加、查找、删除这些规则时不会认错。文件中特意没有加入 NAME_RESOLUTION_CACHE 相关规则,因为普通静态规则在 Windows 校验时会报越界错误。

启动准备与提权运行器接线

这些文件将已解析的沙盒权限转换为可启动状态,并实现用于特权执行路径的提权辅助运行器传输。

windows-sandbox-rs/src/spawn_prep.rs源码 ↗
orchestrationspawn preparation

启动一个受限制的命令,不能只是把程序直接跑起来。这个文件做的事像进考场前检查证件、座位、能带什么东西。它先把用户给的权限配置翻译成 Windows 能执行的规则;再整理环境变量,比如让分页器别卡住交互、按需要继承 PATH、把空设备路径修正好、可选地把当前目录加入 Git 的安全目录;然后创建沙箱日志目录。对于旧式启动方式,它还会准备 Windows 的安全令牌(可以理解成“临时身份证”)和 ACL(访问控制列表,也就是文件夹门口的允许/禁止牌),决定哪些目录可读、哪些目录可写、哪些目录要额外禁止。对于提权启动方式,它会准备一个专门的沙箱用户账号和对应能力 SID(Windows 用来标识某种权限的身份编号)。这个文件重要的地方在于:真正的命令还没启动,它就已经把“能碰哪里、不能碰哪里”定好了。

函数细节21
prepare_spawn_context_common82–120 ↗
fn prepare_spawn_context_common(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    cwd: &Path,
    env_map: &mut HashMap<String, String>,

作用:做所有启动方式都会用到的通用准备。它把权限配置解析好,整理环境变量,建好日志目录,并判断这次命令是否要用“按目录授予写权限”的模式。

数据流:输入权限配置、工作区根目录、Codex 主目录、当前目录、环境变量表、命令和选项 → 它把权限配置变成内部可执行的权限对象,修改环境变量,确保目录存在,开启日志记录 → 输出一个 SpawnContext,同时环境变量表会被原地更新。

调用关系:它是底层公共准备步骤。prepare_legacy_spawn_context 会先调用它再补上旧式网络封锁;测试 common_spawn_env_keeps_network_env_unchanged 也直接用它确认通用准备不会误改网络代理。

调用图:调用 7 个内部函数(ensure_non_interactive_pager, inherit_path_env, normalize_null_device_env, log_start, try_from_permission_profile_for_workspace_roots, ensure_codex_home_exists, inject_git_safe_directory);被 2 处调用(prepare_legacy_spawn_context, common_spawn_env_keeps_network_env_unchanged);外部调用 3 个(join, to_path_buf, create_dir_all)。

prepare_legacy_spawn_context122–144 ↗
fn prepare_legacy_spawn_context(
    permission_profile: &PermissionProfile,
    workspace_roots: &[AbsolutePathBuf],
    codex_home: &Path,
    cwd: &Path,
    env_map: &mut HashMap<String, String>,

作用:为旧式 Windows 沙箱启动方式准备基础上下文。它在通用准备之外,如果权限要求断网,就把环境变量改成“无法联网”的状态。

数据流:输入权限、目录、环境变量、命令和选项 → 先交给 prepare_spawn_context_common 做通用准备,再检查是否要禁止网络 → 输出 SpawnContext,并可能把代理等环境变量改成阻断联网的值。

调用关系:它被旧式沙箱启动流程和带文件系统覆盖的运行流程调用。它把通用准备接过来,再把旧式启动特有的断网处理补上。

调用图:调用 2 个内部函数(apply_no_network_to_env, prepare_spawn_context_common);被 3 处调用(legacy_spawn_env_applies_offline_network_rewrite, spawn_windows_sandbox_session_legacy, run_windows_sandbox_capture_with_filesystem_overrides)。

prepare_legacy_session_security146–181 ↗
fn prepare_legacy_session_security(
    uses_write_capabilities: bool,
    codex_home: &Path,
    cwd: &Path,
    capability_roots: impl IntoIterator<Item = PathBuf>,
) -> Result<LegacySessionSecurity

作用:为旧式沙箱会话准备 Windows 安全身份。简单说,就是给即将启动的命令办一张受限制的“临时通行证”。

数据流:输入是否使用写能力、Codex 主目录、当前目录和可写根目录列表 → 它加载或创建能力 SID;如果是工作区写模式,就为每个可写根目录生成 SID 并创建带这些能力的令牌;否则创建只读能力令牌 → 输出 LegacySessionSecurity,里面有令牌句柄和后续改 ACL 要用的 SID。

调用关系:旧式启动流程在真正创建进程前会调用它。它会调用 root_capability_sids 找到目录对应的能力 SID,并调用 token 模块创建最终的受限令牌。

调用图:调用 6 个内部函数(load_or_create_cap_sids, root_capability_sids, from_string, create_readonly_token_with_cap, create_workspace_write_token_with_caps_from, get_current_token_for_restriction);被 2 处调用(spawn_windows_sandbox_session_legacy, run_windows_sandbox_capture_with_filesystem_overrides);外部调用 3 个(new, bail!, CloseHandle)。

legacy_session_capability_roots183–204 ↗
fn legacy_session_capability_roots(
    permissions: &ResolvedWindowsSandboxPermissions,
    current_dir: &Path,
    env_map: &HashMap<String, String>,
    codex_home: &Path,
) -> Vec<PathBuf>

作用:算出旧式会话里哪些目录需要对应的能力 SID。它解决的问题是:只给真正有效的可写目录发“通行证”,不要把 Codex 内部目录等不该写的地方也放进去。

数据流:输入已解析权限、当前目录、环境变量和 Codex 主目录 → 先算出允许访问的路径;如果当前模式需要写能力,就进一步筛出实际有效的可写根目录;否则直接使用允许路径 → 输出目录列表。

调用关系:旧式启动、旧式预检查和相关测试都会用它。它把权限计算模块和后续 SID 创建步骤连接起来。

调用图:调用 3 个内部函数(compute_allow_paths_for_permissions, uses_write_capabilities_for_cwd, effective_write_roots_for_permissions);被 5 处调用(legacy_capability_roots_use_effective_write_roots, legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root, spawn_windows_sandbox_session_legacy, run_windows_sandbox_capture_with_filesystem_overrides, run_windows_sandbox_legacy_preflight)。

root_capability_sids206–222 ↗
fn root_capability_sids(
    codex_home: &Path,
    cwd: &Path,
    allow_paths: impl IntoIterator<Item = PathBuf>,
) -> Result<Vec<RootCapabilitySid>>

作用:把一批可写根目录转换成对应的能力 SID。能力 SID 可以理解成 Windows 识别“这个目录写权限”的特殊身份牌。

数据流:输入 Codex 主目录、当前目录和一批允许路径 → 它先按规范路径排序并去重,再为每个根目录生成 SID 字符串,并转成可传给 Windows API 的 LocalSid → 输出 RootCapabilitySid 列表。

调用关系:prepare_legacy_session_security 和 prepare_elevated_spawn_context_for_permissions 都依赖它来生成写权限身份。测试也用它确认只包含当前真正启用的目录,不带旧的、无关的 SID。

调用图:调用 2 个内部函数(workspace_write_cap_sid_for_root, from_string);被 5 处调用(prepare_elevated_spawn_context_for_permissions, prepare_legacy_session_security, legacy_deny_path_includes_nested_active_root_sid, root_capability_sids_only_include_active_roots, run_windows_sandbox_legacy_preflight);外部调用 2 个(into_iter, with_capacity)。

matching_root_capability224–232 ↗
fn matching_root_capability(
    path: &Path,
    root_sids: &'a [RootCapabilitySid],
) -> Option<&'a RootCapabilitySid>

作用:给某个路径找最合适的可写根目录 SID。比如一个文件同时在大目录和小目录里,它会选更具体的那个小目录。

数据流:输入一个路径和一组 RootCapabilitySid → 它筛出包含这个路径的根目录,再选匹配范围最具体的一个 → 输出匹配到的 RootCapabilitySid,或者没有匹配时输出空。

调用关系:apply_legacy_session_acl_rules 在给允许路径加访问规则、保护当前工作区内部目录时会用它,确保 ACL 加给正确的能力 SID。

调用图:被 1 处调用(apply_legacy_session_acl_rules);外部调用 1 个(iter)。

deny_root_capabilities_for_path234–247 ↗
fn deny_root_capabilities_for_path(
    path: &Path,
    root_sids: &'a [RootCapabilitySid],
) -> Vec<&'a RootCapabilitySid>

作用:决定某个禁止写入的路径应该对哪些能力 SID 生效。它避免只禁了外层权限,却漏掉嵌套可写根目录的情况。

数据流:输入一个要禁止写的路径和所有可写根目录 SID → 它找出和该路径有重叠的根目录;如果没有重叠,就认为所有根目录都要禁止 → 输出需要加禁止写规则的 SID 列表。

调用关系:apply_legacy_session_acl_rules 用它给 deny 路径加拒绝写入规则。测试 legacy_deny_path_includes_nested_active_root_sid 专门验证嵌套目录不会漏掉。

调用图:被 2 处调用(apply_legacy_session_acl_rules, legacy_deny_path_includes_nested_active_root_sid);外部调用 1 个(iter)。

allow_null_device_for_workspace_write249–264 ↗
fn allow_null_device_for_workspace_write(is_workspace_write: bool)

作用:在工作区写模式下,额外允许当前登录会话访问 Windows 的空设备。空设备类似一个“黑洞文件”,程序常用它丢弃输出或作为占位。

数据流:输入一个布尔值表示是否是工作区写模式 → 如果不是就什么都不做;如果是,就读取当前令牌里的登录 SID,并给这个 SID 放行空设备 → 不返回业务数据,但会修改系统 ACL。

调用关系:旧式沙箱启动和带文件系统覆盖的运行流程会在需要时调用它。它把 token 模块拿到的登录 SID 交给 ACL 模块去放行空设备。

调用图:调用 3 个内部函数(allow_null_device, get_current_token_for_restriction, get_logon_sid_bytes);被 2 处调用(spawn_windows_sandbox_session_legacy, run_windows_sandbox_capture_with_filesystem_overrides);外部调用 1 个(CloseHandle)。

apply_legacy_session_acl_rules267–345 ↗
fn apply_legacy_session_acl_rules(
    permissions: &ResolvedWindowsSandboxPermissions,
    codex_home: &Path,
    current_dir: &Path,
    env_map: &HashMap<String, String>,
    additional_deny_read_p

作用:给旧式沙箱会话真正落地文件访问规则。它会在文件系统上贴上“允许读写”“禁止写”“禁止读”的 Windows ACL 规则。

数据流:输入权限、目录、环境变量、额外禁止读写路径和会话 SID → 它先算出允许和禁止路径,必要时提前创建禁止写目录;然后给允许路径加允许规则,给禁止路径加拒绝写规则,给禁止读路径同步持久 ACL;最后还会放行空设备,并保护工作区里的 .codex 和 agents 目录 → 输出成功或错误,主要效果是修改文件系统 ACL。

调用关系:旧式启动流程和旧式预检查会调用它。它内部会用 matching_root_capability 选正确的允许 SID,用 deny_root_capabilities_for_path 决定拒绝写要加给哪些 SID,并把持久禁止读规则交给 deny_read_state 模块。

调用图:调用 11 个内部函数(add_allow_ace, add_deny_write_ace, allow_null_device, compute_allow_paths_for_permissions, sync_persistent_deny_read_acls, canonicalize_path, deny_root_capabilities_for_path, matching_root_capability, is_command_cwd_root, protect_workspace_agents_dir (+1 more));被 3 处调用(spawn_windows_sandbox_session_legacy, run_windows_sandbox_capture_with_filesystem_overrides, run_windows_sandbox_legacy_preflight);外部调用 3 个(is_empty, bail!, create_dir_all)。

prepare_elevated_spawn_context_for_permissions348–443 ↗
fn prepare_elevated_spawn_context_for_permissions(
    permissions: ResolvedWindowsSandboxPermissions,
    codex_home: &Path,
    cwd: &Path,
    env_map: &mut HashMap<String, String>,
    command: &[

作用:为“提权方式”的沙箱启动准备完整上下文。这里的提权不是让命令随便做事,而是先用更高权限设置好沙箱用户和目录规则,再用受限身份运行命令。

数据流:输入已解析权限、目录、环境变量、命令、读写根目录覆盖项、禁止路径和代理限制标记 → 它整理环境变量和日志目录,计算可写根目录和禁止写路径,调用身份模块准备沙箱登录凭据,再生成本次要使用的能力 SID,并放行空设备 → 输出 ElevatedSpawnContext,里面有沙箱基础目录、日志目录、沙箱账号凭据和能力 SID 字符串。

调用关系:提权启动入口 spawn_windows_sandbox_session_elevated_for_permission_profile 会调用它。它串起权限计算、环境准备、账号准备、能力 SID 创建和空设备 ACL 放行,是提权启动前的总装配步骤。

调用图:调用 14 个内部函数(allow_null_device, compute_allow_paths_for_permissions, load_or_create_cap_sids, ensure_non_interactive_pager, inherit_path_env, normalize_null_device_env, require_logon_sandbox_creds, log_start, uses_write_capabilities_for_cwd, ensure_codex_home_exists (+4 more));被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 5 个(join, is_empty, new, bail!, vec!)。

tests::workspace_profile464–476 ↗
fn workspace_profile(
        network_policy: NetworkSandboxPolicy,
        writable_roots: &[AbsolutePathBuf],
        exclude_tmpdir_env_var: bool,
        exclude_slash_tmp: bool,
    ) -> Permissi

作用:测试用的小帮手,用来快速造一个“工作区可写”的权限配置。这样每个测试不用重复写一大段配置代码。

数据流:输入网络策略、可写根目录和两个排除选项 → 调用 PermissionProfile 的构造函数生成测试权限配置 → 输出 PermissionProfile。

调用关系:多个测试用它准备输入数据,尤其是检查网络策略、有效写根目录这些行为时。

调用图:调用 1 个内部函数(workspace_write_with)。

tests::workspace_roots_for478–480 ↗
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf>

作用:测试用的小帮手,把一个普通路径包装成工作区根目录列表。它让测试代码更短、更清楚。

数据流:输入一个路径 → 确认它是绝对路径并包装成 AbsolutePathBuf → 输出只包含这个根目录的列表。

调用关系:准备权限解析测试时会用它。prepare_legacy_spawn_context 和权限解析相关测试都会通过它构造 workspace_roots 参数。

调用图:外部调用 1 个(vec!)。

tests::should_apply_network_block482–489 ↗
fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool

作用:测试用的小帮手,用一句话判断某个权限配置是否应该触发断网处理。

数据流:输入 PermissionProfile → 先把它解析成 ResolvedWindowsSandboxPermissions,再读取 should_apply_network_block 的结果 → 输出 true 或 false。

调用关系:两个网络策略测试用它确认:默认工作区写模式会断网,而明确允许网络时不会断网。

调用图:调用 1 个内部函数(try_from_permission_profile_for_workspace_roots)。

tests::no_network_env_rewrite_applies_for_workspace_write492–496 ↗
fn no_network_env_rewrite_applies_for_workspace_write()

作用:验证默认的工作区写权限会要求断网。也就是说,沙箱默认更保守,不能随便访问网络。

数据流:没有外部输入 → 创建默认 workspace_write 权限配置,并检查 should_apply_network_block 返回 true → 测试通过或失败。

调用关系:它直接使用 tests::should_apply_network_block,覆盖 prepare_legacy_spawn_context 后面是否会改网络环境变量的前提条件。

调用图:外部调用 1 个(assert!)。

tests::no_network_env_rewrite_skips_when_network_access_is_allowed499–506 ↗
fn no_network_env_rewrite_skips_when_network_access_is_allowed()

作用:验证当权限配置明确允许网络时,不应该再做断网处理。这样用户允许联网的配置不会被旧式准备流程误伤。

数据流:没有外部输入 → 构造一个网络策略为 Enabled 的工作区权限配置,并检查 should_apply_network_block 返回 false → 测试通过或失败。

调用关系:它使用 tests::workspace_profile 和 tests::should_apply_network_block,和前一个测试形成一正一反的网络策略检查。

调用图:外部调用 1 个(assert!)。

tests::legacy_spawn_env_applies_offline_network_rewrite509–534 ↗
fn legacy_spawn_env_applies_offline_network_rewrite()

作用:验证旧式启动准备在需要断网时,确实会改写环境变量。比如把 HTTP_PROXY 指到本机不可用端口,达到阻断网络的效果。

数据流:没有外部输入 → 创建临时 Codex 主目录和当前目录,准备空环境变量表,调用 prepare_legacy_spawn_context → 检查环境变量里出现 SBX_NONET_ACTIVE 和被改写的 HTTP_PROXY。

调用关系:它直接测试 prepare_legacy_spawn_context,确认该函数在 common 准备之后真的调用了 apply_no_network_to_env。

调用图:调用 2 个内部函数(workspace_write, prepare_legacy_spawn_context);外部调用 4 个(new, new, assert_eq!, workspace_roots_for)。

tests::common_spawn_env_keeps_network_env_unchanged537–566 ↗
fn common_spawn_env_keeps_network_env_unchanged()

作用:验证通用准备函数不会擅自改网络代理。断网是旧式外层函数的责任,不应该混进 common 函数里。

数据流:没有外部输入 → 准备一个已有 HTTP_PROXY 的环境变量表,调用 prepare_spawn_context_common → 检查代理仍是原值,并确认这次上下文使用写能力。

调用关系:它直接调用 prepare_spawn_context_common,和 legacy_spawn_env_applies_offline_network_rewrite 一起划清通用准备和旧式断网准备的边界。

调用图:调用 2 个内部函数(workspace_write, prepare_spawn_context_common);外部调用 5 个(from, new, assert!, assert_eq!, workspace_roots_for)。

tests::legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root569–602 ↗
fn legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root()

作用:验证旧式会话在命令从工作区子目录启动时,仍然会把真正的工作区根目录当作能力根目录。

数据流:没有外部输入 → 创建临时 Codex 主目录、工作区和子目录,构造权限并解析,再调用 legacy_session_capability_roots → 检查结果正好是规范化后的工作区根目录。

调用关系:它测试 legacy_session_capability_roots,确保后续 root_capability_sids 拿到的是正确层级的目录,而不是误用当前子目录。

调用图:调用 2 个内部函数(try_from_permission_profile_for_workspace_roots, legacy_session_capability_roots);外部调用 6 个(new, new, assert_eq!, create_dir_all, workspace_profile, workspace_roots_for)。

tests::root_capability_sids_only_include_active_roots605–639 ↗
fn root_capability_sids_only_include_active_roots()

作用:验证生成能力 SID 时,只包含本次真正启用的可写根目录,不会把历史遗留或全局默认的 SID 混进去。

数据流:没有外部输入 → 创建工作区、活跃目录、过期目录,分别算出它们可能对应的 SID,再调用 root_capability_sids 只传入活跃目录和工作区 → 检查输出包含活跃 SID 和工作区 SID,不包含过期 SID 和旧的 workspace SID。

调用关系:它直接测试 root_capability_sids,保护启动安全边界,防止无关目录意外获得写权限。

调用图:调用 3 个内部函数(load_or_create_cap_sids, workspace_write_cap_sid_for_root, root_capability_sids);外部调用 5 个(new, assert!, assert_eq!, create_dir_all, vec!)。

tests::legacy_deny_path_includes_nested_active_root_sid642–675 ↗
fn legacy_deny_path_includes_nested_active_root_sid()

作用:验证禁止写某个目录时,嵌在里面的可写根目录也会被一起禁止。否则内层目录可能绕过外层保护。

数据流:没有外部输入 → 创建工作区、受保护目录、嵌套可写根目录和无关根目录,生成对应 SID,再调用 deny_root_capabilities_for_path → 检查结果包含工作区和嵌套根目录的 SID,但不包含无关根目录。

调用关系:它测试 deny_root_capabilities_for_path,间接保障 apply_legacy_session_acl_rules 在复杂目录重叠时不会漏加拒绝写 ACL。

调用图:调用 3 个内部函数(workspace_write_cap_sid_for_root, deny_root_capabilities_for_path, root_capability_sids);外部调用 5 个(new, assert!, assert_eq!, create_dir_all, vec!)。

tests::legacy_capability_roots_use_effective_write_roots678–715 ↗
fn legacy_capability_roots_use_effective_write_roots()

作用:验证旧式能力根目录会使用“最终有效”的可写根目录,而不是把所有配置里的目录都照单全收。

数据流:没有外部输入 → 创建工作区、活跃可写目录、Codex 主目录和沙箱目录,构造包含这些目录的权限配置,调用 legacy_session_capability_roots → 检查结果包含工作区和活跃目录,但排除 Codex 主目录及其 .sandbox 目录。

调用关系:它测试 legacy_session_capability_roots 和 effective_write_roots_for_permissions 的配合,确保内部运行目录不会被错误授予写能力。

调用图:调用 2 个内部函数(try_from_permission_profile_for_workspace_roots, legacy_session_capability_roots);外部调用 7 个(new, new, assert!, create_dir_all, vec!, workspace_profile, workspace_roots_for)。

windows-sandbox-rs/src/elevated_impl.rs源码 ↗
orchestrationrequest handling

这个文件像一个“带保安的命令执行窗口”。外部只要给它一份请求,里面写着要运行什么命令、在哪个目录运行、允许读写哪些文件、环境变量是什么,它就会先把权限规则翻译成 Windows 能用的形式,再准备沙盒用户的登录凭据。接着它会整理环境变量,比如避免分页器卡住、补上 PATH、让 Git 认可当前目录。然后它创建或读取一组能力标识 SID(Windows 用来表示某种权限身份的编号),并给必要的设备和目录放行。真正运行时,它把请求发给 runner(实际执行命令的小程序),通过管道一帧一帧收消息:标准输出、标准错误、退出状态、超时信息等。如果用户取消任务,它还会另开一个小线程发终止消息。非 Windows 系统上,这个文件只提供一个占位实现,直接报错,避免误以为沙盒可用。

函数细节3
windows_impl::spawn_cancel_writer67–95 ↗
fn spawn_cancel_writer(
        pipe_write: &File,
        cancellation: Option<crate::WindowsSandboxCancellationToken>,
    ) -> Result<Option<(std::thread::JoinHandle<()>, Arc<AtomicBool>)>>

作用:这个函数负责“盯着取消按钮”。如果外部请求取消正在运行的沙盒命令,它会通过管道给 runner 发一个终止消息,让命令尽快停下来。

数据流:进去的是一个可写管道和一个可选的取消令牌。没有取消令牌时,它什么线程都不建,直接返回空结果;有取消令牌时,它复制一份管道句柄,创建一个共享的完成标记,然后启动后台线程。线程每隔很短时间看一次是否已取消;一旦取消,就往管道里写入 Terminate 消息。出来的是线程句柄和完成标记,主流程结束时可以用它们通知线程收工并等待它退出。

调用关系:它是在 windows_impl::run_windows_sandbox_capture_for_permission_profile 已经启动 runner、拿到通信管道之后被调用的。主流程继续读取 runner 输出;这个函数开的后台线程只负责取消信号。等命令结束后,主流程会设置完成标记、唤醒并回收这个线程,避免留下没人管的后台任务。

调用图:外部调用 5 个(clone, new, new, try_clone, spawn)。

windows_impl::run_windows_sandbox_capture_for_permission_profile99–282 ↗
fn run_windows_sandbox_capture_for_permission_profile(
        request: ElevatedSandboxProfileCaptureRequest<'_>,
    ) -> Result<CaptureResult>

作用:这是 Windows 沙盒执行的主函数。调用者把“要跑的命令”和“允许它碰哪些地方”交给它,它负责搭好沙盒、启动命令、收集输出,最后返回退出码、标准输出、标准错误和是否超时。

数据流:进去的是 ElevatedSandboxProfileCaptureRequest,里面包含权限配置、工作区目录、Codex 主目录、命令、当前目录、环境变量、超时时间、取消令牌等信息。它先把权限配置解析成 Windows 沙盒权限,整理环境变量,确保沙盒日志目录存在,并记录启动日志。然后它获取或刷新沙盒用户凭据,准备能力 SID,给需要的空设备访问放行。之后它组装 SpawnRequest 发给 runner,拿到管道后循环读取 runner 发回的消息:输出消息会解码后追加到 stdout 或 stderr;退出消息会给出退出码和超时标记;错误或异常消息会变成失败结果。最后它写成功或失败日志,并返回 CaptureResult。

调用关系:它是这个文件在 Windows 上的核心入口。它串起了很多下游零件:权限解析交给 ResolvedWindowsSandboxPermissions,环境修正交给 env 相关函数,凭据准备交给 require_logon_sandbox_creds,能力 SID 交给 cap 相关函数,真正启动 runner 交给 spawn_runner_transport,消息读写交给 ipc_framed。它还会调用 windows_impl::spawn_cancel_writer,让取消操作能在命令运行中生效。如果发现沙盒凭据过期,它会刷新凭据后再试一次启动。

调用图:调用 12 个内部函数(allow_null_device, load_or_create_cap_sids, ensure_non_interactive_pager, inherit_path_env, normalize_null_device_env, require_logon_sandbox_creds, log_start, try_from_permission_profile_for_workspace_roots, ensure_codex_home_exists, inject_git_safe_directory (+2 more));外部调用 2 个(bail!, vec!)。

stub::run_windows_sandbox_capture_for_permission_profile304–308 ↗
fn run_windows_sandbox_capture_for_permission_profile(
        _request: ElevatedSandboxProfileCaptureRequest<'_>,
    ) -> Result<CaptureResult>

作用:这是给非 Windows 系统用的占位函数。它不真的运行沙盒,只是明确告诉调用者:Windows 沙盒只能在 Windows 上用。

数据流:进去的请求参数会被忽略,因为当前平台不支持这个功能。函数立刻返回一个错误,错误内容说明 Windows sandbox 只在 Windows 可用。它不会启动进程,也不会产生输出或修改沙盒状态。

调用关系:当代码编译到非 Windows 平台时,公开导出的 run_windows_sandbox_capture_for_permission_profile 会指向这个占位版本,而不是 windows_impl 里的真实版本。这样上层代码可以使用同一个函数名,但在不支持的平台上会得到清楚的失败信息,而不是出现更隐蔽的运行错误。

调用图:外部调用 1 个(bail!)。

windows-sandbox-rs/src/elevated/mod.rs源码 ↗
othercompile-time module wiring

在 Rust 里,mod.rs 可以理解成一个文件夹的“目录牌”。这个文件声明了三个子模块:ipc_framedrunner_clientrunner_pipe。它们名字里的意思大致是:一个处理带边界的进程间通信消息,一个像客户端一样去联系运行器,一个处理管道连接。这里的“提权”通常指用更高权限运行某些操作,比如需要管理员权限时。这个文件不写具体算法,也不直接收发数据;它的作用是把这些相关零件放到同一个“elevated”命名空间下面,让项目其他地方可以按结构找到它们。就像一本书的章节页,章节内容在后面,但章节页决定了哪些内容属于这一章。

windows-sandbox-rs/src/elevated/runner_client.rs源码 ↗
io_transportstartup handshake and cross-cutting IPC

可以把这个文件看成“主程序和沙箱执行员之间的接线员”。主程序先创建一对 Windows 命名管道(像两根专用电话线,一根发消息,一根收消息),再用沙箱账号启动 runner 程序。runner 启动后必须接上这两根管道,主程序才会把真正要执行的请求发过去,并等待 runner 回一句“我准备好了”。这里很重视失败处理:登录失败会包装成专门错误,方便上层判断是不是沙箱账号凭据失效;管道连接可能会卡住,所以它放到辅助线程里,并设置超时;如果启动或握手中途失败,会主动杀掉 runner,避免留下孤儿进程。文件里的 RunnerTransport 就是握手成功后的通信通道,后续靠它继续和 runner 说话。

函数细节10
RunnerLogonError::fmt59–61 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:把 runner 登录启动失败的错误码,整理成一段人能看懂的错误文字。这样日志或报错里不会只出现一个冷冰冰的数字。

数据流:进去的是 RunnerLogonError 里的 Windows 错误码,以及一个用于写文字的 formatter → 它把错误码拼进“CreateProcessWithLogonW failed: ...”这句话里 → 出来的是格式化后的错误文本,供报错系统显示。

调用关系:当 spawn_runner_transport 用沙箱账号启动 runner 失败时,会生成 RunnerLogonError;之后任何地方打印这个错误,都会走到这个格式化函数。它只负责把错误说清楚,不参与启动流程本身。

调用图:外部调用 1 个(write!)。

is_stale_sandbox_creds_error71–74 ↗
fn is_stale_sandbox_creds_error(err: &anyhow::Error) -> bool

作用:判断一个错误是不是“沙箱账号登录失败”。这通常意味着保存的沙箱用户名或密码已经不对了,需要上层重新处理凭据。

数据流:进去的是一个 anyhow::Error(可包很多层原因的通用错误)→ 它从错误里找 RunnerLogonError,并检查错误码是不是 Windows 的 ERROR_LOGON_FAILURE → 出来 true 或 false,true 表示很可能是沙箱凭据失效。

调用关系:它会被 spawn_windows_sandbox_session_elevated_for_permission_profile 调用。上层启动沙箱会话失败后,用它区分“账号密码问题”和“别的启动问题”,从而决定后面该怎么恢复。

调用图:被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile)。

RunnerTransport::send_spawn_request77–85 ↗
fn send_spawn_request(&mut self, request: SpawnRequest) -> Result<()>

作用:把“请 runner 启动某个任务”的请求发给 runner。它会先把请求包成带协议版本的消息,保证双方按同一套格式说话。

数据流:进去的是 SpawnRequest,也就是要 runner 执行的启动请求 → 它把请求放进 FramedMessage,写上 IPC_PROTOCOL_VERSION,再标记成 Message::SpawnRequest → 通过 pipe_write 写入管道,成功则返回空结果,失败则返回错误。

调用关系:spawn_runner_transport 在 runner 管道连好后会调用它,作为启动握手的一步。它把真正写管道的细活交给 write_frame,自己负责把请求包装成 runner 能理解的协议消息。

调用图:调用 1 个内部函数(write_frame);外部调用 1 个(new)。

RunnerTransport::read_spawn_ready87–98 ↗
fn read_spawn_ready(&mut self) -> Result<()>

作用:等待 runner 回答“我已经准备好了”。这一步能确认 runner 不只是进程启动了,而是真的收到请求并完成初始化。

数据流:进去的是 RunnerTransport 里的读取管道 → 它先等管道里出现一整条完整消息,再读出这条消息 → 如果消息是 SpawnReady 就成功;如果是 Error 就把 runner 的错误内容抛出来;如果是别的消息,就报“收到的不是预期内容”。

调用关系:spawn_runner_transport 在 send_spawn_request 之后调用它,完成启动握手。它依赖 wait_for_complete_frame 避免读半截消息或无限卡住,然后用 read_frame 解析真正的协议消息。

调用图:调用 2 个内部函数(read_frame, wait_for_complete_frame);外部调用 1 个(anyhow!)。

RunnerTransport::into_files100–102 ↗
fn into_files(self) -> (File, File)

作用:把 RunnerTransport 拆开,交出里面两根管道对应的 File。有人需要直接接管底层读写管道时会用它。

数据流:进去的是整个 RunnerTransport,本函数会取得它的所有权 → 它不再保留包装结构,只把 pipe_write 和 pipe_read 拿出来 → 出来的是一对 File,调用者从此负责使用和关闭它们。

调用关系:这是 RunnerTransport 的一个交接出口。前面的启动函数负责建立好通信通道;如果后续代码想用更底层的 File 来接入别的读写机制,就通过这个函数拿走管道。

try_take_completed_connect_result105–124 ↗
fn try_take_completed_connect_result(
    connect_thread: &mut Option<thread::JoinHandle<()>>,
    connect_result_rx: &mpsc::Receiver<Result<()>>,
    thread_handle: HANDLE,
    pipe_label: &str,
) ->

作用:检查负责连接管道的辅助线程是不是已经结束,并尽量取回它的结果。它主要用在“看起来超时了,但也许线程刚好完成”的边界情况。

数据流:进去的是可选的线程对象、接收连接结果的通道、线程句柄和管道名字 → 它用 WaitForSingleObject 立刻检查线程是否结束;没结束就返回 None;结束了就 join 线程并从通道收结果 → 出来是 Some(连接成功或失败的结果),或者 None 表示还没完。

调用关系:它只被 connect_pipe_with_timeout 使用。connect_pipe_with_timeout 在超时处理前后会问它一次,避免误把刚完成的连接当成超时来取消。

调用图:调用 1 个内部函数(recv);被 1 处调用(connect_pipe_with_timeout);外部调用 1 个(WaitForSingleObject)。

connect_pipe_with_timeout126–236 ↗
fn connect_pipe_with_timeout(
    h_pipe: HANDLE,
    expected_runner_pid: u32,
    pipe_label: &str,
) -> Result<()>

作用:用带超时的方式等待 runner 连上某根命名管道。因为 Windows 的管道连接调用可能一直堵住,所以这里用辅助线程和取消机制防止主流程卡死。

数据流:进去的是管道句柄、预期的 runner 进程号、管道标签 → 它启动一个辅助线程去执行真正的 connect_pipe,并把辅助线程的 Windows 句柄发回主线程;主线程等待结果,超过 15 秒就尝试取消这次阻塞操作 → 出来是连接成功,或带原因的失败;过程中会关闭辅助线程句柄。

调用关系:spawn_runner_transport 会连续用它连接 pipe-in 和 pipe-out 两根管道。它把真正的管道连接交给 connect_pipe,把“有没有完成”的细节交给 try_take_completed_connect_result,并在超时时调用 CancelSynchronousIo 取消阻塞。

调用图:调用 1 个内部函数(try_take_completed_connect_result);外部调用 7 个(anyhow!, format!, sync_channel, new, CloseHandle, GetLastError, CancelSynchronousIo)。

spawn_runner_transport238–368 ↗
fn spawn_runner_transport(
    codex_home: &Path,
    cwd: &Path,
    sandbox_creds: &SandboxCreds,
    log_dir: Option<&Path>,
    spawn_request: SpawnRequest,
) -> Result<RunnerTransport>

作用:这是本文件的核心入口:创建管道、用沙箱账号启动 runner、完成握手,最后返回可通信的 RunnerTransport。它把“启动一个隔离执行员并确认能说话”这整套流程串起来。

数据流:进去的是 Codex 主目录、工作目录、沙箱账号密码、可选日志目录,以及要发送给 runner 的 SpawnRequest → 它生成一对管道名,创建命名管道,找到 runner 可执行文件,拼好命令行,把字符串转成 Windows 需要的宽字符格式,然后调用 CreateProcessWithLogonW 以沙箱账号启动进程;接着连接两根管道,发送启动请求,等待 SpawnReady → 成功时出来 RunnerTransport;任何关键步骤失败时会关闭句柄,必要时终止 runner 进程,并返回错误。

调用关系:上层需要启动提升权限控制下的 Windows 沙箱会话时,会走到这个函数。它调用 pipe_pair、create_named_pipe、find_runner_exe、quote_windows_arg、to_wide、connect_pipe_with_timeout,以及 RunnerTransport::send_spawn_request 和 RunnerTransport::read_spawn_ready,把零散步骤拼成一个安全的启动流程。

调用图:调用 4 个内部函数(create_named_pipe, find_runner_exe, pipe_pair, to_wide);外部调用 9 个(from_raw_handle, format!, null, zeroed, CloseHandle, GetLastError, SetErrorMode, CreateProcessWithLogonW, TerminateProcess)。

wait_for_complete_frame370–414 ↗
fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()>

作用:等待读取管道里出现一整条完整消息,而不是只到了一半就开始读。这样可以避免后续 read_frame 卡住太久,或者读到不完整数据。

数据流:进去的是读取管道和最长等待时间 → 它反复用 PeekNamedPipe 偷看管道里已有多少字节;先看前 4 个字节里的消息长度,再判断“长度头 + 消息正文”是否都到了;没到就短暂睡一下继续等,超时就报错 → 出来是成功确认已有完整帧,或返回超时/管道错误。

调用关系:RunnerTransport::read_spawn_ready 在真正 read_frame 之前调用它。它不解析消息内容,只负责确认“这一包已经完整到货”,像收快递前先确认箱子没少半边。

调用图:被 1 处调用(read_spawn_ready);外部调用 8 个(as_raw_handle, now, anyhow!, null_mut, sleep, from_le_bytes, GetLastError, PeekNamedPipe)。

tests::stale_sandbox_creds_error_recognizes_logon_failures425–434 ↗
fn stale_sandbox_creds_error_recognizes_logon_failures()

作用:验证 is_stale_sandbox_creds_error 只把真正的登录失败认作沙箱凭据失效。这样以后改代码时,不容易把其他错误误判成密码问题。

数据流:进去的是测试里构造的两个错误码:ERROR_LOGON_FAILURE 和 ERROR_NOT_FOUND → 它分别包成 RunnerLogonError,再加上一层上下文文字,然后交给 is_stale_sandbox_creds_error 判断 → 期望出来的结果是 [true, false],也就是登录失败为真,找不到文件为假。

调用关系:这是测试代码,不参与正式运行。它专门保护 is_stale_sandbox_creds_error 的判断规则,确保上层在处理 spawn_windows_sandbox_session_elevated_for_permission_profile 的失败时不会被误导。

调用图:外部调用 1 个(assert_eq!)。

windows-sandbox-rs/src/elevated/runner_pipe.rs源码 ↗
io_transportelevated runner startup / IPC setup

在 Windows 上,提权后的父进程要和沙盒里的 runner 进程说话,不能随便开一个谁都能连的通道。这个文件做的事很像在墙上开两个带门禁的传话筒:先找到 runner 程序在哪里,再生成一对随机名字的命名管道(一种 Windows 进程间通信通道),然后给管道加上 ACL(访问控制列表,也就是“谁能进门”的名单),只允许指定的沙盒用户连接。等 runner 连上来后,它还会检查连接者的进程 ID,确认就是刚刚启动的那个 runner。这样可以避免权限边界被绕过,也避免其他进程抢先连上管道偷听或捣乱。这个文件只用于提权路径;老的受限令牌启动方式不走这里。

函数细节4
find_runner_exe43–45 ↗
fn find_runner_exe(codex_home: &Path, log_dir: Option<&Path>) -> PathBuf

作用:找到真正要启动的提权命令运行器程序。它优先使用项目准备好的 helper 副本,如果找不到,再走旧的旁边文件查找方式。

数据流:输入是 codex_home,也就是项目自己的主目录,以及可选的日志目录 → 它把“我要找 CommandRunner 这个 helper”的需求交给 helper 查找函数 → 输出一个可执行文件路径,后面父进程会用这个路径启动 runner。

调用关系:它是启动 runner 前的第一步之一。spawn_runner_transport 会调用它来确定程序位置;它自己把具体查找规则交给 resolve_helper_for_launch,不在这里重复实现。

调用图:调用 1 个内部函数(resolve_helper_for_launch);被 1 处调用(spawn_runner_transport)。

pipe_pair48–53 ↗
fn pipe_pair() -> (String, String)

作用:生成一对不会轻易撞名的命名管道名字,一根通常用于输入,一根通常用于输出。随机名字能减少被别的进程猜中或抢占的风险。

数据流:输入为空 → 它从系统随机源创建随机数生成器,生成一个很大的随机 nonce → 用这个 nonce 拼出同一个基础名,再分别加上 -in 和 -out → 输出两个字符串形式的管道路径。

调用关系:spawn_runner_transport 在准备父子进程通信时会调用它。它只负责取名字,不负责创建管道;创建工作随后交给 create_named_pipe。

调用图:被 1 处调用(spawn_runner_transport);外部调用 2 个(from_entropy, format!)。

create_named_pipe56–103 ↗
fn create_named_pipe(name: &str, access: u32, sandbox_username: &str) -> io::Result<HANDLE>

作用:创建一根带安全门禁的 Windows 命名管道。它确保只有指定的沙盒用户可以连接这根管道。

数据流:输入是管道名字、访问方向,以及沙盒用户名 → 它先把用户名解析成 SID(Windows 用来唯一识别用户的编号),再把 SID 写进 SDDL 安全描述字符串(用文字描述权限规则的格式)→ Windows 把这段权限规则转换成安全描述符 → 函数用这个安全描述符创建命名管道 → 成功时输出管道句柄,失败时输出带 Windows 错误码的 io 错误;中途申请的安全描述符会被释放。

调用关系:spawn_runner_transport 会用它实际开出父进程这边的管道。它依赖 resolve_sid、string_from_sid_bytes 和 to_wide 准备 Windows 需要的用户名和宽字符字符串,然后调用 CreateNamedPipeW 这样的 Windows 系统接口完成创建。

调用图:调用 3 个内部函数(resolve_sid, string_from_sid_bytes, to_wide);被 1 处调用(spawn_runner_transport);外部调用 7 个(from_raw_os_error, format!, null_mut, GetLastError, LocalFree, ConvertStringSecurityDescriptorToSecurityDescriptorW, CreateNamedPipeW)。

connect_pipe110–135 ↗
fn connect_pipe(h: HANDLE, expected_runner_pid: u32) -> io::Result<()>

作用:等待 runner 连上父进程已经创建好的管道,并确认连上来的进程就是预期的 runner。它是防冒充的一道检查。

数据流:输入是已经创建好的管道句柄,以及预期的 runner 进程 ID → 它调用 Windows 的 ConnectNamedPipe 等待客户端连接;如果客户端已经抢先连好了,也把这种正常情况放行 → 然后读取实际连接客户端的进程 ID → 如果 ID 匹配就返回成功;如果不是同一个进程,就返回权限错误。

调用关系:它位于管道创建之后、正式传输数据之前。它把等待连接交给 ConnectNamedPipe,把身份核对交给 GetNamedPipeClientProcessId;只有通过这一步,后续父进程才应该信任这条管道上的通信。

调用图:外部调用 7 个(from_raw_os_error, new, format!, null_mut, GetLastError, ConnectNamedPipe, GetNamedPipeClientProcessId)。

进程启动与交互式 I/O 内部机制

这些文件处理具体的 Windows 进程创建细节、桌面和启动属性、ConPTY 支持,以及沙盒会话的 stdio 桥接。

windows-sandbox-rs/src/process.rs源码 ↗
io_transportprocess launch and output capture

启动一个受限制用户身份下的 Windows 程序,不只是“运行 exe”这么简单。系统要知道命令怎么拼、环境变量怎么传、程序从哪里读输入、输出写到哪里,还要避免不该继承的句柄被带进子进程。这个文件就像启动程序的“接线员”:先把环境变量整理成 Windows 要求的 UTF-16 格式(Windows 常用的宽字符编码),再准备桌面对象,接着把标准输入、标准输出、标准错误这些通道接好,最后调用 CreateProcessAsUserW 让指定用户身份的进程跑起来。它还提供了带管道启动的版本,让上层代码可以像接水管一样读取子进程输出、选择是否写入输入,并在出错时及时关掉已经打开的系统句柄,防止资源泄漏。

函数细节5
make_env_block39–56 ↗
fn make_env_block(env: &HashMap<String, String>) -> Vec<u16>

作用:把一组环境变量变成 Windows 创建进程时要求的格式。环境变量就是程序启动时能看到的一批“名字=值”配置,比如 PATH、TEMP。

数据流:输入是一张字符串表,里面是环境变量名和值。它先复制出来并按变量名排序,再把每个“名字=值”转成 Windows 用的宽字符,变量之间用空字符隔开,最后再加一个额外的空字符表示整块结束。输出是一段 u16 数组,可以直接交给 Windows 创建进程接口。

调用关系:create_process_as_user 在真正启动进程前会调用它,把普通的 Rust 字符串环境变量打包成 Windows 能读懂的环境块。另一个启动伪终端进程的流程 spawn_conpty_process_as_user 也会用它,说明这是所有 Windows 子进程启动时共用的准备步骤。

调用图:调用 1 个内部函数(to_wide);被 2 处调用(spawn_conpty_process_as_user, create_process_as_user);外部调用 2 个(new, format!)。

ensure_inheritable_stdio58–73 ↗
fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()>

作用:确保当前程序的标准输入、标准输出、标准错误可以被子进程继承。简单说,就是让新启动的程序能继续用同一套键盘输入和屏幕输出。

数据流:输入是一个 Windows 启动信息结构。它读取当前进程的三个标准句柄,检查它们是否有效,然后把这些句柄标记为“允许子进程继承”,再把它们写进启动信息里。成功时返回空结果;如果某个句柄拿不到或设置失败,就返回错误。

调用关系:create_process_as_user 在没有单独指定管道时会调用它。也就是说,如果上层不想自定义输入输出,这个函数就负责把父进程现有的控制台输入输出传给子进程。

调用图:被 1 处调用(create_process_as_user);外部调用 3 个(anyhow!, SetHandleInformation, GetStdHandle)。

create_process_as_user78–199 ↗
fn create_process_as_user(
    h_token: HANDLE,
    argv: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    logs_base_dir: Option<&Path>,
    stdio: Option<(HANDLE, HANDLE, HANDLE)

作用:用指定的 Windows 用户令牌启动一个新进程。用户令牌可以理解成一张“以谁的身份运行”的通行证,这是沙箱隔离里很关键的一步。

数据流:输入包括用户令牌、命令参数、工作目录、环境变量、日志目录、可选的输入输出句柄,以及是否使用私有桌面。它先拼出 Windows 命令行,生成环境块,准备桌面;如果调用者给了标准输入输出句柄,就把这些句柄列入允许继承的清单;如果没给,就使用当前进程自己的标准输入输出。然后它调用 CreateProcessAsUserW 创建进程。成功时输出 CreatedProcess,里面有进程信息和启动信息;失败时会写调试日志并返回错误。

调用关系:这是本文件的核心启动函数。spawn_process_with_pipes 会先建好管道,再把管道句柄交给它启动子进程;run_windows_sandbox_capture_with_filesystem_overrides 也会直接用它启动需要捕获或控制的沙箱程序。它内部会调用 make_env_block、ensure_inheritable_stdio、LaunchDesktop::prepare、argv_to_command_line 等准备工具,把所有启动材料凑齐后交给 Windows 系统接口。

调用图:调用 6 个内部函数(prepare, debug_log, ensure_inheritable_stdio, make_env_block, argv_to_command_line, to_wide);被 2 处调用(spawn_process_with_pipes, run_windows_sandbox_capture_with_filesystem_overrides);外部调用 10 个(anyhow!, format!, zeroed, null, null_mut, new, vec!, GetLastError, SetHandleInformation, CreateProcessAsUserW)。

spawn_process_with_pipes227–326 ↗
fn spawn_process_with_pipes(
    h_token: HANDLE,
    argv: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    stdin_mode: StdinMode,
    stderr_mode: StderrMode,
    use_private_de

作用:启动一个子进程,并把它的输入输出接到匿名管道上。匿名管道可以理解成父进程和子进程之间临时接上的几根水管,用来送输入、收输出。

数据流:输入是用户令牌、命令参数、工作目录、环境变量、标准输入模式、标准错误模式、桌面选项和日志目录。它先创建 stdin、stdout,必要时还创建 stderr 管道;然后把子进程那一端的管道交给 create_process_as_user。启动成功后,它关闭父进程不该继续持有的那几端管道,只把父进程需要的写入端或读取端返回。启动失败时,它会把已经打开的句柄全部关掉再返回错误。

调用关系:spawn_legacy_process 会调用它来启动传统的、通过标准输入输出交互的程序。它自己不直接读写数据,而是负责把管道搭好,并把真正创建进程的工作交给 create_process_as_user。后续读取输出通常会交给 read_handle_loop 这类函数。

调用图:调用 1 个内部函数(create_process_as_user);被 1 处调用(spawn_legacy_process);外部调用 5 个(anyhow!, matches!, null_mut, CloseHandle, CreatePipe)。

read_handle_loop329–355 ↗
fn read_handle_loop(handle: HANDLE, mut on_chunk: F) -> std::thread::JoinHandle<()>

作用:在后台线程里不断读取一个 Windows 句柄里的数据,读到一块就交给调用者处理。常见用途是持续收集子进程的输出。

数据流:输入是一个可读取的 HANDLE,以及一个处理数据块的回调函数。它启动一个新线程,在线程里反复调用 ReadFile 读最多 8192 字节;每读到一块数据,就把这块字节交给回调。读不到数据或读取失败时循环结束,并关闭这个句柄。输出是线程句柄,调用者可以选择等待这个读取线程结束。

调用关系:spawn_output_reader 会调用它,把子进程 stdout 或 stderr 的读取端交进来。它不负责启动进程,只负责在进程运行期间把输出源源不断地读出来,避免管道塞满导致子进程卡住。

调用图:被 1 处调用(spawn_output_reader);外部调用 1 个(spawn)。

windows-sandbox-rs/src/desktop.rs源码 ↗
domain_logicprocess launch setup and teardown

在 Windows 里,“桌面”不只是屏幕背景,而是一组窗口和输入对象的隔间。这个文件就像给沙盒程序安排一个房间:如果不需要隔离,就让它去普通的 Winsta0\Default;如果需要更强隔离,就随机起名创建一个只给这次运行用的私有桌面。创建私有桌面后,还要改它的权限表。权限表可以理解成门禁名单:如果不把当前登录会话的身份加进去,后面用受限身份启动的程序可能进不了这个桌面,窗口也建不起来。LaunchDesktop 是对外的小包装,告诉创建进程的代码该填哪个桌面名;PrivateDesktop 保存真正的系统句柄,并在不用时自动关闭,避免系统资源泄漏。出错时它会写调试日志,方便查为什么桌面创建或授权失败。

函数细节5
LaunchDesktop::prepare63–77 ↗
fn prepare(use_private_desktop: bool, logs_base_dir: Option<&Path>) -> Result<Self>

作用:这是启动进程前的准备步骤,用来决定目标程序该进入默认桌面还是新建的私有桌面。别人要创建沙盒进程时,会先调用它拿到一个可以放进 Windows 启动参数里的桌面名。

数据流:输入是一个开关 use_private_desktop 和可选的日志目录。开关为真时,它创建一个 PrivateDesktop,再把名字拼成类似 Winsta0\某个随机桌面名 的宽字符字符串;开关为假时,它直接使用 Winsta0\Default。输出是 LaunchDesktop 对象;如果创建私有桌面失败,就返回错误,并可能写日志。

调用关系:它被 spawn_conpty_process_as_user 和 create_process_as_user 在真正启动进程前调用。它自己不直接启动程序,只负责把“用哪个桌面”这件事准备好;需要私有桌面时,它把具体创建工作交给 PrivateDesktop::create。

调用图:调用 2 个内部函数(create, to_wide);被 2 处调用(spawn_conpty_process_as_user, create_process_as_user);外部调用 1 个(format!)。

LaunchDesktop::startup_info_desktop79–81 ↗
fn startup_info_desktop(&self) -> *mut u16

作用:这个函数把已经准备好的桌面名交给 Windows 创建进程接口使用。它返回的是指向桌面名字符串的指针,因为底层 Windows API 需要这种形式。

数据流:它不接收额外输入,只读取 LaunchDesktop 里保存的 startup_name。它把内部的宽字符数组地址转成可传给 Windows 的可变指针并返回;它不复制数据,也不修改对象。

调用关系:它通常在外部创建进程代码填充启动信息时被使用。LaunchDesktop::prepare 先准备好名字,这个函数再把名字以底层 API 能识别的方式递出去。

PrivateDesktop::create90–125 ↗
fn create(logs_base_dir: Option<&Path>) -> Result<Self>

作用:这个函数真正向 Windows 申请一个新的私有桌面,并把它设置成沙盒进程可以进入的状态。没有这一步,所谓“私有桌面”就只是一个想法,系统里并没有对应的隔离空间。

数据流:输入是可选日志目录。它先生成一个随机桌面名,转成 Windows 能读的宽字符格式,然后调用 CreateDesktopW 创建桌面;如果失败,就读取系统错误码、写日志并返回错误。创建成功后,它继续调用 grant_desktop_access 给当前登录会话加权限;如果授权失败,它会立刻关闭刚创建的桌面并返回错误。成功时输出 PrivateDesktop,里面保存系统句柄和桌面名。

调用关系:它只由 LaunchDesktop::prepare 在需要私有桌面时调用。它负责创建“房间”,但权限门禁交给 grant_desktop_access;如果任何一步失败,它会清理已经拿到的系统资源,避免留下半成品。

调用图:调用 3 个内部函数(grant_desktop_access, debug_log, to_wide);被 1 处调用(prepare);外部调用 8 个(from_entropy, anyhow!, format!, null, null_mut, GetLastError, CloseDesktop, CreateDesktopW)。

grant_desktop_access128–186 ↗
fn grant_desktop_access(handle: isize, logs_base_dir: Option<&Path>) -> Result<()>

作用:这个函数给新建的私有桌面补上访问权限,让当前登录会话里的受限进程也能使用它。可以把它理解成给新房间的门锁加一张正确的门禁卡。

数据流:输入是私有桌面的系统句柄和可选日志目录。它先拿到当前进程的访问令牌;访问令牌可以理解成 Windows 用来证明“你是谁、你有什么权限”的身份证。然后它从令牌里取出登录会话的 SID,SID 是 Windows 识别用户或会话身份的一串编号。接着它构造一条访问规则,表示这个 SID 拥有该桌面的完整访问权,把规则变成新的权限列表,再写回桌面对象。过程中会关闭令牌句柄、释放临时权限列表;成功时返回空结果,失败时写日志并返回错误。

调用关系:它由 PrivateDesktop::create 在桌面刚创建好后马上调用。它依赖 token 模块提供当前令牌和登录 SID,依赖 Windows 的权限 API 改门禁名单;如果它失败,PrivateDesktop::create 会关闭桌面并中止启动准备。

调用图:调用 3 个内部函数(debug_log, get_current_token_for_restriction, get_logon_sid_bytes);被 1 处调用(create);外部调用 7 个(anyhow!, format!, null_mut, CloseHandle, LocalFree, SetEntriesInAclW, SetSecurityInfo)。

PrivateDesktop::drop189–195 ↗
fn drop(&mut self)

作用:这是 PrivateDesktop 被销毁时自动运行的清理动作,用来关闭 Windows 桌面句柄。它的意义是防止程序忘记释放系统资源,就像离开房间时自动关门。

数据流:输入是即将被丢弃的 PrivateDesktop 自身。它检查内部保存的 handle 是否有效;如果有效,就调用 CloseDesktop 交还给 Windows。它不返回业务结果,也不主动报告关闭失败。

调用关系:它不是由普通代码显式调用,而是 Rust 在 PrivateDesktop 生命周期结束时自动触发。LaunchDesktop 持有 PrivateDesktop,所以只要 LaunchDesktop 还活着,私有桌面就保持打开;LaunchDesktop 结束时,这里负责收尾。

调用图:外部调用 1 个(CloseDesktop)。

windows-sandbox-rs/src/proc_thread_attr.rs源码 ↗
io_transport创建子进程前准备属性,子进程启动后随对象销毁清理

在 Windows 上启动进程时,有些信息不能只靠命令行传进去,比如“哪些句柄可以继承”或者“这个进程要接到哪个伪控制台”。句柄可以理解成系统发给程序的一张“通行证”,用来访问文件、管道、控制台等资源;伪控制台则像一个假的终端窗口,让程序以为自己在普通命令行里运行。这个文件定义了 ProcThreadAttributeList,把 Windows 原生的进程线程属性列表包起来。它先向系统询问需要多大内存,再分配缓冲区并初始化;之后可以往里面塞伪控制台或句柄列表。特别重要的是,句柄列表会被保存在结构体里,避免传给 Windows 的内存提前失效。最后,当这个对象不用了,drop 会自动调用 Windows 的清理函数,就像用完临时登记表后归还给系统,避免资源泄漏。

函数细节5
ProcThreadAttributeList::new19–41 ↗
fn new(attr_count: u32) -> io::Result<Self>

作用:创建一份新的 Windows 进程线程属性列表。调用者告诉它准备放几个属性,它负责向系统要合适大小的空间并完成初始化。

数据流:输入是 attr_count,也就是预计要放多少个属性。函数先用空指针问 Windows 需要多大的缓冲区,再分配一块字节数组,然后把这块内存交给 Windows 初始化。成功后返回一个 ProcThreadAttributeList;如果系统调用失败,就把 Windows 的错误码转换成 Rust 的 io::Error 返回。

调用关系:这是整个对象的入口。后面的 set_pseudoconsole 和 set_handle_list 都必须建立在它创建出来的列表上使用;如果这里没初始化成功,后面就没有安全可用的属性列表。

调用图:外部调用 5 个(from_raw_os_error, null_mut, vec!, GetLastError, InitializeProcThreadAttributeList)。

ProcThreadAttributeList::as_mut_ptr43–45 ↗
fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST

作用:把 Rust 里的缓冲区转换成 Windows API 能看懂的可修改指针。它本身不做业务判断,只是提供一座桥。

数据流:输入是当前 ProcThreadAttributeList 自己。它读取内部的 buffer,把 buffer 的起始地址转换成 LPPROC_THREAD_ATTRIBUTE_LIST,也就是 Windows 期待的属性列表指针,然后把这个指针交出去;它不额外分配内存,也不改变内容。

调用关系:这是内部工具函数。set_pseudoconsole、set_handle_list 和 drop 都会先通过它拿到 Windows 需要的指针,再分别更新属性或释放属性列表。

调用图:被 3 处调用(drop, set_handle_list, set_pseudoconsole)。

ProcThreadAttributeList::set_pseudoconsole47–66 ↗
fn set_pseudoconsole(&mut self, hpc: isize) -> io::Result<()>

作用:把一个伪控制台句柄写进属性列表,让之后创建的子进程连接到这个伪控制台。没有这一步,很多命令行程序就不会把输入输出接到期望的终端环境里。

数据流:输入是 hpc,也就是伪控制台的系统句柄。函数先拿到内部属性列表的指针,再调用 Windows 的 UpdateProcThreadAttribute,把“伪控制台”这个属性和值写进去。成功时返回空结果表示完成;失败时读取 Windows 最后的错误码,转成 io::Error 返回。

调用关系:它通常在 ProcThreadAttributeList::new 之后、真正创建子进程之前调用。它依赖 as_mut_ptr 拿到可交给 Windows 的列表指针,实际写入工作交给 Windows 的 UpdateProcThreadAttribute。

调用图:调用 1 个内部函数(as_mut_ptr);外部调用 4 个(from_raw_os_error, null_mut, GetLastError, UpdateProcThreadAttribute)。

ProcThreadAttributeList::set_handle_list68–91 ↗
fn set_handle_list(&mut self, handles: Vec<HANDLE>) -> io::Result<()>

作用:指定哪些系统句柄允许传给新进程。这样可以精确控制子进程能继承哪些“通行证”,避免它拿到不该拿的资源,也避免它缺少必要的输入输出通道。

数据流:输入是一组 HANDLE。函数先把这组句柄存到结构体的 handle_list 里,确保这块内存在 Windows 使用属性列表期间还活着;然后拿到属性列表指针和句柄数组指针,调用 UpdateProcThreadAttribute 写入“句柄列表”属性。成功后属性列表会引用这组句柄;失败时返回对应的 io::Error。

调用关系:它通常在创建子进程前被调用,用来配置句柄继承规则。它先用 as_mut_ptr 取得 Windows 需要的列表指针,再把实际更新交给 UpdateProcThreadAttribute;内部保存 handle_list 是为了配合 Windows API 对内存生命周期的要求。

调用图:调用 1 个内部函数(as_mut_ptr);外部调用 6 个(from_raw_os_error, other, size_of_val, null_mut, GetLastError, UpdateProcThreadAttribute)。

ProcThreadAttributeList::drop95–99 ↗
fn drop(&mut self)

作用:在 ProcThreadAttributeList 被丢弃时自动清理 Windows 那边的属性列表。调用者不用手动记得释放,减少资源泄漏的风险。

数据流:输入是即将销毁的 ProcThreadAttributeList。函数通过 as_mut_ptr 取得属性列表指针,然后调用 Windows 的 DeleteProcThreadAttributeList 告诉系统清理相关内部状态。它不返回结果,也不会把错误传出去。

调用关系:这是 Rust 的自动收尾机制 Drop。前面 new 负责初始化,set_pseudoconsole 和 set_handle_list 负责填内容,最后对象离开作用域时 drop 自动被调用,完成和 Windows 初始化动作配套的清理。

调用图:调用 1 个内部函数(as_mut_ptr);外部调用 1 个(DeleteProcThreadAttributeList)。

windows-sandbox-rs/src/conpty/mod.rs源码 ↗
io_transportprocess launch / request handling

有些程序必须连着终端才能正常工作,比如会画进度条、读取键盘输入、判断自己是不是在命令行里的工具。Windows 的 ConPTY(伪控制台,可以理解成“没有窗口的命令行终端”)就是为这种场景准备的。这个文件把创建 ConPTY、准备输入输出管道、把它挂到新进程上这些容易出错的 Windows 底层步骤包起来。它还会按指定用户身份启动进程,必要时放到私有桌面里,减少进程和外界桌面的互相影响。核心结构 ConptyInstance 像一个“终端套装盒”,里面装着伪终端本体、写入端、读取端;用完后会自动关掉这些 Windows 句柄,避免资源泄漏。

函数细节6
ConptyInstance::drop43–53 ↗
fn drop(&mut self)

作用:这是 ConptyInstance 被销毁时自动执行的清理动作。它负责关掉还留在手里的 Windows 句柄,避免像忘关水龙头一样泄漏系统资源。

数据流:进去的是这个 ConptyInstance 当前保存的输入写入句柄、输出读取句柄和伪控制台对象 → 它检查句柄是不是有效,若有效就调用 CloseHandle 关闭 → 最后拿走 pseudoconsole,让它也按自己的规则释放;结果是不返回数据,但系统资源被清理掉。

调用关系:它不需要别人手动调用,而是在 ConptyInstance 生命周期结束时由 Rust 自动触发。它只把关闭句柄这件事交给 Windows 的 CloseHandle,保证前面 create_conpty 或 spawn_conpty_process_as_user 打开的资源最终能收尾。

调用图:外部调用 1 个(CloseHandle)。

ConptyInstance::raw_handle57–61 ↗
fn raw_handle(&self) -> Option<HANDLE>

作用:这个函数把 ConPTY 的底层 Windows 句柄拿出来给需要底层操作的代码用。句柄可以理解成系统发给某个资源的“编号”。

数据流:进去的是 ConptyInstance 自己保存的 pseudoconsole → 它看看伪控制台还在不在 → 如果还在,就取出它的原始 HANDLE;如果已经没有了,就返回空值。

调用关系:它是一个查看接口,不创建也不关闭东西。需要把伪控制台交给 Windows 启动参数或其他底层组件时,会用到这个原始句柄;本文件里的 spawn_conpty_process_as_user 也做了类似取句柄的事情来绑定新进程。

ConptyInstance::take_input_write63–65 ↗
fn take_input_write(&mut self) -> HANDLE

作用:这个函数把“往终端输入内容”的那根管道写入端交出去。交出去后,ConptyInstance 就不再负责关闭它,避免同一个句柄被关两次。

数据流:进去的是 ConptyInstance 里保存的 input_write → 它用 replace 把内部值换成 0,同时把旧句柄拿出来 → 出来的是原来的输入写入句柄,结构体内部则标记为已经没有这个句柄了。

调用关系:当外部代码要自己控制给沙盒进程写键盘输入时,会调用它接管这个句柄。它依赖标准库的 replace 做“拿走并清空”的动作,后续 ConptyInstance::drop 看到内部是 0,就不会再关闭这个已交出的句柄。

调用图:外部调用 1 个(replace)。

ConptyInstance::take_output_read67–69 ↗
fn take_output_read(&mut self) -> HANDLE

作用:这个函数把“读取终端输出”的那根管道读取端交出去。这样调用者可以自己去读沙盒进程打印出来的内容。

数据流:进去的是 ConptyInstance 里保存的 output_read → 它用 replace 把内部值换成 0,并返回旧句柄 → 出来的是原来的输出读取句柄,结构体内部则不再持有它。

调用关系:当外部代码要接管终端输出,比如把内容转发给用户界面或日志系统时,会调用它。它和 take_input_write 是一对:一个交出输入通道,一个交出输出通道,并且都通过清空内部句柄来配合 ConptyInstance::drop 的安全清理。

调用图:外部调用 1 个(replace)。

create_conpty77–87 ↗
fn create_conpty(cols: i16, rows: i16) -> Result<ConptyInstance>

作用:这个函数单独创建一个 ConPTY 和配套的输入输出管道。适合那些只想先拿到伪终端、不马上启动进程的调用者。

数据流:进去的是终端大小,也就是列数和行数 → 它调用底层的 new 创建 RawConPty,再把伪控制台、输入写入端、输出读取端拆出来 → 出来的是一个 ConptyInstance,里面打包好了这些句柄,等待后续使用或自动清理。

调用关系:它是较底层的积木块。常见启动流程更多会用 spawn_conpty_process_as_user 一步完成“建终端并启动进程”;但如果别的 Windows 沙盒流程想自己拼装启动过程,就可以先调用 create_conpty。

调用图:调用 1 个内部函数(new)。

spawn_conpty_process_as_user93–158 ↗
fn spawn_conpty_process_as_user(
    h_token: HANDLE,
    argv: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    use_private_desktop: bool,
    logs_base_dir: Option<&Path>,
) ->

作用:这是本文件最主要的入口:按指定 Windows 用户身份启动一个进程,并把它接到 ConPTY 上。简单说,就是“用某个身份开一个带假终端的沙盒程序”。

数据流:进去的是用户令牌 h_token、命令参数 argv、工作目录 cwd、环境变量 env_map、是否使用私有桌面以及日志目录 → 它先把参数安全拼成 Windows 命令行,把环境变量做成 Windows 需要的内存格式,再准备启动信息、私有桌面、ConPTY 和进程属性 → 然后调用 CreateProcessAsUserW 真正创建进程 → 成功时出来的是 PROCESS_INFORMATION 和 ConptyInstance;失败时出来的是带错误码、目录、命令和环境块长度的错误信息。

调用关系:它被 spawn_legacy_process 在需要带终端的沙盒进程时调用。它自己串起了多个零件:quote_windows_arg 和 to_wide 负责把字符串变成 Windows 能懂的样子,make_env_block 负责环境变量,LaunchDesktop::prepare 负责桌面隔离,RawConPty::new 负责建伪终端,ProcThreadAttributeList 负责把伪终端塞进启动参数,最后把真正启动进程的活交给 Windows 的 CreateProcessAsUserW。

调用图:调用 4 个内部函数(new, prepare, make_env_block, to_wide);被 1 处调用(spawn_legacy_process);外部调用 7 个(anyhow!, zeroed, null, null_mut, new, GetLastError, CreateProcessAsUserW)。

utils/pty/src/win/procthreadattr.rs源码 ↗
io_transportprocess spawn setup/teardown

在 Windows 里,如果想让一个新进程连到伪控制台(HPCON,可以理解成“假装出来的命令行窗口接口”),不能只把句柄传过去,还要通过一份特殊的“进程线程属性列表”告诉系统。这个文件就专门做这件事:先向 Windows 询问这份列表需要多大空间,再分配内存并初始化;需要时把伪控制台句柄塞进去;最后对象不用时自动通知 Windows 清理。它像一个一次性信封:启动进程前把地址和内容写好,交给 Windows,用完自动销毁。这里直接调用了底层 Windows API,所以有一些 unsafe(意思是 Rust 编译器不能完全替你检查安全性)的代码;文件把这些危险操作包在 ProcThreadAttributeList 里,让外面的代码不用到处碰这些细节。

函数细节4
ProcThreadAttributeList::with_capacity37–60 ↗
fn with_capacity(num_attributes: DWORD) -> Result<Self, Error>

作用:创建一份能装指定数量属性的 Windows 进程属性列表。启动带伪终端的进程前,外部代码会先用它准备好这块系统需要的内存。

数据流:进去的是属性数量 num_attributes → 它先调用 Windows API 问“需要多少字节”,再分配这么大的字节数组,并真正初始化这份属性列表 → 出来的是一个 ProcThreadAttributeList;如果 Windows 初始化失败,就返回带错误原因的失败结果。

调用关系:它是准备阶段的入口。spawn_command 在要启动命令时会调用它,拿到一份可用的属性列表;后续通常会继续调用 set_pty,把伪控制台放进这份列表里。

调用图:被 1 处调用(spawn_command);外部调用 3 个(with_capacity, ensure!, null_mut)。

ProcThreadAttributeList::as_mut_ptr62–64 ↗
fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST

作用:把内部保存的字节数组,转换成 Windows API 能看懂的原始指针。外部一般不直接关心它,但底层系统调用必须拿到这种指针。

数据流:进去的是当前这个 ProcThreadAttributeList 自己 → 它取出内部 data 的可变内存地址,并把类型转换成 Windows 需要的属性列表指针 → 出来的是一个可交给 Windows API 使用的指针;它不新建数据,只是暴露内部内存的位置。

调用关系:它是内部小帮手。set_pty 要更新属性列表时会用它找到列表位置;drop 要销毁属性列表时也会用它把同一块内存交还给 Windows 清理。

调用图:被 2 处调用(drop, set_pty)。

ProcThreadAttributeList::set_pty66–84 ↗
fn set_pty(&mut self, con: HPCON) -> Result<(), Error>

作用:把伪控制台句柄 HPCON 写进属性列表里。这样 Windows 创建子进程时,才知道这个进程应该连到哪一个伪终端。

数据流:进去的是一个 HPCON 句柄,也就是伪控制台的系统句柄 → 它拿到当前属性列表的指针,然后调用 Windows 的 UpdateProcThreadAttribute,把“伪控制台”这个属性和句柄写进去 → 成功时没有额外结果,只是修改了属性列表;失败时返回系统错误。

调用关系:它发生在属性列表创建之后、真正启动进程之前。spawn_command 一类的启动流程会先准备列表,再通过它把伪终端接上;它内部把取指针的活交给 as_mut_ptr。

调用图:调用 1 个内部函数(as_mut_ptr);外部调用 2 个(ensure!, null_mut)。

ProcThreadAttributeList::drop88–90 ↗
fn drop(&mut self)

作用:在 ProcThreadAttributeList 不再使用时自动清理 Windows 那边的属性列表资源。这样调用者不用手动记得释放,避免资源泄漏。

数据流:进去的是即将被销毁的 ProcThreadAttributeList → 它取出内部属性列表指针,并调用 Windows 的 DeleteProcThreadAttributeList → 没有返回值;结果是系统层面的属性列表被清理掉。

调用关系:它是收尾动作,由 Rust 在对象生命周期结束时自动调用。它依赖 as_mut_ptr 找到要清理的那块内存,和 with_capacity 的初始化动作前后配对。

调用图:调用 1 个内部函数(as_mut_ptr)。

windows-sandbox-rs/src/stdio_bridge.rs源码 ↗
io_transportsandbox session runtime

这个文件做的是“标准输入输出桥接”。标准输入输出就是命令行程序常用的三根管子:stdin 收用户输入,stdout 打正常内容,stderr 打错误内容。这里的主函数会启动几条后台线程:一条把当前程序读到的输入分块送进沙箱;两条把沙箱里程序吐出的正常输出和错误输出写回当前终端。它还会盯着两件事:沙箱进程什么时候退出,以及用户有没有按 Ctrl+C。按了 Ctrl+C 时,它会请求沙箱里的进程结束。输入结束时,它会主动关闭沙箱进程的输入口,避免对方一直等。进程退出后,它不会立刻丢掉最后的输出,而是最多等 5 秒让尾巴内容排空;这样既尽量不漏日志,也不会因为少见的结束信号问题永远卡住。

函数细节3
forward_sandbox_session_stdio12–64 ↗
async fn forward_sandbox_session_stdio(spawned: SpawnedProcess) -> i32

作用:这是整个桥的总开关:把当前程序的输入输出和一个沙箱会话连起来,并等沙箱进程结束后返回退出码。别人想运行沙箱进程并让它像普通命令行程序一样可交互,就会用它。

数据流:进去的是一个已经启动好的 SpawnedProcess,也就是带着沙箱会话、输出接收口和退出通知的进程对象。它读取当前程序的 stdin,把内容送到沙箱;读取沙箱的 stdout 和 stderr,再写到当前程序的 stdout 和 stderr;同时等待退出码或 Ctrl+C。出来的是一个整数退出码;过程中它还可能关闭沙箱输入、请求沙箱终止,并等待输出最多再排空 5 秒。

调用关系:它是这个文件的中心调度者。它先调用 spawn_input_forwarder 启动输入转发线程,再两次调用 spawn_output_forwarder 分别处理正常输出和错误输出。之后它自己负责等待退出、处理 Ctrl+C、收尾和返回退出码。

调用图:调用 2 个内部函数(spawn_input_forwarder, spawn_output_forwarder);外部调用 11 个(clone, new, from_secs, channel, stderr, stdin, stdout, current, select!, spawn (+1 more))。

spawn_input_forwarder66–94 ↗
fn spawn_input_forwarder(
    mut input: R,
    writer_tx: mpsc::Sender<Vec<u8>>,
    stdin_eof_tx: oneshot::Sender<()>,
) -> std::thread::JoinHandle<()>

作用:这个函数开一条后台线程,专门把某个输入源读到的数据送进沙箱进程的输入口。它的作用像一个传送带:用户这边一有字节,就搬到沙箱那边。

数据流:进去的是一个可读取的输入源、一个发给沙箱的消息通道,以及一个用来通知“输入已经结束”的一次性信号。线程会每次最多读 8KB 数据,读到多少就发送多少;如果读到文件结束、发送失败或遇到不可恢复错误,就停下来。最后它会发出输入结束信号,提醒外层可以关闭沙箱的 stdin。

调用关系:它由 forward_sandbox_session_stdio 在会话开始时调用。它只管输入这一路,不等沙箱退出;当它发现输入没了,就通过信号告诉 forward_sandbox_session_stdio 里启动的异步任务,由那个任务去关闭沙箱输入。

调用图:被 1 处调用(forward_sandbox_session_stdio);外部调用 1 个(spawn)。

spawn_output_forwarder96–122 ↗
fn spawn_output_forwarder(
    tokio_runtime: tokio::runtime::Handle,
    output_rx: mpsc::Receiver<Vec<u8>>,
    mut writer: W,
) -> (std::thread::JoinHandle<()>, oneshot::Receiver<()>)

作用:这个函数开一条后台线程,把沙箱发来的输出数据写到指定的地方,比如当前终端的 stdout 或 stderr。它让沙箱里的打印内容能及时出现在用户眼前。

数据流:进去的是 Tokio 运行时句柄、一个接收输出数据块的通道,以及一个可写的目标。线程会不断从通道里取出字节块,写入目标并立刻 flush,也就是把缓冲区内容尽快刷出去;如果写入或刷新失败,就打印错误并停止。出来的是线程句柄,以及一个“已经写完”的通知接收器。

调用关系:它由 forward_sandbox_session_stdio 调用两次:一次接 stdout,一次接 stderr。它内部借用 Tokio 运行时从异步通道收数据,但实际写终端放在线程里做。会话结束后,forward_sandbox_session_stdio 会等它们发出完成通知,不过最多只等 5 秒,防止收尾时卡死。

调用图:被 1 处调用(forward_sandbox_session_stdio);外部调用 2 个(channel, spawn)。