Files

3.4 KiB

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,调用接口不变。