Repository navigation
[BUG] ops_write_file 新建的文件只留一条调用者显式 ACE、丢掉父目录全部继承 ACE:刚建的文件同一会话删不掉,跨用户/工具访问也被切断 #420
Copy link
Copy link
Open
Description
摘要
ops_write_file 新建的文件只带一条显式 ACE PC-…\Administrator:(F),没有从父目录继承任何 ACE(连 DSH 自己给 workspace 授权的沙箱写 SID S-1-4-<hash>:(OI)(CI)(W,D,DC) 都没有)。后果不是“权限稍微严一点”,而是两个实际故障:
- 自己能建、自己不能删:同一会话里
rm刚由ops_write_file创建的文件 →Permission denied(DSH 侧提示[sandbox: file access denied under workspace-write mode]),因为受限令牌的写 SID 在该文件的 DACL 里没有 ACE; - 继承被切断:父目录的
Authenticated Users:(OI)(CI)(IO)(M)、Users:(OI)(CI)(IO)(GR,GE)、Administrators:(OI)(CI)(IO)(F)全部丢失,多用户/服务账户/后续工具的访问会失败,与本工具描述里的 “Windows preserves owner/group/ordered DACL” 给人的预期相反(preserve 只对被覆盖的旧文件成立,新建时是替换)。
覆盖写一个已存在的文件时 DACL 不会被修好——它保持那条孤儿 ACE 不变,所以“先建后删/先建后改名”的构建流程会一直卡住。
环境
| 项 | 值 |
|---|---|
| OS | Windows 10 专业版 19045.4291(NTFS 卷) |
| DSH | 0.2.1-rc.3(Desktop) |
| dsh-ops | 0.4.1 |
| 会话策略 | workspace-write,审批 ask(期间未使用 danger-full-access) |
| 工作区 | 普通目录,本身已由 DSH 授权:S-1-4-<hash>:(OI)(CI)(W,D,DC) + Mandatory Label\Low:(OI)(CI)(NW) |
最小复现(A/B 对照,同一目录)
A:ops_write_file 新建
{ "path": "_del_probe_a.txt", "content": "x\n", "expectedRevision": "absent" }
→ write_file: completed; … metadata={"created":true}
B:沙箱内 bash 新建(对照组)
printf 'x\n' > _del_probe_b.txt
DACL 对比(icacls 原样输出):
A _del_probe_a.txt PC-20250512ZCYU\Administrator:(F) ← 显式 ACE,无 (I)
Mandatory Label\Low Mandatory Level:(I)(NW)
B _del_probe_b.txt S-1-4-<hash>:(I)(W,D,DC) ← 来自父目录继承
BUILTIN\Administrators:(I)(F)
NT AUTHORITY\SYSTEM:(I)(F)
NT AUTHORITY\Authenticated Users:(I)(M)
BUILTIN\Users:(I)(RX)
Mandatory Label\Low Mandatory Level:(I)(NW)
删除测试(同一会话、同一令牌):
rm -f _del_probe_b.txt → rc=0 ✅
rm -f _del_probe_a.txt → Permission denied / [sandbox: file access denied under workspace-write mode] ❌
再做一次覆盖写(expectedRevision 给 A 的当前 revision):metadata={"attributes":true,"dacl":true,"group":true,"owner":true} 回报一切正常,但 icacls 显示 A 仍是 Administrator:(F) 一条 —— 覆盖写不会把继承修回来。
作为反证:宿主进程(非沙箱通道)写的 *.md 在同样目录里沙箱可以正常 rm,说明问题只在 ops_write_file 新建这条路径上,不是 workspace 授权缺失。
机制线索(供作者确认)
native/crates/ops-files/src/里 grep 不到 任何DACL/SECURITY_ATTRIBUTES/SetSecurityInfo/SetNamedSecurityInfo调用;file_publication.rs:29/153-161用的是create(true)/create_new(true),cache_io.rs:44-45用create_new(true).mode(0o600)(mode在 Windows 上无意义)。- 因此“只留一条调用者 ACE 的显式 DACL”不是 ops-files crate 自己设的,更像是发布阶段把某个临时件(staging/temp,DACL 只给当前用户)改名/复制到工作区后继承了它的安全描述符,或由宿主侧“preserve metadata”步骤显式写入。真正的低完整性标签
Low:(I)(NW)是继承来的,说明文件确实落在工作区父目录下 —— 两种解释都能解释标签,但只有“显式/受保护 DACL”能解释继承 ACE 全丢。 - 这条如果成立,它和“原子发布”的实现方式强相关(
file_publication.rs的 stage-then-publish)。
建议修复
- 新建/发布时不要带显式或受保护 DACL:去掉
PROTECTED_DACL_SECURITY_INFORMATION,让新文件按父目录继承;若确实需要从 staging 复制,则在 publish 时用SetFileSecurity重算继承(SE_DACL_AUTO_INHERITED)。 - 加一条回归测试:
ops_write_file新建后,文件的 DACL 必须包含父目录的所有可继承 ACE(至少沙箱写 SID),并且同一会话能删除该文件。 - 顺手校正文档:现在写的是 “Windows preserves owner/group/ordered DACL”,容易被读成“新建也会按目录策略落 ACL”;建议明确“覆盖保留 / 新建继承”。
相关
- DSH core 侧同一现象的成因链见 [BUG] Windows:danger-full-access 下准备结果缓存仍收窄为 workspace-write,工作区被写入常驻 Low 完整性标签 #411(danger-full-access 准备缓存 + Low 标签)与 [BUG] 并发调用 ops_bash 时第二个 bash 启动失败:CreateFileMapping S-1-5-21-<user>-1001.1, Win32 error 5 #416/[BUG] Windows: 非受限 MSYS 先创建的 per-user 共享段仍无 logon SID ACE,受限 bash 在 #389 之后仍会崩在 CreateFileMapping S-<sid>.<n> #409(受限令牌找不到 per-user 共享段),本条是文件对象级的同类缺口,独立于上述进程级问题。
- 与本仓库另一条契约问题 [BUG] ops_write_file 的 create-only 只能靠字面量 "absent" 触发:expectedRevision 声明为 required,description 却写 "absent is create-only",省略与空串两种自然写法都先硬失败 #419(
expectedRevisioncreate-only 只能传字面量"absent")无关,只是同为 Windows 首次落地时撞上的坑。
清理
A/B 两个探针文件已删除(A 因上述缺陷需要用更宽权限删除,正好是这条 bug 的自证)。
/cc @T-Auto 这条想请作者看一下修复方向是否认同(尤其是“发布时显式 DACL 导致继承丢失”这个判断),需要的话我可以补更细的追踪(Procmon 或 handle 级证据)。
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
cryaciccl commentedon Oct 27, 2026
定位已确定:设 DACL 的不是
ops-files,而是同仓native/windows-metadata的create_private;缺的是「新建分支」发布后的继承回落主帖「机制线索」里说在
crates/ops-files/srcgrep 不到SetSecurityInfo—— 这点是对的,因为设 DACL 的代码在同仓另一个 cratenative/windows-metadata。按主帖那条/cc,这里补上精确链路、可复现解释,以及一个不破坏「暂存期保密」的修法。确切链路(0.4.1,源码随包发布)
① 暂存件一律带受保护 DACL
native/crates/ops-files/src/file_publication.rs:99-106,create_sibling的 Windows 分支把source直接丢掉:②
create_private的 DACL 是显式构造的native/windows-metadata/src/lib.rs:87-89的文档注释自陈(原文)::106-119的private_descriptor()最终 SDDL:D:P即 受保护 DACL,一条 ACE、inherited=False—— 与主帖icacls输出逐字吻合。③ 新建与覆盖的差别,决定「覆盖也修不回来」
新建路径
native/crates/ops-files/src/file_single.rs:377-378:新文件
source == None→preserved == None→:392-401的preserved.restore(&file)整段跳过;随后:435用hard_link发布,安全描述符随 inode 一起过去。覆盖路径
native/crates/ops-files/src/file.rs:297先Preserved::capture(&checked, true)抓目标原有的 DACL,:311再restore回去。所以覆盖一个已坏的文件只是把那条孤儿 ACE 原样搬回去 —— 这正是主帖观察到的「覆盖写不会把继承修回来」。顺带解释了
metadata={"dacl":true}为何如实回报成功:它确实「保留」了,只是保留的对象本身是错的。私有暂存是对的,缺的是发布后的继承回落
我不建议主帖建议 1 的「新建时不要带受保护 DACL」:暂存期间若正常继承,同目录的其他主体/受限进程能读到写了一半的内容,而
create_private的注释表明这是有意设计。(create_cache_object的write_sid分支更是必须保持私有。)缺的是新文件发布之后的一步:此时字节已完整,不必再保密,应把父目录的可继承 ACE 施加到目标上。
修法草案
windows-metadata里已有全部原语(descriptor()=GetSecurityInfo、sddl()、SetSecurityDescriptorControl、SetKernelObjectSecurity),新增一个同族函数即可:调用点:
file_single.rs::publish_new,source.is_none()的 Windows 分支,在hard_link(:435)之后、sync_applied之前。不要动file.rs(覆盖,必须保留目标 SD)和create_cache_object(缓存件就该私有)。两个实现坑:
NULLDACL 让系统补算继承」那套:pDacl == NULL且置了SE_DACL_PRESENT时会把 DACL 设成 NULL,等于 everyone 完全控制。必须显式构造父目录的可继承集。SE_DACL_PROTECTED不会让 Windows 自动补 ACE(重算只发生在父目录变更传播或显式 auto-inherit 请求时),所以还是得主动施加一次;SE_DACL_AUTO_INHERITED那一位写不写无所谓,verify_cache只比对D:之后的部分。回归测试建议
ops_write_file(expectedRevision: "absent")新建到工作区后:S-1-4-<hash>:(OI)(CI)(W,D,DC));rm掉它;icacls /inheritance:r过的文件后其 DACL 不变 —— 防止把 1 的修法做成「所有写入都重算 ACL」,破坏 W1a/W1b 那类显式 ACE / deny 保留语义。补充实测(可作对照)
同一台机、同一工作区、同样
workspace-write:ops_write_file新建 → 1 条非继承 ACE,rm失败;write(走dsh-fs-local的writeFileAtomic,同样是暂存 + rename)没有这个问题:新文件 ACE 全部inherited=True且含沙箱能力 SID,rm成功。差别只在dsh-fs-local的暂存目录是普通目录(ACE 继承自父目录),而create_private显式设了受保护 DACL。所以既不是 OS 行为,也不是 DSH 工作区授权缺失,范围收敛在
create_private产出物的新建发布路径上。需要的话我可以按上面草案提 PR(含
windows-metadata单测 + 第 3 条反向断言)。本机 Windows 10 19045 + dsh-ops 0.4.1,也能顺手在真实受限令牌下做端到端验证。cryaciccl commentedon Oct 27, 2026
已按上面的链路实现并验证,PR:#438
windows-metadata新增adopt_parent_dacl(parent, child):只取父目录带OBJECT_INHERIT_ACE的 ACE,以未保护 DACL 写回;创建者FA头一条,发布永不收窄发布者自己。publish_new在hard_link之前、字节写完之后调用它(source.is_none()的 Windows 分支)。create_cache_object不动。验证:
cargo test --workspace --locked55 passed / 0 failed / 1 ignored(既有#[ignore]);新增两个用例,撤掉调用后new_file_adopts_directory_grants变红(14 passed; 1 failed)。端到端:把重编 worker 换进已安装插件后,ops_write_file新建文件的 DACL 从 1 条变成 7 条,含S-1-4-…(W,D,DC)能力 ACE。cryaciccl commentedon Oct 27, 2026
补上受限令牌下的 A/B(同一台机、同一工作区、
workspace-write沙箱,只换 worker 二进制):另一点值得写进 release note:修复只作用于新建文件,已存在的旧文件不会自愈(实测旧文件换装补丁版后仍
Permission denied),需要icacls <file> /inheritance:e一次性恢复继承。PR:#438