# SparkSandbox **Repository Path**: jiangbyte/SparkSandbox ## Basic Information - **Project Name**: SparkSandbox - **Description**: An HTTP service for isolated user-code execution - **Primary Language**: C++ - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-18 - **Last Updated**: 2026-09-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SparkSandbox [English](README.md) | [中文](README.zh-CN.md) ![Version](https://img.shields.io/badge/version-0.1.1-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Linux-lightgrey) ![C++](https://img.shields.io/badge/C%2B%2B-20-00599C?logo=cplusplus) ![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white) ![FastAPI](https://img.shields.io/badge/FastAPI-0.110%2B-009688?logo=fastapi&logoColor=white) 隔离执行用户代码的 **HTTP 服务**(非对外 SDK):自行部署后,对接方用任意语言发 HTTP 完成编译/运行,并按需自行实现 HMAC 验签。返回 stdout、stderr、资源用量与执行状态;支持多测例、checker、interactive pipeline、文件仓与 SSE。 **不提供对外客户端/SDK**;仓库内 Python 模块与脚本仅用于本仓库开发与测试。 | 层 | 实现 | 职责 | |----|------|------| | 引擎 | C++20 `sparksandbox`(`src/`) | namespaces、cgroup、seccomp、fork/wait、状态分类、interactive | | 编排 | FastAPI `spark_sandbox` | HTTP、语言表、worker 池、CompileCache、FileStore、多测例并行 | 运行时路径:`routes` → `Orchestrator.run_limited` → `SandboxWorkerPool.borrow` → 常驻 `sparksandbox worker`(NDJSON:`judge` / `interactive`)。 ## 执行状态 与 `spark_sandbox.types.Status` / 引擎分类一致: | 值 | 含义 | |----|------| | `succeeded` | 正常结束 | | `compile_failed` | 编译失败 | | `runtime_error` | 运行异常 | | `time_limit_exceeded` | 超时 | | `memory_limit_exceeded` | 超内存 | | `output_limit_exceeded` | 输出超限 | | `security_violation` | seccomp / 安全策略拦截 | | `internal_error` | 内部错误 | 进程结果另含 `stage`:`none` / `compile` / `run`。 ## 语言 命令、环境变量与 seccomp 名在 [`configs/languages.yaml`](configs/languages.yaml);对外暴露由各服务 YAML 的 `enabled_languages` 决定。 | 配置 | 启用 KEY | |------|----------| | `default.yaml` / `dev.yaml` / 代码默认 | `c11` `cpp17` `python3` `java17` `go` `nodejs` | | `docker.yaml`(容器默认) | 上述 + `rust` `sql_sqlite` | | YAML 已定义、默认未启用 | `sql_mysql` `sql_sqlserver`(需客户端;请求 `variables` 注入连接参数) | 引擎已注册 seccomp 名:`none`、`c_cpp`、`c_cpp_file_io`、`general`、`golang`、`node`、`sql_client`(`none` 表示不装过滤器)。 ### 添加语言 1. 在 `languages.yaml` 增加条目,`compile.seccomp` / `run.seccomp` 填已注册名。 2. 不够则在 `src/seccomp/profiles/` 注册新族并重编引擎(已注册名见上;YAML 只能引用已注册名)。 3. 把 KEY 写入目标配置的 `enabled_languages`;主机或 base 镜像安装对应工具链。 远程 SQL 语言设 `isolate_network: false`;连接用请求体 `variables`:`db_host` `db_port` `db_user` `db_password` `db_name`。 ## Docker 两层镜像:工具链在 **base**,日常改代码只重建 **server**。容器需 `--privileged --cgroupns=host`(namespace + cgroup)。 ### 构建 base(语言工具链,一般只需一次) ```bash bash scripts/build_base.sh # 默认标签 spark-sandbox:base # 覆盖标签:BASE_IMAGE=my/base:1 bash scripts/build_base.sh # 强制拉取 debian 基础层:PULL=1 bash scripts/build_base.sh ``` ### 构建 server(引擎 + Python 编排 + 配置) ```bash docker build --pull=false \ --build-arg BASE_IMAGE=spark-sandbox:base \ -f scripts/Dockerfile.server \ -t spark-sandbox:server . ``` ### 启动常驻容器 ```bash docker rm -f spark-sandbox 2>/dev/null || true docker run -d --name spark-sandbox \ --privileged --cgroupns=host \ -p 8080:8080 \ spark-sandbox:server # 健康检查 curl -fsS http://127.0.0.1:8080/v1/health ``` 镜像内默认:`SPARK_SANDBOX_BINARY=/usr/local/bin/sparksandbox`,`SPARK_SANDBOX_CONFIG=/app/configs/docker.yaml`(隔离开、多语言含 rust / sql_sqlite)。 开启 HMAC 验签示例: ```bash docker run -d --name spark-sandbox \ --privileged --cgroupns=host \ -p 8080:8080 \ -e SPARK_SANDBOX_SIGNING_ENABLED=1 \ -e SPARK_SANDBOX_SIGNING_SECRET='your-long-random-secret' \ spark-sandbox:server ``` ### 一键 e2e(构建 → 临时容器 → 测完清理) 在仓库根目录(需本机 `.venv` 与 `pytest`): ```bash bash scripts/docker_e2e.sh ``` 会依次:必要时构建 base → 构建 `spark-sandbox:server` → 在 `:18080` 起临时容器 → 跑 capability / security / 全语言 smoke / 语言 bench / HTTP bench → 写报告到 `/tmp/spark-sandbox-e2e` → **删除临时容器**。 常用环境变量: | 变量 | 默认 | 含义 | |------|------|------| | `SKIP_BUILD=1` | `0` | 跳过构建,直接用已有 `spark-sandbox:server` | | `BUILD_BASE=1` | `0` | 强制重建 base | | `HOST_PORT` | `18080` | 宿主机映射端口 | | `IMAGE` | `spark-sandbox:server` | server 镜像名 | | `BASE_IMAGE` | `spark-sandbox:base` | base 镜像名 | | `REPORT_DIR` | `/tmp/spark-sandbox-e2e` | 报告目录 | 示例:镜像已就绪只跑测试: ```bash SKIP_BUILD=1 bash scripts/docker_e2e.sh ``` ### 对已启动服务单独测 服务在 `http://127.0.0.1:8080`(或 e2e 的 `18080`)时: ```bash export PYTHONPATH=. export SPARK_SANDBOX_BASE_URL=http://127.0.0.1:8080 # 若容器开启了验签: # export SPARK_SANDBOX_SIGNING_SECRET='your-long-random-secret' .venv/bin/python -m pytest tests/test_capability_suite.py -v .venv/bin/python -m pytest tests/test_security_boundaries.py -v .venv/bin/python scripts/smoke_languages.py .venv/bin/python scripts/bench_languages.py --count 24 --concurrency 4 --out /tmp/bench_langs.json .venv/bin/python scripts/bench_http.py --count 40 --concurrency 8 --out /tmp/bench.json ``` ## 本地开发 依赖:Linux(推荐 WSL)+ `g++`(C++20)+ `libseccomp` + `pkg-config` + Python 3.10+。 ```bash make python3 -m venv .venv && .venv/bin/pip install -r requirements.txt export SPARK_SANDBOX_BINARY=./build/sparksandbox export SPARK_SANDBOX_CONFIG=configs/dev.yaml export PYTHONPATH=. .venv/bin/python -m spark_sandbox --host 127.0.0.1 --port 8080 ``` 或 `make run`(默认 `configs/dev.yaml`)。 `dev.yaml` 关闭 `isolation.enable_namespaces` 与 `cgroup.enabled`,便于无特权开发;引擎仍可按语言安装 seccomp。也可用 `SPARK_SANDBOX_DISABLE_ISOLATION=1` 强制关隔离。 ## API 完整传参 / 出参 / 配置见 **[`docs/api.md`](docs/api.md)**(含 Limits、`result_policy`、multipart、文件仓、SSE、exec)。 下面为速查;约定:**仅 GET / POST**(删除用 `POST /v1/files/delete`)。 ### 请求验签(可选,可开关) 由配置控制,与静态 token / Bearer **无关**: | 配置 / 环境变量 | 默认 | 含义 | |-----------------|------|------| | `signing_enabled` / `SPARK_SANDBOX_SIGNING_ENABLED` | `false` | 是否强制验签 | | `signing_secret` / `SPARK_SANDBOX_SIGNING_SECRET` | `""` | HMAC 密钥;**开启时必填**,否则进程拒绝启动 | | `auth_skew_seconds` / `SPARK_SANDBOX_AUTH_SKEW_SECONDS` | `300` | `X-Spark-Timestamp` 允许与服务器时钟偏差(秒) | | `signing_enabled` | 行为 | |-------------------|------| | `false` | **所有接口都不验签**(即使配了 `signing_secret` 也不校验) | | `true` | 下表「须验签」的接口必须带齐合法签名头,否则 **401**;「公开」接口仍可不带 | #### 开启后:哪些请求必须带验签 **须验签**(缺头 / 签名错误 / 时间戳超窗 / nonce 重放 → 401): | 方法 | 路径 | 说明 | |------|------|------| | POST | `/v1/run` | 单次编译运行 | | POST | `/v1/run/stream` | SSE 流式运行 | | POST | `/v1/run_cases` | 多测例 | | POST | `/v1/run_cases/batch` | 批量多测例 | | POST | `/v1/run_pipeline` | checker / interactive | | POST | `/v1/exec` | 底层命令序列 | | GET | `/v1/files` | 文件仓列表(GET 也要签) | | POST | `/v1/files` | 单文件上传 | | POST | `/v1/files/batch` | 批量上传 | | POST | `/v1/files/bundle` | 测例包上传 | | GET | `/v1/files/{file_id}` | 下载 | | POST | `/v1/files/delete` | 删除 | **公开、不要求验签**(开启验签后也可裸调): | 方法 | 路径 | 说明 | |------|------|------| | GET | `/` | 服务名 / 文档入口 | | GET | `/v1/health` | 健康与池统计 | | GET | `/v1/version` | 版本 | | GET | `/v1/languages` | 已启用语言 | | GET | `/v1/metrics` | Prometheus(`enable_metrics=false` 时 404) | 根因:上述受保护路由均挂了 `Depends(verify_auth)`;公开路由未挂。 #### 签名头与算法 每次**须验签**的请求都要带: | Header | 含义 | |--------|------| | `X-Spark-Timestamp` | Unix 秒(UTC);须在 `auth_skew_seconds` 内 | | `X-Spark-Nonce` | 一次性随机串(建议 UUID);同一 nonce 在 TTL 内不可复用 | | `X-Spark-Signature` | `hex(HMAC-SHA256(signing_secret, canonical))`(小写 hex) | **Canonical string**(两行,中间一个 `\n`): ``` {timestamp} {nonce} ``` `timestamp` / `nonce` 须与请求头字符串完全一致。 验签只证明持有密钥 + 新鲜度/防重放,**不绑定** METHOD、PATH、body;请求体完整性由链路或业务自行保证。 `Authorization` 等头**不参与、也不替代**验签;仅带 Bearer 仍会 401。 #### 调用示例 对接方自行构造 HTTP 与验签(以下为参考实现,非官方 SDK)。 `signing_enabled=false` 时无需验签头: ```bash curl -s http://127.0.0.1:8080/v1/run -H 'Content-Type: application/json' \ -d '{"language":"python3","code":"print(input())","stdin":"hi"}' ``` `signing_enabled=true` 时自行加签: ```python import hmac import hashlib import json import time import uuid import httpx secret = "your-secret" body = json.dumps({"language": "python3", "code": "print(1)"}).encode() ts = str(int(time.time())) nonce = uuid.uuid4().hex canonical = f"{ts}\n{nonce}" sig = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Spark-Timestamp": ts, "X-Spark-Nonce": nonce, "X-Spark-Signature": sig, } httpx.post("http://127.0.0.1:8080/v1/run", content=body, headers=headers) ``` ### 主动心跳上报(可选) 沙箱可按间隔**主动**向业务方 URL 发 `POST`(不是入站 `/v1/heartbeat`)。默认关闭。 | 配置 / 环境变量 | 默认 | 含义 | |-----------------|------|------| | `heartbeat_enabled` / `SPARK_SANDBOX_HEARTBEAT_ENABLED` | `false` | 总开关 | | `heartbeat_url` / `SPARK_SANDBOX_HEARTBEAT_URL` | `""` | 接收心跳的完整 URL | | `heartbeat_interval_seconds` / `SPARK_SANDBOX_HEARTBEAT_INTERVAL_SECONDS` | `30` | 间隔(秒,最小 5) | 开启时须同时配置非空 `heartbeat_url` 与 `signing_secret`,否则进程拒绝启动。心跳**始终加签**(三个 `X-Spark-*` 头),且 **body 始终 AES-256-GCM 加密**(密钥由同一 `signing_secret` 派生),与入站 `signing_enabled` 无关。失败只打日志,不拖垮主服务。 线上请求体仅为密文信封(看不到节点/资源等明文): ```json { "v": 1, "alg": "A256GCM", "iv": "", "ciphertext": "" } ``` **解密(接收方)** 1. (推荐)校验三个 `X-Spark-*` 头(算法同入站验签)。 2. AES 密钥:`SHA256("spark-sandbox-heartbeat-aes256-v1" || 0x00 || utf8(signing_secret))`(32 字节)。 3. AAD = `utf8(X-Spark-Timestamp)`(须与请求头完全一致)。 4. `AES-256-GCM` 解密 `iv` + `ciphertext`,得到 UTF-8 JSON 明文。 明文字段示例(解密后,与加密前完整一致): ```json { "service": "SparkSandbox", "version": "0.1.1", "status": "ok", "started_at": 1710000000.0, "sent_at": 1710000030.0, "uptime_seconds": 30.0, "node": { "hostname": "sandbox-1", "pid": 1, "platform": "Linux", "platform_release": "6.1.0", "arch": "x86_64", "python": "3.11.2", "ips": ["172.17.0.2"], "primary_ip": "172.17.0.2" }, "listen": { "host": "0.0.0.0", "port": 8080 }, "capacity": { "max_concurrency": 8, "worker_pool_size": 4 }, "resources": { "cpu_count": 4, "load_avg_1m": 0.12, "load_avg_5m": 0.08, "load_avg_15m": 0.05, "memory_total_bytes": 8589934592, "memory_available_bytes": 4294967296, "memory_used_bytes": 4294967296, "memory_used_percent": 50.0, "process_rss_bytes": 67108864, "process_vms_bytes": 268435456 }, "pool": { "pool_size": 4, "available_count": 4, "total_count": 4, "active_count": 0, "borrow_count": 0, "borrow_avg_wait_ms": 0, "borrow_last_wait_ms": 0, "borrow_max_wait_ms": 0, "waiting_count": 0, "emergency_count": 0, "queue_timeout_count": 0, "replaced_count": 0 }, "compile_cache": { "enabled": true, "entries": 0, "total_bytes": 0, "hits": 0, "misses": 0 }, "languages": { "keys": ["c11", "cpp17", "python3", "java17", "go", "nodejs", "rust", "sql_sqlite"], "languages": [ { "id": "python3", "label": "Python 3", "source_filename": "main.py" } ] } } ``` 加密只包一层信封,**不删减字段**;`pool` / `compile_cache` 与 `GET /v1/health` 同结构;`languages` 与 `GET /v1/languages` 同结构。无密钥无法读明文。 Python 解密参考(非 SDK): ```python import base64, hashlib, json from cryptography.hazmat.primitives.ciphers.aead import AESGCM def decrypt_heartbeat(secret: str, envelope: dict, timestamp_header: str) -> dict: key = hashlib.sha256(b"spark-sandbox-heartbeat-aes256-v1\0" + secret.encode()).digest() iv = base64.b64decode(envelope["iv"]) ct = base64.b64decode(envelope["ciphertext"]) pt = AESGCM(key).decrypt(iv, ct, timestamp_header.encode()) return json.loads(pt) ``` ### 接口一览 表中「验签」列:仅在 `signing_enabled=true` 时生效。 | 方法 | 路径 | 验签 | 作用 | |------|------|------|------| | GET | `/v1/health` | 否 | 健康;含 `pool`、`compile_cache` 统计 | | GET | `/v1/version` | 否 | `name` + `version` | | GET | `/v1/languages` | 否 | `keys` + 公开元数据 | | GET | `/v1/metrics` | 否 | Prometheus 文本(`enable_metrics=false` 时 404) | | POST | `/v1/run` | **是** | 单次编译运行(字段 `code`) | | POST | `/v1/run/stream` | **是** | SSE 流式(见下) | | POST | `/v1/run_cases` | **是** | 多测例(字段 `source`;可选 `checker`) | | POST | `/v1/run_cases/batch` | **是** | `submissions[]` 提交级有界并行 | | POST | `/v1/run_pipeline` | **是** | `mode=checker` \| `interactive` | | POST | `/v1/exec` | **是** | 底层顺序执行多条命令 | | GET | `/v1/files` | **是** | 文件仓元数据列表 | | POST | `/v1/files` | **是** | 单文件上传 | | POST | `/v1/files/batch` | **是** | 批量上传 | | POST | `/v1/files/bundle` | **是** | 测例包 zip/tar → 解包入库 | | GET | `/v1/files/{file_id}` | **是** | 下载 | | POST | `/v1/files/delete` | **是** | 删除(body: `file_id`) | 多数执行接口支持 JSON 或 **multipart**(表单字段 `payload` = JSON + 文件字段);验签不绑定 body。 ### `/v1/run` 主要请求字段:`language` `code` `stdin` `files` `fetch_files` `limits` `compile_limits` `result_policy` `output_mode` `max_stdout_bytes` `max_stderr_bytes` `session_id` `variables`。 响应含 `status`、`compile` / `run`(`ProcessResult`)、stdout/stderr(或 `*_file_id` / `*_preview`)、`feedback`、`session_id`。 ### `/v1/run/stream`(SSE) 事件名(`event:`):`compile_start` → `compile_done` → 运行中 `stdout` / `stderr` → `metrics` → `done`;失败可为 `error`。与 `/run` 共享准入 Semaphore。 ### 多测例 / pipeline - 测例字段:`stdin` `stdin_file_id` `input_file_id` `answer_file_id` `files`;也可 `case_bundle_file_id` - `result_policy`:`auto` \| `inline` \| `file_id` \| `summary`(大输出可进 FileStore;响应路径脱敏) - `checker` / `interactor`:按规格编译运行,返回进程结果与原始 `exit_code` - `interactive`:引擎双向 pipe,两侧结果一并返回 ```bash curl -s http://127.0.0.1:8080/v1/run -H 'Content-Type: application/json' \ -d '{"language":"python3","code":"print(input())","stdin":"hi"}' ``` ## 配置 | 文件 | 用途 | |------|------| | [`configs/default.yaml`](configs/default.yaml) | 完整默认(注释最全;隔离开) | | [`configs/dev.yaml`](configs/dev.yaml) | 本机 / WSL(关 namespaces / cgroup) | | [`configs/docker.yaml`](configs/docker.yaml) | 容器(隔离开;多启用 rust / sql_sqlite) | | [`configs/languages.yaml`](configs/languages.yaml) | 语言编译 / 运行命令与 seccomp | 环境变量(见 `spark_sandbox/config.py`,覆盖 YAML): | 变量 | 含义 | |------|------| | `SPARK_SANDBOX_CONFIG` | 服务 YAML 路径 | | `SPARK_SANDBOX_BINARY` | 引擎二进制 | | `SPARK_SANDBOX_HOST` / `SPARK_SANDBOX_PORT` | 监听地址 | | `SPARK_SANDBOX_SIGNING_ENABLED` | 是否强制 HMAC 验签 | | `SPARK_SANDBOX_SIGNING_SECRET` | HMAC 密钥 | | `SPARK_SANDBOX_AUTH_SKEW_SECONDS` | 验签时间戳偏差(秒) | | `SPARK_SANDBOX_HEARTBEAT_ENABLED` | 是否主动心跳上报 | | `SPARK_SANDBOX_HEARTBEAT_URL` | 心跳接收 URL | | `SPARK_SANDBOX_HEARTBEAT_INTERVAL_SECONDS` | 心跳间隔(秒,最小 5) | | `SPARK_SANDBOX_DISABLE_ISOLATION` | `1` 关 namespaces + cgroup | | `SPARK_SANDBOX_MAX_CONCURRENCY` | HTTP 准入并发 | | `SPARK_SANDBOX_WORKER_POOL_SIZE` | 常驻 worker 数 | | `SPARK_SANDBOX_CASE_PARALLELISM` | 单次多测例并行上限 | | `SPARK_SANDBOX_BATCH_PARALLELISM` | batch 提交并行 | | `SPARK_SANDBOX_ALLOW_EMERGENCY_WORKER` | 池耗尽时是否临时拉起 worker | | `SPARK_SANDBOX_MAX_QUEUE_WAIT_SECONDS` | 借 worker 排队硬超时 | | `SPARK_SANDBOX_ENABLE_METRICS` | 是否暴露 `/v1/metrics` | ## 测试 ```bash export SPARK_SANDBOX_BINARY=./build/sparksandbox PYTHONPATH=. .venv/bin/python -m pytest tests/ -q # 多语言 / 文件 / 并发 PYTHONPATH=. .venv/bin/python -m pytest tests/test_capability_suite.py -v # TLE/OLE、路径穿越、fork/网络/写盘等 PYTHONPATH=. .venv/bin/python -m pytest tests/test_security_boundaries.py -v # 对已启动服务 SPARK_SANDBOX_BASE_URL=http://127.0.0.1:8080 PYTHONPATH=. \ .venv/bin/python scripts/smoke_languages.py SPARK_SANDBOX_BASE_URL=http://127.0.0.1:8080 PYTHONPATH=. \ .venv/bin/python scripts/bench_languages.py --out /tmp/bench_langs.json PYTHONPATH=. .venv/bin/python scripts/bench_http.py \ --base-url http://127.0.0.1:8080 --out /tmp/bench.json ``` `make test`:编译引擎 + 单元测试 + smoke + HTTP 集成。 ## 目录 ``` configs/ 服务与语言 YAML include/ src/ C++ 引擎、seccomp profiles spark_sandbox/ HTTP 服务编排(开发/测试用辅助模块不对外提供 SDK) scripts/ Dockerfile.base|server、e2e、smoke、bench tests/ pytest 与能力 / 安全套件 docs/api.md HTTP API 完整说明(传参 / 出参 / 配置) ``` ## License MIT