DeepSeek Harness 沙箱:confine 到底怎么把命令关进笼子
生命不息,折腾不止。上一篇把「权限」和「沙箱」两个旋钮盘明白了,这一篇钻进去看沙箱本身——dsh 到底靠什么把命令关进笼子,又是怎么靠一条命令就能验出这笼子是铁的、还是纸糊的。
上一篇聊到最后,我留了一句挺关键的话:dsh 的沙箱不是「绝对安全边界」。这话只说了一半,今天把它补完整——沙箱这条链路上,ctx.sandbox.confine 到底在做什么,三套后端(Linux 的 bwrap / Landlock、macOS 的 Seatbelt、Windows 的 ACL)各自怎么落地文件隔离,为什么有的地方只能「部分生效」,以及最要命的:文件被锁死了,沙箱就真的安全了吗?
一、先破除一个错觉:dsh 的沙箱,只管「文件写」这一件事
一提「沙箱」,很多人脑子里浮现的是 Docker 那套——网络隔离、进程隔离、文件系统隔离全套。但 dsh 的 ctx.sandbox 不是这么回事,它的官方定义窄得有点反直觉:
SandboxMode只管 filesystem effects(文件系统的写入效果),三个档:read-only(只读,只放行/dev/null这一个可写点,所以>/dev/null还能用)、workspace-write(只许写工作区根目录 + 后端承诺的临时区)、danger-full-access(完全绕过 confinement)。- 网络、进程可见性、系统调用、设备、凭据,全都不在这个词汇表里。
这一条是整个沙箱理解的地基,也是后面两个真实漏洞的伏笔——文件被锁得死死的,但 loopback 网络是通的。你先记住这句话:dsh 的沙箱是「同世界(same-world)隔离」,它和你的宿主机共用同一个内核、同一套文件系统,它只约束文件效果,别的什么都不承诺。
二、一条链路:ctx.sandbox.confine 到底做了什么
核心契约就一句话:
1 | ctx.sandbox.confine(argv, policy) // 返回一个「包装过的 argv」,你拿去 spawn 的是它 |
也就是说,你要跑一条命令,confine 不替你跑,而是还给你一个换过壳的 argv——被包装后的进程,以及它再 spawn 出来的所有子进程,全都在受限状态下运行。几个必须记住的细节:
- argv 是精确的 argv 数组,不是 shell 字符串。dsh 里的 bash 执行器内部就是
confine(['bash', '-c', command], policy)这么调,shell 那层自己包。 - 返回值
ConfinedArgv带四个字段:argv(包装后)、enforcement(full/partial)、denialSignatures(被拒时的 stderr 方言)、runnerFailureRules(runner 故障证据规则)。 - fail-closed 是铁律:如果这个平台上没有任何可用后端,
confine直接抛SandboxUnavailableError(错误码SANDBOX_UNAVAILABLE),绝不把原始 argv 裸着还给你。「悄悄无隔离地放行」在这条链路里是永远不合法的。
三、三套半后端:bwrap / Landlock / Seatbelt / Windows ACL
dsh 把「定义」和「实现」拆得很干净:dsh-sandbox 只定契约和词汇表,真正干活的 dsh-sandbox-local 按平台选后端:
- Linux:优先
bwrap(bubblewrap),它的 mount profile 最贴近 mode 词汇表;bwrap 不可用就落到 Landlock launcher(@deepseek-ai/node-addon-landlock-run),需要内核支持 Landlock。 - macOS:Seatbelt,通过
sandbox-exec -p走 profile(allow default+deny file-write*+ 写白名单)。Apple 已经把sandbox-exec这个 CLI 标记为 deprecated,但每个 macOS 都还 ship 着,探针探测到不可用就 fail closed。 - Windows:ACL restricted-token runner(
@deepseek-ai/dsh-sandbox-windows-acl),用 write-SID 白名单来限制写。
后端选择是「缓存的一次性决策」——provider 生命周期内把 runner 结论缓存下来。所以你装了、修了、删了 bwrap,得重载插件(重启)才会重新选,这个坑很多人踩。
四、enforcement:为什么有 full 和 partial 之分
enforcement 是「报告出来的事实」,不是后端自己吹的:
full:后端管住了 mode 承诺的每一个文件效果。partial:活跃的后端或老内核 ABI 只覆盖一个子集。
当前 partial 的两大来源,官方文档写得明明白白:
- 老内核的 Landlock ABI:只 confine 它暴露出来的那些 access class,所以报告 partial 而不是硬说 full。
- Windows ACL runner:永远 partial。原因很具体——restricted token 必须保留
Everyone这个 SID 才能完成进程初始化,于是任何给Everyone开了写权限的外部对象,依然可写;NTFS 的 hard link 还能把一个工作区文件别名到工作区外的路径;而且 Windows 上 reads 是完全不受限的(ACL runner 只限写)。
所以结论很简单:bwrap / Seatbelt 是 full,Landlock 靠探针区分 full/partial,Windows ACL 恒为 partial。看到 partial 就说明这个边界不完整,要绝对边界就得拒绝执行或者上容器。
五、一条命令,验出沙箱是真关住了还是挂个名
验证分两层,都不用看源码。
第一层:看 deny 到底有没有发生。 在默认的 workspace-write 模式下,让命令写工作区之外的文件:
1 | touch /home/ubuntu/escape-test.txt |
如果沙箱真在干活,你会看到一条 deny,而且 stderr 的报错方言对应你的后端:
| 后端 | 报错方言 |
|---|---|
| bwrap | Read-only file system(EROFS,因为 bwrap 用只读 bind) |
| Landlock | Permission denied(EACCES) |
| Seatbelt | Operation not permitted(EPERM) |
这三个方言就是 denialSignatures——它故意做成「各后端各自的方言」,而不是一个大杂烩,因为大杂烩会把某个后端根本不会产生的 deny 也算进去。同时结果对象会带 sandbox: { mode, denied: true } 标记。
第二层:看 enforcement。 如果拿到 partial,说明边界不完整,需要绝对边界的话就得拒绝或换环境。
六、两个真实漏洞:文件沙箱没关住的两条逃逸路
这节是重头,也是你最该知道的部分——「文件 deny 生效了」不等于「沙箱安全了」。
CVE-2026-82533(CVSS 9.4):安全团队 OX Research 发现,dsh 的 agent 控制 API 挂在本地 127.0.0.1:3080,没有鉴权,靠客户端提供的 Host 请求头判断「是否可信」,而不是验证真实对端地址。而沙箱只限文件、不限 loopback 网络。于是沙箱里的 Agent 用一条 curl 就能调这个 API,把自己的会话提到 danger-full-access、把 approval 设成 never——从内部自己关掉了自己的沙箱。受影响版本 0.1.1-rc.2 及以前,0.1.2-alpha.1 修复。两个前提全是「出厂默认」,不用改配置、不用暴露网络、不用凭据。
GitHub discussion #1769:bwrap 的 workspace-write 沙箱可以被 mount -o remount,rw 逃逸,写直接落回宿主机文件系统——这是另一条「文件沙箱没想象中严」的路径。
这两条加起来的教训是:dsh 的沙箱只约束文件效果,不是完整的环境隔离。要真隔离,得换容器 / microVM / 远程执行器——它们是替换整个 ctx.shell / ctx.fs 的 capability 实现,不是给 ctx.sandbox 加一个 backend。想明白这一层,你才不会被「开了沙箱就刀枪不入」骗到。
七、生产环境怎么配:几条硬建议
- 升级到
>= 0.1.2-alpha.1,堵掉 CVE-2026-82533。 - 永远别把
127.0.0.1:3080这个本地 API 端口暴露到公网——隧道、反代、SSH 转发、编辑器端口转发,都算。 - 部署默认 mode 从 sandbox-policy 的配置取,出厂默认就是
workspace-write,可配在cordis.yml里:
1 | - id: sandbox |
这里补一句上一篇埋的线:权限预设管「要不要问」,沙箱 mode 管「文件能写到哪」,两层叠加才是完整边界,别混成一个。
- 在意绝对边界的话,读到
enforcement: partial就拒绝执行,或者干脆换容器/microVM。 - 无人值守 CI 里,别为了省事开
danger-full-access——那等于绕过 confinement,provider 根本不会被咨询。真要开,先把「喂给它的输入可不可信」想清楚。
生命不息,折腾不止。下一篇《DeepSeek Harness 多 Agent 编排:一个活拆给一队子 Agent 并行干》,聊 dsh 怎么用 subagent / 事件总线把一个复杂任务拆给多个子 Agent 协作,跟之前 LangGraph 的 Supervisor、Claude Code 的 Subagent 对照着看,瞧瞧「一切皆插件」的架构里多智能体到底怎么落地。