Replies: 1 comment
|
环境:Windows 11 / 补充实测:Windows 下
|
| 分类 | 数量 |
|---|---|
| 全树对象 | 729 |
| 缺少 capability ACE | 170 |
| ├ 一级子目录 | 14(docs momo tests tools utils dataset log temp .cache …) |
| ├ 根级文件 | 5(README.md 项目结构.md requirements.txt pylock.toml .gitignore) |
| ├ 嵌套对象 | 19(__pycache__ 下 11、.cache 下 8) |
└ tempfile.mkdtemp() 残留目录 |
141 |
注意「根级文件也中招」这一点:缺失并不局限于子目录,而是所有在传播发生时调用者不可写的对象。
3. 判据修正:不能只看"ACE 在不在"
.cache\pytest 这类对象 capability ACE 存在,但受限会话仍然双向被拒:
Get-Acl → Attempted to perform an unauthorized operation
LIST → Access to the path ... is denied
WRITE → Access to the path ... is denied
原因:读/列举走正常令牌,写/删走受限令牌,两侧都要满足。该对象的 DACL 里只有 Administrators/SYSTEM + capability SID,没有给登录用户/Authenticated Users 的 ACE,因此"读"这一侧永远不过。若自检只匹配 capability SID,这类对象会被漏报。建议自检同时校验两侧可达性。
4. 用户无法在外部补救:icacls 路线被堵死
> icacls <workspaceRoot> /grant "*S-1-4-<a>-<b>:(OI|CI)(M)" /T /C /L
*S-1-4-<a>-<b>: 账户名与安全标识间无任何映射完成。
已成功处理 0 个文件; 处理 1 个文件时失败
即 Win32 ERROR_NONE_MAPPED (1332):capability SID 由 DSH 用路径哈希生成,系统中没有账户/包映射,icacls 每次 /grant 都会先做 LookupAccountSid,所以任何基于 icacls 的补救途径都会被拒(/T /C 也救不回来,它连根目录那一层都没过)。
唯一可行的是 SetNamedSecurityInfoW(或 .NET Get-Acl/Set-Acl 配 SecurityIdentifier 对象),且必须提权(这些后代上只有 AU:(M),无 WRITE_DAC;提权后靠 BUILTIN\Administrators: FullControl 才写得动 DACL)。也就是说:后代补授只能由 DSH 自己完成,用户无法在沙箱外自动化修复。
5. 与 #463 叠加放大
缺失对象中 141/170 是 tempfile.mkdtemp() 残留。实测(Windows 11 / Python 3.13.15 / 受限令牌)——mkdtemp() 建出的目录,随后:
Get-Acl → Attempted to perform an unauthorized operation
写子文件 → Access is denied
Remove-Item -Force → Access is denied
cmd rd /s /q → Access is denied
python shutil.rmtree → PermissionError: [WinError 5]
即该目录的安全描述符把除提权外的一切途径堵死(同一受限令牌的其它进程同样不能访问)。当沙箱把 TEMP/TMP 指向工作区(专用沙箱账户写不了用户级 %TEMP% 时的常见做法)时,每一轮测试都会在工作区根级留下若干个既删不掉、又缺 capability ACE 的目录——两个缺陷叠加,工作区被持续污染、授权被持续破坏。名字指纹可确认来源:tmp + 8 位 [a-z0-9_],tempfile.gettempprefix() == 'tmp'、_RandomNameSequence.characters == 'abcdefghijklmnopqrstuvwxyz0123456789_'。
6. 建议
hasExactGrant从"根上存在即短路"改为校验后代覆盖;- 物化 ACE 后遍历后代补授;对确实无
WRITE_DAC无法补授的对象显式上报(返回值/警告/日志),不要静默成功; - 自检纳入「正常令牌 + 受限令牌」两侧可达性,而不是只匹配 capability SID;
- 可选:把工作区内的
TEMP/TMP指向 DSH 可写可清理的私有目录,避免mkdtemp残留落进工作区。
我在本机按上述方式手工提权修完后:缺失 170 → 5(仅剩 ignore\ 下刻意保留的对象),沙箱视角可达性扫描(105 目录 / 320 文件)只剩红线路径不可写,ruff format 写子目录文件恢复正常。
7. 复现(约 5 分钟)
- 准备一个工作区,其中部分子目录/文件由另一套沙箱工具(或普通用户之外的账户)创建;
- 用
workspace-write启动 DSH 会话(首次会话生效); - 审计(在任意窗口):
$root = '<workspaceRoot>'
$sid = (Get-Acl -LiteralPath $root).Access |
Where-Object { $_.IdentityReference -like 'S-1-4-*' } |
Select-Object -First 1 -ExpandProperty IdentityReference
Get-ChildItem -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { (Get-Acl -LiteralPath $_.FullName).Sddl -notmatch [regex]::Escape($sid.Value) } |
Select-Object -First 20 -ExpandProperty FullName- 或直接在 DSH 会话内做可达性扫描(更贴近真实判据):
$root = (Get-Location).Path
$bad = [System.Collections.Generic.List[string]]::new()
$stack = [System.Collections.Generic.Stack[string]]::new(); $stack.Push($root)
while ($stack.Count) {
$d = $stack.Pop()
try {
foreach ($e in [System.IO.Directory]::EnumerateFileSystemEntries($d)) {
$attr = [System.IO.File]::GetAttributes($e)
if ($attr -band [System.IO.FileAttributes]::Directory) {
try { [System.IO.Directory]::EnumerateFileSystemEntries($e).GetEnumerator().MoveNext() | Out-Null }
catch { $bad.Add("LIST $e"); continue }
$stack.Push($e)
} else {
try {
$fs = [System.IO.File]::Open($e,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::ReadWrite)
$fs.Close()
} catch { $bad.Add("WRITE $e") }
}
}
} catch { $bad.Add("LIST $d") }
}
$bad
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
[Bug Report] Windows 工作区:连接后外部创建/移入的子目录永远无法写入(capability ACE 永不补授)
摘要
dsh 的 Windows ACL 沙箱对 workspace root 的 capability-SID ACE 是「一次性授权 + 永久驻留」:
grantWrite在 root 已有精确 ACE 时直接跳过(hasExactGrant),而唯一会把可继承 ACE 传播到现有子树的操作(SetNamedSecurityInfoW的应用)只在首次 provision 时发生。因此,任何在工作区首次授权之后才出现、且没有继承 capability SID 的子目录——典型场景:外部进程在别处创建后移动进工作区(Windows 同卷移动保留源 DACL,已实测)——在后续所有 workspace-write 会话中都无法写入,表现为[sandbox: file access denied under workspace-write mode]。已用 rc.6 发布包完整复现并验证修复方向(见下方验证材料)。复现步骤(Windows 11 · dsh 0.1.0-rc.6 · Node v24.16.0,全部实证)
D:\ws连接为工作区并执行一次 workspace-write 工具 → 首次 provision 对 root 施加 standing ACE:icacls ws显示S-1-4-…:(OI)(CI)(W,D,DC)。late-child,再移动进D:\ws\late-child(或 git checkout / 解压 / 跨目录移动,效果相同)。icacls D:\ws\late-child→ 没有 capability SID(移动保留源 DACL)。grantWrite命中 exact-ACE skip,late-child仍未补授。→
Access is denied,工具层按拒绝签名呈现为[sandbox: file access denied under workspace-write mode]。同一 session 写D:\ws\test.txt(root)成功。对照:在工作区内原位创建的子目录正常继承 ACE、可写(受限进程创建的文件实测同样靠继承获得 ACE:
icacls全部带(I)标记);Agent 自建目录同理,无需任何兜底机制。根因(源码定位,rc.6)
dsh-sandbox-windows-aclgrantWrite():root 显式 DACL 已含精确 ACE 时直接 return,不再SetNamedSecurityInfoW——该调用正是把可继承 ACE 传播到现有子树(eager inheritance)的唯一途径;注释自述不跳过会"minutes on large workspaces"。dsh-sandbox-localmaterializeAclGrant():workspace grant 按 provider 生命周期缓存一次;新生命周期首次调用仍走上述 skip。workspaceWriteSid()由 canonical 路径确定性派生 → 跨生命周期同一 SID → skip 恒命中,任何后续出现的缺 ACE 子目录永无补授机会。dsh-fs-sandbox只做路径包含检查;真正的拒绝发生在 NTFS 层(受限令牌的 restricting-SID 交集检查)。SetNamedSecurityInfoWWin32 5),嵌套受限令牌也无法加入新 SID(实测CreateRestrictedTokenWin32 87)。建议修复
方案 A(推荐)· provision 时增量补授:
materializeAclGrant在 workspace grant 命中 skip 后,遍历工作区目录树,对缺精确 ACE 的最高层目录执行grantWrite(其应用会自动向该目录现有子树传播),已有 ACE 的目录不再下钻。成本:每 server 生命周期一次元数据遍历 + 仅缺失目录的 DACL 写(可用 mtime 缓存进一步优化)。与现有「standing ACE = 复用缓存」设计正交,不破坏性能假设。方案 B · denial 自愈:工具层识别 ACCESS_DENIED 签名且路径在 workspace 内时,对目标目录补授后重试一次。可覆盖 shell 写入,但改动面更大。
方案 C(去掉 skip、每次全量重传)在大工作区不可接受。
影响
环境
47f943859bef60e4160492346772ded9b24f765a· Node v24.15.0 · pnpm 11.19.0验证材料(全部在 rc.6 发布包上实测)
E1 · 首次 provision(等价 seam 对 workspace root 的一次 standing grant):
E2 · 原位创建的子目录正常继承(外部普通进程直接在工作区内创建):
E3 · 外部创建后移入的子目录没有 SID(在 ws 外创建
late-child,再移动进 ws;Windows 同卷移动保留源 DACL):E4 · 第二次 provision 不自愈:再次执行与 E1 完全相同的 grant 调用(等价新 server 生命周期),
grantWrite命中 exact-ACE skip,late-child的 ACL 无任何变化。E5 · 受限写入端到端(真实 runner 以 workspace-write 受限令牌派生 cmd):
E6 · 修复方向验证:对
late-child补授同一 capability SID 后,同样的受限写入立即成功 → 「provision 时对缺 ACE 子目录增量补授」的修复方向有效。用户侧 workaround:对该子目录的写入审批提升到
danger-full-access可成功(不受限进程凭普通用户 ACE 写入)。完整日志(含 icacls 全量输出)与复现脚本可随时提供。
Found by community user reproducing discussion #401. Happy to open a PR with fix option A.
All reactions