feat(sandbox): 不可信字节在沙箱子进程里解析(撤销权限、超时与内存强杀);种子、文件预览、字幕解析迁入

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 01:44:31 +08:00
1 parent 1a76d2d700
commit eeec91e892
21 files changed
+632 -178

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
# ADR-005:不可信字节在沙箱子进程里解析
- 状态:采纳(2026-10-03,用户确认整体规划)。
- 关联:ADR-004(Deno 运行时与权限)、`packages/adapters/src/fs/fsproxy.ts`(同一子进程模式)、docs/plan/subtitles.md。
## 背景
我们执行的都是自己的代码,但要解析大量**外部的不可信数据**:站点下载的种子、字幕与压缩包、文件管理里的文件、站点网页与 RSS、验证码图片。风险不是代码逃逸,而是畸形 / 恶意数据让解析器卡死、撑爆内存,或者触发解析库的漏洞,把整个服务拖垮。
Deno 官方对不可信代码的建议是多层防护:启动即收紧权限(`--frozen`)、权限更少的 Web Worker、操作系统级沙箱、VM。我们在 Deno 2.9.7 上实测:
| 方案 | 权限隔离 | 卡死后能否收回 | 内存上限 |
| --- | --- | --- | --- |
| Worker `deno.permissions: "none"` | 可以,但要 `--unstable-worker-options` | 不能:`terminate()` 返回后,死循环的线程仍占满 CPU | 不支持 |
| 子进程,启动后 `Deno.permissions.revokeSync` 撤销全部权限 | 可以(稳定 API) | 可以:SIGKILL | 可以:父进程读 `/proc/<pid>/status` 的 RSS,超限 SIGKILL |
编译产物不认 `DENO_V8_FLAGS`,所以内存上限统一靠父进程的 RSS 看门狗;开发模式另加两倍的 V8 堆上限兜底。
## 决定
1. **沙箱 = 一次性子进程**(`apps/server/src/sandbox/`),起法同 fsproxy:deno CLI 下 `deno run worker.ts`,编译二进制下 `<自身> --role sandbox`(`main.ts` 在加载其余模块前分派)。子进程加载任务表后撤销 read / write / net / env / run / ffi / sys 全部权限,从 stdin 读一条 `{job, input}`,stdout 写回 `{ok, result | error}`;清空环境变量启动。
2. **调用**:`runSandbox(任务名, 输入, { timeoutMs, memoryMb })`,缺省 20 s / 512 MB;失败分类 `timeout / memory / crash / job / spawn`。起不来子进程时退回进程内执行并记日志(仍有超时,但卡死无法收回)。
3. **任务表**(`sandbox/jobs.ts`)只放纯函数:字节或文本进、可 JSON 化的数据出,二进制用 base64。
4. **adapters 不依赖服务端**:不可信字节的解析入口做成可替换的(`packages/adapters/src/isolation.ts`,缺省进程内),服务端启动时 `installSandboxParsers()` 换成沙箱实现。
5. **约定**:新功能凡是解析外部下载 / 上传的文件或字节,一律走沙箱任务(写进 CLAUDE.md)。
## 范围与顺序
- 已迁移:种子解析(站点下载校验、qB 提交前算 hash、浏览器下载核验、公开 BT 站详情读文件列表)、文件管理预览(种子 / 字幕 / 文本 / 图片头)、补字幕(解码、zip 解压、语言与季集识别)。
- 随补字幕第 2 步:rar / 7z 解压、SubHD / 字幕库验证码识别。
- 第二批:站点网页、RSS、网页抓取的 HTML / XML 解析。调用频率高,要先把适配器里「请求」与「解析」拆开,再接常驻子进程池(复用进程、出事重启),避免每次约 50 ms 的启动开销。
- 已隔离、不动:ffprobe(外部程序)、文件操作(fsworker)。
## 代价
- 每次调用起一个进程,约 50 ms;一次性大文件(字幕包、种子)可以忽略,高频解析留给常驻池。
- 输入输出要 JSON 化,二进制 base64 会多约三分之一体积;单次输入有上限(字幕 10 MB / 文件,种子 4 MB)。
- 以后 Deno 的 Worker 权限转正、`terminate()` 能中断卡死线程,常驻池可以换成 Worker,调用接口不变。