From 9ac9427742866bc1efe02a70248ea5361105f767 Mon Sep 17 00:00:00 2001 From: ggboyxkw666 <3510903266@qq.com> Date: Sun, 6 Sep 2026 11:12:52 +0800 Subject: [PATCH 1/3] docs: define sandbox isolation threat model --- docs/sandbox-threat-model.md | 358 +++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 docs/sandbox-threat-model.md diff --git a/docs/sandbox-threat-model.md b/docs/sandbox-threat-model.md new file mode 100644 index 0000000..eedc281 --- /dev/null +++ b/docs/sandbox-threat-model.md @@ -0,0 +1,358 @@ +# C++ 评测:后代进程清理与 OS 级隔离设计 + +> 文档状态:设计评审稿,面向接管前的风险识别与分阶段实施。本文不引入生产代码、系统调用、容器配置或权限变更。 +> +> 适用基线:PR #9(`fix: bound C++ sandbox stdout and stderr`)已合并到上游 `main`。本文基于当前代码对后代进程清理和 OS 级隔离做威胁建模、方案比较、最小复现和回归测试设计。 + +## 1. 结论摘要与范围 + +PR #9 已解决一个明确的输出缓冲风险:评测进程分别以有界读取线程消费 `stdout` 和 `stderr`,超过单流上限后终止直接子进程,并返回不可通过的结果。这个改动解决的是“输出管道在运行期间无限增长”的问题,不等于完整的进程或操作系统隔离。 + +本设计聚焦以下问题: + +- 评测程序启动孙进程、孙进程继续运行时,直接调用 `process.kill()` 是否会留下后代进程; +- 进程树、CPU、内存、磁盘、网络和文件权限边界是否可能突破当前应用工作进程的安全边界; +- Windows 和 Linux 上可采用哪些机制,以及哪些机制适合先落地; +- 如何用安全、可重复、与真实 AI 无关的最小复现和回归矩阵验证方案。 + +明确不在本 PR 中做的事情: + +- 不新增 `os.system`、shell 拼接、任意高风险系统调用或特权操作; +- 不实现 Job Object、cgroup、namespace、seccomp、容器或虚拟机; +- 不修改评测业务逻辑、接口协议或生产部署配置; +- 不把当前评测器描述成可以安全执行不可信代码的完整沙箱。 + +## 2. 当前实现基线 + +### 2.1 核心运行流程 + +当前评测路径可概括为: + +```text +提交任务 + -> submission task / worker + -> run_test_cases + -> TemporaryDirectory 创建工作目录 + -> 写入 solution.cpp + -> g++ -std=c++17 -O2 -Wall 编译 + -> 为每个测试用例启动被测程序 + -> 有界读取 stdout/stderr + -> 比较规范化输出并汇总结果 +``` + +`utils/sandbox_runner.py` 当前具有以下边界: + +| 控制项 | 当前行为 | 评价 | +| --- | --- | --- | +| 编译器 | `g++ -std=c++17 -O2 -Wall` | 保持 C++17 兼容;依赖宿主机工具链 | +| 编译超时 | 15 秒 | 只覆盖被等待的直接编译进程 | +| 运行超时 | 5 秒 | 需要与后代清理组合使用 | +| 标准输出 | 单流 4096 字节有界读取 | PR #9 已覆盖运行期间增长 | +| 标准错误 | 单流 4096 字节有界读取 | PR #9 已覆盖运行期间增长 | +| 超限结果 | 终止直接子进程,返回失败原因 | 不应被误判为通过 | +| 工作目录 | 临时目录,运行结束清理 | 没有磁盘配额或宿主文件系统隔离 | +| 子进程启动 | 参数列表形式,不通过 shell | 降低 shell 注入面,但不等于 OS 隔离 | +| 后代进程 | `process.kill()` 只保证直接子进程 | 仍可能残留孙进程及其资源 | +| 网络 | 沿用宿主/服务进程网络权限 | 未实施默认拒绝或 allowlist | +| 权限 | 继承应用 worker 的用户、环境和可访问资源 | 不是专用低权限执行身份 | +| CPU/内存/PID | 没有 OS 级配额 | 进程树扩张和资源耗尽仍是风险 | + +另一个需要注意的边界是:评测逻辑由应用任务 worker 直接调用。即使单次评测有超时,若后代进程未被清理,风险可能从单个任务扩展到 worker、宿主机和同机其他任务。 + +### 2.2 PR #9 的安全收益与剩余缺口 + +PR #9 的有界读取线程会持续消费两个管道,并多读一个字节判断是否超过上限;因此不能再把“进程结束后截取前 4096 字节”误认为运行期间的内存上限。这个行为应作为后续隔离方案的固定回归基线。 + +剩余缺口不是输出截断本身,而是子进程的生命周期和权限边界: + +1. 终止直接进程不自动终止其子树; +2. 没有独立的进程数、CPU、内存和磁盘配额; +3. 没有默认拒绝的网络边界; +4. 临时工作目录不代表只能访问该目录; +5. worker 的环境变量、用户权限和可继承句柄可能进入被测程序; +6. 没有针对恶意或故障任务的独立 worker/容器/虚拟机故障域。 + +## 3. 威胁模型 + +### 3.1 资产、参与者与信任边界 + +保护对象: + +- 应用 worker 进程、任务队列和数据库连接; +- 宿主机上的源代码、环境变量、密钥、上传文件和其他租户任务; +- 评测服务的可用性、结果正确性和运维可观测性。 + +参与者和假设: + +- 被测 C++ 程序视为“不可信但可执行”的输入;它可能是恶意代码,也可能只是错误程序; +- AI 生成的代码不能作为安全边界,AI 服务的调用权限也不能替代 OS 隔离; +- 评测服务本身、编译器和部署镜像属于受信组件,但编译器执行输入仍会处理不可信源代码; +- 本文不假设可以通过业务层检查识别所有恶意行为。 + +主要信任边界: + +```text +Web/API/任务队列 + │ + ▼ +应用 worker(受信,但承载业务凭据) + │ 当前边界不足 + ▼ +编译器与被测程序(不可信代码) + │ + ├─ 文件系统 / 环境变量 / 句柄 + ├─ 网络 + ├─ CPU / 内存 / PID / 磁盘 + └─ 后代进程生命周期 +``` + +### 3.2 威胁清单 + +| 编号 | 威胁 | 可能后果 | 当前状态 | 目标控制 | +| --- | --- | --- | --- | --- | +| T1 | 孙进程在父进程被终止后继续运行 | 任务泄漏、持续占用 CPU/端口/文件 | 未解决 | 按进程树或作业对象终止;验证无残留 | +| T2 | 子程序批量创建进程 | PID 耗尽、worker 不可用 | 未限制 | Windows Job Object / Linux cgroup `pids` 或容器配额 | +| T3 | 后代进程绕过直接子进程超时 | 超时任务继续消耗资源 | 未解决 | 统一生命周期控制 + OS CPU 配额 | +| T4 | 内存持续增长 | worker 或宿主机 OOM | 未限制 | Job Object 内存限制 / cgroup memory | +| T5 | 临时文件大量写入 | 磁盘耗尽、影响其他任务 | 仅临时目录清理 | 独立工作盘或配额、大小限制和清理告警 | +| T6 | 读取宿主文件或环境变量 | 密钥、源码、租户数据泄露 | 继承应用权限 | 专用低权限身份、最小环境、文件系统隔离 | +| T7 | 网络访问外部或内网 | 数据外传、横向访问、依赖不受控 | 继承网络 | 默认断网,必要时显式 allowlist | +| T8 | 编译器/运行时句柄和管道泄漏 | 资源不释放、结果串扰 | 部分受控 | close-on-exec、独立 worker、结束后句柄审计 | +| T9 | stdout/stderr 高速输出 | 内存增长、线程阻塞、结果误判 | PR #9 已缓解 | 保留有界读取和失败原因断言 | +| T10 | 多任务并发放大单任务风险 | 资源争抢、服务雪崩 | 未系统限制 | worker 池、队列限流、每任务和全局配额 | +| T11 | 只依赖业务超时 | OS 调度或后代活动绕过业务控制 | 风险存在 | 增加 OS 级硬上限和外部监控 | +| T12 | 清理失败但没有告警 | 隔离失效被长期忽略 | 未定义 | 记录 PID 树、退出原因、残留检测和升级策略 | + +风险排序建议:T1/T2/T3/T6/T7/T10 为上线前优先项;T4/T5/T8/T12 是必须纳入同一隔离边界的配套项;T9 作为 PR #9 的回归基线保留。 + +## 4. Windows/Linux 方案对比 + +### 4.1 机制比较 + +| 能力 | Windows 方向 | Linux 方向 | 取舍与备注 | +| --- | --- | --- | --- | +| 进程树生命周期 | Job Object,加入作业后使用作业级终止 | 进程组/会话用于基础清理;更可靠的边界应使用 cgroup 或容器 | 单独 `kill(pid)` 两端都不足;进程组也要防止脱离和继承问题 | +| CPU/内存/PID | Job Object 资源限制 | cgroup v2 的 CPU、memory、pids 控制器 | 需要部署主机/运行时支持,不能只靠 Python 参数 | +| 文件系统 | 专用服务账号、ACL、临时目录;更强隔离可用容器/VM | 非 root 用户、容器 mount 白名单;namespace/chroot 单独使用不等于完整沙箱 | 目标是最小可见文件树,而不是仅改变 cwd | +| 网络 | Windows Firewall/容器网络策略,默认拒绝 | network namespace、容器网络和 egress policy,默认断网 | 需要明确 DNS、代理和 allowlist 行为 | +| 系统调用/权限 | 受限 token、服务隔离、应用容器等平台能力 | seccomp 配置、namespace、capability drop、no-new-privileges | seccomp/受限 token 属高风险实施项,必须独立评审 | +| 观测 | 事件日志、Job Object 统计、进程树 | cgroup 统计、procfs、容器日志、审计 | 必须能证明“超时后无残留”和“配额生效” | +| 运维复杂度 | 需要 Windows 原生 worker 和权限配置 | Linux 容器/专用 worker 生态更成熟 | 先按实际部署 OS 选主路径,不维护两套等价实现的假象 | + +### 4.2 建议路线 + +如果生产评测 worker 部署在 Linux,优先路线是: + +1. 将评测移到专用 worker 或容器; +2. 使用非 root 身份、默认无网络、只读基础镜像和独立可写工作目录; +3. 用 cgroup v2 约束 CPU、内存、PID 和必要的 I/O; +4. 用容器/namespace 建立文件系统和网络边界; +5. 在明确的系统调用白名单和 capability 策略经过验证后,再评估 seccomp 等进一步限制。 + +如果生产评测 worker 部署在 Windows,优先路线是: + +1. 使用专用低权限服务账号和独立 worker; +2. 将编译器及被测程序加入 Job Object,设置作业级生命周期和资源限制; +3. 通过 ACL 限制工作目录及可读写路径; +4. 使用防火墙/网络策略默认拒绝外联; +5. 对受限 token、Windows Sandbox 或 VM 等更强机制另立专项验证。 + +进程组、Job Object 或 cgroup 不是互相排斥的单一开关:生命周期清理、资源配额、文件系统和网络控制应作为一组边界验收。本文不选择在业务代码中临时拼接“杀 PID 树”的实现,因为这既难以覆盖跨平台行为,也无法提供资源和权限隔离。 + +## 5. 最小复现设计 + +以下复现仅用于隔离测试环境,代码不应接收用户输入、不应在生产机运行。它们不引入系统调用,仅帮助证明当前直接子进程终止无法覆盖后代进程。 + +### 5.1 后代进程残留 + +准备两个短小的 Python 程序: + +```python +# parent.py +import subprocess +import sys +import time + +child = subprocess.Popen([sys.executable, "grandchild.py"]) +print(child.pid, flush=True) +time.sleep(60) +``` + +```python +# grandchild.py +import time + +print("grandchild-alive", flush=True) +while True: + time.sleep(1) +``` + +复现步骤: + +1. 由测试 harness 启动 `parent.py`,记录输出的子 PID; +2. 只终止 `parent.py` 的 PID; +3. 在短暂等待后检查子 PID 是否仍存在; +4. 记录 Windows 进程树或 Linux `ps`/`/proc` 观察结果; +5. 测试结束时由 harness 清理整个测试树,并断言没有残留。 + +这个用例的预期是:当前实现可能观察到后代仍存活;未来的 Job Object/cgroup/专用容器实现必须断言后代也被清理。测试应该有明确的最大等待时间,不能用无限等待掩盖清理失败。 + +### 5.2 有界资源行为 + +使用短时、可控规模的测试程序分别模拟: + +- stdout 连续写入略高于 4096 字节; +- stderr 连续写入略高于 4096 字节; +- 直接子进程返回非零码; +- 直接进程在运行超时前不退出; +- 父进程启动一个短暂 CPU 循环的后代; +- 创建固定数量(例如 8 个)后代,而不是无限 fork; +- 在临时工作目录内写入固定上限的数据; +- 尝试访问网络或工作目录外的受保护文件,并只验证“被拒绝/无网络”,不读取真实敏感数据。 + +资源复现必须有 harness 级超时和清理兜底。不能把无限 fork、无限内存分配或真实网络扫描作为自动化回归测试内容。 + +## 6. 回归测试设计与矩阵 + +### 6.1 现有行为回归 + +| 场景 | 输入/操作 | 期望结果 | PR #9 基线 | +| --- | --- | --- | --- | +| 正常输出 | 输出 `42` 并退出 0 | 规范化后通过 | 已有测试 | +| stdout 超限 | stdout 超过配置上限 | 失败;原因明确为 stdout 超限;不得通过 | 已有测试 | +| stderr 超限 | stderr 超过配置上限 | 失败;原因明确为 stderr 超限;不得通过 | 已有测试 | +| 运行时错误 | 返回非零码并写入 stderr | 失败;保留错误信息 | 已有测试 | +| 运行超时 | 进程超过运行时限 | 失败;原因明确为超时 | 已有测试 | +| 编译超时 | 编译阶段超过时限 | 失败;不进入运行阶段 | 既有能力,需保留 | +| 输出规范化 | 允许的尾空格/换行差异 | 与原有规则兼容 | 需持续回归 | + +### 6.2 后代和 OS 边界回归 + +| 场景 | Linux 目标断言 | Windows 目标断言 | 当前状态 | +| --- | --- | --- | --- | +| 父进程超时 | cgroup/容器内无残留进程 | Job Object 内无残留进程 | 未实现 | +| 父进程非零退出 | 后代按策略清理 | 后代按策略清理 | 未实现 | +| 固定数量后代 | PID 上限生效,任务失败且 worker 健康 | Job Object 进程数/资源上限生效 | 未实现 | +| CPU 超限 | cgroup CPU 配额或外部硬超时生效 | Job Object CPU 限制或等效策略生效 | 未实现 | +| 内存超限 | cgroup memory 事件可观测且任务失败 | Job Object 内存限制可观测且任务失败 | 未实现 | +| 磁盘超限 | 工作卷/配额阻止写满宿主盘 | 工作目录 ACL/配额阻止写满宿主盘 | 未实现 | +| 网络访问 | 默认断网或只允许明确 allowlist | 默认断网或只允许明确 allowlist | 未实现 | +| 工作目录外文件 | 访问被拒绝 | 访问被拒绝 | 未实现 | +| 环境/凭据 | 被测程序看不到业务密钥 | 被测程序看不到业务密钥 | 未实现 | +| 并发评测 | 每任务配额独立,整体队列有上限 | 每任务配额独立,整体队列有上限 | 未实现 | +| 清理失败 | 产生结构化告警并阻断复用 | 产生结构化告警并阻断复用 | 未实现 | + +### 6.3 验收规则 + +隔离实现至少应满足: + +- 正常、输出超限、运行时错误、超时结果与 PR #9 保持兼容; +- 任何配额触发、清理失败或隔离策略未启用,都不能返回通过; +- 超时后在固定观察窗口内没有残留后代; +- 同一 worker 的下一项任务不受上一项任务的进程、端口、文件和环境影响; +- 失败原因可区分:编译失败、运行时错误、超时、stdout/stderr 超限、资源配额、隔离配置错误; +- 测试在 Windows 和 Linux 的目标部署环境分别执行,不能只在开发机上模拟平台行为。 + +## 7. 资源、网络与权限边界 + +| 边界 | 当前实现 | 部署目标 | 验证证据 | +| --- | --- | --- | --- | +| 墙钟时间 | 编译 15 秒、运行 5 秒 | 业务超时 + OS 硬上限 | 计时日志、失败原因、无残留 | +| CPU | 无独立配额 | cgroup/Job Object + 全局并发限制 | 配额事件和资源曲线 | +| 内存 | 无独立配额 | cgroup memory/Job Object memory | 超限退出且 worker 仍健康 | +| 进程数 | 无上限 | cgroup `pids`/Job Object | 固定规模后代测试 | +| 输出 | stdout/stderr 各 4096 字节有界读取 | 保持现有行为 | PR #9 回归测试 | +| 磁盘 | 临时目录清理,无配额 | 独立卷/配额、单任务写入上限 | 写满测试、清理检查 | +| 文件系统 | cwd 是临时目录,但继承用户可见路径 | 最小 mount/ACL,非 root/低权限账号 | 受保护路径访问测试 | +| 网络 | 继承 worker 网络 | 默认拒绝,必要时 allowlist | 断网/allowlist 测试 | +| 环境变量 | 当前从 `os.environ` 继承并调整 PATH | 最小环境,显式保留必要变量 | 环境快照审计 | +| 句柄 | 依赖子进程启动和运行时行为 | 不向被测程序暴露业务句柄 | 句柄/FD 审计 | +| 后代生命周期 | 只保证直接子进程 | 作业/控制组/容器统一回收 | 进程树回归 | +| 并发 | 由应用 worker/队列承载 | 专用 worker 池、限流、隔离故障域 | 压测和故障注入 | + +## 8. 部署前提 + +在任何面向不可信用户开放评测前,至少需要完成并记录: + +1. 明确生产评测 worker 是 Linux 还是 Windows,并选择对应主路线; +2. 评测 worker 使用专用低权限身份,不持有数据库写权限、云密钥或无关租户数据; +3. 评测执行环境与 Web/API worker 解耦,至少有独立进程故障域; +4. 所有评测任务拥有独立工作目录和可验证的清理结果; +5. 网络默认拒绝,编译和运行不依赖未登记的外部服务; +6. 有 CPU、内存、进程数、磁盘和并发上限,并能在日志中区分触发原因; +7. g++ 版本、基础镜像/主机补丁和运行时依赖可重复部署; +8. 监控能发现残留进程、孤儿 worker、配额触发、清理失败和队列堆积; +9. 通过 Windows/Linux 目标环境的对抗性回归后,才允许扩大用户范围; +10. 当前未完成 OS 隔离前,只能把评测能力视为受限内部能力,不能宣称为完整恶意代码沙箱。 + +## 9. 回滚与故障处置 + +本 PR 是文档-only,不改变运行时,因此回滚只需关闭/回退该文档 PR,不涉及数据库迁移和生产配置。 + +未来实施隔离时建议采用可回滚发布: + +- 先部署新的专用 worker,保留旧 worker 作为短期回退路径; +- 用 feature flag 或队列路由将少量内部任务导向新 worker; +- 发现清理失败、任务误杀、编译兼容性回归或资源配额异常时,停止投递新任务,等待/终止新 worker,再切回已验证版本; +- 保留失败任务的 request id、进程树摘要、配额事件和 worker 日志,但不要把被测程序原始输出当作可信日志内容; +- 如果出现残留进程或宿主资源异常,按运维预案隔离受影响 worker/主机,禁止直接在业务代码中追加未经评审的“杀全树”命令; +- 回滚后仍应限制为内部/低风险用户,直到根因和清理证据完成复核。 + +## 10. 分阶段计划与退出条件 + +### P0:设计与复现(本文) + +- 固化威胁模型、平台方案、边界和最小复现; +- 不修改业务代码,不引入高风险系统调用; +- 退出条件:评审确认部署 OS、威胁优先级和验收矩阵。 + +### P1:无风险可观测性和契约测试 + +- 补充结构化失败原因、进程树/作业标识和清理结果的观测设计; +- 将 PR #9 五类基线测试作为跨平台契约; +- 先在测试环境验证最小复现,不接入真实用户任务; +- 退出条件:失败不可误判通过,能定位超时、超限和清理失败。 + +### P2:Linux 专用 worker 隔离 + +- 采用专用 worker/容器、非 root、默认无网络、最小文件系统; +- 接入 cgroup v2 的 CPU、memory、pids 和必要 I/O 约束; +- 退出条件:Linux 矩阵全部通过,包含后代清理和资源配额证据。 + +### P3:Windows 专用 worker 隔离 + +- 采用专用低权限 worker 和 Job Object; +- 验证作业级进程树清理、CPU/内存/PID 限制和 ACL; +- 退出条件:Windows 矩阵全部通过,且与 PR #9 输出/超时结果兼容。 + +### P4:权限与网络收紧 + +- 收缩环境变量、句柄、文件路径和出站网络; +- 根据实际部署需要评审 seccomp、受限 token、VM 等更强机制; +- 退出条件:资源、网络、权限边界都有可重复的拒绝测试和监控。 + +### P5:灰度和上线决策 + +- 在隔离 worker 上灰度,限制并发和用户范围; +- 观察残留进程、配额触发、队列延迟、误杀和清理失败; +- 退出条件:安全评审签字、回滚演练通过、所有未解决高风险项有明确负责人和期限。 + +## 11. 未决问题与未验证项 + +- 生产目标平台尚未在本文中选定,因此 Linux 和 Windows 两条路线都保留; +- 当前没有实现或验证 Job Object、cgroup、namespace、seccomp、容器网络和 Windows ACL; +- 没有在真实生产内核、生产服务账号或生产网络策略下执行后代清理测试; +- `process.kill()` 对直接子进程之外的后代清理能力仍未证明; +- 尚未确定单任务 CPU、内存、PID、磁盘、网络和并发配额的数值; +- 尚未决定隔离失败时是拒绝任务、摘除 worker,还是转人工处理; +- `pytest`、`g++` 和完整部署依赖是否可用,取决于执行环境;本设计文档不把本地可运行性当作生产隔离证明; +- 完整 OS 级隔离需要单独的实现 PR、平台测试和安全评审,不能由本文档替代。 + +## 12. 相关代码与验证基线 + +- `utils/sandbox_runner.py`:编译、运行、超时和有界输出的当前实现; +- `tests/test_sandbox_output_limits.py`:正常输出、stdout 超限、stderr 超限、运行时错误和超时回归; +- `DEPLOYMENT.md`:当前部署拓扑、g++ 依赖、低权限运行和“尚未完成不可信代码隔离”的说明; +- PR #9:。 + +本文档只描述设计和验证计划,不应被解释为上述 OS 级控制已经上线。 From 0d0ef6ac2b838afbc6f78f308f62986e0291a19d Mon Sep 17 00:00:00 2001 From: ggboyxkw666 <3510903266@qq.com> Date: Sun, 6 Sep 2026 15:04:06 +0800 Subject: [PATCH 2/3] docs: close sandbox threat model review gaps --- docs/sandbox-threat-model.md | 41 ++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/sandbox-threat-model.md b/docs/sandbox-threat-model.md index eedc281..d9500ef 100644 --- a/docs/sandbox-threat-model.md +++ b/docs/sandbox-threat-model.md @@ -157,6 +157,28 @@ Web/API/任务队列 4. 使用防火墙/网络策略默认拒绝外联; 5. 对受限 token、Windows Sandbox 或 VM 等更强机制另立专项验证。 +#### 4.2.1 执行前归组与失败关闭 + +两条路线都必须满足同一个时序不变量:不可信编译器或被测程序第一次执行用户代码前,隔离单元已经创建、边界已经配置、进程已经归组并完成归组校验。不得先 `exec`/启动,再事后补加入 Job Object 或 cgroup。创建隔离单元、配置限制、归组或校验任一步失败,都必须关闭/清理该隔离单元并拒绝执行,不能降级为“仅靠 Python 超时”的评测。 + +Linux 计划按以下顺序实现: + +1. 由受信任的 worker 创建并持有 manager-owned cgroup v2,先配置 CPU、memory、pids 等控制器和本次任务的上限; +2. 在不可信代码启动前确认 cgroup 权限不向任务委托:被测程序不能写入 `cgroup.procs`/`cgroup.subtree_control`,不能把自己或后代迁出,也不能创建可绕过 worker 管理的子 cgroup; +3. 让编译器及被测程序直接在该 cgroup(以及已选定的容器/namespace 边界)内启动,并在“运行开始”判定前确认 PID 归属; +4. 归组或归属校验失败时,不允许进入编译/运行阶段,终止已创建的受控进程并回收 cgroup; +5. 编译失败、运行时错误、输出超限、超时、正常返回和 worker 异常都走同一套隔离单元清理流程,确认无残留后才能释放或复用 worker。 + +Windows 计划按以下顺序实现: + +1. 由受信任的 worker 创建 Job Object,并先配置 kill-on-job-close、进程数、CPU、内存等作业级限制; +2. 以挂起方式创建编译器/被测程序(例如 `CREATE_SUSPENDED`),此时不允许其执行不可信代码; +3. 在进程仍挂起时调用 `AssignProcessToJobObject`,检查返回值并记录作业标识; +4. 只有归组成功且校验通过后才恢复进程。归组失败必须终止挂起进程、关闭 Job Object 并拒绝执行,不能先恢复再补归组; +5. 所有结束路径都结束/关闭整个 Job Object,并在释放或复用 worker 前确认作业内无残留进程及继承的输出句柄。 + +该时序是后续实现和验收的强制约束,不代表本 PR 已经调用上述系统 API。验收至少要覆盖:子进程启动后立即创建后代、任务尝试把自己或后代迁出边界、归组失败,以及边界建立前不得执行用户代码;每个场景都必须证明不存在可继续运行的边界外进程。 + 进程组、Job Object 或 cgroup 不是互相排斥的单一开关:生命周期清理、资源配额、文件系统和网络控制应作为一组边界验收。本文不选择在业务代码中临时拼接“杀 PID 树”的实现,因为这既难以覆盖跨平台行为,也无法提供资源和权限隔离。 ## 5. 最小复现设计 @@ -230,8 +252,14 @@ while True: | 场景 | Linux 目标断言 | Windows 目标断言 | 当前状态 | | --- | --- | --- | --- | +| 启动前归组失败 | 不执行不可信代码;清理 cgroup 且无残留 | 不恢复挂起进程;关闭 Job Object 且无残留 | 未实现 | +| 后代立即创建 | 后代自动落入同一 cgroup,结束路径统一回收 | 后代自动继承同一 Job Object,结束路径统一回收 | 未实现 | +| 后代尝试脱离边界 | 迁移权限被拒绝或任务失败关闭;无边界外残留 | 脱离/归组操作被拒绝或任务失败关闭;无 Job 外残留 | 未实现 | | 父进程超时 | cgroup/容器内无残留进程 | Job Object 内无残留进程 | 未实现 | | 父进程非零退出 | 后代按策略清理 | 后代按策略清理 | 未实现 | +| 父进程输出 `42`、返回 0,但后代继续存活 | 结果仍有界;整个 cgroup 清理完成;无残留 | 结果仍有界;整个 Job 清理完成;无残留 | 未实现 | +| 正常返回且后代保留 stdout/stderr 句柄 | 结果收集不因 EOF 延迟而无限等待;清理后无残留 | 结果收集不因句柄继承而无限等待;清理后无残留 | 未实现 | +| 正常返回且后代关闭 stdout/stderr 句柄 | 仍清理整个隔离单元;下一任务不受影响 | 仍清理整个隔离单元;下一任务不受影响 | 未实现 | | 固定数量后代 | PID 上限生效,任务失败且 worker 健康 | Job Object 进程数/资源上限生效 | 未实现 | | CPU 超限 | cgroup CPU 配额或外部硬超时生效 | Job Object CPU 限制或等效策略生效 | 未实现 | | 内存超限 | cgroup memory 事件可观测且任务失败 | Job Object 内存限制可观测且任务失败 | 未实现 | @@ -240,6 +268,7 @@ while True: | 工作目录外文件 | 访问被拒绝 | 访问被拒绝 | 未实现 | | 环境/凭据 | 被测程序看不到业务密钥 | 被测程序看不到业务密钥 | 未实现 | | 并发评测 | 每任务配额独立,整体队列有上限 | 每任务配额独立,整体队列有上限 | 未实现 | +| 清理后复用 worker | 验证 cgroup、进程和输出句柄均已释放,下一任务可独立完成 | 验证 Job、进程和输出句柄均已释放,下一任务可独立完成 | 未实现 | | 清理失败 | 产生结构化告警并阻断复用 | 产生结构化告警并阻断复用 | 未实现 | ### 6.3 验收规则 @@ -248,11 +277,15 @@ while True: - 正常、输出超限、运行时错误、超时结果与 PR #9 保持兼容; - 任何配额触发、清理失败或隔离策略未启用,都不能返回通过; -- 超时后在固定观察窗口内没有残留后代; +- 所有结束路径(编译失败、运行时错误、正常返回、输出超限、超时和 worker 异常)都必须清理整个隔离单元;父进程返回 0 或输出正确答案不能代替清理证明; +- 在固定观察窗口内确认没有残留后代、边界外进程或仍持有 stdout/stderr 的继承句柄后,才能释放或复用 worker; +- 归组、边界配置或归属校验失败时,不得执行不可信代码,并且结果必须是明确失败而不是通过; - 同一 worker 的下一项任务不受上一项任务的进程、端口、文件和环境影响; - 失败原因可区分:编译失败、运行时错误、超时、stdout/stderr 超限、资源配额、隔离配置错误; - 测试在 Windows 和 Linux 的目标部署环境分别执行,不能只在开发机上模拟平台行为。 +必须单独执行一组正常返回清理验收:父进程输出 `42` 并返回 0,同时启动持续存活的后代;分别测试后代继续持有 stdout/stderr 句柄和主动关闭这些句柄。断言结果收集有界、任务不会因等待 EOF 无限阻塞、整个隔离单元被回收、没有残留进程,并且紧接着运行的下一项任务不受影响。该用例与超时、非零退出用例同等重要。 + ## 7. 资源、网络与权限边界 | 边界 | 当前实现 | 部署目标 | 验证证据 | @@ -317,12 +350,12 @@ while True: - 采用专用 worker/容器、非 root、默认无网络、最小文件系统; - 接入 cgroup v2 的 CPU、memory、pids 和必要 I/O 约束; -- 退出条件:Linux 矩阵全部通过,包含后代清理和资源配额证据。 +- 先完成“启动前归组、归组失败不执行、后代不能迁出”的验收,再进入资源测试;退出条件:Linux 矩阵全部通过,包含正常返回后的后代清理、输出句柄和资源配额证据。 ### P3:Windows 专用 worker 隔离 - 采用专用低权限 worker 和 Job Object; -- 验证作业级进程树清理、CPU/内存/PID 限制和 ACL; +- 先完成“挂起创建、归组成功后恢复、归组失败不执行、后代不能脱离”的验收,再验证作业级进程树清理、CPU/内存/PID 限制和 ACL; - 退出条件:Windows 矩阵全部通过,且与 PR #9 输出/超时结果兼容。 ### P4:权限与网络收紧 @@ -341,7 +374,7 @@ while True: - 生产目标平台尚未在本文中选定,因此 Linux 和 Windows 两条路线都保留; - 当前没有实现或验证 Job Object、cgroup、namespace、seccomp、容器网络和 Windows ACL; -- 没有在真实生产内核、生产服务账号或生产网络策略下执行后代清理测试; +- 没有在真实生产内核、生产服务账号或生产网络策略下执行启动前归组、归组失败、后代脱离、正常返回后代清理或输出句柄保留测试; - `process.kill()` 对直接子进程之外的后代清理能力仍未证明; - 尚未确定单任务 CPU、内存、PID、磁盘、网络和并发配额的数值; - 尚未决定隔离失败时是拒绝任务、摘除 worker,还是转人工处理; From 8f37f14b995eb59fa58bf23467d70658cb44272b Mon Sep 17 00:00:00 2001 From: ggboyxkw666 <3510903266@qq.com> Date: Sun, 6 Sep 2026 15:07:26 +0800 Subject: [PATCH 3/3] docs: fail closed on unsafe sandbox rollback --- docs/sandbox-threat-model.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/sandbox-threat-model.md b/docs/sandbox-threat-model.md index d9500ef..41f1555 100644 --- a/docs/sandbox-threat-model.md +++ b/docs/sandbox-threat-model.md @@ -324,13 +324,16 @@ while True: 未来实施隔离时建议采用可回滚发布: -- 先部署新的专用 worker,保留旧 worker 作为短期回退路径; +- 先部署新的专用 worker,并保留一个满足相同隔离要求、已经过目标平台验收且可证明无残留的已知安全版本作为回退路径;“旧”不等于“安全”,当前没有 OS 隔离的 worker 不能作为该回退版本; - 用 feature flag 或队列路由将少量内部任务导向新 worker; -- 发现清理失败、任务误杀、编译兼容性回归或资源配额异常时,停止投递新任务,等待/终止新 worker,再切回已验证版本; +- 发现清理失败、任务误杀、编译兼容性回归或资源配额异常时,停止投递新任务,等待/终止新 worker,再切回满足同等隔离要求的已验证版本;内部/低风险用户也不能成为绕过隔离边界的理由; +- 如果不存在这样的已验证版本,或无法证明 Job Object/cgroup、文件系统、网络、资源和清理边界仍然可用,则暂停评测或保留队列并返回明确的不可用状态,禁止把任务路由到当前无 OS 隔离的旧 worker; - 保留失败任务的 request id、进程树摘要、配额事件和 worker 日志,但不要把被测程序原始输出当作可信日志内容; - 如果出现残留进程或宿主资源异常,按运维预案隔离受影响 worker/主机,禁止直接在业务代码中追加未经评审的“杀全树”命令; - 回滚后仍应限制为内部/低风险用户,直到根因和清理证据完成复核。 +回滚验收必须包含隔离不可用场景:主动使隔离单元创建、配置、归组或清理校验失败,确认任务不会进入旧执行路径;队列要么保持未执行并可恢复,要么以明确失败结束,且不产生未隔离的编译器、被测程序或后代进程。只有在隔离边界和清理证据重新通过后,才允许恢复投递。 + ## 10. 分阶段计划与退出条件 ### P0:设计与复现(本文)