Skip to content

fix(私有文件): icacls 授权对象改用当前用户 SID,避免同名机器上把自己锁死 - #742

Open
colorgarden wants to merge 1 commit into
Ebony-Vinyl:mainfrom
colorgarden:fix/738-icacls-grantee-sid
Open

colorgarden wants to merge 1 commit into
Ebony-Vinyl:mainfrom
colorgarden:fix/738-icacls-grantee-sid

Conversation

@colorgarden

@colorgarden colorgarden commented Nov 2, 2026 •

Copy link
Copy Markdown

Fixes #738。

根因

restrictPermissions() 用裸 process.env.USERNAME 调 icacls /inheritance:r /grant:r:

const user = process.env.USERNAME ?? ''
execFileSync(path.join(system32, 'icacls.exe'),
  [file, '/inheritance:r', '/grant:r', `${user}:${directory ? '(OI)(CI)F' : 'F'}`],
  { stdio: 'ignore', windowsHide: true, shell: false })

计算机名与登录名只差大小写时(Windows 允许、个人机常见),icacls 在账户数据库与计算机账户之间解析这个裸名字,计算机账户胜出。写入的 ACE 因此指向 S-1-5-21-<machine>——没有任何用户令牌携带它;而同一次调用里的 /inheritance:r 已先删掉继承 ACE。文件只剩一条谁都匹配不上的 ACE,紧接着的 renameSync 必然 EPERM,文件连同已写入的内容一起变成不可读、不可写、不可删。

函数被 10 个模块的 14 处私有落盘点复用,所以这台机器上登录令牌、settings.json、catalog.json、stats.json、upgrade-state.json 全部只留在内存里;升级流程也停在这一步。

改动

src/open-external.js 新增 currentUserGrantee(),restrictPermissions() 改用它取授权对象:

  1. whoami /user /fo csv /nh 取当前令牌 SID,以 *S-1-... 形式交给 icacls——* 明确表示这是 SID,不要做名字解析。
  2. whoami 缺失或失败时回退到 USERDOMAIN\USERNAME 全限定名;仍比裸名严格,因为它点名了账户域,不再把二选一留给 icacls。
  3. 结果按进程记忆化:令牌 SID 在进程生命周期内不变,而这些存储定时 flush。

保留 PR #637 的原始动机:whoami 的账户名输出走 OEM 代码页,非 ASCII 机器名会被 UTF-8 解码毁掉。SID 是纯 ASCII、不含可本地化文本,这条顾虑对 SID 不适用——这也正是选 SID 而不是改回「解析账户名」的原因。

restrictPermissions() 的语义、icacls 失败即抛错放弃写入的行为、POSIX 分支、以及全部 14 处调用点均未改动。

回归套件

scripts/private-file-permissions-test.mjs:

已执行检查

本机:Windows 10.0.26300.9818 / Node v24.15.0,COMPUTERNAME=COLOR、USERNAME=color、USERDOMAIN=COLOR(触发条件成立)。

  • 修复前 node scripts/private-file-permissions-test.mjs → 5 private-file check(s) failed,首个失败 the DACL does not name S-1-5-21-...-1001: COLOR\:(F),renameSync 复现 EPERM。
  • 修复后 → 5/5 private-file checks passed。
  • 直接受影响的套件全部通过:private-file 5/5、open-external、updater、subscriptions 38/38。
  • node --check、git diff --check:通过。
  • node scripts/test-all.mjs --mode contributor:103/108。失败 5 项已用 git stash 在干净 baseline 上逐项对标,与本改动无关:
    • legacy-upgrade:本机浅克隆缺 f14d261 对象,CI 用 fetch-depth: 0 不触发;
    • magpie-engine:4 项 reviewer 回归,baseline 同样失败;
    • magpie-policy、standalone-management:registry.npmjs.org 在本机不可达 + 残留 EPERM 临时目录,baseline 同样失败;
    • forward:并发抖动,单跑 2/2 通过。

验证记录:docs/verification/2026-11-02-issue-738-icacls-grantee.md。

未验证范围

  • 未覆盖域账户与 Microsoft 账户实机(本机为本地账户);回退分支只保证不比原实现弱,未在无 whoami 的环境实测。
  • 未在 POSIX 实机运行;该分支代码未改动,CI Linux 覆盖。
  • 已存在的坏 ACE 文件不会自愈:restrictPermissions() 只处理新写入的临时文件。历史残留仍需 icacls <path> /reset 手工恢复(已验证可恢复且内容不丢)——如果认为需要在启动时做一次孤儿 tmp 自愈,那是另一个改动,本 PR 不做。
  • 未跑 test:release(签名清单对齐属正式发布流程,本分支不触碰)。

`restrictPermissions()` 用裸 `process.env.USERNAME` 调 `icacls /inheritance:r /grant:r`。
计算机名与登录名只差大小写时(Windows 允许、个人机常见),icacls 在账户数据库与计算机
账户之间解析该名字,计算机账户胜出,写入的 ACE 指向 `S-1-5-21-<machine>`——没有任何
用户令牌携带它;而同一调用里的 `/inheritance:r` 已先删掉继承 ACE。文件只剩一条谁都
匹配不上的 ACE,随后的 `renameSync` 必然 EPERM,文件连同内容一起变成不可读、不可写、
不可删。

该函数服务于全部「临时文件 + rename」的私有落盘路径,所以这台机器上登录令牌、
settings.json、catalog.json、upgrade-state.json 全部只留在内存里,升级也卡在状态
落盘(Ebony-Vinyl#738)。

改为 `whoami /user /fo csv /nh` 取当前令牌 SID,以 `*S-1-...` 形式交给 icacls
(`*` 明确表示 SID,不做名字解析);whoami 缺失或失败时回退到
`USERDOMAIN\USERNAME` 全限定名,仍比裸名严格。结果按进程记忆化——令牌 SID 在进程
生命周期内不变,而这些存储定时 flush。

保留 PR Ebony-Vinyl#637 的原始动机:whoami 的账户名输出走 OEM 代码页会毁掉非 ASCII 机器名;
SID 是纯 ASCII 且不含可本地化文本,该顾虑不适用。

回归套件的断言口径由「acl 含 (F) 且不含 (I)」加强为「DACL 的 SDDL 必须出现当前令牌
SID」——旧的 (F) 断言在坏 ACE 上同样成立,捕不到本缺陷;并新增一条端到端用例,在
USERNAME 与 COMPUTERNAME 相同的前提下走完写 tmp → restrictPermissions → rename
覆盖 → 回读。

Fixes Ebony-Vinyl#738

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant