Zed `sandbox` crate:跨平台 Shell 命令沙箱与 TOCTOU 竞态防御设计
2026/9/7 3:56:09 网站建设 项目流程

Zedsandboxcrate:跨平台 Shell 命令沙箱与 TOCTOU 竞态防御设计

【免费下载链接】zedCode at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.项目地址: https://gitcode.com/GitHub_Trending/ze/zed

本文以 Zed 仓库中 crates/sandbox/README.md 为核心骨架,结合该 crate 的 Rust 源码、测试脚本与 Cargo 配置,完整拆解 Zed Agent 运行不可信 Shell 命令时的沙箱实现:SandboxPolicy策略抽象、Seatbelt / Bubblewrap / WSL 三大平台后端、限制网络时的回环代理架构,以及 Linux 上最精彩的部分——针对renameat2(2)符号链接交换攻击的 TOCTOU 竞态的三轮修复过程(FD 固定 → 捕获循环缺陷 → 审批时持久化规范路径),外加 seccomp-BPF 对 Unix 域套接字逃逸的封杀。读完你应能理解 Zed 如何把“用户批准的目录”与“实际被挂载的 inode”严格绑定,以及这些设计对构建 AI 代码代理安全边界的启示。

一、sandboxcrate 是什么:跨平台命令沙箱

sandbox crate 的目标是“cross-platform sandboxing for shell commands”——为在用户机器上替 AI Agent 执行的不可信命令提供文件系统与网络层面的隔离。其核心抽象只有两个:

  • SandboxPolicy(沙箱策略):以“意图”而非平台细节描述命令被允许做什么。它表达三件事:
    • 允许哪些文件系统操作(哪些子树可写,其余只读);
    • 允许哪些网络操作(全放行 / 全阻断 / 仅允许特定域名);
    • 哪些路径即使在可写子树内部也保持受保护(可读不可写)。
  • Sandbox(活的沙箱实例):按策略创建后,用它来运行受该策略约束的命令。

这个抽象在 沙箱入口 中有精确的 Rust 定义:SandboxPolicyfs: SandboxFsPolicynetwork: SandboxNetPolicy组成。文件系统策略分两种:

pub enum SandboxFsPolicy { /// 允许不受限的文件系统写入,但受保护路径保持只读 Unrestricted { protected_paths: Vec<HostFilesystemLocation> }, /// 到处可读;写入被限制在这些位置(外加平台提供的标准临时目录) Restricted { writable_paths: Vec<HostFilesystemLocation>, protected_paths: Vec<HostFilesystemLocation>, }, }

网络策略分Unrestricted/Blocked/Restricted { allowed_domains }三档,其中受限档只允许精确主机名或前缀*.子域名通配,由进程内 HTTP(S) 代理强制(见 sandbox.rs 网络策略定义)。

调用方(主要是 Zed Agent 的 沙箱化逻辑)只需要描述意图;每个策略最终落到什么——Seatbelt 规则文件、bwrap命令行参数、回环代理端口——都是实现细节。这也是公开 API 刻意保持平台中立的原因:Sandbox::new验证策略合法性,Sandbox::wrap把一条CommandAndArgs转换成可在当前平台运行的WrappedCommand(含bwrap包裹、sandbox-exec -f配置或wsl.exe调用),交互式调用方(PTY)用wrap,非交互式可用execute直接拿到Output

值得注意的是,策略可以合并:SandboxPolicy::merge把两层策略合成“满足两层的、最不严格的”策略——可写子树取并集(并归并为最小覆盖),受保护路径取并集,Unrestricted网络访问占优而Blocked是单位元。这对应 Zed 中“项目级默认授权 + 工具级请求授权”的叠加模型。

二、安全模型:默认配置并不安全,文档直言不讳

原文档在“Security model”一节给出了一个极其重要的定性,值得完整继承:

沙箱本身假设所有不可信代码都是最大敌意的。它并不假设不可信代码是“由一个善意但可能略有偏差的 AI 代理编写的”。

但实际的 Zed 默认配置并不是对攻击安全的(the default profile in Zed not secure against attacks)。文档举了一个真实可行的攻击链:拥有当前目录读写权限的攻击者可以——

  1. 在当前目录创建一个 Rust 项目;
  2. 在其中创建一个包含恶意代码的proc macro 库
  3. 在项目中某处使用该宏;
  4. rust-analyzer 会在沙箱外执行该 proc macro(语言服务器运行在宿主侧,不受沙箱约束)。

文档给出的缓解手段有两条:

  • 禁用任何“有能力执行不可信代码”的语言服务器;
  • 保护敏感的项目元数据,尤其是.git——因为对.git的写访问可以经 hooks、$EDITOR等机制升级为沙箱外的代码执行。

并且文档坦承“这不是万能的”:例如在 Linux 上,如果.git尚不存在,就无法预先保护它。这个诚实的边界声明很关键——它把sandbox定位为降低攻击面、约束文件与网络写权限的机制,而不是一个完备的不可信代码执行容器。理解这一点,后面所有 TOCTOU 防御的细节就都顺理成章:既然攻击者被假定是敌意的,那么“检查完路径再挂载”之间的任何一纳秒窗口都必须被堵死。

三、三大平台后端:Seatbelt、Bubblewrap、WSL

实现高度平台相关(README “Implementation” 一节):

平台后端机制
macOSSeatbelt(sandbox-exec生成规则文件,路径在系统调用时解析与检查
Linuxbubblewrap基于 Linux namespaces 的用户命名空间沙箱
WindowsWSL:同 Linux;非 WSL:不支持WSL 内的 bwrap + 宿主侧辅助进程

几个值得注意的工程细节:

  • WSL 的覆盖面比直觉大:无论文件存在 Linux 文件系统还是 Windows DrvFs,所有 Windows 项目都可以使用 WSL shell 来跑沙箱命令。
  • Zed Agent 的默认授权(虽然不由本 crate 定义,但 README 明确列出):
    • 对全部文件只读访问;
    • 对当前项目目录读写访问——但项目内任何 Git 元数据保持只读;
    • 对一个隔离的 tempdir读写,且它在终端之间会被清空;在 macOS 上该 tempdir 通过$TMPDIR指定,不是/tmp
  • bwrap 的可用性与拒绝策略:SandboxError 中定义了BwrapNotFound(PATH 上找不到可用 bwrap)与BwrapSetuidRejected——Zed明确拒绝运行 setuid-root 的 bwrap,只接受无特权用户命名空间路线。Sandbox::can_create会跑一个极简探测沙箱(在其中执行true)来回答“这个宿主上环境上到底能不能建沙箱”这一纯环境问题,它刻意不依赖任何具体命令的可写授权。
  • Linux 特有依赖:从 Cargo.toml 可见,Linux 目标依赖libcnix(为SCM_RIGHTS传 FD 和fstat提供安全封装,避免手写msghdr/CMSG_*unsafe)、以及seccompiler(构建沙箱内 seccomp-BPF 过滤器)。

四、网络限制的通用架构:禁用网络 + 唯一回环端口 + 宿主侧代理

文件系统限制各平台迥异,但网络限制在各平台遵循同一套思路(README “Architecture” 一节):

  1. 沙箱内禁用网络,只留一个 localhost 端口可用;
  2. 在沙箱内设置HTTP_PROXY等环境变量,让程序都往那个 socket 通信;
  3. 在 Zed 宿主侧运行一个代理,监听该端口并强制执行域名过滤

源码印证:Sandbox结构体持有一个进程内代理句柄proxy: Option<ProxyHandle>(来自 workspace 的http_proxycrate),在首次wrap时懒启动——懒启动是为了让代理能从被包裹命令的环境变量里读出既有的上游代理并做链式转发(见 ensure_restricted_proxy)。Sandbox::drop时代理的监听线程 join 被刻意扔到新建线程上执行,以免在 UI 线程上意外 drop 时卡住界面;后台线程上则可用drop_on_current_thread同步完成拆除。

Linux 上还有一个特殊环节:需要一个“中间 socket”让数据流出沙箱。原因是 Seatbelt 与 bubblewrap 的网络模型不同——bwrap 把被沙箱程序放进完全独立的网络栈(它的localhost与宿主的不是同一个),所以宿主侧代理不能直接监听沙箱内程序能到达的地址;而 Linux 实现用一个挂载进沙箱的 Unix socket 作为中继(ProxyHandle::spawn_unix_temp会同时给出 TCP 端口与 socket 路径,见 wrap_linux 中NetworkAccess::LocalhostPortproxy_socket_path的传递)。

五、Linux 后端的攻防史:从 naive bind 到三轮 TOCTOU 修复

这是 README 篇幅最重、信息密度最高的部分。Linux 上的朴素实现大概是:

  • 判断哪些路径只读、哪些可读写;
  • 通过bwrap运行,只读路径用--ro-bind,可读写路径用--bind

问题在于一个“讨厌的 TOCTOU”(time-of-check to time-of-use)。

5.1 攻击场景:renameat2RENAME_EXCHANGE

攻击者诱骗用户打开了含有恶意AGENTS.mdproject,并让用户授予了project内一个可写路径。此时沙箱拿到的权限是:

  • project读写;
  • project/cache读写;
  • 对隔离的/tmp读写;
  • /只读。

恶意AGENTS.md指示 LLM 派生两个子代理:

  • 子代理 1:用renameat2(2)RENAME_EXCHANGE标志,原子交换project/cache与一个指向/home/alice的符号链接;
  • 子代理 2:执行echo 'export PATH="proj/obfuscated.../evil_eavesdropping_sudo/bin:$PATH"' >> proj/cache/.bashrc,注入窃取凭据的恶意 PATH。

Zed 在把路径交给 bubblewrap 之前确实会检查路径是不是指向允许范围外的符号链接,但检查与挂载之间存在时间差。在这段窗口里renameat2一旦成功:

  • 检查时刻,project/cacheproject的子目录;
  • 挂载时刻,project/cache/home/alice的符号链接;
  • 攻击者于是获得了对/home/alice读写沙箱,第二条命令成功注入恶意 sudo。

仓库里附带了一个最小复现脚本 bind_source_toctou_test.sh:它用bwrap --ro-bind / / --bind $READ_WRITE_DIR ...建沙箱,在沙箱内尝试“先软链再mv -fT原子替换”把可写目录换成符号链接——由于父目录只读,这个替换应当(也确实)以权限失败告终,脚本以PASS: swap blocked结束。它同时说明了单层授权不足以利用此漏洞:利用它必须能篡改“被用作--bind参数的那个路径”,而没有嵌套目录可写时,你只能篡改授权路径本身,而 bubblewrap 模型下替换一个目录需要对其父目录有写权限——这正是 README 特别强调的“该攻击需要两个嵌套的可写目录”的原因。

5.2 天真的修复:禁止嵌套授权(行不通)

“那就不允许嵌套目录呗”——README 明确说这在理论上成立(“若不存在互为祖先的读写授权对,此 TOCTOU 攻击不可能发生”),但作为对策不可行,理由有二:

  1. 它要求全系统范围内任何时刻都不能存在这样的授权对:在 A 窗口打开/foo、在 B 窗口打开/foo/bar就重新打开了漏洞;即便强行把/foo/bar拓宽为/foo(本身是不可接受的权限升级),也无法控制非 Zed 进程。
  2. 它会堵死一种有用模式:/foo可写、/foo/bar只读、/foo/bar/baz可写——祖先-后代关系里夹杂不同权限等级。

于是需要一个更稳健的方案。

5.3 正确的修复:以文件描述符为真相之源

核心洞见:FD 一旦打开就稳定,路径怎么变都不影响它指向的 inode。符号链接交换攻击不会改变 FD 指向的 inode。

剩下的工程问题是:怎么让bwrap用上 FD?bwrap支持--bind-fd,但 FD 怎么进到 bwrap 进程里?两个选项:

  1. 在 zed 里打开 FD、清除CLOEXEC,然后 fork/exec bwrap;
  2. 通过SCM_RIGHTSsocket 把 FD 送进沙箱内的辅助进程,在沙箱内部校验。

README 说选择了方案 2,因为沙箱里本来就已经有一个辅助进程(用来搭 HTTP 代理)。完整流程:

  • 对每个要--bind可写路径打开O_PATHFD——O_PATH固定(pin)inode 但不授予其内容的读写权限,恰好是 bind 源所需;
  • 创建SCM_RIGHTSsocket 用于发送这些 FD;
  • 执行bwrap --bind /path1 /path1 ... -- zed --zed-linux-sandbox-launcher <不可信命令参数>(注意:这里用的是可能被交换过的路径;socket 也被挂载进沙箱);
  • 沙箱桥接进程从 socket 读出 FD,对每个可写 bind:
    • fstatFD 得到(device, inode)
    • lstat对应挂载路径得到它的(device, inode)
    • 校验两者一致——这本质上就是bwrap --bind-fd内部做的事;
  • 全部匹配才执行不可信命令,否则拒绝执行。

对应到源码,linux_bubblewrap.rs 中validate_binds(约 L839 起)正是接收SCM_RIGHTS传来的描述符并与挂载路径做fstat/lstat对比的函数;宿主侧的发送端是Sandbox拥有的ValidationFdSender(一个进程内短生命周期后台线程,服务完一个客户端即自毁,见 wrap_linux 中“每个 wrap 新建一个”的注释)。

这里有一个值得注意的实现细节:README 说 FD 是通过 socket 传给沙箱内辅助进程的,而 host_filesystem_location.rs 的文档注释解释了为什么不能直接用 bwrap 自带的--bind-fd——“bwrap 由无法继承额外 fd 的 PTY 启动,所以 bind 用普通--bind <canonical>,由沙箱内校验器在挂载后比对fstat(fd)lstat(mount)”。FD 与 bind 路径在 wrap_linux 中被构造为顺序对齐(路径, fd)对列表,保证校验器侧一一对应;且任何拿不到可校验 FD 的 bind 会失败关闭(drop 该 bind 而不是未校验就挂载)。

5.4 微妙缺陷:捕获本身是循环的

上面的方案堵住了“捕获后交换”的窗口,但 README 揭示了一个隐藏缺陷:原始捕获步骤是循环的

最初的实现每次运行时都新开一个O_PATHFD(跟随符号链接),然后从同一个 FD 反推bind 路径(readlink(/proc/self/fd/N))。也就是说,固定的 inode 与 bind 路径都来自“一次对攻击者可影响路径的 open”——那么捕获之前就换好的符号链接会被静默跟随:open 固定的是攻击者的 inode,readlink 报告的也是攻击者的路径。沙箱内校验器比较fstat(fd)lstat(mount)时,两侧都一致地“错”,校验通过。校验器只能证明 FD 与挂载名指向同一对象,无法证明该对象就是用户批准的那个

5.5 完整修复:持久化审批时的规范路径

修复原则是:在审批时固定身份,在执行时重新证明,而不是每次运行时从 FD 重新推导。具体做法:

  • 审批时:把请求路径解析为无符号链接的规范(canonical)目标,并把这个规范路径与请求一起持久化
  • 执行时:用O_PATH | O_NOFOLLOW打开持久化的规范路径,拒绝符号链接叶节点(S_IFLNK检查——因为对O_NOFOLLOW符号链接 FD 做readlink返回的是链接自身的路径,会假性匹配),并要求readlink(/proc/self/fd/N) == 持久化规范路径。审批之后任何组件变成符号链接,FD 就会解析到别处,校验失配,失败关闭

这个宿主侧的挂载前readlink 检查与既有的沙箱内挂载后fstat(fd) == lstat(mount)检查组合起来,覆盖了所有时间窗口:

  • 宿主 open之前的交换 → 被 readlink 检查抓到;
  • 宿主检查与挂载之间的交换 → 被沙箱内校验抓到。

还有一个容易被忽略的约束:readlink 那一半必须在宿主侧做——一旦 FD 被 bind 进沙箱,它的 inode 在沙箱内被重映射到挂载目标,沙箱内 readlink 只会报告挂载路径,什么都证明不了。

由于 FD 无法跨越进程重启存活,即使是“仅对当前线程有效”的瞬时授权,也持久化为(requested, resolved-canonical)字符串对,在重建时重新验证。requested路径仅保留用于展示与溯源,绝不回喂到执行逻辑中。

这部分在源码中有逐条对应:canonical_path.rs 中from_canonical正是“验证一个声称的 canonical 路径”的实现——O_PATH | O_CLOEXEC | O_NOFOLLOW打开、S_IFLNK检查拒绝符号链接叶(返回PermissionDenied)、readlink(/proc/self/fd/N)与声称路径比对,任何不符都失败关闭。而 host_filesystem_location.rs 的两个构造函数capture(跟随符号链接,用于首次发现真实目标并持久化)与reopen(从持久化对重建并证明当前对象即被批准对象)与 README 的描述完全一致。其测试模块(同文件 L291 起)用真实目录构造了完整攻防用例:reopen_rejects_intermediate_component_swapped_for_symlink(审批后中间组件被换成符号链接 → 拒绝)、reopen_rejects_symlink_leaf(授权叶本身被换成符号链接 → 拒绝)、capture_resolves_symlink_and_reports_redirect(符号链接请求解析出真实目标并标记is_redirected供审批 UI 高亮)。

5.6 封杀 IPC 套接字逃逸:seccomp-BPF

README 最后揭示的逃逸面同样深刻:只读 bind 挡不住connect()。内核有意豁免套接字(以及 FIFO、设备节点)于只读文件系统的写检查——connect 不修改任何文件系统数据。于是即便--ro-bind / /,沙箱内命令仍能connect()$XDG_RUNTIME_DIR下的会话 IPC socket(Wayland 合成器、D-Bus 会话总线)或 Docker daemon 这类系统 socket,借它在沙箱外起进程,一举击穿文件与网络双重限制。--unshare-net帮不上忙:它隔离的是抽象命名空间与 TCP/IP,而这些是绑定文件系统上的路径套接字

修复是一个用seccompiler构建的 seccomp-BPF 过滤器,在不可信命令运行前一刻装上。思路不是隐藏每个 socket,而是阻止命令获得它能用来逃逸的 socket:

  • socket()只允许AF_INET/AF_INET6/AF_NETLINK;其余家族——特别是AF_UNIX(会话 IPC)与AF_VSOCK(VM 宿主)——一律EPERM
  • socketpair()只允许AF_UNIX(进程本地对,够不到沙箱外);
  • 拒绝io_uring_*(其环操作可绕过被过滤系统调用建 socket)、ptrace/process_vm_*
  • connect/recvmsg/sendmsg/bind/listen/accept保留允许——既然无法创建被禁止的 socket、也没有继承下来的危险 fd,这些调用就没有危险对象可作用;直接封connect反而会打断合法的 loopback/代理流量;
  • seccompiler的架构检查会杀外来架构系统调用,顺手堵死 32 位socketcall旁路。

关键约束:过滤器必须作用于命令不作用于launcher/bridge 进程——后者要一直用AF_UNIX去连宿主侧代理。所以过滤器在直接执行场景中于exec前一刻内联安装,在受限网络的 bridge 场景中通过子进程pre_exec安装。因为过滤器住在沙箱内的 launcher 里,launcher 从此永远运行(哪怕没有可写 bind 要校验、也没有 bridge),以保证过滤器始终装上。源码中 build_command_seccomp_program / install_command_seccomp_filter 即此实现,且注释明确指出“过滤器安装的时机就在不可信命令执行前”。

此机制是 Linux/WSL 特有的:macOS 上 Seatbelt 把 Unix socket 的connect作为独立的network-outbound能力门控并默认拒绝,同一条逃逸路线天然关闭。

六、Windows:宿主在 Windows,FD 在 Linux 里

README 的 Windows 一节自述“高度依赖 Linux 实现的细节”。理论上 Linux 方案在 WSL 中完美可用(WSL 用的是“正规 Linux 内核”),但存在一个实际障碍:创建 FD 的 zed 宿主代码跑在 Windows 上,而你需要的是 Linux 文件描述符

解法是启动zed --wsl-sandbox-helper——一个运行在 WSL 里的 shim,负责捕获 FD 并搭建 socket。它被下载到~/.local/libexec/zed,以避免与 WSL 会注入进 Linux$PATH的 Windowszed.exe冲突(是的,.exe后缀会被剥掉)。源码中该机制由共享常量 WSL_SANDBOX_HELPER_FLAG(--wsl-sandbox-helper)在 Windows 侧与 Linux/WSL 侧共用,保证两侧不漂移;windows_wsl.rs 构建wsl.exe调用,Linux 侧(在 WSL 内部)解析该标志。测试设施也自成体系:Cargo.toml 定义了两个默认关闭的 feature——nixos-test(构建bwrap_test_helper,由nix/tests/sandboxing下的 NixOS 沙箱 VM 测试驱动,行为级验证 Linux bwrap 沙箱)与wsl-test(构建wsl_sandbox_test_helper,由script/test-wsl-sandbox.ps1/cargo xtask wsl-sandbox-tests驱动,端到端驱动真实的 WSL/Bubblewrap 沙箱)。

Windows 后端还有两个平台特有的保守处理:受限网络策略(域名白名单)在 Windows 上直接拒绝resolve_restricted_network返回UnsupportedPolicy,见 sandbox.rs);对 DrvFs(Windows 盘经 WSL 挂载)上的路径,沙箱完整性保证弱于发行版原生文件系统,crate 提供path_is_on_windows_drive做廉价的结构性检查(sandbox.rs 注释明确说这只供 UI 门控,真正的逐授权分类在 WSL 内做)。此外CanonicalPathBuf在 Windows 上被构造约束为两种合法形状:Windows 盘符路径(NTFS)或 Linux 绝对路径(WSL 发行版内),\\wsl.localhost\...等 UNC 与相对路径一律拒绝(见 canonical_path.rs 及其测试)。

七、macOS:Seatbelt 规则文件与mach-lookup的陷阱

macOS 用 Seatbelt 强制执行规则文件,这让执行相对直接:与 Linux 不同,路径在系统调用时被解析与检查,所以符号链接交换攻击在 macOS 上不会成功——这也是 host_filesystem_location.rs 中reopen在 macOS 上“持久化 canonical 被原样信任”的原因:Seatbelt 匹配的是解析后的访问路径,审批后组件交换会在系统调用时被拒绝而非被重定向,无需 FD/readlink 检查。

但规则文件的某些部分需要格外小心,尤其是mach-lookup——它控制对 Launch Services 等服务的访问,而 Launch Services 允许沙箱外代码执行。macos_seatbelt.rs 中生成的规则刻意使用白名单形式(allow mach-lookup ...),而非无限制的(allow mach-lookup);其测试甚至显式断言配置中“包含(allow mach-lookup但不包含不带参数的(allow mach-lookup)”(约 L528-L537),防止有人手滑退化回 blanket 形式。网络限制则体现为(allow network-outbound (remote tcp "localhost:{port}"))之类的精确规则(约 L376)。

README 还提到被拒服务里有些“争议项”,例如com.apple.FontObjectsServer:应用用它有正当用途,但字体可以包含可执行代码且历史上被用于 RCE;考虑到 Zed Agent 中可以轻易退出沙箱,拒绝是合理选择——不过文档也承认“可能以后要重新评估”。

八、代码设计:HostFilesystemLocationSandboxFilesystemLocation

README “Code design” 一节提出了两个类型的分工,这是整个 crate 对 TOCTOU 的类型层面防御。

HostFilesystemLocation:宿主位置的不可变身份句柄

macOS 不受那个 TOCTOU 影响,但只要“两次规范化之间有时间差”就有风险。为此,敏感 API 一律接受HostFilesystemLocation,它包装一个完全cfg过的、按平台区分的内部结构,每个平台恰好携带其执行层所需的捕获身份:

  • Linux:{ O_PATH fd, canonical_path, untrusted_raw_path }
  • macOS:{ canonical_path, untrusted_raw_path }
  • 其他平台:{ untrusted_raw_path }(真正的捕获发生在 WSL 侧)

该类型故意不透明:不实现Deref,路径只能通过展示视图读取,永远不会以“可按字符串回喂构造器”的值形式泄漏出去;相等性反映真实文件系统对象(Linux 上 FD 背后的 inode,macOS 上 canonical 路径),而非文本路径。

两个构造器的分工(见 host_filesystem_location.rs):

  • capture(raw):把raw解析为规范目标,跟随符号链接(Linux:O_PATHopen 后readlink(/proc/self/fd/N);macOS:canonicalize)。用于从持久状态重建的位置——项目自己的 worktree 根与受保护路径(它们的父目录沙箱命令篡改不了)——以及审批时解析用户请求路径以便展示与持久化真实目标。
  • reopen(raw, canonical):从持久化授权重建并证明当前位于canonical的对象就是被批准的那个。Linux 上用O_PATH | O_CLOEXEC | O_NOFOLLOW打开canonical、拒绝符号链接叶(S_IFLNK检查)、要求readlink(/proc/self/fd/N) == canonical,否则失败关闭;macOS 上持久化 canonical 原样信任(理由见第七节)。

display()返回HostFilesystemLocationDisplay,同时暴露原始请求与规范目标,外加“请求是否经符号链接重定向”的标志,让审批 UI 能渲染知情且诚实的“requested → granted”披露。

Linux上还有一个精巧的身份工具:判断可写路径是否与受保护路径重叠时,linux_location_is_equal_or_descendant 不用路径前缀比较,而是沿openat(fd, "..")逐层上溯父目录、比较fstat(device, inode)——完全基于 FD 身份,避免字符串比较在符号链接下的失效。策略合并时的位置去重(normalize_host_filesystem_locations,host_filesystem_location.rs)也在捕获时固定的 canonical 字符串上判断包含关系,而非实时文件系统;源码注释论证了它“只会收缩、绝不扩大授权”,因此即使攻击者中途交换组件也是安全的,并配有proptest性质测试验证“最小覆盖 + 幂等 + 子集”三条不变式。

SandboxFilesystemLocation:沙箱内路径,无需加固

与之对照,SandboxFilesystemLocation只是 PathBuf 的薄包装,表示沙箱内的位置(如 Linux 的 bind 挂载目标)。它不需要加固——被篡改的沙箱内路径最坏也只能把“已经授权的宿主文件”暴露在沙箱内的另一个路径上,无法扩大可达宿主文件的集合。这个“宿主侧严格、沙箱侧宽松”的不对称正是威胁模型的自然结果:危险永远来自宿主侧路径的解析,而非沙箱命名空间内部。

九、小结:一套可借鉴的“批准-执行”分离范式

把 README 的叙述与源码证据对照后可以提炼出sandboxcrate 的三层不变式,它们相互正交、共同构成完整的防线:

  1. 身份捕获与执行分离:授权在审批时固化(capture+ 持久化 canonical),执行时重新证明(reopen+readlink比对),挂载时再做 inode 级交叉验证(沙箱内fstat/lstat)——任何时间点发生的符号链接交换都落在某一个检查的覆盖窗口内;
  2. 路径文本永不进入执行决策HostFilesystemLocation不透明、requested路径仅作展示、相等性基于 inode——把“字符串身份”与“内核身份”彻底分开;
  3. 内核能力面主动收缩:seccomp 禁AF_UNIX/io_uring/ptrace、拒绝 setuid bwrap、macOS 拒绝 blanketmach-lookup——不止防“写”,也防“借道”。

这套设计对任何要在宿主上执行 LLM 生成命令的编辑器/代理系统都有直接参考价值;同时 README 对默认配置残余风险(语言服务器执行 proc macro、.git不可预保护)的坦率披露,也为读者划清了“沙箱能给什么、不能给什么”的边界。若想继续深入,建议按此顺序阅读:README 全文 → sandbox.rs 公开 API → canonical_path.rs 与 host_filesystem_location.rs(含攻防测试)→ linux_bubblewrap.rs(validate_binds与 seccomp 构建)→ bind_source_toctou_test.sh 的最小复现脚本。

【免费下载链接】zedCode at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.项目地址: https://gitcode.com/GitHub_Trending/ze/zed

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询