- 人工智能
- AI Agent
- 应用安全
- 漏洞扫描
【免费下载链接】defending-code-reference-harness
Skills for threat modeling, scanning, triage, patching, plus an autonomous scanning harness you can /customize
本文基于仓库文档 docs/patching.md 与配套源码编写,聚焦开源参考流水线 Defending Code Reference Harness 中「补丁生成与验证」这一环节:如何把一个
vuln-pipeline run产出的已验证崩溃,变成一个能通过可执行验证阶梯(Verification Ladder)的候选修复,并在人工审查后合入上游。
导读
patch是 Defending Code Reference Harness 自主漏洞发现流水线(recon → find → triage → report → patch)的最后一段:它把 docs/triage.md 之后获得的已验证、已排序的崩溃队列,逐个转化为可供你审查并提交上游的候选修复。本文会带你走完从运行bin/vp-sandboxed patch命令、理解「双容器信任边界」下的补丁循环,到掌握 T0–T3 可执行验证阶梯的每一级判据、再到人工审查补丁和 /patch skill 静态模式的完整流程。读完你将能独立为流水线产出的崩溃生成、验证、审查并准备上游提交的补丁,也能为不适合「文件输入」驱动的目标编写自定义 re-attack 驱动脚本。
⚠️ 前置安全提醒:补丁评估器会执行目标代码并把模型生成的 diff 应用到目标代码上。请与流水线其他阶段保持同等隔离级别,详见 docs/security.md。
patch 阶段在流水线中的位置
patch 阶段是 triage 之后的自然下一步。此时你手上已经有一条经过验证(grade 通过)、去重(dedup)、并附带可利用性报告(report)的崩溃队列,patch阶段的任务是把队列里的每个崩溃变成一份候选修复:
- 输入:
vuln-pipeline run产出的results/<target>/<ts>/结果目录(其中包含崩溃的 PoC 字节、复现命令与 ASAN 轨迹); - 输出:落在
<results_dir>/reports/bug_NN/下的patch.diff与patch_result.json,与既有的可利用性报告(report.json)并排存放; - 过程:每轮迭代的对话记录分别流式写入
patch_transcript_itN.jsonl与reattack_transcript_itN.jsonl(N 为迭代序号)。
值得注意的是,/patchskill 有两种模式(见下),文档对 CLI 模式着墨最多,但「审查生成的补丁」一节对静态模式同样适用。
快速开始
patch 阶段随流水线一同交付,无需额外安装。唯一的前置条件在目标(target)的config.yaml中:
build_command:必需,用于在容器内重新构建打过补丁的源码树(验证阶梯 T0 依赖它);test_command:可选,用于回归验证(T2);未配置时 T2 被跳过。
仓库自带的四个目标(targets/alsa/、targets/canary/、targets/drlibs/、targets/htslib/)均已配置build_command,其中部分目标额外配置了test_command。例如 targets/canary/config.yaml 中:
build_command: gcc -O1 -g -fsanitize=address -fno-omit-frame-pointer -o /work/entry /work/entry.c test_command: >- printf 'A\003abc' > /tmp/t_a && printf 'Bhello' > /tmp/t_b && printf 'C\001\002' > /tmp/t_c && /work/entry /tmp/t_a && /work/entry /tmp/t_b && /work/entry /tmp/t_c而 targets/drlibs/config.yaml 只配置了build_command(gcc -O1 -g -fsanitize=address -fno-omit-frame-pointer -o /work/entry /work/entry.c -lm),没有test_command——此时验证阶梯会跳过 T2。
两种运行方式:
# 方式一:流水线运行产出结果之后 bin/vp-sandboxed patch results/<target>/<ts>/ --model <model> # 方式二:在预烘焙的 canary fixture 上独立试跑(无需先跑流水线) bin/vp-sandboxed patch targets/canary/fixtures/results_sample --model <model>第二种方式非常适合第一次上手:仓库在 targets/canary/fixtures/results_sample/ 里预置了三份result.json(run_000/run_001/run_002),可以直接作为 patch 阶段的输入,无需先完成一次完整的 find+grade 循环。
补丁循环(patch loop)如何工作:双容器信任边界
补丁循环在 harness/patch.py 中实现,其核心设计是双容器信任边界:
patch agent 运行在容器 A(沙箱化,见 docs/agent-sandbox.md)。容器内提供源码、PoC、复现命令与 ASAN 轨迹。它的提示词(见 harness/prompts/patch_prompt.py 的
FULL_TASK)要求它:- 修复根因而非崩溃点——先从崩溃点反向追踪坏值的来源("Trace backward from the crash site to where the bad value originated. The fix usually belongs there, not at the
memcpy/deref that ASAN flagged."); - 排查同类调用点——grep 具有相同模式的兄弟调用点,diff 要么覆盖全部,要么在 rationale 中说明原因;
- 保持 diff 最小化——"Smallest change that fixes the root cause. No refactoring, no drive-by cleanup, no reformatting.";
- 对抗性自检——以攻击者视角重新读自己的 diff,若能命名一个绕过该检查到达同一坏状态的输入变体,说明修复在错误层级,应回到第 2 步。
- 修复根因而非崩溃点——先从崩溃点反向追踪坏值的来源("Trace backward from the crash site to where the bad value originated. The fix usually belongs there, not at the
grader agent 运行在容器 B,从同一镜像全新创建。从容器 A 穿越到容器 B 的唯一东西是 diff 字节本身——grader 永远看不到 patch agent 的推理过程,因此不可能被"说服"批准一个糟糕的修复(harness/patch_grade.py 的模块注释明确写了这一设计意图)。grader 应用 diff 后攀爬验证阶梯(详见下一节),任何一级失败立即停止。
失败证据回填。若某一级失败,失败证据(如编译错误、ASAN 轨迹、回归测试输出)会进入下一轮尝试的提示词(
patch_prompt.py中的RETRY_SECTION:"Your last diff was graded and failed at tier{failed_tier}"),补丁循环重新运行,最多--max-iterations次。测试 tests/test_patch.py 的test_run_patch_retries_on_failed_grade验证了这一点:第一次 grade 返回 t1 失败后,第二次调用的 prompt 中必须包含"Previous attempt failed"与"still crashes"证据。
从源码结构看,循环的判定逻辑(harness/patch.py 的_failed_tier)按固定优先级报告失败层级:t0(build)→ t1(PoC 仍崩溃)→ t2(测试回归)→ re-attack(发现变体)。此外,patch agent 必须通过<patch_path>标签声明其生成的 diff 路径;若未输出该标签或标签指向的文件为空,也会作为失败证据要求重试(对应测试test_run_patch_no_tag_retries)。
另一个值得注意的工程细节:run_patch会确保源码根目录是一个 git 仓库并创建基线提交,从而让 agent 的git diff输出确定可复现;同时把构建出的二进制加入.gitignore(patch.py中的注释解释了原因——否则重建的二进制会进入 diff,导致 grader 侧git apply拒绝)。
验证阶梯(The Verification Ladder)
在补丁循环中,grader agent 按顺序执行四个检查,遇到第一个失败即停止。每一级都是一个可执行判定器(executable oracle)——运行某段程序,根据结果做通过/失败判定,没有任何一级基于模型的主观判断。另外还有第五个可选步骤(--style启用),用模型审查补丁风格,但仅作咨询,永不把关。
| 层级 | 要回答的问题 | 判定器(Oracle) | patch_result.json中的字段 |
|---|---|---|---|
| Build(T0) | 打过补丁的源码树能编译吗? | git apply+build_command退出码 | t0_builds |
| Reproduce(T1) | 原始崩溃消失了吗? | 退出码为 0且输出中无AddressSanitizer: | t1_poc_stops |
| Regress(T2) | 是否破坏了既有行为? | test_command退出码(未配置则跳过) | t2_tests_pass |
| Re-attack | 根因被修复了,还是只修了这条输入? | 全新的 50 轮 find agent 攻击打过补丁的二进制;由 ASAN 裁决 | re_attack_clean |
| Style(T3) | 维护者会接受它吗? | LLM 打分 0-10;仅咨询,永不把关 | t3_style_score |
一个补丁在 build、reproduce、regress(或无测试套件)、re-attack全部干净时才算通过。源码中这一判定浓缩在 harness/artifacts.py 的PatchVerdict.passed属性:
@property def passed(self) -> bool: return ( self.t0_builds and self.t1_poc_stops and self.t2_tests_pass is not False # None = 无测试套件,不算失败 and self.re_attack_clean is not False # None = 未运行 re-attack(--no-reattack) )各层级的具体实现位于 harness/patch_grade.py:
- T0:将 diff 写入容器,依次
git apply --check与git apply(多 diff 时先 check 跳过无法干净应用的),然后执行target.build_command,受build_timeout_s(config.yaml 中可配,默认 1800 秒)约束;退出码非 0 即失败并截取证据(EVIDENCE_LIMIT = 4000字符裁剪)。 - T1:把 PoC 字节写入容器,执行适配后的复现命令(PoC 路径替换为
/tmp/poc.bin),判定条件_t1_passes为rc == 0 and "AddressSanitizer:" not in (stdout + stderr);600 秒超时视为失败,错误信息甚至提示"hang — likely a botched loop bound"。 - T2:仅当
target.test_command存在时执行,1200 秒超时,退出码为 0 才通过。 - Re-attack:将打过补丁的容器
docker commit成临时镜像,用run_find启动一个50 轮(REATTACK_MAX_TURNS)的全新 find agent(accept_dos=False)攻击该二进制,ASAN 决定是否崩溃;崩溃即失败,并把崩溃轨迹作为证据。临时镜像在finally块中清理(docker_ops.rmi)。 - T3:单次无工具的 LLM 调用(
max_turns=5),对 diff 打 0-10 风格分(STYLE_JUDGE_TEMPLATE:0-3 为错误层级/压制症状,4-6 为正确但嘈杂,7-10 为最小化、精准、匹配风格),分数解析失败或越界则记None,永不参与通过判定。
T0–T2 阶段只运行目标代码(docker_ops.exec_sh),从不发起claude -p调用,因此 grader 容器被配置为auth=None(不把 API 凭据放进一个运行着"被 PoC 专门设计去崩溃的二进制"的容器环境)且network="none"(无外部调用需求,也就不需要 egress;注释特别指出若不加这个限制,沙箱默认会给 bridge 网络即完整出网,这对一个喂入攻击者控制输入的进程是危险的)。只有 re-attack 与 style judge 阶段才自行派生带agent_env与网络的容器。
为什么需要 re-attack?
一个能编译、能挡住特定 PoC 的补丁通常很容易写出来。已发表的模型生成安全补丁评测显示:约60%能通过 build+reproduce 检查,但不足 15%能撑过模糊测试与差分测试。占主导的失败模式是:在崩溃点加一个边界检查,却让坏值仍能从略有不同的输入到达。re-attack 正是针对这一失败模式设置的防线——一个 50 轮的 find agent 会试图构造绕过该补丁的变体输入。
文档同时给出严谨的边界说明:re-attack 通过只是有用的信号,不等于"根因已被证明修复"。它在 50 轮内可构造绕过输入时判别力很好,但可能漏掉"错误层级修复"(wrong-layer fix)——这类修复的绕过输入往往更难构造。
审查生成的补丁(Reviewing Generated Patches)
验证阶梯证明了"特定崩溃被修复",但不能证明根因被解决,也不能证明 diff 没有引入新问题。阶梯不会对 diff 做语义审查——不检查是否引入了新漏洞、是否在修复之外破坏了逻辑,或其他任何问题。
因此,请把patch.diff当作一份需要人工阅读的强草稿,在它去向任何地方之前先审查。文档列出的常见问题包括:
- 范围蔓延(Scope creep):修改了与崩溃路径无关的文件或函数;
- 压制而非修复(Suppression instead of fix):
try/except: pass、对精确 PoC 值提前 return、禁用触发崩溃的断言; - 新攻击面(New attack surface):新增解析逻辑、信任来自输入的新长度字段、为了让修复"生效"而削弱其他地方的校验;
- 诊断正确、修复错误(Correct diagnosis, wrong fix):正确识别了需要修改的模块,但补丁过窄,破坏了其他东西。
在上游提交前,有两个相对容易的额外步骤值得做:
- 对抗性验证(Adversarial validation):在一个全新会话中对最终
patch.diff重跑一次——"说出一个输入变体,它到达同一坏状态却不会触发补丁的检查"。patch agent 在生成时已经问过自己类似的问题,但在全新上下文中对着最终 diff 问,更有可能找到修复的缺口。 - 请求简化(Ask to simplify):同样在全新会话中,只要求"简化为能修复根因的最小改动"。patch agent 被提示词要求产出最小 diff,但它对"最小"的认知锚定在刚推理过的 finding 上;全新上下文的简化通常能可靠地裁剪 diff。
提示注入威胁模型
patch agent 的提示词会读取目标衍生的数据(ASAN 轨迹、可利用性报告,以及重试时的 build/test 输出)。流水线用每次调用随机分隔符(make_nonce(),见 harness/prompts/untrusted.py)将这些数据围栏起来,并指示 agent 把它们当作数据而非指令处理(patch_prompt.py中的<untrusted_data id="{nonce}">块与 untrusted-data 说明)。但文档明确提醒:提示词层级的围栏是缓解措施,不是保证。如果你针对的是不完全可信的第三方代码,被投毒的目标影响可能渗透到 diff 生成与审查中。更完整的威胁模型见 docs/security.md。
CLI 参考
bin/vp-sandboxed patch <results_dir> --model <m> # patch 所有唯一 bug bin/vp-sandboxed patch <results_dir> --bug N # 只 patch bug_NN bin/vp-sandboxed patch <results_dir> --parallel # 并发运行 patch agent bin/vp-sandboxed patch <results_dir> --no-reattack # 跳过阶梯中的 re-attack 步骤(更快,但更弱) bin/vp-sandboxed patch <results_dir> --style # 运行可选的、仅咨询的风格评审 bin/vp-sandboxed patch <results_dir> --max-iterations N # 最大补丁循环次数(默认 5) bin/vp-sandboxed patch <results_dir> --max-turns N # 每轮迭代的 agent 预算(默认 200)以上参数与 harness/cli.py 中patch子命令的 argparse 定义一一对应(常量PATCH_MAX_TURNS = 200、DEFAULT_MAX_ITERATIONS = 5定义于 harness/patch.py)。_cmd_patch的实现要点:
- 先对
results_dir做 dedup 分组,并按"有通过验证的组优先、再按签名排序"排出与 report 阶段一致的bug_NN序号(确保这里的bug_NN与reports/bug_NN/对应); --bug N只处理序号为 N 的组;- 目标必须配置了
build_command,否则直接报错退出("the patch grader needs an in-container rebuild step"); - 若目标镜像不存在则先构建;
- 每个 bug 会读取同目录下已有的
report.json文本,作为可利用性报告上下文注入 patch agent 的提示词(harness/prompts/patch_prompt.py 的report_section,截断至 4000 字符); - 运行结束后按
patch_verified/patch_rejected/no_diff汇总,任一非patch_verified都会使命令退出码为 2。
--model是必填的(或通过VULN_PIPELINE_MODEL环境变量提供);认证由 harness/auth.py 解析(Bedrock / Vertex /ANTHROPIC_API_KEY/CLAUDE_CODE_OAUTH_TOKEN四选一,详见 docs/agent-sandbox.md)。patch 属于会派生自主 agent 的子命令,因此在非显式覆盖(--dangerously-no-sandbox)时会拒绝在 gVisor 沙箱之外启动。
Harness-driven re-attack:自定义目标驱动
re-attack 默认通过"以输入文件驱动二进制"(./binary <input>)来攻击目标。对于无法这样驱动的目标——需要启动器、环境设置、多进程编排或非文件输入通道——你可以自己提供驱动脚本:
- 写一个知道如何运行你的目标的脚本;
- 把它打进目标的 Docker 镜像;
- 在目标
config.yaml中用reattack_harness:指向它。
re-attack 期间,find agent 把候选 PoC 写成/poc/下的文件并运行你的脚本;补丁循环的其余部分不变。该字段由 harness/config.py 的TargetConfig.reattack_harness承载(config.yaml可选键),并被 harness/find.py 与 harness/prompts/find_prompt.py 接入——后者在reproduction_command标签中直接使用该脚本路径,并要求"3 次中复现 3 次"才算有效崩溃。测试 tests/test_focus.py 的test_reattack_harness_switches_template验证了该字段会切换提示词模板。
你的脚本是唯一需要理解目标的东西,agent 与流水线只依赖脚本的退出码。契约如下:
- 对
/poc/下的每个文件,用插桩后的目标运行一次(每个 PoC 使用全新状态),并捕获各自的 sanitizer 输出; - 若有任何 PoC 使目标崩溃,退出码
1并打印 sanitizer 轨迹; - 若所有 PoC 均未崩溃,退出码
0; - 若目标完全无法启动,退出码
2。
Campaign-style patching:/patchskill 静态模式
bin/vp-sandboxed patch依赖bin/vp-sandboxed run的输出。如果 finding 来自别处(独立扫描器、人工审查、纯文字报告),或者你要修的是跨越多个调用点的整类 bug而非单个崩溃,CLI 模式就不适用了。此时用/patchskill 的静态模式:
# 为 TRIAGE.json 中严重度最高的 5 个已确认 finding 起草修复 > /patch ./TRIAGE.json --repo ./my-service --top 5静态模式接受静态 finding(TRIAGE.json或VULN-FINDINGS.json)。对每个 finding,skill 运行两个 agent:
- patch agent阅读相关代码并写出候选修复(diff);
- reviewer agent在干净上下文中评判该 diff——评估范围、有效性以及引入的新攻击面(不执行任何代码)。
不会直接应用任何东西到你的仓库。生成的 diff 落在./PATCHES/供你审查,每个结果都标注为verified: static_review_only,以区别于走完验证阶梯的补丁。静态模式不涉及运行 PoC 或修改目标代码,因此它属于只读/只写操作(详见 README.md 的安全说明),无需沙箱即可交互式运行。
静态模式没有 PoC 可以重跑,所以回归测试是唯一真正可执行的检查。文档给出的操作法:先让 patch agent在写补丁之前写一个能复现该 bug 的测试——在当前代码上运行并确认它失败,然后应用修复并确认它通过。没有"先失败后通过"的测试,你就无法证明这个 bug 曾经真实存在,修复也可能在日后悄然回归。
当修复本质上是迁移(When the fix is really a migration)
有时 finding 不是一个 bug,而是一个模式——同一处不安全调用散布在几十个调用点,一个 PR 装不下。此时可按如下工作流运行:
- 研究(Research):一个 agent 阅读 finding 与代码库,写出迁移计划——哪个模式不安全、安全替代是什么、调用点都在哪里;
- 把计划变成测试(Turn the plan into tests):为每个调用点写一个"现在失败、迁移后通过"的测试。这套测试扮演流水线中 ASAN 的角色,即判定"何时算做完"的检查;
- 拆分成工单(Split into tickets):把测试分成可独立合并、小到足以审查的块;
- 并行补丁(Patch in parallel):每个工单一个 worker 子 agent,各在独立 git worktree 中,循环修改其分块,直到"它的测试"与"项目既有测试"全部通过;
- PR 前把关(Gate before the PR):每个 worker 的 diff 必须通过测试、一次独立 bug 扫描、以及一个不授予任何工具的 agent 的代码审查,外加项目本身要求的检查。PR 本身由人类审查。
⚠️ 与 CLI 模式相同,静态模式下 patch agent 同样读取目标衍生的数据(ASAN 轨迹、可利用性报告等),围栏与注入威胁模型同上一节所述,处理第三方不完全可信代码时需保持同等警惕。
小结与边界
patch阶段把"已验证的崩溃"升级为"通过可执行验证阶梯的候选修复":T0 编译、T1 原 PoC 不再崩溃、T2 测试套件不回归、re-attack 用全新 find agent 抵御绕过变体,全程以编译退出码、ASAN 输出与测试退出码这些可执行判定器把关,模型判断只出现在可选的风格咨询(T3)中。但文档同时反复强调其边界:验证阶梯证明的是"特定崩溃被修复",不是"根因被解决"或"diff 无新问题";re-attack 通过只是有用信号。上游合入之前,人工审查(识别范围蔓延、压制式修复、新攻击面、错误层级修复)+ 全新会话的对抗性验证与简化,仍是不可省略的步骤。对无法用文件输入驱动的目标,reattack_harness提供了一条把"如何运行目标"委托给自定义脚本的通道;对静态 finding 与整类 bug 迁移,/patchskill 的静态模式则提供了一条不经运行验证、以static_review_only标注的轻量路径。
- 人工智能
- AI Agent
- 应用安全
- 漏洞扫描
【免费下载链接】defending-code-reference-harness
Skills for threat modeling, scanning, triage, patching, plus an autonomous scanning harness you can /customize
相关推荐
Harness 实战:一条提示词生成 Claude Code 多智能体团队的六阶段流水线与验证方法
Harness 实战:一条提示词生成 Claude Code 多智能体团队的六阶段流水线与验证方法 Harness 是一个运行在 Claude Code 中的"
AI 技能人工智能notebooklm-py 如何启用 Android gRPC 后端并完成首次 --backend android 调用?
notebooklm py 如何启用 Android gRPC 后端并完成首次 backend android 调用? notebooklm py 默认走 We
人工智能AI Agent应用安全漏洞扫描ReviewHog 验证器实验运行实录:Sonnet 5 @ xhigh 验证阶段(C 臂)的裁决分析与源码验证
ReviewHog 验证器实验运行实录:Sonnet 5 @ xhigh 验证阶段(C 臂)的裁决分析与源码验证 导读 :本文基于 PostHog 开源仓库中
数据分析后端前端数据可视化大数据
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考