| 1 | # 沙箱威胁模型 |
| 2 | |
| 3 | > 英文原文:[SANDBOX.md](../SANDBOX.md)。 |
| 4 | > 最后与英文同步日期(last synced with English revision):2026-09-29。 |
| 5 | |
| 6 | Codewhale 可以执行由模型提出的 shell 命令。审批策略、感知工作区的工具, |
| 7 | 以及操作系统层面的命令包装器,这三套是彼此独立的控制手段:一次审批不等于沙箱, |
| 8 | 选择 `workspace-write` 也不代表当前平台真的提供了可用的操作系统包装器。 |
| 9 | |
| 10 | 本文只描述已经接入命令执行路径的行为。至于执行到达这条边界之前会先经过哪些 |
| 11 | 策略层,见 [授权顺序(Authorization order)](AUTHORIZATION_ORDER.md)。 |
| 12 | |
| 13 | ## 平台概览 |
| 14 | |
| 15 | | 机制 | 平台 | 选择方式 | Codewhale 报告的结果 | |
| 16 | |---|---|---|---| |
| 17 | | Seatbelt(`sandbox-exec`) | macOS | 运行时探测成功时自动启用 | `macos-seatbelt` | |
| 18 | | Bubblewrap(`/usr/bin/bwrap`) | Linux | `prefer_bwrap = true` 且该文件可执行 | `linux-bwrap` | |
| 19 | | 无操作系统包装器 | Linux,没有可用且已启用的 bwrap | 默认 | `none` | |
| 20 | | 无操作系统包装器 | Windows | 当前实现 | `none` | |
| 21 | | 兼容 OpenSandbox 的服务 | 任何受支持的主机 | `sandbox_backend = "opensandbox"` | 外部执行路径 | |
| 22 | |
| 23 | 仓库里有一个 seccomp 实现模块,还有一份面向未来的 Windows 辅助程序契约 |
| 24 | (helper contract)。两者都没有接入子命令的启动流程,所以 Codewhale 不会对外声明它们 |
| 25 | 是生效中的沙箱。光有沙箱源码,不能证明某条命令真的被限制过。 |
| 26 | |
| 27 | ## macOS:Seatbelt |
| 28 | |
| 29 | Codewhale 用一个最小 profile 探测 `/usr/bin/sandbox-exec`。当探测通过、 |
| 30 | 并且选定的 `SandboxPolicy` 要求沙箱时,子命令外面会套上一层生成的 |
| 31 | Seatbelt profile。 |
| 32 | |
| 33 | 这层 profile 可以提供: |
| 34 | |
| 35 | - 大范围的文件系统读取; |
| 36 | - 写入受所选策略限制,范围包括工作区,以及受支持工具所需的特定运行时/缓存路径; |
| 37 | - 只有在策略允许时才放开网络访问。 |
| 38 | |
| 39 | 探测失败,或 `sandbox-exec` 不可用时,Codewhale 会报告未启用操作系统沙箱, |
| 40 | 直接启动命令,不套 Seatbelt 包装器。这条回退路径上也不会打任何 Seatbelt 标记。 |
| 41 | |
| 42 | ## Linux:需要主动启用的 bubblewrap |
| 43 | |
| 44 | Linux 下的命令沙箱需要主动启用。设置顶层配置项: |
| 45 | |
| 46 | ```toml |
| 47 | prefer_bwrap = true |
| 48 | ``` |
| 49 | |
| 50 | 只有当 `/usr/bin/bwrap` 是普通的可执行文件时,Codewhale 才会选用 bubblewrap。 |
| 51 | 包装器根据解析后的 `SandboxPolicy` 推导自己的挂载点和网络命名空间: |
| 52 | |
| 53 | ```text |
| 54 | /usr/bin/bwrap \ |
| 55 | --unshare-all \ |
| 56 | [--share-net] \ |
| 57 | --ro-bind / / \ |
| 58 | --dev /dev \ |
| 59 | --proc /proc \ |
| 60 | --tmpfs /tmp \ |
| 61 | [--dev-bind <device-root> <device-root> ...] \ |
| 62 | --bind <writable-root> <writable-root> ... \ |
| 63 | --ro-bind <protected-descendant> <protected-descendant> ... \ |
| 64 | [--ro-bind <extra-ro-root> <extra-ro-root> ...] \ |
| 65 | --chdir <cwd> \ |
| 66 | -- <program> <args> |
| 67 | ``` |
| 68 | |
| 69 | 沙箱总会拿到私有的 `/dev`(全新的设备节点,所以 `>/dev/null` 照常可用)、 |
| 70 | 私有的 `/proc`,以及 tmpfs 挂载的 `/tmp`(#5410)。还有两个可选的顶层配置项 |
| 71 | 可以扩展挂载:`bwrap_ro_roots` 把额外的主机路径以只读方式 bind mount 进来, |
| 72 | 最后才应用,因此能够收窄策略允许写入的路径;`bwrap_dev_roots` 把主机的 |
| 73 | 字符/块设备节点以读写方式 bind mount 进来,目录一律不予采纳。路径不存在时 |
| 74 | 静默跳过。 |
| 75 | |
| 76 | 这样,子进程看到的是一个只读的根视图。在 `workspace-write` 下,每一个安全且 |
| 77 | 确实存在的策略根都会以读写方式挂载:工作目录、配置的额外根目录、未被排除的 |
| 78 | `/tmp` 和 `TMPDIR`,以及经过校验的 Git worktree 元数据根。已经存在的 |
| 79 | `.codewhale` 和 `.deepseek` 子路径,会在可写父目录挂好之后重新挂为只读。不存在的 |
| 80 | 路径、非目录路径以及 `/`,都不会被提升为可写挂载。 |
| 81 | |
| 82 | 在 `read-only` 下没有任何可写绑定,所以工作目录仍留在只读根视图里。 |
| 83 | `--unshare-all` 默认隔离网络命名空间;只有当策略中的 `network_access` 为 true |
| 84 | 时,Codewhale 才补上 `--share-net`。`danger-full-access` 和 `external-sandbox` |
| 85 | 完全绕过本地包装器。 |
| 86 | |
| 87 | 如果用户没有主动启用,或者 `/usr/bin/bwrap` 不存在、不可执行,Codewhale 会 |
| 88 | 报告 `none`,直接启动命令,不带任何 Linux 操作系统包装器。这里没有回退做法: |
| 89 | 不会只打个标记,就把它当成另一种 Linux 沙箱。 |
| 90 | |
| 91 | 如果这套主动启用的方案适合你的工作流,请另行安装 bubblewrap: |
| 92 | |
| 93 | - Ubuntu/Debian:`apt install bubblewrap` |
| 94 | - Fedora:`dnf install bubblewrap` |
| 95 | - Arch:`pacman -S bubblewrap` |
| 96 | |
| 97 | Codewhale 不自带 bubblewrap。 |
| 98 | |
| 99 | ## Windows:不声明任何操作系统沙箱 |
| 100 | |
| 101 | Windows 上的命令路径目前报告未启用操作系统沙箱。源码树里有一份面向未来的辅助 |
| 102 | 程序契约,用于清理 Job Object 进程树,但它没有接入选择逻辑,也不能说成 |
| 103 | 下面任何一种能力: |
| 104 | |
| 105 | - 只读文件系统或 workspace-write 的强制执行; |
| 106 | - 网络阻断; |
| 107 | - 注册表隔离; |
| 108 | - 受限令牌(restricted token)或 AppContainer 隔离。 |
| 109 | |
| 110 | Windows 主机的权限和审批策略仍然适用,但它们不是 Codewhale 的操作系统命令沙箱。 |
| 111 | |
| 112 | ## Linux 的进程加固不等于命令沙箱 |
| 113 | |
| 114 | 在 Linux 上启动时,Codewhale 会尽力对自己的进程设置 `PR_SET_DUMPABLE=0`、 |
| 115 | `PR_SET_NO_NEW_PRIVS=1` 和 `RLIMIT_CORE=0`。任何一项失败都会记录日志, |
| 116 | 启动继续进行。这些控制可以降低进程被窥探、权限被提升以及产生 core dump 的风险; |
| 117 | 它们不为子命令建立文件系统或网络隔离,也不会被列为沙箱后端。 |
| 118 | |
| 119 | 唯一的例外是启动姿态(posture)本身。当启动沙箱模式解析为 |
| 120 | `danger-full-access`(通过 `CODEWHALE_SANDBOX_MODE` 或配置文件里的 |
| 121 | `sandbox_mode` 键)时,Codewhale 会跳过 `PR_SET_NO_NEW_PRIVS`,好让 |
| 122 | `sudo`/`su`/setuid 辅助程序能在智能体的 shell 里照常工作(#5723)—— |
| 123 | Full Access(完全访问)指的就是这种姿态。任何更窄的启动姿态都会保留该标志作为纵深防御, |
| 124 | 而 `CODEWHALE_NO_NEW_PRIVS` 可以双向覆盖姿态(#5413):假值一律跳过该标志, |
| 125 | 真值一律设置它。该标志对整个进程树都不可逆,所以只能在启动时决定;会话内单次调用升级沙箱, |
| 126 | 也无法解除它。 |
| 127 | |
| 128 | ## 外部 OpenSandbox 执行 |
| 129 | |
| 130 | 配置 `sandbox_backend = "opensandbox"` 后,shell 执行会发往配置好的、兼容 |
| 131 | OpenSandbox 的 HTTP 端点,而不是在本地启动子进程。Codewhale 会校验请求与响应 |
| 132 | 的契约,但隔离保证归所配置的服务及其运维方所有。 |
| 133 | |
| 134 | ```toml |
| 135 | sandbox_backend = "opensandbox" |
| 136 | sandbox_url = "http://localhost:8080" |
| 137 | sandbox_api_key = "YOUR_API_KEY" |
| 138 | ``` |
| 139 | |
| 140 | `sandbox_backend = "none"`(或省略该键)会保持本地执行。不受支持的后端设置 |
| 141 | 会拒绝 shell 执行,绝不会悄悄改用本地执行。请选择受支持的后端,或显式指定 |
| 142 | `none`。 |
| 143 | |
| 144 | ## 策略与回退 |
| 145 | |
| 146 | 本地 `sandbox_mode` 的取值有: |
| 147 | |
| 148 | ```toml |
| 149 | sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access | external-sandbox |
| 150 | ``` |
| 151 | |
| 152 | - 只有当对应的包装器被选中且可用时,`read-only` 和 `workspace-write` 才由 |
| 153 | Seatbelt 或 bubblewrap 强制执行。 |
| 154 | - `danger-full-access` 有意绕过本地操作系统包装器。在 Linux 上,它还会在启动时 |
| 155 | 跳过 `PR_SET_NO_NEW_PRIVS` 进程加固标志,让 `sudo`/setuid 工作流继续可用 |
| 156 | (#5723);见上面的进程加固一节。 |
| 157 | - `external-sandbox` 表示执行已经在外部隔离,因此不再套第二层本地包装器。 |
| 158 | - 没有选中任何包装器时,shell 命令运行时就没有 Codewhale 的操作系统隔离。 |
| 159 | 审批规则和感知工作区的原生文件工具仍是彼此独立的控制手段。 |
| 160 | |
| 161 | `sandbox_mode` 和外部后端都有规范的环境变量覆盖方式: |
| 162 | |
| 163 | - `CODEWHALE_SANDBOX_MODE` |
| 164 | - `CODEWHALE_SANDBOX_BACKEND` |
| 165 | - `CODEWHALE_SANDBOX_URL` |
| 166 | - `CODEWHALE_SANDBOX_API_KEY` |
| 167 | |
| 168 | 不存在 `CODEWHALE_PREFER_BWRAP` 环境变量覆盖;请使用顶层的 `prefer_bwrap` 配置项。 |
| 169 | |
| 170 | ## 诊断与失败归因 |
| 171 | |
| 172 | `codewhale setup --status`、`codewhale doctor`、`codewhale doctor --json` 以及 |
| 173 | `diagnostics` 工具,都会先应用解析后的 bubblewrap 偏好,再报告本地可用的 |
| 174 | 包装器。只要某条命令的策略不要求沙箱,它仍然可以绕过这个包装器。在 Linux 上, |
| 175 | 仅仅找到某个与沙箱相关的系统调用或源码模块,并不会让 `sandbox_available` |
| 176 | 变成 true。 |
| 177 | |
| 178 | 拒绝归因刻意做得很保守: |
| 179 | |
| 180 | - Seatbelt 用的是它自己那套包装器专属的拒绝模式。 |
| 181 | - Bubblewrap 的设置错误必须以 `bwrap:` 开头;bwrap 文件系统视图报出的只读 |
| 182 | 文件系统错误,也能标识出这条边界。 |
| 183 | - 子命令报出的通用 `Permission denied` 或 `Operation not permitted`, |
| 184 | 本身不能证明是 Codewhale 的沙箱挡下了它。 |
| 185 | - 没在沙箱中运行的命令一旦失败,永远不会被标成沙箱拒绝。 |
| 186 | |
| 187 | ## 局限 |
| 188 | |
| 189 | - 可用性在启动前检查;选中的包装器仍可能因为主机策略、容器限制,或探测之后 |
| 190 | 出现的竞态而失败。 |
| 191 | - 如果配置的可写根不存在、不是目录,或规范化之后等于 `/`,bubblewrap 会忽略它; |
| 192 | 路径也可能在策略解析与包装器启动之间消失。 |
| 193 | - Seatbelt profile 在运行时生成,必须拿它要支持的那些命令实测过。 |
| 194 | - Windows 上目前没有任何本地包装器对外声明。 |
| 195 | - 外部沙箱后端的安全性,只取决于它所配置的服务。 |
| 196 | - 没有任何沙箱能防住内核漏洞,也不能覆盖所有资源耗尽攻击与侧信道攻击。 |
| 197 | |
| 198 | ## 实现参考 |
| 199 | |
| 200 | - `crates/tui/src/sandbox/mod.rs` — 如实报告的选择逻辑与公开能力标记 |
| 201 | - `crates/tui/src/sandbox/seatbelt.rs` — macOS 包装器与可用性探测 |
| 202 | - `crates/tui/src/sandbox/bwrap.rs` — Linux 主动启用式包装器 |
| 203 | - `crates/tui/src/sandbox/process_hardening.rs` — Linux 父进程加固 |
| 204 | - `crates/tui/src/sandbox/backend.rs` — 外部后端选择 |
| 205 | - `crates/tui/src/tools/diagnostics.rs` — 机器可读的诊断信息 |
| 206 |