Repository navigation
Releases
dsh-acl-sandbox-patch v0.1.2
dsh-acl-sandbox-patch — Release Notes
0.1.2(首个发布版)
上游缺陷修复:DSH 0.1.7 Windows ACL 沙箱在非系统盘工作区(D:/E:/F:)下,workspace-write 的所有命令在 spawn 前直接失败——不动任何系统 ACL,让命令恢复可执行。
上游已知问题:Discussion #7771(工作区所有者问题触发)、#7720(另一半触发条件)、#7804(详细实验报告)。0.1.7-rc.2 的沙箱包与 rc.1 逐字节相同,未修复。
这是什么
0.1.7 起,workspace-write 的每条命令在 spawn 前都要给工作区做 ACL 授权。@deepseek-ai/dsh-sandbox-windows-acl 用一次 SetNamedSecurityInfoW 同时下发三样东西:capability SID 的 Allow ACE、world SID 的 FILE_DELETE_CHILD Deny ACE、Low 完整性强制标签。写标签要求调用者对该目录持有 WRITE_OWNER,而非系统盘新建目录的默认 ACL 只有 Authenticated Users: Modify;DSH 跑在 UAC 过滤令牌下又吃不到 BUILTIN\Administrators 的 FullControl(该组是 deny-only);属主隐式权限只有 READ_CONTROL + WRITE_DAC,不含 WRITE_OWNER。授权返回 ERROR_ACCESS_DENIED (5),沙箱 fail-closed,命令从未 spawn:
SetNamedSecurityInfoW failed (Win32 5): grantWrite(<工作区路径>)
read-only 不受影响(不授予可写根,走不到这个调用);位于 %USERPROFILE% 下的工作区也不触发(用户 Profile 天生给用户 SID FullControl)。
原理
只包装沙箱自己发起的 FFI 调用,不改任何系统 ACL。两条注入路径互兜底:
- A. koffi 钩子:hook
Module._load拦require('koffi'),包装koffi.load()返回对象的.func(),对 dsh-win32-process 的 4 参数调用形式返回包装函数(C 声明式形式原样透传)。随NODE_OPTIONS=--require=lib/preload.cjs预加载,覆盖 sandbox runner 等所有 node 子进程。 - B. api 表直改:解析沙箱包后创建一次性
AclWriteGrant物化进程级共享绑定表,直接替换表上目标属性后 dispose。与绑定时序无关,覆盖 DSH 服务端进程。
三个包装点:
SetNamedSecurityInfoW:strict-first——先按原语义完整尝试(含 Low 标签),仅当返回 Win32 5 时降级重试为 DACL-only(剥掉LABEL_SECURITY_INFORMATION,SACL 传 NULL)。DACL 写入只需属主隐式的WRITE_DAC,可以成功。SetTokenInformation(TokenIntegrityLevel=25):双判据命中其一即跳过把子进程令牌降到 Low——工作区没有 Low 标签,Low 子进程按 no-write-up 什么都写不了。判据 1:状态文件(插件目录.degraded.state主路径)/ 环境变量 / 进程标志;判据 2:runner 从自身 argv 取--workspace直侦该目录有无 Low 标签,无 ⇒ 降级环境。判据 2 不依赖任何文件与环境,是最终兜底。GetNamedSecurityInfoW:降级发生后,对含 LABEL 位的查询返回合成的"已带精确 Low 标签" SACL(与hasExactLabel的逐字段比对同构),让幂等跳过重新命中。
环境健康时行为与出厂一致(完整授权、正常降完整性);只有真的打不上标签的环境才降级。
兼容性
宿主要求:DSH ≥ 0.1.7-rc.1(该版本引入 @deepseek-ai/dsh-sandbox-windows-acl,缺陷于此引入;0.1.5 及更早无此沙箱,无需本插件)。该下限已写入 package.json 的 engines.dsh。
| 宿主版本 | 状态 | 说明 |
|---|---|---|
| 0.1.7-rc.1 | ✅ 支持(最低) | 缺陷引入版本 |
| 0.1.7-rc.2 | ✅ 已验证 | 沙箱包与 rc.1 逐字节相同;本版全部实测数据取自该版本 |
| ≤ 0.1.5 | ➖ 不需要 | 无 ACL 沙箱,不存在该失败 |
平台:仅 Windows(NTFS);其他平台插件自动 no-op。策略:仅 workspace-write 需要(read-only / danger-full-access 不受影响)。
性能
| 场景 | 实测 |
|---|---|
| 未打补丁 | 任何命令 spawn 前失败(Win32 5) |
打补丁后普通 pwsh 执行 |
直接返回,无需逐条批准 |
重启 DSH 后首条 workspace-write 命令 |
约 20 秒(一次性 DACL 全树传播,沙箱机制的固有代价;健康工作区首次授权同样要付) |
| 之后同进程内的命令 | 约 1.7 秒 |
| 新开会话的命令 | 2 秒以内(seam 的 per-provider 复用缓存生效) |
| 沙箱内令牌完整性 | Medium(修复前为 Low,沙箱内写文件全被 no-write-up 拒) |
| 沙箱内文件操作(新建/覆写/删除) | 全部正常(修复前全部 Access denied) |
安全边界(诚实话在此)
保留:WRITE_RESTRICTED 受限令牌(双列表访问检查)、capability SID Allow ACE(只有工作区和会话私有 temp 可写)、world SID FILE_DELETE_CHILD Deny(删除权限收敛)、fail-closed(其他任何 Win32 失败照旧抛错,绝不去掉沙箱)。
失去(仅发生在本来就打不上 Low 标签的环境):Low 完整性强制标签这一层纵深防御(子进程不再被强制 Low 完整性)。
读限制、网络限制、进程可见性限制本来就不在沙箱范围内,与出厂一致。
已知限制
- 重启后首条命令慢:降级后工作区的 exact-ACE skip 不再命中(Low 标签永远不在),每次 DSH 重启后对该工作区的首条
workspace-write命令要做一次 DACL 全树传播(实测 44k 文件约 20 秒)。同进程内的后续命令与新会话走复用缓存。 - 重启前已建立的会话,其命令通道可能是重启前的旧 runner 孤儿进程(旧版行为:Low 令牌、无状态文件感知)——这是 DSH 重启不回收旧 runner 的进程管理行为,非插件缺陷;新开会话即恢复正常。
- 上游修复发布后应卸载本插件恢复出厂行为(对应 discussion 关闭或沙箱包升级时,严格路径直接成功、插件退化为无异于出厂)。
安装
dsh plugin --profile web add github:masknull/dsh-acl-sandbox-patch
该命令会链接包、记入依赖,并因包声明了 dsh.bundle 而自动加入 profile 的 bundle 列表。重启 DSH 使新的 bundle 层生效,启动日志应出现:
[dsh-acl-sandbox-patch] active — Windows ACL 沙箱 Low 标签/完整性降级补丁已安装(上游 Discussions #7771/#7720/#7804)
供应链安全可固定提交:
github:masknull/dsh-acl-sandbox-patch#<commit sha>。
卸载
dsh plugin --profile web remove dsh-acl-sandbox-patch
然后重启 DSH。插件不产生任何持久副作用:不改系统 ACL、不留文件(NODE_OPTIONS 只在本进程内设置并被子进程继承;状态文件位于插件目录/tmpdir,每次启动先清、运行期生成)、无后台任务。
升级注意
首个发布版,无升级路径。0.1.0 / 0.1.1 为开发期内部迭代(未发布):0.1.1 修复了 koffi lib.func 声明式调用形式误判(该 bug 曾导致所有"覆盖文件"的写操作失败);0.1.2 将降级状态文件主路径迁入插件目录(0.1.1 放在 os.tmpdir(),被 runner 链路的 TMP/TEMP 改写打败)并新增工作区标签直侦测判据。最终发布版即 0.1.2,无历史版本需要迁移。