ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

BSC 客户端模糊测试实战指南:go-fuzz 构建、运行与崩溃收敛

BSC 客户端模糊测试实战指南:go-fuzz 构建、运行与崩溃收敛 BSC 客户端模糊测试实战指南go-fuzz 构建、运行与崩溃收敛【免费下载链接】bscA BNB Smart Chain client based on the go-ethereum fork项目地址: https://gitcode.com/GitHub_Trending/bs/bsc导读本文以tests/fuzzers/README.md为骨架系统讲解 BNB Smart Chain 客户端BSC基于 go-ethereum 的 fork中模糊测试Fuzzing的完整工作流从安装 go-fuzz、构建 fuzz 二进制、运行并解读实时指标到借助suppressions机制去重崩溃、通过唯一退出点原则定位缺陷类型。同时结合本仓库tests/fuzzers下各模块的源码实现剖析 difficulty、rangeproof、txfetcher、bn256、bls12381、secp256k1 等 fuzzer 的构造原理帮助开发者在本仓库中快速上手、复现崩溃并验证修复。一、背景为什么客户端仓库需要模糊测试区块链节点长期暴露在不可信的网络输入之下——区块头、交易、Merkle 证明、预编译合约入参等都可能来自恶意对手方。一个未经充分模糊测试的解析路径一旦在某个畸形输入上触发panic轻则导致单节点崩溃重则可能被利用制造分叉或拒绝服务。本仓库将全部模糊测试目标集中在 tests/fuzzers 目录下每个子目录对应一个待测试的组件或算法并随附种子语料seed corpus与原生 fuzz 测试入口。根据 tests/fuzzers/README.md 的说明本目录的定位是如何在本仓库本地运行 fuzzer的操作手册配合 go-fuzz 工具链完成从构建到崩溃复现的闭环。说明仓库现有 fuzzer 子目录为bls12381、bn256、difficulty、rangeproof、secp256k1、txfetcher。README 中以rlpfuzzer 为例演示 go-fuzz 命令该例源自上游 go-ethereum 的对应目录在实际使用本仓库时把示例中的包路径替换为上述任一目录即可。二、环境准备安装 go-fuzz根据 README 第一步要求本地运行 fuzzer 前需要先安装 go-fuzz 工具链。go-fuzz 由 Dmitry Vyukov 开发是经典的覆盖率引导coverage-guided模糊测试工具它由两个组件组成go-fuzz-build将目标包编译为携带覆盖率插桩的 fuzz 二进制go-fuzz基于该二进制持续生成、变异输入并观察覆盖率。安装命令Go 模块方式go install github.com/dvyukov/go-fuzz/go-fuzzlatest go install github.com/dvyukov/go-fuzz/go-fuzz-buildlatest安装完成后go-fuzz-build与go-fuzz两个可执行文件会出现在$GOBIN通常为$HOME/go/bin下请确保该目录已加入PATH。关于 CGO_ENABLED0README 中的构建命令显式设置了CGO_ENABLED0。这是因为 go-fuzz 的覆盖率插桩依赖对目标包的重新编译关闭 CGO 可以避免链接到依赖 C 库的包例如本仓库 crypto/secp256k1 下的 C 实现时引入的构建不确定性保证插桩二进制的可移植性与复现性。对于纯 Go 实现的包如 RLP 编解码该设置同样无害且推荐。三、构建 fuzz 二进制go-fuzz-build 工作流README 给出的标准构建命令如下(cd ./rlp CGO_ENABLED0 go-fuzz-build .)go-fuzz-build会分析当前目录下的 Go 包寻找符合约定的 fuzz 入口函数并对所有可达路径插入覆盖率探针最终在包目录下生成一个名为rlp-fuzz.zip的二进制归档名称取自包名若你已在目标目录内直接执行go-fuzz-build .即可生成物rlp-fuzz.zip即后续go-fuzz的输入它同时被打包了初始化好的语料与插桩代码。需要特别说明的是go-fuzz 的入口约定是包内存在func Fuzz(data []byte) int这样的导出函数。而在本仓库中现有 fuzzer如 tests/fuzzers/difficulty/difficulty-fuzz.go采用了小写func fuzz(data []byte) int形式并由同目录下的原生 Go fuzz 测试testing.F驱动例如 difficulty_test.go 中的func Fuzz(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { fuzz(data) }) }从源码结构可以推断本仓库的模糊测试已从外部 go-fuzz 工具驱动迁移到Go 原生 fuzz 测试驱动的模式——底层的fuzz(data []byte) int语义保持一致但运行入口统一收敛到了go test -fuzz见第七、八节。因此若要在本仓库中复现 README 的 go-fuzz 外部工具工作流需要为对应目录补充导出形式的Fuzz入口而直接使用go test原生方式则无需任何改动。四、运行 fuzzer 与输出指标解读构建出rlp-fuzz.zip之后README 提供了两种运行方式。方式一进入包目录直接运行[userwork rlp]$ go-fuzzgo-fuzz会默认查找当前目录下的*-fuzz.zip自动加载并开始模糊测试。README 贴出了典型的实时输出2019/11/26 13:36:54 workers: 6, corpus: 3 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2019/11/26 13:36:57 workers: 6, corpus: 3 (6s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 1054, uptime: 6s 2019/11/26 13:37:00 workers: 6, corpus: 3 (9s ago), crashers: 0, restarts: 1/8358, execs: 25074 (2786/sec), cover: 1054, uptime: 9s 2019/11/26 13:37:03 workers: 6, corpus: 3 (12s ago), crashers: 0, restarts: 1/8497, execs: 50986 (4249/sec), cover: 1054, uptime: 12s 2019/11/26 13:37:06 workers: 6, corpus: 3 (15s ago), crashers: 0, restarts: 1/9330, execs: 74640 (4976/sec), cover: 1054, uptime: 15s 2019/11/26 13:37:09 workers: 6, corpus: 3 (18s ago), crashers: 0, restarts: 1/9948, execs: 99482 (5527/sec), cover: 1054, uptime: 18s 2019/11/26 13:37:12 workers: 6, corpus: 3 (21s ago), crashers: 0, restarts: 1/9428, execs: 122568 (5836/sec), cover: 1054, uptime: 21s 2019/11/26 13:37:15 workers: 6, corpus: 3 (24s ago), crashers: 0, restarts: 1/9676, execs: 145152 (6048/sec), cover: 1054, uptime: 24s 2019/11/26 13:37:18 workers: 6, corpus: 3 (27s ago), crashers: 0, restarts: 1/9855, execs: 167538 (6205/sec), cover: 1054, uptime: 27s 2019/11/26 13:37:21 workers: 6, corpus: 3 (30s ago), crashers: 0, restarts: 1/9645, execs: 192901 (6430/sec), cover: 1054, uptime: 30s 2019/11/26 13:37:24 workers: 6, corpus: 3 (33s ago), crashers: 0, restarts: 1/9967, execs: 219294 (6645/sec), cover: 1054, uptime: 33s方式二显式指定二进制路径go-fuzz -bin ./rlp/rlp-fuzz.zip无论在工作目录还是仓库根目录均可通过-bin参数精确指定要运行的 fuzz 归档。实时指标字段含义字段含义观察要点workers并发执行模糊输入的 worker 数默认为 CPU 核数反映并行度corpus当前语料库规模被确认有价值的输入数稳定增长说明正在探索新路径停滞说明覆盖面趋饱和crashers已发现的崩溃输入数大于 0 时需立即收敛并复现restarts形如1/9676表示每 N 次执行发生 1 次重启输入导致目标超时或内存超限后的重试分子为重启次数分母为执行次数execs累计执行次数与每秒执行速率execs/sec衡量模糊测试吞吐量cover已覆盖的代码边/块数量覆盖率越高说明输入对代码路径的探索越充分uptime已运行时长用于评估是否已达收益递减点以示例输出为例6 个 worker、语料规模 3、覆盖率在 6 秒内从 0 跃升至 1054 并保持稳定执行速率从 0 攀升至 6000 execs/sec——这是典型的初始语料触发大量新路径随后覆盖率进入平台期的过程。五、suppressions崩溃去重与修复验证README 特别强调了 go-fuzz 的崩溃去重机制一旦发现一个 crasherfuzzer 会尽量避免重复报告同一输入因此会把该故障记录在suppressions文件夹中。这意味着重复输入不再反复触发同一崩溃向量只报告一次避免日志被刷屏修复后必须清理如果你修改代码修复了某个 bug应当删除suppressions文件夹中的所有数据再重新运行 fuzzer以确认该问题确实被解决。否则旧的 suppression 记录仍会屏蔽同类输入导致假修复——崩溃其实还在只是被静默抑制了。在 go-fuzz 的目录约定中suppressions文件夹通常出现在 fuzz 二进制的运行目录下与crashers崩溃输入归档、corpus有价值输入同级。六、唯一退出点原则让每个失败类型可区分README 给出了一个容易被忽视但非常关键的工程实践如果多种不同类型的测试共用一个退出点suppression 机制会让 fuzzer 隐藏掉不同类型的错误。原因在于go-fuzz 对崩溃的指纹取决于崩溃发生的位置栈特征。若多个错误类型都在同一个panic处退出它们会被视为同一种崩溃而只记录一次其余类型永远无法被单独暴露。因此README 要求确保每一种失败类型都有唯一的退出点即使用不同的panic消息或不同的失败分支。其给出的 rlp fuzzer 示例使用计数器i区分不同 caseif !bytes.Equal(input, output) { panic(fmt.Sprintf(case %d: encode-decode is not equal, \ninput : %x\noutput: %x, i, input, output)) }当第i个 case 失败时panic 消息中直接携带 case 编号崩溃指纹天然不同suppression 便无法掩盖其他 case 的问题。这一原则在本仓库的 fuzzer 中得到了严格执行。例如 difficulty-fuzz.go 在对比三组难度计算实现时panic 消息携带了配对序号i与完整的上下文参数for i, pair : range []struct { bigFn calculator u256Fn calculator }{ {ethash.FrontierDifficultyCalculator, ethash.CalcDifficultyFrontierU256}, {ethash.HomesteadDifficultyCalculator, ethash.CalcDifficultyHomesteadU256}, {ethash.DynamicDifficultyCalculator(bombDelay), ethash.MakeDifficultyCalculatorU256(bombDelay)}, } { want : pair.bigFn(time, header) have : pair.u256Fn(time, header) if want.Cmp(have) ! 0 { panic(fmt.Sprintf(pair %d: want %x have %x\nparent.Number: %x\np.Time: %x\nc.Time: %x\nBombdelay: %v\n, i, want, have, header.Number, header.Time, time, bombDelay)) } }同理bn256_fuzz.go 中的跨库对比 panic 消息明确区分了add mismatch: cloudflare/google与add mismatch: cloudflare/gnarkbls12381_fuzz.go 使用pairing mismatch blst / geth、G1 point addition mismatch blst / geth 等差异化消息。这些都是唯一退出点原则在仓库源码中的具体体现。七、仓库内现有 fuzzer 源码剖析本仓库的tests/fuzzers下共有六个模糊测试目标它们的fuzz(data []byte) int返回值遵循 go-fuzz 约定1表示该输入有价值、应提高优先级并加入语料库0表示普通输入-1表示即使带来新覆盖率也不应入语料库保留给未来使用。下面结合源码逐一剖析。7.1 difficulty新旧难度算法的回归一致性difficulty-fuzz.go 从输入字节流中随机构造父区块头难度、区块号、时间戳与子区块时间、炸弹延迟bomb delay随后对同一组参数分别调用两套难度计算实现并断言结果一致大整数big.Int版本ethash.FrontierDifficultyCalculator、ethash.HomesteadDifficultyCalculator、ethash.DynamicDifficultyCalculator256 位定点u256版本ethash.CalcDifficultyFrontierU256、ethash.CalcDifficultyHomesteadU256、ethash.MakeDifficultyCalculatorU256值得注意的细节为了规避难度炸弹中大整数幂运算karatsuba导致单次执行超时fuzzer 将区块号限制在 4 字节最大约 40 亿以内并将难度下限钳制在0x2000同时保证父、子时间戳都落在uint64范围内。这些约束体现了模糊测试中避免无意义慢路径、聚焦真实状态空间的设计思想。7.2 rangeproofMerkle 范围证明的六类恶意变异rangeproof-fuzzer.go 首先构造一棵随机 Trie一部分填充键值 一部分随机键值然后调用trie.VerifyRangeProof验证范围证明同时对输入施加六类变异testcase % 6case变异方式检验目的0修改随机 key证明与 key 集不匹配时验证器是否正确拒绝1修改随机 valuevalue 被篡改时的检测2删除中间一个键值对gap键集出现空洞时的行为3交换两个键的顺序键集无序时的行为4将随机 key 置为 nil空键的处理5将随机 value 置为 nil模拟删除删除语义的正确性fuzzer 还断言了一个重要的不变量当VerifyRangeProof返回错误时hasMore指示是否还有更多键未覆盖必须为 false否则直接panic(err ! nil hasMore true)。该用例被 rangeproof_test.go 中的原生Fuzz测试驱动种子语料存放于 tests/fuzzers/rangeproof/corpus 目录。7.3 txfetcher事件驱动的交易抓取状态机txfetcher_fuzzer.go 与前三者不同它测试的是一个有状态、事件驱动的组件eth/fetcher中的交易抓取器TxFetcher。fuzzer 将输入字节流解释为一串命令驱动抓取器状态机命令0Notify——某 peer 宣布一批交易哈希命令1Enqueue——某 peer 交付一批交易含 direct 标志命令2Drop——断开某 peer命令3clock.Run——推进模拟时钟每次 100ms 增量触发超时重取逻辑。fuzzer 使用确定性的随机种子0x3a29预生成 65536 笔交易与 10 个模拟 peer并根据输入首字节将交易空间裁剪为 4/256/4096/全部四档以便在小空间内集中测试哈希冲突与边界限额。整个测试通过fetcher.NewTxFetcherForTests注入模拟时钟与随机源任何Notify/Enqueue/Drop返回错误都会立即panic。这正是对 README 所述对每个失败类型保留唯一退出点精神的延续——不同命令路径的错误各自触发。7.4 bn256 / bls12381 / secp256k1跨实现一致性验证密码学组件是最值得模糊测试的领域之一仓库采用同一操作、多套实现、断言一致的差分测试differential testing策略bn256bn256_fuzz.go 对crypto/bn256下的 Cloudflare、Google、gnark 三套椭圆曲线实现同时执行点加法fuzzAdd、标量乘法fuzzMul、双线性配对fuzzPair与 G1/G2 反序列化fuzzUnmarshalG1/G2任何两套实现输出不一致即 panic。其中 GT 元素还通过normalizeGTToGnark按 eprint 2015/192 中的s 2u(6u²3u1)因子归一化后再与 gnark 结果比较。标量长度被限制在 128 字节以内注释明确说明这是为了避免 236KB 大整数导致 OSS-Fuzz 将执行标记为超时。bls12381bls12381_fuzz.go 将 gnark-crypto 与 blst 两套 BLS12-381 实现做交叉对比覆盖配对、G1/G2 点加、多标量乘MultiExp与子群检查precompile_fuzzer.go 则直接从 EVM 层面对core/vm中地址0x0b–0x11的 BLS 预编译合约PrecompiledContractsBLS发起模糊测试即使输入非法也必须保证不崩溃且预编译不得修改入参数据input data modified即 panicGas 计算超过 2500 万则跳过。种子语料 zip 存放在 tests/fuzzers/bls12381/testdata。secp256k1secp_test.go 对比 geth 内置的crypto/secp256k1与 decred 的dcrec/secp256k1两套曲线实现随机输入下做点加法并断言坐标一致。这三组用例的共同点在于用多实现互相校验替代人工构造期望值任何一方的实现偏差都会以 panic 形式暴露为可复现的崩溃输入。7.5 原生 Go fuzz 测试入口所有上述 fuzzer 均配套了原生 Go fuzz 测试Go 1.18 的testing.F例如difficulty_test.gorangeproof_test.gotxfetcher_test.gobn256_test.go含FuzzAdd、FuzzMul、FuzzPair、FuzzUnmarshalG1/G2等多个入口bls12381_test.go含 14 个FuzzCross*/Fuzz*入口它们统一采用f.Fuzz(func(t *testing.T, data []byte) { fuzz(data) })的模式将内部实现暴露给 Go 原生 fuzzer这也印证了前文已迁移到原生 fuzz 测试的推断。八、实战在本仓库中运行与复现崩溃8.1 运行原生 fuzz 测试无需安装任何外部工具直接使用 Go 原生能力即可对任意 fuzzer 发起模糊测试。例如对 rangeproof 运行 30 秒go test -fuzzFuzz -fuzztime30s ./tests/fuzzers/rangeproof对 bls12381需要 CGO 支持见文件头部的//go:build cgo约束go test -fuzzFuzzCrossPairing -fuzztime30s ./tests/fuzzers/bls12381首次运行go test -fuzz前可先用-runFuzzXXX模式执行一次语料回放seed corpus确认种子输入不崩溃go test -runFuzz ./tests/fuzzers/rangeproof本仓库各 fuzzer 的种子语料位于对应的corpus/如 rangeproof/corpus、txfetcher/corpus或testdata/目录如 bls12381/testdata它们为 fuzzer 提供了良好的初始路径覆盖。8.2 若坚持使用 go-fuzz 外部工具若希望复刻 README 描述的 go-fuzz 工作流可按第三节命令为包目录生成 zip 归档但需注意go-fuzz 要求包内存在导出的func Fuzz(data []byte) int入口而本仓库现有 fuzzer 为小写fuzztesting.F包装形式需自行补充导出入口后方可被go-fuzz-build识别。8.3 崩溃后的标准处置流程结合 README 的注意事项当发现 crasher 后建议按以下步骤处置复现用go test -runFuzzXXX回放崩溃输入或检查 go-fuzz 的crashers/目录中保存的输入文件定位根据 panic 消息中的唯一标识如pair %d、case %d、input data modified确定失败类型与涉及组件修复修改对应源码后清空suppressions文件夹go-fuzz 工作流重新运行回归确认 fuzzer 长时间运行不再产生相同崩溃同时保证原语料库全部通过。8.4 输入规模控制避免慢崩溃误报从仓库源码可以总结出几条通用的 fuzz 输入约束经验它们共同服务于一个目标让崩溃收敛到真实缺陷而不是超时/内存问题txfetcher_fuzzer.go 拒绝超过 16KB 的输入不要生成疯狂大的测试用例价值不大bn256_fuzz.go 将标量限制在 128 字节以内防止大整数运算拖慢执行precompile_fuzzer.go 对 Gas 超过 2500 万的输入直接返回 0difficulty-fuzz.go 将区块号限制在 4 字节规避难度炸弹的 karatsuba 超时。这些做法与 README 的 suppressions、唯一退出点原则共同构成了本仓库模糊测试工程化的完整方法论既要尽量多地探索代码路径也要让每个失败都可被单独识别、复现与验证。参考路径速览本文依据的主文档tests/fuzzers/README.md难度算法一致性 fuzzertests/fuzzers/difficulty/difficulty-fuzz.go、difficulty_test.go范围证明 fuzzertests/fuzzers/rangeproof/rangeproof-fuzzer.go、rangeproof_test.go交易抓取器 fuzzertests/fuzzers/txfetcher/txfetcher_fuzzer.go、txfetcher_test.gobn256 跨实现一致性tests/fuzzers/bn256/bn256_fuzz.go、bn256_test.goBLS12-381 与预编译 fuzzertests/fuzzers/bls12381/bls12381_fuzz.go、bls12381_test.go、precompile_fuzzer.gosecp256k1 跨实现对比tests/fuzzers/secp256k1/secp_test.go相关被测实现crypto/bn256、crypto/secp256k1、core/vm【免费下载链接】bscA BNB Smart Chain client based on the go-ethereum fork项目地址: https://gitcode.com/GitHub_Trending/bs/bsc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表