生命不息,折腾不止。上一篇把「权限」和「沙箱」两个旋钮盘明白了,这一篇钻进去看沙箱本身——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 出来的所有子进程,全都在受限状态下运行。几个必须记住的细节:

  1. argv 是精确的 argv 数组,不是 shell 字符串。dsh 里的 bash 执行器内部就是 confine(['bash', '-c', command], policy) 这么调,shell 那层自己包。
  2. 返回值 ConfinedArgv 带四个字段:argv(包装后)、enforcement(full / partial)、denialSignatures(被拒时的 stderr 方言)、runnerFailureRules(runner 故障证据规则)。
  3. 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 的两大来源,官方文档写得明明白白:

  1. 老内核的 Landlock ABI:只 confine 它暴露出来的那些 access class,所以报告 partial 而不是硬说 full。
  2. 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。想明白这一层,你才不会被「开了沙箱就刀枪不入」骗到。

七、生产环境怎么配:几条硬建议

  1. 升级到 >= 0.1.2-alpha.1,堵掉 CVE-2026-82533。
  2. 永远别把 127.0.0.1:3080 这个本地 API 端口暴露到公网——隧道、反代、SSH 转发、编辑器端口转发,都算。
  3. 部署默认 mode 从 sandbox-policy 的配置取,出厂默认就是 workspace-write,可配在 cordis.yml 里:
1
2
3
4
5
6
7
8
9
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: workspace-write # 部署默认:每个会话从这里起步
workspaceRoot: !!js process.cwd() # workspace-write 可写的边界
- id: bash
name: '@deepseek-ai/dsh-bash-sandbox'

这里补一句上一篇埋的线:权限预设管「要不要问」,沙箱 mode 管「文件能写到哪」,两层叠加才是完整边界,别混成一个。

  1. 在意绝对边界的话,读到 enforcement: partial 就拒绝执行,或者干脆换容器/microVM。
  2. 无人值守 CI 里,别为了省事开 danger-full-access——那等于绕过 confinement,provider 根本不会被咨询。真要开,先把「喂给它的输入可不可信」想清楚。

生命不息,折腾不止。下一篇《DeepSeek Harness 多 Agent 编排:一个活拆给一队子 Agent 并行干》,聊 dsh 怎么用 subagent / 事件总线把一个复杂任务拆给多个子 Agent 协作,跟之前 LangGraph 的 Supervisor、Claude Code 的 Subagent 对照着看,瞧瞧「一切皆插件」的架构里多智能体到底怎么落地。