☰
Dart SDK 模糊测试工具 DartFuzz 完全指南:随机程序生成、跨模式发散检测与最小化
2026/9/26 18:35:54 网站建设 项目流程
  • 编程语言
  • 编译器
  • 语言运行时
  • 标准库
  • 开发工具

【免费下载链接】sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.

项目地址:https://gitcode.com/gh_mirrors/sdk1/sdk
点击查看免费下载

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-x64dart2js 编译 + 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 的提供方式有三种(优先级从高到低):

  1. 命令行--dart-top;
  2. 环境变量DART_TOP;
  3. 默认取当前目录(即在 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)完整地勾勒了生成流程:

  1. 用种子初始化Random;
  2. 由--fp/--flat构建 Dart 类型集合(DartType.fromDartConfig);
  3. 初始化最小化状态(initMinimization);
  4. 构建库 API(DartApi(ffi),若开启 FFI 则注入 FFI 测试函数);
  5. 填充全局变量、全局方法、类字段、类方法(含统一原型的call方法)与虚方法覆盖;
  6. 依次输出:头部(含版本注释、// @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读取)。

最小化分为两个阶段:

  1. Minimize statements(语句最小化);
  2. 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 参数

参数含义默认值
--dartfuzzdartfuzz 命令字符串(含固定 seed 等,如dart dartfuzz.dart --no-ffi --no-fp --seed 243123600)必填
--dart用于运行测试的 Dart 命令字符串(如./sdk/out/ReleaseX64/dart)必填
--dart-ref参考构建的 Dart 命令(发散测试场景下使用),配合--err-ref匹配参考输出无
--testfiledartfuzz 输出并被 Dart 命令执行的程序文件名(如fuzz.dart)必填
--err标识 Dart 命令出错的字符串(不支持多行匹配)必填
--err-ref参考命令发散输出的匹配串无
--smask十六进制语句掩码"0"
--emask十六进制表达式掩码"0"
--typs(仅语句)/e(仅表达式)/se(两者)必填
--tries每条 Dart 命令的重试次数1
--tries-ref参考命令的重试次数1
--threads用于重试的线程数4
--timeoutDart 命令超时(秒)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 目录中提供了一条完整的模糊测试闭环:

  1. 生成:dart dartfuzz.dart --seed <S> fuzz.dart依据种子确定性地产出随机但合法的 Dart 程序;
  2. 探测:dart dartfuzz_test.dart在 JIT/AOT/dart2js 与多架构模式下并行执行,按退出码与 stdout 判定发散,并对返回码类异常自动重跑;
  3. 复现:发散报告自动输出--seed与两模式复现命令,配合版本号可精确回放;
  4. 最小化:minimize.py --typ s然后--typ e两阶段把用例压到最小,产出可直接提交 issue 的--mini --smask/--emask命令;
  5. 扩展: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.

项目地址:https://gitcode.com/gh_mirrors/sdk1/sdk
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询