# sandctl **Repository Path**: FunnyAWM/sandctl ## Basic Information - **Project Name**: sandctl - **Description**: 2026北京开源行业解决方案创新赛-全域代码执行边界约束沙箱系统 - **Primary Language**: Go - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-10-16 - **Last Updated**: 2026-10-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # sandctl > 全域代码执行边界约束沙箱系统 · 后端无关的 LLM 不可信代码边界约束与供应链运行时强制 > 语言:Go 1.25+ | 许可:[Apache-2.0](LICENSE) | 状态:Linux 上的 L1/L2/L3 三档均已实测,Windows 的两档 L1 已在 Windows 11 实测,macOS 仅代码就绪 > 规模:109 个 Go 文件 17,911 行,其中测试 42 个文件 7,395 行(约 41%);A 组越界用例 8/8 拦截,L1 冷启动 p95 105ms > 设计细节见 [`docs/PRD-全域代码执行边界约束沙箱系统.md`](docs/PRD-全域代码执行边界约束沙箱系统.md)(v0.3) --- ## 这是什么 > Und wenn du lange in einen Abgrund blickst, blickt der Abgrund auch in dich hinein.(当你在凝视深渊的时候,深渊也在凝视你。) 把“LLM 生成的代码 / Agent Skill”放进声明式边界内执行,不靠净化(sanitize): 1. 一层与宿主无关的策略(`policy.yaml`)描述文件系统作用域、网络出站白名单、资源上限、工具白名单、凭证隔离; 2. 编译 Skill 声明为该策略,Skill 是不可信输入,只能与安全下限(floor)求交,不得放宽; 3. 交给可插拔的分层隔离后端执行,并留下按 Skill 身份归因的审计。 三条不可退让的红线:只收紧不放宽 · 默认拒绝 + 安全反序列化 · 凭证永不进沙箱。 能力覆盖不足时 fail closed(exit 3),绝不静默降级。 --- ## 快速开始 ```bash # Linux / macOS:make bin 用 git describe --tags --dirty 注入版本号。 # HEAD 正好在 tag 上时是 1.0.0;tag 之后还有提交时形如 1.0.0-50-gddf875f;工作区脏时带 -dirty make bin # 没有 make 时等价写法;不注入版本号,`sandctl version` 显示 dev go build -o bin/sandctl ./cmd/sandctl # Windows 必须显式写 .exe go build -o bin/sandctl.exe ./cmd/sandctl ./bin/sandctl doctor # 宿主事实 + 各后端可用性与原因 ./bin/sandctl list-backends # 能力矩阵:全部已注册后端,含不可用原因 ./bin/sandctl policy validate -p policies/base.yaml ./bin/sandctl run -p policies/base.yaml -- [args...] ``` > Windows 构建有一处容易出错的地方:`go build -o bin/sandctl` 生成的是没有扩展名的 `bin\sandctl`, > 不会覆盖已有的 `bin\sandctl.exe`。如果你的 shell 里写的是 `.\bin\sandctl.exe`, > 运行到的仍是上一次构建的旧版本,表现为“代码明明改了,`list-backends` 却没变”。 > 要么固定写 `-o bin/sandctl.exe`,要么运行 `.\bin\sandctl`。 `run` 的边界来源必须二选一,都不给即拒绝执行(无边界声明就不执行): ```bash # 入口一:直接给策略 ./bin/sandctl run -p policies/base.yaml -- python3 -c "print('hi')" # 入口二:按 ID 加载已安装 Skill,编译为策略(可与 -p 同给,此时 -p 充当 floor) ./bin/sandctl skill list --dir ./skills # 列出搜索目录下的 Skill(含 [不可用] 项) ./bin/sandctl skill inspect sandctl --dir ./skills # 看与 floor 求交后的策略 + 被削掉的越权项 ./bin/sandctl run --skill sandctl --skills-dir ./skills -- python3 -c "print('hi')" # 入口二的离线版本:把求交结果落成策略文件,再交给 run -p ./bin/sandctl policy compile --skill ./skills/sandctl/SKILL.md -p policies/base.yaml -o /tmp/compiled.yaml ./bin/sandctl run -p /tmp/compiled.yaml -- python3 -c "print('hi')" ``` `policy compile` 输出的是普通策略文件,与手写 `policy.yaml` 走同一条校验链路;被削掉的越权项在 stderr 逐条报告。`run` 的常用可选 flag:`--backend auto|l1|l2|l3` 钉住隔离档,`--level 1|2|3` 与 `--threat T0|T1|T2` 只升不降,`--tighten key=value` 追加收紧项(可重复),`--strict` 让任何降级都算失败(供 CI),`--allow-degraded` 在执行比策略更松时仍继续并在审计记 `decision=loosened`,`--json` 把执行摘要输出为 JSON,`--workdir` 指定命令的工作目录(缺省为 sandctl 所在目录)。 Skill 搜索顺序(先命中者生效,同名不覆盖):`--skills-dir` / `SANDCTL_SKILLS_DIR` → `~/.sandctl/skills` → `./.sandctl/skills`。ID 即相对路径(如 `pdf-tool`、`acme/pdf-tool`), 会先做形状校验再拼路径(`..`、绝对路径、盘符一律拒绝)。 一键演示(9 条用例,第 9 条是攻击套件,默认 SKIP,加 `--attack` 才跑;控制台直接说明每条在证明什么;`--help` 查看清单与判定口径): ```bash bash scripts/demo.sh # 用例间停 5 秒,约 50 秒 bash scripts/demo.sh --delay 0 # 不停顿,约 4 秒 bash scripts/demo.sh --attack # 追加恶意样本套件,约 65 秒 ``` 审计不占用终端输出:缺省落盘到操作系统缓存目录下的 `sandctl/audit`,按 UTC 日一文件 (Linux `~/.cache/sandctl/audit/audit-YYYY-MM-DD.jsonl`,Windows `%LocalAppData%\sandctl\audit\`)。用 `sandctl audit query` 或 `audit export` 查看; `SANDCTL_AUDIT_DIR` 可改目录,`--audit-out` 可指定单个文件。 ```bash ./bin/sandctl run -p policies/base.yaml -- # 落盘到缺省目录 ./bin/sandctl audit query # 查最新那份日志 ./bin/sandctl audit query --event deny # 只看拒绝 ./bin/sandctl audit query --kind proc.limit # 只看某一类越界 ./bin/sandctl audit query --raw | jq .deny # 原始 JSONL 交给下游处理 ./bin/sandctl audit export # 合并导出到当前目录 audit.log ./bin/sandctl audit export -o /tmp/audit-archive.log --event deny # 指定位置,只导拒绝事件 ./bin/sandctl audit export --force # 目标已存在时必须显式覆盖 ./bin/sandctl run -p policies/base.yaml --audit-out run.jsonl -- ./bin/sandctl audit query --audit-in run.jsonl # 查指定文件 ./bin/sandctl audit query --audit-in - < run.jsonl # 显式读 stdin ``` PowerShell 不支持 `<` 输入重定向,`--audit-in` 在两端都能用。 两个过滤条件不是一回事:`--event` 是事件类型(`allow`/`deny`/`start`/`stop`/`error`/`degraded`), `--kind` 是越界类别(`fs.read`/`fs.write`/`net.connect`/`priv.escalate`/`proc.limit`)。 另可加 `--since 1h` 限定时间窗。 退出码:`0` 成功 | `1` 通用错误(含沙箱内的命令自身失败) | `2` 用法或策略校验失败 | `3` 拒绝执行(无可用后端,或结果会比策略更松,后者需 `--allow-degraded`) | `4` 越界被拒。 ### Windows 宿主:构建、MXC 安装与验证 Windows 上 L1 有两个后端,选择器同档内优先用能力更完整的那个: | 后端 | 依赖 | 强制的边界 | |---|---|---| | `l1_windows_mxc`(首选) | MXC SDK | 文件按策略逐路径授权(`fs.deny` 生效)、出站默认全断;资源上限由 Job Object | | `l1_windows`(回退) | 无 | 只有资源上限(进程数 / 内存 / CPU / 墙钟),文件与网络不隔离 | 第 1 步 · 构建(注意 `.exe`,见上节说明): ```powershell go build -o bin/sandctl.exe ./cmd/sandctl ``` 第 2 步 · 安装 MXC(`wxc-exec.exe` 是唯一硬依赖): 要求 Windows 11 24H2(build 26100)及以上。 ```powershell # 方式一:npm 全局安装(推荐) npm i -g @microsoft/mxc-sdk # 方式二:从 GitHub Releases 下载后解压 # https://github.com/microsoft/mxc/releases → mxc--win-x64.zip # 解压后 wxc-exec.exe 位于 package\bin\x64\ ``` 两种安装方式都会被自动发现,不需要配置环境变量。查找顺序:`SANDCTL_WXC_EXEC` → `PATH` → 独立安装目录 → npm 全局包的 `node_modules\@microsoft\mxc-sdk\bin\\`。 只有把 `wxc-exec.exe` 放在非常规位置时才需要显式指定: ```powershell $env:SANDCTL_WXC_EXEC = "D:\your-installation\mxc\package\bin\x64\wxc-exec.exe" ``` 第 3 步 · 确认宿主能力: ```powershell .\bin\sandctl.exe list-backends ``` 期望 `l1_windows_mxc` 一行是 `yes full yes no yes ...`,依次对应可用、拒绝路径 full、可断网、无域名白名单、可按 CIDR 放行。 若显示不可用,`DETAIL` 列会直接给出原因(未找到、版本不支持、`--dry-run` 失败)。 第 4 步 · 真实拦截(路径改成你自己的绝对路径): ```powershell @' apiVersion: sandctl/v1 kind: Policy metadata: { id: win-mxc, threat: T1 } spec: fs: read: ['C:\work\demo'] deny: ['C:\work\demo\.git'] proc: { wallTime: 10s } '@ | Set-Content -Path "$env:TEMP\win-mxc.yaml" -Encoding utf8 # 读被 deny 的路径 → exit 4。审计的 kind 靠输出里的路径加策略授予推断: # 路径出现在输出里记 deny(fs.read),拿不到路径时按写越界兜底记 deny(fs.write) .\bin\sandctl.exe run -p "$env:TEMP\win-mxc.yaml" -- cmd /c "type C:\work\demo\.git\config" # 出站被阻断 → 命令报"无法解析主机",exit 1 .\bin\sandctl.exe run -p "$env:TEMP\win-mxc.yaml" -- curl.exe -s -m 5 http://example.com ``` > 可执行名写 `cmd` 或 `cmd.exe` 都可以:MXC 不按 PATH 查找命令,sandctl 会在宿主侧先 > 解析成绝对路径再交给它。 未安装 MXC 时:`run` 会回退到 `l1_windows`。带非空 `fs.deny` 或要求断网的策略在该档按 VR-17 不可选(exit 3),错误里会写明“未找到 wxc-exec.exe”及安装办法,不会带着全通网络 或未生效的 deny 继续执行。 > MXC 的 `network.egress.allow` 只接受 CIDR,无法表达“域名:端口”。因此需要域名白名单的策略 > 会被降级为全断并给出 `[WARN]` 提示;要精确放行请用 L2(Linux 宿主)。 ### Linux 宿主:10 分钟完成配置与验证 以下命令在本仓库实测宿主(Linux + bubblewrap 0.11.0 + cgroup v2 + systemd 255 + docker/runsc)逐条执行通过, 计时走查记录见 [`evidence/ac-16-demo-walkthrough.txt`](evidence/ac-16-demo-walkthrough.txt)。 第 0 步 · 前置自检(缺什么补什么,不满足时后端会如实报不可用而不是假装可用): ```bash stat -fc %T /sys/fs/cgroup # 期望 cgroup2fs(L1 资源上限依赖它) systemctl --user is-system-running # 期望 running systemd-run --user --scope -q -p MemoryMax=8388608 -- true # exit 0 才可用 command -v bwrap docker runsc # L1 需要 bwrap;L2 需要 docker + 已注册 runsc docker info | grep -i runsc # 运行时应含 runsc ``` 第 1 步 · 构建: ```bash go build -o bin/sandctl ./cmd/sandctl ``` 第 2 步 · 宿主事实与能力矩阵(可用后端、能力声明、不可用原因): ```bash ./bin/sandctl doctor ./bin/sandctl list-backends ``` 第 3 步 · 策略校验(两条命令口径不同:Go 侧打印 `19 rules passed`;参考实现打印 `OK=21 FAIL=0`、规则覆盖 `19/19`,其余 11 条是需真实隔离后端的运行时用例,记 SKIP): ```bash ./bin/sandctl policy validate -p policies/base.yaml ./bin/sandctl policy validate -p policies/base.yaml --strict-warnings # warning 也算失败(供 CI) ./bin/sandctl policy validate -p policies/base.yaml --json # 结果输出 JSON .venv/bin/python scripts/validate.py | tail -3 # 未建 Python 环境先:uv venv .venv && uv pip install --python .venv/bin/python pyyaml jsonschema ``` 第 4 步 · 真实越界拦截(L1 Linux;夹具放 `/tmp`,不碰真实家目录): ```bash mkdir -p /tmp/sandctl-demo/home/.ssh /tmp/sandctl-demo/ws printf 'HOMESECRET\n' > /tmp/sandctl-demo/home/.ssh/id_rsa cat > /tmp/sandctl-demo/policy.yaml <<'YAML' apiVersion: sandctl/v1 kind: Policy metadata: { id: demo, threat: T1 } spec: fs: read: ["/tmp/sandctl-demo/**"] write: ["/tmp/sandctl-demo/ws/**"] deny: ["~/.ssh/**"] net: { default: deny, allow: [], egressProxy: true } proc: { cpu: "1", mem: "256Mi", pids: 32, wallTime: "10s", noNewPrivs: true } secrets: { inject: false, proxy: true, allow: [] } tools: [cat] YAML # 越界:读被 deny 的 ~/.ssh/id_rsa → exit 4 + 审计 deny(fs.read) # 正例:工作区内读写 → exit 0 + 审计 allow(granted) # 威胁下限:T2 只要 L1 → fail closed(exit 3) # 子 shell 内 export HOME:让 shell 的 ~ 展开与后端策略的 ~ 展开都指向夹具,不碰真实家目录 ( export HOME=/tmp/sandctl-demo/home ./bin/sandctl run -p /tmp/sandctl-demo/policy.yaml \ --audit-out /tmp/sandctl-demo/deny.jsonl -- cat ~/.ssh/id_rsa; echo "exit=$?" ./bin/sandctl run -p /tmp/sandctl-demo/policy.yaml \ --audit-out /tmp/sandctl-demo/ok.jsonl -- sh -c 'printf ok > /tmp/sandctl-demo/ws/r.txt && cat /tmp/sandctl-demo/ws/r.txt'; echo "exit=$?" ) ./bin/sandctl run --threat T2 --backend l1 -p /tmp/sandctl-demo/policy.yaml -- true; echo "exit=$?" ``` 第 5 步 · L2 gVisor(需 `SANDCTL_L2_IMAGE` 或预拉镜像;`proc.pids` 必须 ≥64, 低于 gVisor 运行下限会直接 fail closed): ```bash cat > /tmp/sandctl-demo/policy-l2.yaml <<'YAML' apiVersion: sandctl/v1 kind: Policy metadata: { id: demo-l2, threat: T1 } spec: fs: { read: ["/tmp/**"], write: ["/tmp/**"], deny: [] } net: { default: deny, allow: [], egressProxy: true } proc: { cpu: "1", mem: "256Mi", pids: 64, wallTime: "30s", noNewPrivs: true } secrets: { inject: false, proxy: true, allow: [] } tools: [sh] YAML SANDCTL_L2_IMAGE=debian:stable-slim ./bin/sandctl run -p /tmp/sandctl-demo/policy-l2.yaml \ --backend l2 -- sh -c 'echo l2-ok; id -u' # 期望 exit 0,输出 l2-ok 与调用方 uid(不给 root) ``` 需要“白名单放行”正例时,先给 runsc 开 `--host-uds`(改 docker 运行时注册)并 `export SANDCTL_L2_HOST_UDS=1` 声明该宿主前置条件;未声明时带 `net.allow` 的策略按 VR-17 fail closed(E9003),由 L1 承接(见 `evidence/ac-10-l2-gvisor.log` §8)。 第 6 步 · L3 Kata(可选;需 `/dev/kvm` + 已注册 handler): ```bash # Kata 4.x 的 kata-runtime 已不是 OCI 运行时,必须注册 containerd shim: sudo ln -sf /opt/kata/bin/containerd-shim-kata-v2 /usr/local/bin/ # /etc/docker/daemon.json 中: # "kata-qemu": { "runtimeType": "io.containerd.kata.v2", # "options": { "ConfigPath": "/opt/kata/share/defaults/kata-containers/configuration-qemu.toml" } } sudo systemctl restart docker docker run --rm --runtime=kata-qemu debian:stable-slim uname -r # 必须 ≠ 宿主内核 cat > /tmp/sandctl-demo/policy-l3.yaml <<'YAML' apiVersion: sandctl/v1 kind: Policy metadata: { id: demo-l3, threat: T2 } spec: fs: { read: ["/tmp/**"], write: ["/tmp/**"], deny: [] } net: { default: deny, allow: [], egressProxy: true } proc: { cpu: "1", mem: "256Mi", pids: 64, wallTime: "30s", noNewPrivs: true } secrets: { inject: false, proxy: true, allow: [] } tools: [sh] YAML SANDCTL_L3_IMAGE=debian:stable-slim ./bin/sandctl run -p /tmp/sandctl-demo/policy-l3.yaml \ --backend l3 -- sh -c 'echo l3-ok; uname -r; id -u' ``` 期望:exit 0;`uname -r` 是 guest 内核(与宿主不同);uid 为调用方(非 root)。 L3 首版不声明域名/IP 白名单与 pids 上限:带 `net.allow` 的策略退化为全断并 `[WARN]`; `proc.pids` 被 Kata 运行时剥离(`Resources.Pids = nil`),每次运行打一条 `[FAIL]` 并默认 exit 3, 需要显式 `--allow-degraded` 才继续(审计记 `decision=loosened`)。syscall 过滤是条件性能力: 宿主 Kata 设 `disable_guest_seccomp=false` 且 `ConfigPath` 指向 `/etc` 时才生效,否则同样按缺口处理。 详见 `evidence/limitation-l3-pids-seccomp.log` 与 `evidence/l3-guest-seccomp.log`。 第 7 步 · 审计查询(`--event` 是事件类型,`--kind` 是越界类别,两者不是一回事): ```bash ./bin/sandctl audit query --audit-in /tmp/sandctl-demo/deny.jsonl --event deny ./bin/sandctl audit query --audit-in /tmp/sandctl-demo/ok.jsonl --kind proc.limit ``` 跑完后 `rm -rf /tmp/sandctl-demo` 清理夹具。恶意 Skill 越权被削 + 运行期拦截的完整 Demo 见 [`evidence/ac-08-skill-runtime.log`](evidence/ac-08-skill-runtime.log)。 ### 作为 Agent 工具调用(function calling / MCP) [`integrations/agent/tools.json`](integrations/agent/tools.json) 用 JSON Schema 声明了 6 个工具 (`sandctl_run`、`sandctl_policy_validate`、`sandctl_list_backends`、`sandctl_skill_list`、`sandctl_skill_inspect`、`sandctl_audit_query`), 每个工具都附 CLI 调用模板;调用纪律(argv 数组、必开审计、退出码 3/4 不得降级重试)见 [`integrations/agent/README.md`](integrations/agent/README.md) 与 `AGENTS.md` §9.7。 ```json {"name": "sandctl_run", "arguments": {"policy": "/tmp/policy.yaml", "command": ["sh", "-c", "id"], "auditOut": "/tmp/run.jsonl"}} ``` 声明只解决“可调用”:仓库不含 MCP server,Agent 仍可直接执行命令绕过边界; 需要强制时只能收窄执行入口或把 Agent 本身放进沙箱(见该文档 §6)。 对 DeepSeek Harness(dsh)的插件职责与收口方式,见 [`dsh/README.md`](dsh/README.md)。 ### 本机可复现的一次“真实的越界回收” `policies/base.yaml` 带非空 `fs.deny` 与 `net.allow`:装了 MXC 的宿主可直接跑(`fs.deny` 生效、 `net.allow` 降级为全断并提示);没装 MXC 时回退档能力不足,按 VR-17 fail closed(exit 3), 这是设计预期,不是故障。要在回退档看到真实执行 + 真实拦截, 用只声明资源上限的策略(`fs.deny` 为空、`net.allow` 为空,该档能力才覆盖得住): ```yaml # 存为 /tmp/l1win.yaml(Windows 可用 %TEMP%\l1win.yaml) apiVersion: sandctl/v1 kind: Policy metadata: { id: l1win, threat: T1 } spec: fs: { read: ["/workspace/**"] } net: { default: deny } proc: { cpu: "1", mem: 256Mi, pids: 16, wallTime: 1s } ``` ```bash # 正常结束(exit 0):审计为 start → allow → stop ./bin/sandctl run -p /tmp/l1win.yaml --audit-out ok.jsonl -- cmd /c "echo hello" # 命中墙钟上限(exit 4):审计为 start → deny(proc.limit, killed) → stop ./bin/sandctl run -p /tmp/l1win.yaml --audit-out kill.jsonl -- cmd /c "ping -n 30 127.0.0.1" ``` 两条路径合并后的审计样本见 [`evidence/ac-03-audit-sample-l1win.jsonl`](evidence/ac-03-audit-sample-l1win.jsonl)。 > Windows 上进程与命令请用 `cmd /c ...`;Linux/macOS 用 `/bin/sh -c ...`。 --- ## 能力与受限声明(如实,不静默通过) 下表列的是每个后端设计上能做到什么,以及在本机实测到的结果。能力随宿主变化:同一份二进制在没装 gVisor、没有 KVM 的机器上会少几档,实际以 `sandctl list-backends` 的输出为准。 | 后端 | 状态 | 强制的能力 | 未实现(未缓解风险) | |---|---|---|---| | `l1_linux` | 可用(Linux 已实测) | bubblewrap:`fs.deny`、写作用域、断网、`no_new_privs`、墙钟回收、`syscall.profile` 的 restricted/strict(BPF 过滤器,命中返回 EPERM);外层 systemd scope:内存/CPU/进程数硬上限 | syscall 过滤是 deny-list 而非白名单,`profile: default` 时不加载;探针不支持 seccomp 的宿主会把它报为否,带非 default 档的策略默认 exit 3;资源上限依赖 cgroup v2 + systemd user scope,不满足时 `list-backends` 显示为否,带这类要求的策略默认 exit 3 | | `l1_windows_mxc` | 可用(Windows 11 24H2+ + MXC SDK,已实测) | MXC processcontainer:`fs.read/write/deny` 逐路径授权、出站默认全断、`net.allowCidr` 按 IP 段放行;外层 Job Object:进程数 / 内存 / CPU / 墙钟 | 无域名白名单(`egress.allow` 只接受 CIDR,写 `net.allow` 时降级为全断并提示);无 syscall 过滤;被拒路径的读/写方向靠输出里的路径加策略授予推断:deny 覆盖记 `fs.read`、读授权内记 `fs.write`,路径没出现在输出里则记 `unattributed`;该推断未在 Windows 真机复测 | | `l1_windows` | 回退档(MXC 缺失时启用,已实测) | 进程数 / 内存 / CPU / 墙钟(Job Object) | 不实现 `fs.deny` 精确拒绝与断网;带 `fs.deny` 或要求断网的策略在该档按 VR-17 不可选(exit 3),不会静默放行 | | `l1_macos` | 不可用(仅 macOS 宿主,未实测) | Seatbelt/SBPL:`fs.deny`、写作用域、网络出口 | 代码就绪但未在 macOS 宿主实测;无 CPU/内存/进程数上限与 syscall 过滤 | | `l2_gvisor` | 可用(Linux + runsc 已注册,A 组已实测) | nerdctl/docker + runsc:只读根、`--cap-drop=ALL`、资源上限、`fs.deny` 掩码、默认断网、`syscall.profile`(容器 seccomp 文档);开 `host-uds` 后白名单经宿主网关 socket 放行(`SANDCTL_L2_HOST_UDS=1` 声明,已实测) | 未声明 host-uds 时白名单策略 fail closed(E9003,由 L1 承接);TC-06 被 pids 上限强制但 gVisor 静默终止沙箱、无法归因 deny;用户态内核,非硬件隔离 | | `l3_kata`(L3 微 VM,仅 Linux) | 可用(Kata 4.3 + docker 29.9.1;A 组 8/8,冷启动 1.76~2.13s) | Kata Containers + 共享容器驱动:独立 guest 内核(`KernelIsolated`)、只读根、`--cap-drop=ALL`、CPU/内存上限、`fs.deny` 掩码、默认断网 | 进程数上限无效(限制到不了 guest);系统调用过滤需宿主开启;无域名白名单与 CIDR 放行;Windows L3 不实现 | > L3 的三条限制(结论见上表,细节在这里): > ① 进程数上限不生效,Kata 运行时不会把 `proc.pids` 送进 guest,带该要求的策略默认 exit 3, > 根因与日志见 [`evidence/limitation-l3-pids-seccomp.log`](evidence/limitation-l3-pids-seccomp.log); > ② 系统调用过滤是条件性能力,宿主 Kata 打开 `disable_guest_seccomp` 才生效,否则该后端按“不支持”处理并降级; > ③ 域名白名单 / CIDR / 凭证注入,网关 socket 进不了 guest,`net.allow`、`net.allowCidr`、`secrets.inject` > 一律退化为全断/无凭证并 `[WARN]`。 > 网关出口:沙箱内唯一出口是宿主网关的 unix socket(后端挂载)。明文 HTTP 白名单与 HTTPS > 均实测放行:`example.com` 明文 → 200;`pypi.org:443` CONNECT → 200 Connection Established → > TLS 握手 → HTTP 200;非白名单域名(明文或 CONNECT)→ 403 + deny 审计。HTTPS 隧道不解密 > (纯字节转发,不需要证书),也不注入凭证(需要凭证的作用域遇到 CONNECT 一律 403)。 > 客户端必须显式使用 `$SANDCTL_GATEWAY_SOCKET`:curl 8.14 无法把 unix socket 当代理用。 > L2(gVisor)需 runsc `--host-uds` 才能透传宿主 unix socket:开启并以 `SANDCTL_L2_HOST_UDS=1` > 声明后白名单经网关放行(已实测,`evidence/ac-10-l2-gvisor.log` §8);未声明则带 `net.allow` > 的策略按 VR-17 fail closed(E9003,由 L1 承接)。 > 详见 `evidence/ac-07-gateway-egress.log` 与 T-017 记录。 > L1 Linux 资源上限:需要 cgroup v2 + systemd user scope。满足时由 `systemd-run --user --scope` > 强制 `MemoryMax` / `MemorySwapMax=0` / `CPUQuota` / `TasksMax`(实测 256Mi 策略下分配 1GiB → > OOM kill + `proc.limit` + exit 4);不满足时 `list-backends` 会把资源上限显示为否,带这类要求的策略默认 exit 3,不会静默放行。 > 同时 `--ro-bind / /` 会暴露的宿主控制 socket(`/run/user`、`/run/dbus`、docker/containerd socket) > 一律被掩码,避免沙箱内直连 user systemd 或容器运行时逃逸。 能力不足即 fail closed:后端能力覆盖不了策略 → 该后端不可选(`SANDCTL-E9003`);无可用后端 → exit 3。 声明优先于承诺:`list-backends` 与 `doctor` 列出全部已注册后端,本平台不适用或依赖缺失时标不可用并给出原因(AC-06)。 `compatibility` 字段本期未实现:声明它既不放宽也不收紧边界。 ## 免责声明 - 边界即声明,不是无逃逸承诺。 sandctl 保证的是“把策略编译成后端原语,并在越界时失败”;内核、 虚拟机监视器或容器运行时自身的漏洞不在本项目防护范围内(PRD §1.5、§17)。 - 能力以本机探测为准。 `doctor` 与 `list-backends` 的输出是该宿主的事实;上面的矩阵是设计基线与 实测记录,换宿主可能不同。两者不一致时以后端探测为准,并应作为缺陷提 issue。 - 未验证项不算通过。标为未实测、部分或受限的能力(L3 pids、L1 macOS、L2 的 TC-06 归因等)不会 因为策略里写了就生效;这类要求默认让执行失败关闭(exit 3),不静默放宽。 - 上游缺陷不受本项目控制。例如 Kata 剥离 `Resources.Pids`、宿主的 `disable_guest_seccomp` 默认值, sandctl 只能如实降级并失败关闭,不能替上游修复;需要该能力时请改用 L2 或升级上游。 - 审计是尽力而为。写盘失败会在 stderr 提示;绕过 sandctl 直接调用底层运行时的行为没有证据。 - 不构成合规认证,也不替代宿主加固、最小权限与供应链审查。 - 本软件按 Apache-2.0 提供,“AS IS”,不附带任何明示或默示担保。 --- ## 目录结构 ``` sandctl/ ├── docs/ │ ├── README.md # 文档索引 │ ├── PRD-全域代码执行边界约束沙箱系统.md # 产品需求文档 v0.3(21 节) │ ├── 全域代码执行边界约束沙箱系统-可行性报告.html # 可行性报告(9 节,自包含) │ ├── DEEP_RESEARCH_Windows-L3-Isolation-Backends.md # Windows L3 内核隔离后端调研 │ ├── Windows-L3-Isolation-Report.html # 同一调研的报告版 │ ├── validation-rules.md # 校验规则 VR-01~VR-19 + 错误码 + 覆盖矩阵 │ ├── acceptance-criteria.md # 验收标准 AC-01~AC-18 + 能力受限项声明 │ ├── IMPLEMENT_YOUR_OWN.md # 如何实现自己的后端(接入点、契约、测试与检查表) │ └── development-task-list.md # 开发任务顺序、进度与任务级验收跟踪 ├── schema/policy.schema.json # 统一策略对象的 JSON Schema(严格结构,对齐 AST04) ├── policies/ # 示例策略:base.yaml(Linux floor)/ base-windows.yaml(Windows floor)/ strict.yaml(T2)/ README.md(恶意 Skill 演示) ├── skills/sandctl/SKILL.md # 使用指南 Skill:策略编写、run 用法、隔离档选择(--skills-dir ./skills 可直接加载) ├── scripts/demo.sh # 一键演示(9 条用例) ├── scripts/validate.py # 校验规则参考实现 + 用例自检 + 覆盖矩阵(Python) ├── scripts/run-attack-suite.sh # 攻击场景套件执行驱动(L1/L2) ├── scripts/glob_parity.py # glob 覆盖语义对拍(Go 与参考实现逐行比对) ├── scripts/test-windows.ps1 # Windows 侧冒烟(demo.sh 的可执行子集) ├── testdata/cases/manifest.yaml # 32 条安全用例(越界 8 / 正例 3 / 策略校验 19 / 越权 2) ├── testdata/cases/attack-suite.yaml # 攻击场景套件规格(独立于 32 条,不并表) ├── testdata/cases/glob-parity.yaml # glob 对拍表(两侧各自断言) ├── evidence/ # 验收证据归档(约定见 evidence/README.md) ├── internal/ │ ├── policy/ # 策略模型 + 校验(VR) + 合并(tighten) │ ├── skill/ # SKILL.md 解析、安装/发现、编译为 Policy(VR-18/19) │ ├── backend/ # Backend/Session 接口、能力矩阵、选择器 │ │ ├── builtin/ # 平台接线层 │ │ ├── l1_linux/ # L1 bubblewrap │ │ ├── l1_macos/ # L1 Seatbelt(SBPL,纯 Go) │ │ ├── l1_windows/ # L1 回退档(仅 Job Object 资源上限) │ │ ├── l1_windows_mxc/ # L1 首选档(MXC processcontainer) │ │ ├── winjob/ # Job Object 与进程启动(两个 Windows 后端共用) │ │ ├── l2_gvisor/ # L2 nerdctl/docker + runsc │ │ ├── l3_kata/ # L3 Kata Containers(独立 guest 内核) │ │ └── container/ # L2/L3 共用的容器运行时与 guest 探测 │ ├── gateway/ # 网络代理 + 凭证治理(宿主侧唯一出口) │ ├── audit/ # 结构化审计 JSONL │ └── runtime/ # 会话生命周期与回收 ├── cmd/sandctl/ # CLI 入口 ├── integrations/agent/ # Agent 工具声明(tools.json)与调用纪律 ├── dsh/ # DeepSeek Harness 插件(dsh/sandctl-plugin) ├── Makefile # 可复现的检查/测试入口 ├── AGENTS.md # 工程约束(动手之前必读) └── LICENSE / CONTRIBUTING.md ``` `native/`(Windows Job Object / macOS Seatbelt 的 CGO shim)尚未建立:Windows 后端走 `golang.org/x/sys/windows` 系统调用;macOS 后端走 `sandbox-exec -p `(纯 Go 生成与加载, 与 `sandbox_init` 是同一份内核策略),两者都不需要独立原生层。 --- ## 开发与门禁 本地检查统一走 Makefile,共 14 个 target: | target | 作用 | |---|---| | `make fmt` | gofmt 格式化 | | `make vet` | `go vet ./...` | | `make build` | 全包编译检查,不留产物 | | `make bin` | 构建 `bin/sandctl`,版本号取自 `git describe` | | `make test` | 单元测试 | | `make race` | `go test -race ./...`(AC-14) | | `make cover` | 覆盖率 | | `make bench` | 启动与资源基准(AC-13) | | `make fuzz` | `FuzzPolicy` 30 秒(策略解析,OWASP AST04) | | `make probe` | 能力矩阵探针,每个可用后端起一次真实沙箱(AC-06,需 docker + runsc/Kata) | | `make lint` | golangci-lint v2.14.0(版本 pin 在 Makefile) | | `make audit` | govulncheck | | `make deadcode` | 不可达代码 | | `make all` | `fmt vet build race lint audit` | 没有 docker 或 runsc/Kata 的宿主上 `make probe` 仍会通过,但探针字段全为 false,这只证明不了任何事,宿主事实要单独记进 `evidence/`。测试与门禁都跑过之后,按 [`docs/acceptance-criteria.md`](docs/acceptance-criteria.md) 的 DoD 逐条核对。 --- ## 隔离档与威胁下限 | 威胁 | 含义 | 最低档 | 说明 | |---|---|---|---| | `T0` | 本地可信、开发调试 | L1 | 允许 `net.allowAny` | | `T1` | 不可信代码、单用户(缺省) | L1 | 硬策略 + deny-by-default | | `T2` | 对抗性、多租户 | L2 | 必须 gVisor 及以上 | | 显式 | `--level=3` | L3 | 只升不降 | 选择顺序是“满足下限的最低档优先”(性能优先),`--level=N` 显式提升;无可用后端则 exit 3。 --- ## 状态 Linux:L1(bubblewrap)、L2(gVisor)、L3(Kata)三档均已实测,A 组 8/8 拦截,L1 冷启动 p95 105ms,L3 冷启动 1.76~2.13s。 Windows:两档 L1 已在 Windows 11 实测。MXC 档能挡文件访问与出站;回退档只有 Job Object 资源上限,带 `fs.deny` 或断网要求的策略在该档不可选(exit 3)。L3 不实现(PRD §7.6.3)。 macOS:Seatbelt 后端代码就绪,未在真实宿主验证。 策略解析、校验规则、选择器、Skill 编译、网关与凭证治理、审计闭环均已完成。逐条进度、阻塞原因与证据路径见 [`docs/development-task-list.md`](docs/development-task-list.md)。 --- ## 文档导航 | 想了解 | 看这里 | |---|---| | 为什么做、竞品如何、选型为何是 Go | [`docs/全域代码执行边界约束沙箱系统-可行性报告.html`](docs/全域代码执行边界约束沙箱系统-可行性报告.html) | | 需求、架构、策略规范、CLI、里程碑 | [`docs/PRD-全域代码执行边界约束沙箱系统.md`](docs/PRD-全域代码执行边界约束沙箱系统.md) | | 哪些策略会被拒绝、错误码是什么 | [`docs/validation-rules.md`](docs/validation-rules.md) | | 怎么算通过、证据怎么交 | [`docs/acceptance-criteria.md`](docs/acceptance-criteria.md) | | 现在做到哪、剩下什么、卡在哪 | [`docs/development-task-list.md`](docs/development-task-list.md) | | 已归档的原始证据 | [`evidence/README.md`](evidence/README.md) | | 怎么给自己加一种隔离后端 | [`docs/IMPLEMENT_YOUR_OWN.md`](docs/IMPLEMENT_YOUR_OWN.md) | | 怎么参与、门禁是什么、实现纪律 | [`CONTRIBUTING.md`](CONTRIBUTING.md)、[`AGENTS.md`](AGENTS.md) |