- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
DartFuzz 是 Dart SDK 仓库中随附的生成式模糊测试(fuzz testing)工具,用于持续发现 Dart VM(JIT/AOT)、dart2js 等不同执行后端之间的一致性缺陷与崩溃问题。本文以 runtime/tools/dartfuzz/README.md 为主线,结合 dartfuzz.dart、dartfuzz_test.dart 与 minimize.py 等源码,系统讲解 DartFuzz 的命令行用法、执行模式、底层生成机制、发散判定逻辑以及崩溃用例最小化流程,帮助读者掌握一套可直接落地的 Dart 编译器/VM 模糊测试方案。
DartFuzz 是什么
DartFuzz 是一个以“随机生成 Dart 程序”为手段的生成式模糊测试工具,目标是对 Dart 项目本身做模糊测试。每个随机生成的程序可以运行在多种执行模式下:
- JIT:Dart VM 的即时编译模式;
- AOT:Dart VM 的预编译(快照)模式;
- dart2js + Node.JS:经 dart2js 编译为 JavaScript 后在 Node 上执行;
- 多种目标架构:x64、arm32、arm64、riscv32、riscv64(含 debug/release 与仿真器变体)。
不同执行模式对同一随机程序输出的任何差异(divergence,发散),都可能暴露某个执行后端中的缺陷。这正是 DartFuzz 的核心思想:与其让测试程序受限于“程序员会怎么编写”,不如让随机性把系统的边界彻底推开。
从模糊测试方法论看,DartFuzz 属于**生成式(generation-based)模糊测试:它并非对已有输入做变异,而是从零构造“随机但格式合法”的程序。与之相对,仓库内还提供了DartLibFuzzer(runtime/vm/libfuzzer/README.md),它是基于 LLVM LibFuzzer 的覆盖率引导(coverage-guided)**变异式模糊引擎,以dart_libfuzzer二进制形式对 VM/编译器内特定“target function”进行有方向的输入变异;二者互为补充。
提示:DartFuzz 的版本号定义在 dartfuzz.dart 中(当前为
version = '1.101')。版本号的每次递增都保证“给定随机种子时,同一版本 DartFuzz 生成完全相同的模糊程序”,这使得缺陷可以被精确复现和回放。
生成单个随机 Dart 程序:dartfuzz.dart 用法
DartFuzz 提供一个可运行的 main isolate。生成单个随机 Dart 程序的完整命令行如下:
dart dartfuzz.dart [--help] [--seed SEED] [--[no-]fp] FILENAME一个典型的单次测试流程是:
dart dartfuzz.dart fuzz.dart dart fuzz.dart第一步在指定文件名中生成随机程序,第二步用当前 SDK 的 dart 直接运行它。
命令行参数详解
| 参数 | 含义 | 默认值 |
|---|---|---|
--help | 打印帮助信息后退出 | — |
--seed SEED | 定义随机种子;传 0 时由系统按时间选取(并在运行时打印一个非零种子) | 系统自动设定 |
--[no-]fp | 启用/禁用浮点运算 | 开启(on) |
--[no-]ffi | 启用/禁用 FFI 方法调用 | 关闭(off) |
--[no-]flat | 启用/禁用 flat types(扁平类型,即禁止嵌套类型) | 关闭(off) |
--[no-]mini | 启用最小化模式 | 关闭(off) |
--smask | 位掩码,指示省略哪些语句(Bit=1 表示省略) | "0" |
--emask | 位掩码,指示省略哪些表达式(Bit=1 表示省略) | "0" |
从 dartfuzz.dart 的main函数可以看到这些选项的底层解析:--seed默认'0',--fp默认开启,--ffi与--flat默认关闭,--mini默认关闭,--smask/--emask默认'0';FILENAME通过results.rest.single取唯一的位置参数,并调用File(...).openSync(mode: FileMode.write)以覆盖写方式输出程序。若参数解析失败,会打印完整 usage 并设置退出码 255。
关于种子还有一处细节:getSeed()(dartfuzz.dart)在用户传入 0 时,会反复从Random()中取nextInt(1 << 32)直到得到一个非零种子。这样既保留了“指定种子即可复现”的能力,又保证了随机性的来源可见。
启动一次完整的 DartFuzz 测试会话:dartfuzz_test.dart
要真正开始“模糊测试会话”,运行:
dart dartfuzz_test.dart [--help] [--isolates ISOLATES ] [--repeat REPEAT] [--time TIME] [--num-output-lines NUMOUTPUTLINES] [--true_divergence] [--show-stats] [--dart-top DARTTOP] [--mode1 MODE] [--mode2 MODE] [--[no-]rerun]参数详解
| 参数 | 含义 | 默认值 |
|---|---|---|
--help | 打印帮助后退出 | — |
--isolates | 会话使用的 isolate 数量(并发度) | 1 |
--repeat | 要运行的测试数量 | 1000 |
--time | 时间上限(秒),0 表示不限时 | 无限制 |
--num-output-lines | 发生发散时打印的输出行数 | 200 |
--true-divergence | 只报告“真发散”(true divergence) | true |
--show-stats | 会话过程中显示统计信息 | true |
--dart-top | 通过命令行显式指定 DART_TOP | 见下文 |
--mode1 | 模式 m1 | 随机 |
--mode2 | 模式 m2 | 随机 |
--[no-]rerun | 若测试仅在返回码上发散(且返回值超出 [-255,+255] 范围),则重跑该用例;若第二次运行不再发散,则忽略第一次结果 | true |
若不指定任何模式,会随机挑选一种组合。
执行模式(modes)
模式取值为以下三者之一(对应 dartfuzz_test.dart 中的TestRunner.getTestRunner与getTag解析逻辑):
| 模式 | 含义 |
|---|---|
jit-[debug-][x64\|arm32\|arm64] | Dart JIT(x64/arm32/arm64,可选 debug) |
aot-[debug-][x64\|arm32\|arm64] | Dart AOT(同样支持多架构) |
djs-x64 | dart2js 编译 + Node.JS 运行 |
从源码看,标签(tag)与产物目录严格对应,例如x64→ReleaseX64、debug-x64→DebugX64、arm32→ReleaseSIMARM、arm64→ReleaseSIMARM64,riscv 变体对应ReleaseSIMRISCV32/64等。这些字符串会直接用于拼接${DART_TOP}/out/<tag>/dartvm等二进制路径。
DART_TOP 前提
该模糊测试工具必须能访问一个Dart SDK 开发树顶层(DART_TOP),且其中所有必需的二进制都已构建完毕。例如测试jit-x64会调用${DART_TOP}/out/ReleaseX64/dart来启动 Dart VM。DART_TOP 的提供方式有三种(优先级从高到低):
- 命令行
--dart-top; - 环境变量
DART_TOP; - 默认取当前目录(即在 SDK 顶层目录直接调用工具)。
对应实现见 dartfuzz_test.dart 的getTop():依次读取命令行参数、Platform.environment['DART_TOP'],最后回退为'.'。会话启动时会同时打印 Fuzz 版本、Dart SDK Revision(通过dart --version获取)、isolate 数、测试数、时间限制、true-divergence与show-stats等会话信息。
会话输出与统计
会话过程中(--show-stats开启时)每个 isolate 会持续更新一行统计:Tests / Success (Rerun) / Skipped / Timeout / Divergences。全部 isolate 结束后,若无发散输出success,否则输出failure (N divergences)并设置退出码 1(见DartFuzzTestSession.start)。每次发散报告还会附上一段-- BEGIN REPRODUCE --块,包含 Dart SDK revision、完整的dartfuzz.dart --seed ...复现命令以及 RUN 1 / RUN 2 两条执行命令,便于任何人离线复现缺陷。
底层原理:DartFuzz 如何“随机但合法”地生成程序
DartFuzz类(dartfuzz.dart)是整个生成器的核心。它的run()方法(dartfuzz.dart)完整地勾勒了生成流程:
- 用种子初始化
Random; - 由
--fp/--flat构建 Dart 类型集合(DartType.fromDartConfig); - 初始化最小化状态(
initMinimization); - 构建库 API(
DartApi(ffi),若开启 FFI 则注入 FFI 测试函数); - 填充全局变量、全局方法、类字段、类方法(含统一原型的
call方法)与虚方法覆盖; - 依次输出:头部(含版本注释、
// @dart=2.14与dart:async、dart:io、dart:isolate、dart:typed_data等 import)→ 全局变量声明 → 全局方法 → Dart 类型上的扩展方法 → 类定义 →main。
类型与命名约束
生成规模由一系列常量约束(dartfuzz.dart):语句深度stmtDepth=1、表达式深度exprDepth=2、循环嵌套nestDepth=1、每处语句数numStatements=2、全局变量 4、局部变量 4、全局方法 4、方法参数 4、类 4、每类扩展方法 3、Dart 类型扩展 5。命名约定为var/par/loc/fld/foo前缀加数字下标。此外还有一个很有价值的细节:interestingIntegers(dartfuzz.dart)内置了一批“有趣整数”(0x7fffffff、0x80000000、0x7fffffffffffffff等 32/64 位边界值),emitInt()有 1/7 概率选择它们,从而系统性覆盖整数溢出与符号边界;字符串生成则混合常规字符与\u2665、\u{1f600}等 Unicode 转义。
语句与表达式
语句生成入口是emitStatement/emitStatements(dartfuzz.dart),emitSingleStatement通过choose(17)在 print、return、throw、单/双向 if、for、for-in、while、do-while、break/continue、switch、新作用域、try/catch/finally、forEach、库调用、赋值等 17 种语句间随机选择;表达式生成入口是emitExpr(dartfuzz.dart),在终端值、一元、二元、三元、前后缀自增自减、库调用、方法调用中随机选择。生成时深度越深,继续嵌套的概率越低(choose(depth + 1) > exprDepth时回退为终端),从而保证程序结构合法且不会无限膨胀。
值得注意的几个“避免假发散/假崩溃”的工程细节:
- 可增长类型(String、List 等)的赋值只用
=,避免+=、*=在循环中引发 OOM; a = a * 100这类自递归赋值通过RhsFilter(dartfuzz.dart)过滤掉左值变量;try/catch生成时对OutOfMemoryError与StackOverflowError显式exit(254)(oomExitCode,dartfuzz.dart),把资源类异常转成统一退出码,便于发散判定;- 迭代变量被记录在
iterVars中,循环体内修改迭代变量是低概率事件,以避免死循环; - 库调用参数带
Restriction.small约束(如sumManyInts、regress37069),用小数避免 32/64 位intptr_t宽度差异造成的假发散。
方法与类层次
生成的方法分为五类(MethodType枚举,dartfuzz.dart):全局方法、FFI 方法、实例方法、call方法、扩展方法。类定义中,除X0外每个类随机选择父类并随机使用extends继承或withmixin,且run()方法会调用super.run();父类方法还会以 1/8 概率被随机覆盖(emitVirtualMethods,dartfuzz.dart),以覆盖动态分派路径。方法体生成时还随机添加@pragma("vm:always-consider-inlining")、@pragma("vm:entry-point")、@pragma("vm:never-inline")等注解(emitFunctionAnnotations,dartfuzz.dart),从而把 VM 的内联/入口点路径也纳入测试面。
递归控制
Method基类(dartfuzz.dart)通过硬币翻转决定方法是否允许递归。允许递归时,方法会追加一个“递归深度参数”(类型为 int):非递归调用传 0,递归调用传深度+1;同时生成基准情形if (depth >= smallPositiveInt) return ...(该 return 在禁用递归的作用域内生成),从结构上保证递归必然终止。
发散判定与重跑机制:dartfuzz_test.dart 的执行逻辑
DartFuzzTest类(dartfuzz_test.dart)负责每个用例的生成、双模式执行与发散判定,关键点如下。
三种 TestRunner
- TestRunnerJIT:命令为
${DART_TOP}/out/<tag>/dartvm+ 随机 JIT 运行期 flag +--old_gen_heap_size=128(常量dartHeapSize)+ 测试文件;每个进程带 60 秒超时(timeout常量),超时则 SIGKILL。 - TestRunnerAOT:先用
pkg/vm/tool/precompiler2(设置DART_CONFIGURATION/DART_VM_FLAGS='--enable-asserts')把测试文件编译为快照,再用dartaotruntime运行;两步任一失败都作为结果返回。 - TestRunnerDJS:先用
sdk/bin/dart2js编译出out.js,再用nodejs运行。
checkDivergence 判定规则
checkDivergence(dartfuzz_test.dart)的逻辑:
- 两边都超时 → 记
timeout,种子上报; - 退出码相同:都为 0 时比较 stdout,输出不同则报告发散;都为非 0 时记
skipped(双方都出错,无信息量); - 退出码不同:若开启
--true-divergence,先剔除“至少一方超时”和“一方为 OOM 退出码 254”的情况(分别归入 timeout/skipped),再报告发散;随后检查退出码绝对值是否超出 255——超出说明不是 VM 正常 exit 码(可能是原生崩溃/信号),需要重跑确认。
rerun 机制
--rerun(默认开启)对应ReportStatus.rerun:对返回码类发散,若两个 runner 的返回码绝对值有超出[-255,+255]的,会先把发散计数回退,原样重跑一次;第二次若无发散则忽略第一次结果并记Rerun。这能有效过滤偶发性的原生崩溃抖动。
集群/分片与随机 flag
DartFuzzTestSession(dartfuzz_test.dart)通过Isolate.spawn并发运行多个会话,每个 isolate 内先随机getMode挑选两种模式(无指定时从clusterModes中随机,且保证 m2 ≠ m1),再自动决定本次用例是否开 FP(仅当两模式架构相同,避免 32/64 位假发散)、是否开 FFI(仅 jit×jit 且非 arm/riscv,1/5 概率)、是否用 flat types(含 arm/riscv 的昂贵模式强制扁平)。会话还支持--shards/--shard分片,用于大规模集群任务。
另外,flag_fuzzer.dart 是 DartFuzz 的补充:它以out/ReleaseX64/dartvm为基准输出(expected),然后对 JIT/AOT 随机组合 profiler(--profile_vm、--max_profile_depth等)、GC(--use_compactor、--verify_after_gc、--marker_tasks等)与编译优化(--optimization_counter_threshold、--no_intrinsify、--no_use_osr等)flag,逐任务对比 stdout,检测 flag 组合引发的崩溃或行为差异。flag_fuzzer_dart2js.dart 与 flag_fuzzer_dart2wasm.dart 则把同样的思路扩展到 dart2js 与 dart2wasm 编译器本身。
缺陷用例最小化:minimize.py 与两阶段最小化
一旦发现崩溃或发散,通常需要把触发用例“瘦身”成最小可复现程序,便于提交 bug 与定位根因。minimize.py 完成这件事,方式是对 dartfuzz.dart 的--mini模式配合--smask/--emask位掩码反复重生成程序:掩码中 Bit=1 的位置对应被省略的语句/表达式。由于语句与表达式是嵌套的,屏蔽一个外层语句会连带屏蔽其内部子语句/子表达式(dartfuzz.dart 的processStmtOpen/processExprOpen会在每次生成后输出更新后的掩码与语句/表达式总数,供minimize.py读取)。
最小化分为两个阶段:
- Minimize statements(语句最小化);
- Minimize expressions(表达式最小化)。
完整流程文档见 runtime/tools/dartfuzz/README_minimize.md。其示例做法如下。
示例:复现一个崩溃
先生成一个可能触发缺陷的程序(以--seed 790976770为例):
dart dartfuzz.dart --no-ffi --no-fp --seed 790976770 test.dart在特定 flag 下运行并观察崩溃(示例为段错误):
dart --optimization_counter_threshold=1 test.dart ... ===== CRASH ===== si_signo=Segmentation fault(11), si_code=1, si_addr=(nil) version=2.6.0-edge.de7ad46797d36a25e6d2800820f61f4af3bd1135 (Wed Sep 11 18:20:46 2019 +0000) on "linux_x64" thread=183944, isolate=main(0x559bd215cc00) ... pc 0x0000559bd0e40a69 fp 0x00007f73d7a7de70 ../../../../sdk/out/ReleaseX64/dart+0x190ca69 -- End of DumpStackTrace从输出中挑选一个标识缺陷的关键词,例如Segmentation,作为--err参数。若缺陷不是确定性的,则调大--tries,使得多次尝试中至少有一次以较高概率触发错误。
阶段 1:语句最小化
python3 minimize.py \ --dartfuzz "dart dartfuzz.dart --no-ffi --no-fp --seed 790976770" \ --dart "dart --optimization_counter_threshold=1" \ --testfile mini.dart \ --err Segmentation \ --tries 4 \ --threads 4 \ --typ s \ --verbose 3fffffffffffffffffffffffffffffffffffffffffffffffff error 7fffffffffffffffffffffffffffffffffffffffffffffffff error STOP Best I could do is 198/198 dart dartfuzz.dart --no-ffi --no-fp --seed 790976770 mini.dart --mini --smask 0x7fffffffffffffffffffffffffffffffffffffffffffffffff --emask 0示例中所有语句都被消除(198/198)。但此时mini.dart中函数的参数仍然保留,需要在阶段 2 中继续最小化表达式。
阶段 2:表达式最小化
python3 minimize.py \ --dartfuzz "dart dartfuzz.dart --no-ffi --no-fp --seed 790976770" \ --dart "dart --optimization_counter_threshold=1" \ --testfile mini.dart \ --err Segmentation \ --tries 4 \ --threads 4 \ --typ e \ --verbose \ --smask 0x7fffffffffffffffffffffffffffffffffffffffffffffffff .. STOP Best I could do is 4626/4628 dart dartfuzz.dart --no-ffi --no-fp --seed 790976770 mini.dart --mini --smask 0x7fffffffffffffffffffffffffffffffffffffffffffffffff \ --emask 0x1ff...ff2ff...fff注意阶段 2 需要把阶段 1 得到的--smask原样传入。
minimize.py 参数
| 参数 | 含义 | 默认值 |
|---|---|---|
--dartfuzz | dartfuzz 命令字符串(含固定 seed 等,如dart dartfuzz.dart --no-ffi --no-fp --seed 243123600) | 必填 |
--dart | 用于运行测试的 Dart 命令字符串(如./sdk/out/ReleaseX64/dart) | 必填 |
--dart-ref | 参考构建的 Dart 命令(发散测试场景下使用),配合--err-ref匹配参考输出 | 无 |
--testfile | dartfuzz 输出并被 Dart 命令执行的程序文件名(如fuzz.dart) | 必填 |
--err | 标识 Dart 命令出错的字符串(不支持多行匹配) | 必填 |
--err-ref | 参考命令发散输出的匹配串 | 无 |
--smask | 十六进制语句掩码 | "0" |
--emask | 十六进制表达式掩码 | "0" |
--typ | s(仅语句)/e(仅表达式)/se(两者) | 必填 |
--tries | 每条 Dart 命令的重试次数 | 1 |
--tries-ref | 参考命令的重试次数 | 1 |
--threads | 用于重试的线程数 | 4 |
--timeout | Dart 命令超时(秒) | 60 |
--verbose | 打印中间结果 | 关闭 |
其核心算法是MaskGen(minimize.py)生成的卷积位模式序列:从全 1 掩码开始,逐步用低密度filter_mask折叠出“半开半闭”的新掩码;凡是仍能触发错误的掩码就并入当前最优掩码(假设“两个能触发错误的掩码的并集仍能触发错误”),且每个掩码只测一次。语句级输出在捕获错误后还会尝试互补掩码(inverse),尽可能多消一行。借助multiprocessing.Pool与--tries并行重试,可显著提升对非确定性崩溃的命中率。
环境与依赖
DartFuzz 以独立 Dart 包形式存在(runtime/tools/dartfuzz/pubspec.yaml),name: dartfuzz,声明publish_to: none,即不面向 pub.dev 发布;其environment.sdk约束为>=2.10.0 <3.0.0,依赖analyzer、args、matcher三个包(其中args用于命令行解析、matcher用于发散时的字符串差异报告)。工具目录还包含生成 API 表的 gen_api_table.dart、类型表的 gen_type_table.dart 及 FFI 桩代码 dartfuzz_ffi_api.dart(生成的库调用表分别落在 dartfuzz_api_table.dart 与 dartfuzz_type_table.dart),以及 collect_data.py、update_spreadsheet.py 等配套脚本。
结语:一条可落地的 Dart 模糊测试工作流
把以上内容串起来,DartFuzz 在 runtime/tools/dartfuzz 目录中提供了一条完整的模糊测试闭环:
- 生成:
dart dartfuzz.dart --seed <S> fuzz.dart依据种子确定性地产出随机但合法的 Dart 程序; - 探测:
dart dartfuzz_test.dart在 JIT/AOT/dart2js 与多架构模式下并行执行,按退出码与 stdout 判定发散,并对返回码类异常自动重跑; - 复现:发散报告自动输出
--seed与两模式复现命令,配合版本号可精确回放; - 最小化:
minimize.py --typ s然后--typ e两阶段把用例压到最小,产出可直接提交 issue 的--mini --smask/--emask命令; - 扩展:flag_fuzzer.dart 系列把同样的思路覆盖到 VM flag、dart2js 与 dart2wasm 编译器,runtime/vm/libfuzzer/README.md 的 DartLibFuzzer 则从生成式之外提供覆盖率引导的变异式补充。
对任何想为 Dart VM、dart2js 或 dart2wasm 做系统化健壮性验证的开发者而言,这套工具链既有“开箱即用”的命令行入口,又有清晰的源码结构可供二次开发,是研究编译器/VM 模糊测试的绝佳参考实现。
- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
相关推荐
Dart SDK 模糊测试指南:深入 DartLibFuzzer——面向 Dart 运行时与编译器的 LibFuzzer 模糊测试工具
Dart SDK 模糊测试指南:深入 DartLibFuzzer——面向 Dart 运行时与编译器的 LibFuzzer 模糊测试工具 DartLibFuzze
编程语言编译器语言运行时标准库开发工具randfill 库深度指南:为 Go 对象生成随机值,强化序列化测试与 go-fuzz 模糊测试
randfill 库深度指南:为 Go 对象生成随机值,强化序列化测试与 go fuzz 模糊测试 本指南以 VictoriaMetrics 仓库 vendor
时序数据库数据库指标监控可观测性后端如何5分钟完成QQ空间零丢失本地存档?GetQzonehistory历史说说一键导出Excel完整指南
如何5分钟完成QQ空间零丢失本地存档?GetQzonehistory历史说说一键导出Excel完整指南 你在QQ空间发的说说存了十几年,平台却不保证一直留着。手
网页爬虫数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考