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

86 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()`(改前):
```ts
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 秒),已在上一轮汇报中说明影响。