Files
dsh_shenxian/dsh-server-docs/04-调整方案/68-候选池启停的root属主污染根治.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

5.1 KiB
Raw Blame History

68 · 候选池启停的 root 属主污染:根治

  • 日期:2026-09-12
  • 触发:投放 @dsh-local/[email protected] 时,guest 升级失败(EACCES: permission denied, open '.../node_modules/.bin/modlens')
  • 结论一句话:不停在"把它 chown 回去",而是三层一起做 —— 修根因(候选池启停改为 setpriv 降权)、加防复发(每次改插件前自动属主自愈)、清存量(561 项)。
  • 状态:✅ 全部完成并验证

TL;DR|结论:门户候选池的 install/uninstall 一直以平台进程身份(root)跑 pnpm,装出来的文件属主是 root → 该用户此后任何 pnpm add/remove 都必然 EACCES。 关键:guest profile 下积了 561 个 root 属主项;平台脚本(ensure-biz-plugins.cjs 等)用的是 setpriv 正姿,两条路线不同所以这个 bug 长期没暴露。 状态:✅ 已上线(修根因 + 自愈 + 清存量三层)


一、现象与规模

用户 profile 下 root 属主项 分布
guest(uid 100002) 561 .pnpm 555 / @liustack 2 / .bin 2 / dsh-univer-office 1 / 一个 .bak
admin(uid 114801) 5 全在 .pnpm

表现:该用户无法升级或启用任何插件 —— 以用户身份跑 pnpm add 时连旧文件都覆盖不了。

二、根因

src/web/routes/business-plugins.ts 的 install() / uninstall()(改前):

execFileSync('pnpm', ['add', 'file:' + plugin.tgzPath, ...], { cwd: dir, env: pnpmEnv(user.id) })

没有 setpriv 降权 → 以平台进程身份(root)执行 pnpm → 产物属主是 root。

证据吻合:audit_log 记录 guest 在 01:58 通过候选池启用 @liustack/modlens + dsh-univer-office —— 正是这批 .pnpm / @liustack / dsh-univer-office 项的来源。

为什么长期没暴露:平台侧脚本(ensure-biz-plugins.cjs / ensure-portal-entry.cjs)用的是 setpriv --reuid <uid> --regid <uid> --clear-groups 正姿;档案 43 修「平台 root 污染用户工作区」时也只覆盖了脚本那条线。门户候选池是第三条线,没人走过。

三、修复(三层,按"长期最佳"而非"先能跑")

3.1 修根因 —— install() / uninstall() 改为以用户身份执行

新增两个 helper 并替换两处调用:

helper 职责
uidOf(userId) 取该用户 uid(读 <root>/home 属主 —— 编排器创建时已 chown,少一处 DB 耦合)
runPnpmAs(userId, args, cwd) setpriv --reuid <uid> --regid <uid> --clear-groups env HOME=<ws> pnpm …
healOwnership(userId) 见 §3.2

HOME 仍指向 <root>/ws(pnpm 的 store/cache 落在这里,不设会报 ERR_PNPM_UNEXPECTED_STORE)—— 原 pnpmEnv() 因此变成死代码,已删,其说明移到 wsDir() 上。

3.2 防复发 —— 每次改插件前跑一次属主自愈

healOwnership(userId):find <profile> -user root 数一遍,有则 find … -exec chown -h <uid>:<uid> {} +。

  • 放在 install() / uninstall() 最前面(否则以用户身份跑 pnpm 连旧文件都覆盖不了);
  • 用 -exec … + 而非一次传全部路径 —— 561 条会撞命令行长度上限;
  • 用 chown -h —— .bin/* 是指向别处的符号链接,要改链接本身而非其目标;
  • 命中时写 heal_plugin_ownership 审计(含阶段与条数)。

这一层才是"长期"的关键:即便将来别的写入路径又污染了属主,用户下次启停插件时会被自动清理,不必再人工排查一次。

3.3 清存量 —— 手工跑一次同样的清理

对两个用户的 profile 执行与 healOwnership 等价的 find … -exec chown -h … {} +(属主污染是数据错误,修代码不会自动修正已存在的 561 个文件)。

四、验证

项 结果
npm run typecheck / build exit 0
部署 备份 .bak-ownfix-20260912 → scp → md5 一致;服务 active + HTTP=200
清存量 guest 剩余 0 / admin 剩余 0
投放 0.2.5 admin ✅ 0.2.4 → 0.2.5;guest ✅ 0.2.3 → 0.2.5(修复前该步必失败)
平台代码的 setpriv 路径 ⏳ 待用户下次在「功能管理」里启停插件时自然验证(该路径需用户会话)

五、附带修复:npm pack 把上一版 tgz 打进了产物

打包 0.2.5 时发现包体从 8.9 KB 涨到 19.3 KB —— 因为目录里上一版留下的 business-plugins-0.2.4.tgz 被一并打进 package/,且会被永久带进用户的 node_modules。

修复:加 .npmignore(*.tgz 等)而不是"每次记得先手工删" —— 同样是"长期最佳优先于补丁"。重打包后 9.5 KB / 4 个文件,与预期一致。

六、红线遵守

  • R7:批量 chown 限于两个用户的 profile 目录、只改属主不改内容;执行前已在汇报中列出污染清单与规模(561 / 5)。
  • R8:重启平台服务(实测有实例在跑时约 90 秒、无实例约 3 秒),已在上一轮汇报中说明影响。