Files
KF-OnlineJudge/judge/README.md
T

13 KiB
Raw Blame History

KF-OnlineJudge 判题工程

crate 职责
kf-contracts 平台与判题服务的通知契约和共享样例
kf-engine 执行编排端口、输出比较和判题汇总;不进行权限操作
kf-sandbox Linux namespace、运行根、身份、seccomp、cgroup、监督和清理
kf-coordinator PostgreSQL 事务、领取/租约/阶段/结果、取消和恢复;不操作账号或题库
kf-worker 普通用户工件准备与执行适配器注入;不操作评测数据库

依赖保持 worker → engine / sandbox / contracts、coordinator → contracts、engine → contracts、sandbox → contracts。原始系统调用和权限操作集中在 sandbox 内。

普通执行入口

普通构建包含真实执行内核。支持 Linux x86_64、内核至少 6.6、cgroup v2 以及配置要求的控制器和隔离接口。必须由独立、单线程、未预装 seccomp 的 root helper 调用,拒绝 setuid 调用。业务 Worker 不以 root 运行。

kf-sandbox validate --spec request.json
kf-sandbox check-runtime --config /etc/kf-judge/runtime.json
kf-sandbox run --config /etc/kf-judge/runtime.json --spec request.json   --artifacts /var/lib/kf-judge/publications/input   --output /var/lib/kf-judge/publications/output

check-runtime 是只读诊断,始终输出 execution_allowed=false 和 active_validation_required;目录和接口可见不能证明隔离。run 不接受绕过验证的开关:配置、运行根和祖先必须由 root 持有且不可被其他身份写入;配置为有界、单链接普通文件;运行镜像必须包含 root 保护的 manifest.json:

{"image_kind":"linux_gnu_cxx_x86_64_v1","architecture":"x86_64","revision":1}

部署方负责可信 GNU 镜像的内容、发布与存储。每次任务仍需完成真实 namespace、只读根、输入、身份、seccomp 和 exec 门控,任何阶段失败都不放行用户程序。helper 所在 cgroup 必须在任务委派子树外,实际内存、swap、进程和 CPU 上限不得超过可信配置的监督器预留;仅设置配置字段不生效。

run 缺少配置参数、环境不支持或准备失败时退出 2,stderr 输出结构化错误;failure 与 cleanup_failure 分别保留原始阶段错误和清理错误,诊断只含固定代码与 errno,不回显私有路径或输入。正常完成时 stdout 输出执行报告、归因和发布 ID;程序退出 0 不等于 Accepted。

输入仅允许逻辑工件引用,通过受控目录 FD 与 openat2 的 BENEATH/NO_SYMLINKS/NO_MAGICLINKS/NO_XDEV 解析;只读封印快照绑定到每任务 work。stdin 不挂载为用户文件。执行请求不能选择宿主挂载、运行镜像、身份池或委派根,也不继承宿主环境或拼接 shell。每任务独立 tmpfs、UID/GID、网络 namespace,无外部网络。

完成任务树终止、cgroup 空读回、owned 子进程回收、最终计量和导出后,才移除资源并释放 UID。stdout/stderr 和输出文件为不可变快照。普通文件入口按 run ID 排他发布 stdout、stderr、artifact-N,先写自身临时目录再原子安装,不覆盖已有目录;清理未完成时不发布。

Worker 和 Engine 的最小链路

服务管理器先在有限监督器 cgroup 内启动一个新的 kf-sandbox serve-one --config /etc/kf-judge/runtime.json --socket /run/kf-judge/task.sock。socket 的祖先须由 root 持有、不可被 Worker 写入并允许其搜索;socket 排他创建为 root:worker 的 0660,既有路径不覆盖。一次新进程只接收一次请求,等待连接和准备帧有绝对截止时间,不提供常驻消费循环。

普通用户运行:

kf-worker judge-once --socket /run/kf-judge/task.sock \
  --job native-job.json --artifacts /srv/kf-worker/publication

本地单用例作业示例(不是平台任务通知协议):

{"request":{"run_id":"case-001","program_artifact":"program-001","stdin_artifact":"input-001"},"expected_artifact":"expected-001"}

Worker 选取业务授权的自身发布目录,准备有界封印的期望输出,并向 Engine 注入 ExecutionPort。适配器构造 native 规格,仅绑定程序与 stdin,不将期望输出放入用户运行根。helper 根据 SO_PEERCRED 核验配置中的 Worker UID/GID;客户端核验 helper 的 root 凭据。有界私有帧与 SCM_RIGHTS 只传持有的目录能力和只读封印输出,收到的 FD 重新检查类型、长度、访问模式、封印和 CLOEXEC,不传任意宿主挂载路径。

helper 不接管 root 或其他身份的输入工件;每任务保留相同的隔离和预算门控。执行中客户端断开会取消全树,额外控制数据归为协议错误并拒绝执行,清理后退出。socket 清理核对自身 inode,不删除外来替换。业务发布目录的选择与业务授权仍属于可信 Worker;文件所有权不是业务授权证明。

Engine 仅在真实、无基础设施错误的 exit 0 后逐字节比较输出。非零退出/信号为 RuntimeError,CPU/墙钟、内存、输出限制分别映射 TLE/MLE/OLE,取消与 SystemError 单独表达;不可用计量保持 null。汇总在 Engine 内按输入顺序确定,基础设施错误或取消停止后续执行;空用例和重复 run ID 在执行前拒绝。judge-once 返回结构化 verdict、用例与实际计量。新增 serde 直接依赖用于本地作业/领域结果类型,Engine 不引入 sandbox 或 OS 库。

本地已提供 native 单用例、编译一次多测试点与平台消费/持久化协调入口; 用户成绩展示、交互题、SPJ 和生产部署仍未实现。kf-worker --check 只检查入口,不消费或执行任务。权限 helper 和镜像必须由部署方配置,Worker 不调用 sudo、不修改账号或权限。

编译一次、多测试点

Root 服务 kf-sandbox factory --config /etc/kf-judge/runtime.json 先核对有限 cgroup 预留、配置/镜像权限和已确认归属的恢复记录,逐次产生独立单线程 helper。 普通用户通过 FactoryClient 申请固定 run ID 的连接能力;配置、镜像、二进制、 UID 池和宿主目录由服务决定。槽位满直接拒绝;有限持久 ID 记录跨重启阻止重放。 工厂退出通过生命线取消 helper,清理失败保留租约并停止分配;服务配置见 部署说明。

kf-worker judge-submission --socket /var/lib/kf-judge/factory/control.sock \
  --job submission.json --artifacts /srv/kf-worker/authorized \
  --staging /srv/kf-worker/staging

本地作业包含版本化 manifest、binding 和 manifest_digest,清单摘要采用共享 规范序列化。工件发布按已授权的 SHA-256 名称读取,暂存父目录由普通 Worker 持有且为 0700。暂存日志记录进程启动时间、内核启动标识和目录/文件 inode; 进程重启只回收已确认归属的旧文件,未知内容保留并拒绝继续。

Engine 的编译、运行、期望值和时钟/取消端口分离。C17/C++20 映射固定 GNU 参数, 无 shell 或用户 flags;每 attempt 编译一次,封印产物、计算摘要并完成清理后, 按清单顺序逐点申请新 helper。每点只传程序和 stdin,期望值留在 Worker 比较端。 编译失败/超限为 CE,准备、导出或清理失败为 SystemError;取消/基础设施失败停止 后续点并列出跳过原因。计量分开保存编译消耗与运行 CPU 总量、内存峰值,缺失值 保留 null;完整清单缺点不能 Accepted。judge-submission 返回真实报告与阶段 PID。

监督和恢复

CPU 总时间与墙钟使用可信监督器和单调时钟;CPU 带宽仅用于节流。内存 OOM 和 pids 超限依据所属 cgroup 的真实事件及计量,不能凭 SIGKILL 推断 MLE。并发排空二进制 stdout/stderr,独立限额、截断和提前 stdin 关闭均保留实际证据。监督器最终报告保留计量不可用状态、全部限制观察、取消、退出状态和清理错误;比较与成绩属于 Engine。

root 保护的 v2 journal 与 owner flock 记录启动标识、PID 起始时间、资源 inode 和生命周期。服务启动恢复只回收证实归属且原 owner 已退出的租约;未知、跨启动或替换资源拒绝接管。外来文件不递归删除;清理失败继续保留身份,修复后可重复恢复。重启恢复 API 为 kf_sandbox::cleanup::recover,调用方须采用相同可信配置和独立单线程 helper 模型。

验证

make architecture check-judge
make check

宿主普通检查不执行权限、恶意或超限测试。vm-acceptance 仅增加独立 VM 的固定验收入口与故障注入,不是生产执行授权。ignored 的 Linux 测试必须在无宿主共享挂载、无业务凭据、无 NIC 且整体资源上限已实际读回的独立 VM 内执行;KF_SANDBOX_INDEPENDENT_VM=1 只是测试防误用标记,不能代替环境证据。未执行或跳过不能计为 Linux 安全验收。

覆盖真实 GNU C/C++ 编译后运行、危险调用、宿主和跨任务文件、权限提升、网络、CPU/内存/pids/输出/文件限制,以及取消、初始化回滚、真实 owner SIGKILL、服务恢复和重复清理。验收记录保存在本地忽略的计划目录;不包含远端开发文档或业务凭据。

单机持久化协调与常驻 Worker

先通过 Backend Alembic 迁移,分别创建仅继承 kf_oj_platform、kf_oj_coordinator 的普通数据库登录角色。Coordinator 不迁移、不读账号/写题库;Worker 不接收数据库 连接、不写评测状态、不调用 sudo。令牌是内部服务身份,与浏览器会话及作业短期租约分离。

kf-coordinator --config /etc/kf-judge/coordinator.json
kf-worker serve --config /etc/kf-judge/worker.json
kf-worker check-stopped --socket /var/lib/kf-judge/factory/control.sock

两个配置都须为 0600、有界、单链接普通文件。Coordinator 配置包含 listen、 database_url、workers(id/64位十六进制 token)、不同的 platform_token、 admission、lease_seconds(5–300)及 recovery_seconds(1–60)。 Worker 配置包含 worker_id、service_token、coordinator_url、platform_url、 amqp_url、固定 socket、自身 0700 staging 与同一 admission。 本轮只允许单机数值 loopback HTTP/AMQP 与 loopback/绝对 Unix socket PostgreSQL; 未提供跨机器 TLS 模式。Worker 固定并发/prefetch 为1,缺少有效领取权不能执行。

/health/live 表示进程存活;配置后 /health/ready 连接数据库并核对最小权限。 内部 HTTP 先认证再解析有界请求,整体10秒,数据库连接槽位4,handler槽位16。 claim 使用数据库时间、行锁和有限全局容量锁,提交后才返回新 attempt/递增 fence/ 短期令牌与清单摘要。阶段严格递增,续租和结果核对当前绑定与令牌;用例、attempt 及终态同事务提交后才确认。完整同一报告可重复确认,修改或旧 attempt 被拒绝。

取消与过期撤销执行权,先进入 cancelling;不能把租约到期直接当作任务已停止。 Worker 续租失败在有界期限内取消 helper,通过 Root 工厂的停止查询核对 owned journal、 任务目录、身份和 cgroup 全部排空,才发送服务身份认证的停止报告。 未知资源保留并使工厂停止分配;Coordinator 保留 cleanup_failed,占用容量, 由管理员处理明确的外来资源后重启工厂/Worker。停止查询不能选取宿主路径、命令或任务。 只有确认实际清理的基础设施失败可按 admission 有限重试,普通成绩不重试。

恢复扫描每批最多16项;已投递而30秒后仍待领取的事件重新通知,broker/Worker 断线 采用最多30秒退避。AMQP 操作各5秒,HTTP 连接1秒/请求2秒;结果最多重试3次相同报告。 Worker SIGTERM 取消当前执行后退出,SIGKILL 由 Root EOF 清理、租约过期和重启协调收敛。 消息手工 ack 只表示通知已处理,持久化状态与补偿负责恢复,不把 ack 当成最终成绩。

输出预算必须同时满足每流不超过单文件上限、stdout/stderr 合计不超过捕获存储 上限;Python/Rust 清单在准入时拒绝不一致限额,实际沙箱仍独立核对。连续测试点 遇到完成 helper 尚待回收的暂忙槽位时,仅对不消耗 run ID 的 Busy 分配响应做 最多2秒/64次有界等待,仍受作业截止时间与取消约束;拒绝、重放和执行响应不明 不会重新执行同一请求。

新增外部依赖职责:Coordinator 的 tokio-postgres 0.7.15 只负责专用角色数据库事务; Worker 的 lapin 2.5.5 / futures-lite 2.6.1 负责 AMQP 通知,reqwest 0.12.24 负责有界 loopback 请求并禁用代理与重定向;uuid 1.27.0 生成身份,sha2 0.10.9 验证清单/工件/报告摘要。Engine 不引入数据库、HTTP、队列或权限依赖。