[链接器的世界-实验篇15] 让反例决定质量:差分、变异与鲁棒性
本实验不再添加一种链接功能,而是检验此前实现面对损坏输入时是否能正常拒绝,以及面对有效输入时是否稳定、正确。你将实现一个确定性变异器和调查工具;发现的问题通常要回到此前 class 修复。
起点:class15 配套代码与测试,位于 private 仓库,需要访问权限。变异菜单、随机数生成器定义和调查流程见 README;质量验证的通用方法见原理篇 16。
实现任务
| 任务 | 接口 | 要达到的结果 |
|---|---|---|
| C15.1 | Rng、mutate | 按规定的随机抽取顺序生成输入副本,不修改原始数据 |
| C15.2 | survey | 调用被测后端,统计接受与拒绝,捕获并记录 panic,恢复原 panic hook |
| C15.3 | 无新增接口 | 用提供的原生测试检验此前后端,修复暴露的问题 |
调查按 sample = iteration % corpus.len() 轮流选择样本;
以 README 合同中的 seed 9 为例,调查器先按 iteration % corpus.len() 选 corpus,再从同一个 Rng 消耗一次随机数选输入,之后按变异菜单继续消耗随机数;失败记录中的 iteration、sample、原始输入副本和 message 足以从第 0 轮重放到失败轮次。这里的 survey 是固定菜单的鲁棒性调查,不是 coverage-guided fuzzer,也不能由有限轮次推出任意 ELF 都不会超时或耗尽内存。class13 的 ET_EXEC/PIE 与 class14 的 abort-only 驱动是分别沿用的后端路径;本章比较它们的证据,不宣称一个 CLI 已经支持所有后端功能。随机选择样本中的一个输入,再用同一个 Rng 变异它。每个样本必须至少有一个输入。空 corpus 或零次迭代返回空报告,不调用链接函数。
Panic 保存 iteration、sample、input 和 message;seed 由调用方保存并在测试失败信息中给出。重放时从该 seed 的第 0 次迭代开始,按原顺序消耗随机数直到失败迭代,不能直接跳过去。每次都从原始输入复制,变异不累积到 corpus;即使 below(1) 必然得到零,也仍须消耗一次随机数。README 用合同测试中的 seed 9 逐项展示抽取顺序和首条 panic 记录。正常返回错误属于拒绝,panic 才是调查记录的鲁棒性失败。
变异可能保留合法格式,也可能不改变字节,因此不要求每次调用都拒绝。每次调用只计入接受、拒绝或 panic 之一,三者总数必须等于迭代次数。原始有效样本先由支持它的后端成功链接;仅通过“不 panic”和“有拒绝”不能证明接受产物的语义正确,后面的确定性与行为差分另行检查。
调查器使用 catch_unwind 捕获会展开的 Rust panic;它不负责截获 abort、进程信号、超时或资源耗尽。panic hook 是进程全局状态,调查期间临时静默后必须恢复,且不要与其他修改 hook 的工作并发运行。这是本次调查器的边界,不是对所有失败方式的隔离保证。
三种证据不能互相替代
变异调查回答“坏输入会不会 panic”,确定性测试回答“同一有效输入是否依赖宿主地址或隐藏状态”,行为差分回答“有效程序是否仍产生正确结果”。先验证未变异样本能成功,再解释每次变异的接受/拒绝/panic 计数;否则“没有 panic”可能只是后端拒绝了所有输入。
重放记录必须包含 seed、iteration、sample、input 和完整随机数消耗规则。修复时将失败归因到具体不变量,并保留最小回归样本;不要把一次有限 survey 的绿色结果扩展成对任意 ELF 或任意资源消耗的保证。
从任务到测试
| 任务 | 关键测试 | 测试所证明的性质 |
|---|---|---|
C15.1 Rng/mutate | generator_matches_xorshift64_star、mutations_follow_the_menu | 随机序列、抽样顺序和变异菜单可重放,原始输入不被修改。 |
C15.2 survey | survey_counts_outcomes_and_catches_panics | 接受、拒绝和 panic 三类计数互斥且总和等于迭代次数,panic 记录可重放。 |
| C15.3 回归修复 | backends_never_panic_on_mutated_objects、outputs_are_reproducible、programs_behave_as_under_gnu_ld | 鲁棒性、确定性和行为正确性分别成立;任一维度失败都不能由另外两项掩盖。 |
先用未变异样本建立成功基线,再解释变异结果;拒绝不是错误,panic 才是本章要追踪的鲁棒性失败。
验收及其含义
在克隆后的仓库根目录、原生 x86-64 Linux 上执行:
cargo test --locked -p class15cargo test --locked -p class15 --releasepython3 scripts/grade.py class15合同测试固定生成器序列、变异菜单及抽取顺序,并检查调查计数、panic 信息、重放一致性和零次运行。原生测试分三类:
- 损坏输入:对 class10、class11、class13 的 ET_EXEC 与 PIE 入口,以及 class14 累积的 abort-only 后端,各运行 1000 次变异链接,另对真实 Rust 对象及其归档运行 40 次。要求没有 panic,且确实有输入被拒绝。
- 确定性:有效样本重复链接,并复制输入到不同的宿主缓冲区再链接,要求输出字节完全相同,再运行产物。这检查结果不依赖输入缓冲区地址,不是高低加载地址测试。
- 行为差分:三个 C 程序分别由 class13 和 GNU ld 链接,比较 stdout 与退出码,并确认测试程序按预期以 42 退出。这里不比较 ELF 字节,也没有 LLD 对照。
这些检查验证指定样本上的拒绝行为、确定性和程序结果。class13 的 ET_EXEC/PIE 与 class14 驱动已沿用累积流程,但一次有限的变异调查不能穷尽该流程的输入组合,也不能证明任意输入都不会卡住或申请巨量内存。处理失败时,先重放一条用例、定位被破坏的不变量,再修复相应阶段;不要把正常拒绝改成接受来消除错误。