libFuzzer – 一个用于覆盖引导模糊测试的库。

介绍

LibFuzzer 是一个进程内、覆盖引导、进化式模糊测试引擎。

LibFuzzer 与被测库链接,并通过特定的模糊测试入口点(也称为“目标函数”)向库提供模糊测试输入;然后,模糊测试器跟踪哪些代码区域被访问,并对输入数据集合进行变异,以最大化代码覆盖率。libFuzzer 的代码覆盖率信息由 LLVM 的 SanitizerCoverage 检测提供。

联系方式:libfuzzer(#)googlegroups.com

状态

libFuzzer 的最初作者已停止对其进行积极开发,并转向另一个模糊测试引擎 Centipede 的开发。LibFuzzer 仍然得到完全支持,这意味着重要的错误将得到修复。但是,请不要期望出现主要的新功能或代码审查,除非是错误修复。

版本

LibFuzzer 需要与 Clang 版本匹配。

入门

模糊测试目标

在库上使用 libFuzzer 的第一步是实现一个模糊测试目标——一个接受字节数组并使用被测 API 对这些字节执行一些有趣操作的函数。例如:

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Values other than 0 and -1 are reserved for future use.
}

请注意,此模糊测试目标不依赖于 libFuzzer,因此可以使用其他模糊测试引擎(例如 AFL 和/或 Radamsa)对其进行使用,甚至建议这样做。

关于模糊测试目标,需要记住一些重要事项:

  • 模糊测试引擎将使用同一进程中不同的输入多次执行模糊测试目标。

  • 它必须能够容忍任何类型的输入(空、巨大、格式错误等)。

  • 它不得在任何输入上exit()

  • 它可以使用线程,但理想情况下,所有线程都应在函数结束时加入。

  • 它必须尽可能确定性。非确定性(例如,不基于输入字节的随机决策)将使模糊测试效率低下。

  • 它必须快速。尝试避免三次或更高次方的复杂度、日志记录或过多的内存消耗。

  • 理想情况下,它不应修改任何全局状态(尽管这不是严格要求)。

  • 通常,目标越窄越好。例如,如果您的目标可以解析多种数据格式,则将其拆分为多个目标,每个格式一个。

模糊测试器用法

最近版本的 Clang(从 6.0 开始)包含 libFuzzer,无需额外安装。

为了构建您的模糊测试器二进制文件,在编译和链接期间使用-fsanitize=fuzzer标志。在大多数情况下,您可能希望将 libFuzzer 与 AddressSanitizer (ASAN)、UndefinedBehaviorSanitizer (UBSAN) 或两者结合使用。您还可以使用 MemorySanitizer (MSAN) 进行构建,但支持尚处于实验阶段。

clang -g -O1 -fsanitize=fuzzer                         mytarget.c # Builds the fuzz target w/o sanitizers
clang -g -O1 -fsanitize=fuzzer,address                 mytarget.c # Builds the fuzz target with ASAN
clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c # Builds the fuzz target with a part of UBSAN
clang -g -O1 -fsanitize=fuzzer,memory                  mytarget.c # Builds the fuzz target with MSAN

这将执行必要的检测,以及与 libFuzzer 库的链接。请注意,-fsanitize=fuzzer 会链接 libFuzzer 的 main() 符号。

如果修改大型项目的 CFLAGS,该项目也编译需要其自身 main 符号的可执行文件,则可能需要仅请求检测而不进行链接。

clang -fsanitize=fuzzer-no-link mytarget.c

然后可以通过在链接阶段传递 -fsanitize=fuzzer 将 libFuzzer 链接到所需的驱动程序。

语料库

像 libFuzzer 这样的覆盖引导模糊测试器依赖于被测代码的样本输入集合。此语料库理想情况下应使用被测代码的各种有效和无效输入进行填充;例如,对于图形库,初始语料库可能包含各种不同的 PNG/JPG/GIF 小文件。模糊测试器基于当前语料库中的样本输入生成随机变异。如果变异触发了被测代码中先前未覆盖路径的执行,则该变异将保存到语料库中以供将来变异使用。

LibFuzzer 可以在没有任何初始种子文件的情况下工作,但如果被测库接受复杂、结构化的输入,则效率会降低。

语料库还可以充当健全性/回归检查,以确认模糊测试入口点仍然有效,并且所有样本输入都可以在被测代码中顺利运行。

如果您有一个大型语料库(通过模糊测试生成或通过其他方式获取),您可能希望对其进行最小化,同时仍保留完整的覆盖率。一种方法是使用-merge=1标志。

mkdir NEW_CORPUS_DIR  # Store minimized corpus here.
./my_fuzzer -merge=1 NEW_CORPUS_DIR FULL_CORPUS_DIR

您可以使用相同的标志向现有语料库添加更多有趣的项目。只有触发新覆盖率的输入才会添加到第一个语料库。

./my_fuzzer -merge=1 CURRENT_CORPUS_DIR NEW_POTENTIALLY_INTERESTING_INPUTS_DIR

运行

要运行模糊测试器,首先创建一个包含初始“种子”样本输入的 语料库 目录。

mkdir CORPUS_DIR
cp /some/input/samples/* CORPUS_DIR

然后在语料库目录上运行模糊测试器。

./my_fuzzer CORPUS_DIR  # -max_len=1000 -jobs=20 ...

当模糊测试器发现新的有趣的测试用例(即触发被测代码中新路径覆盖的测试用例)时,这些测试用例将添加到语料库目录。

默认情况下,模糊测试过程将无限期地继续——至少在发现错误之前。任何崩溃或消毒器错误都将像往常一样报告,停止模糊测试过程,并将其触发错误的特定输入写入磁盘(通常为 crash-<sha1>leak-<sha1>timeout-<sha1>)。

并行模糊测试

每个 libFuzzer 进程都是单线程的,除非被测库启动了自己的线程。但是,可以使用共享语料库目录并行运行多个 libFuzzer 进程;这样做的好处是,一个模糊测试器进程发现的任何新输入都将可用于其他模糊测试器进程(除非您使用 -reload=0 选项禁用此功能)。

这主要由 -jobs=N 选项控制,该选项指示应运行 N 个模糊测试作业直至完成(即,直到发现错误或达到时间/迭代限制)。这些作业将在默认情况下使用一半可用 CPU 内核的一组工作进程中运行;工作进程的数量可以通过 -workers=N 选项覆盖。例如,在 12 核机器上使用 -jobs=30 运行,默认情况下将运行 6 个工作进程,每个工作进程平均在整个过程完成后发现 5 个错误。

分叉模式

实验性模式 -fork=N(其中 N 是并行作业的数量)启用使用单独进程(使用 fork-exec,而不仅仅是 fork)的内存不足、超时和崩溃恢复模糊测试。

顶级 libFuzzer 进程本身不会执行任何模糊测试,但会产生多达 N 个并发子进程,为它们提供语料库的小型随机子集。子进程退出后,顶级进程会将子进程生成的语料库合并回主语料库。

相关标志

-ignore_ooms

默认为 True。如果子进程之一的模糊测试过程中发生内存不足错误,则将重现器保存在磁盘上,并继续模糊测试。

-ignore_timeouts

默认为 True,与 -ignore_ooms 相同,但针对超时。

-ignore_crashes

默认为 False,与 -ignore_ooms 相同,但针对所有其他崩溃。

计划最终用 -fork=N 替换 -jobs=N-workers=N

恢复合并

合并大型语料库可能需要花费时间,并且通常希望在可抢占式虚拟机上执行此操作,在这些虚拟机上,进程可能随时被终止。为了无缝恢复合并,请使用 -merge_control_file 标志,并使用 killall -SIGUSR1 /path/to/fuzzer/binary 以优雅地停止合并。示例:

% rm -f SomeLocalPath
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-INNER: using the control file 'SomeLocalPath'
...
# While this is running, do `killall -SIGUSR1 my_fuzzer` in another console
==9015== INFO: libFuzzer: exiting as requested

# This will leave the file SomeLocalPath with the partial state of the merge.
# Now, you can continue the merge by executing the same command. The merge
# will continue from where it has been interrupted.
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-OUTER: non-empty control file provided: 'SomeLocalPath'
MERGE-OUTER: control file ok, 32 files total, first not processed file 20
...

选项

要运行模糊测试器,请将零个或多个语料库目录作为命令行参数传递。模糊测试器将从每个语料库目录读取测试输入,并且生成的任何新测试输入都将写回第一个语料库目录。

./fuzzer [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]

如果将文件列表(而不是目录)传递给模糊测试器程序,则它将重新运行这些文件作为测试输入,但不会执行任何模糊测试。在此模式下,模糊测试器二进制文件可以用作回归测试(例如,在持续集成系统上),以检查目标函数和保存的输入是否仍然有效。

最重要的命令行选项是:

-help

打印帮助消息(-help=1)。

-seed

随机种子。如果为 0(默认值),则生成种子。

-runs

单个测试运行的次数,-1(默认值)表示无限运行。

-max_len

测试输入的最大长度。如果为 0(默认值),libFuzzer 会根据语料库尝试猜测一个合适的值(并报告该值)。

-len_control

首先尝试生成较小的输入,然后随着时间的推移尝试更大的输入。指定长度限制增加的速度(越小越快)。默认值为 100。如果为 0,则立即尝试最大长度为 max_len 的输入。

-timeout

超时时间(秒),默认为 1200。如果输入花费的时间超过此超时时间,则该进程将被视为失败案例。

-rss_limit_mb

内存使用限制(MB),默认为 2048。使用 0 禁用限制。如果输入需要超过此 RSS 内存量才能执行,则该进程将被视为失败案例。限制每秒在单独的线程中检查一次。如果在没有 ASAN/MSAN 的情况下运行,可以使用“ulimit -v”代替。

-malloc_limit_mb

如果非零,则如果目标尝试使用一次 malloc 调用分配此数量的 MB 内存,模糊器将退出。如果为零(默认值),则应用与 rss_limit_mb 相同的限制。

-timeout_exitcode

如果 libFuzzer 报告超时,则使用的退出代码(默认值 77)。

-error_exitcode

如果 libFuzzer 本身(而不是 sanitizer)报告错误(泄漏、内存不足等),则使用的退出代码(默认值 77)。

-max_total_time

如果为正,则表示运行模糊器最大总时间(秒)。如果为 0(默认值),则无限运行。

-merge

如果设置为 1,则来自第 2 个、第 3 个等语料库目录的任何触发新代码覆盖率的语料库输入都将合并到第一个语料库目录中。默认为 0。此标志可用于最小化语料库。

-merge_control_file

指定用于合并过程的控制文件。如果合并过程被终止,它会尝试将此文件保留在适合恢复合并的状态。默认情况下将使用临时文件。

-minimize_crash

如果为 1,则最小化提供的崩溃输入。与 -runs=N 或 -max_total_time=N 一起使用以限制尝试次数。

-reload

如果设置为 1(默认值),则定期重新读取语料库目录以检查是否有新输入;这允许检测由其他模糊处理过程发现的新输入。

-jobs

要运行到完成的模糊作业数。默认值为 0,它运行单个模糊进程直到完成。如果值为 >= 1,则运行此数量的执行模糊的作业,在一个并行独立工作进程的集合中;每个这样的工作进程将其stdout/stderr重定向到fuzz-<JOB>.log

-workers

运行模糊作业到完成的并发工作进程数。如果为 0(默认值),则使用min(jobs, NumberOfCpuCores()/2)

-dict

提供输入关键字的字典;请参阅字典

-use_counters

使用覆盖率计数器生成代码块命中次数的近似计数;默认为 1。

-reduce_inputs

尝试在保留其完整功能集的同时减小输入的大小;默认为 1。

-use_value_profile

使用值配置文件指导语料库扩展;默认为 0。

-only_ascii

如果为 1,则仅生成 ASCII(isprint``+``isspace)输入。默认为 0。

-artifact_prefix

提供一个前缀,在将模糊工件(崩溃、超时或缓慢输入)保存为$(artifact_prefix)file时使用。默认为空。

-exact_artifact_path

如果为空(默认值),则忽略。如果非空,则在发生故障(崩溃、超时)时将单个工件写入$(exact_artifact_path)。这会覆盖-artifact_prefix,并且不会在文件名中使用校验和。不要对多个并行进程使用相同的路径。

-print_pcs

如果为 1,则打印新覆盖的 PC。默认为 0。

-print_final_stats

如果为 1,则在退出时打印统计信息。默认为 0。

-detect_leaks

如果为 1(默认值)并且启用了 LeakSanitizer,则尝试在模糊期间检测内存泄漏(即不仅在关闭时检测)。

-close_fd_mask

指示在启动时要关闭的输出流。请注意,这将删除目标代码的诊断输出(例如,断言失败时的消息)。

  • 0(默认值):既不关闭stdout也不关闭stderr

  • 1:关闭stdout

  • 2:关闭stderr

  • 3:同时关闭stdoutstderr

有关标志的完整列表,请使用-help=1运行模糊器二进制文件。

输出

在操作过程中,模糊器会将信息打印到stderr,例如

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
...

输出的早期部分包含有关模糊器选项和配置的信息,包括当前随机种子(在Seed:行中;这可以通过-seed=N标志覆盖)。

后续输出行采用事件代码和统计信息的形式。可能的事件代码为

READ

模糊器已从语料库目录中读取所有提供的输入样本。

INITED

模糊器已完成初始化,包括运行每个初始输入样本以通过正在测试的代码。

NEW

模糊器已创建了一个覆盖正在测试的代码的新区域的测试输入。此输入将保存到主语料库目录。

REDUCE

模糊器已找到一个更好的(更小的)输入,该输入触发了先前发现的功能(设置-reduce_inputs=0以禁用)。

pulse

模糊器已生成 2n 个输入(定期生成以向用户保证模糊器仍在工作)。

DONE

模糊器已完成操作,因为它已达到指定的迭代限制(-runs)或时间限制(-max_total_time)。

RELOAD

模糊器正在执行来自语料库目录的输入的定期重新加载;这使它能够发现由其他模糊器进程发现的任何输入(请参阅并行模糊)。

每行输出还报告以下统计信息(非零时)

cov

执行当前语料库覆盖的代码块或边的总数。

ft

libFuzzer 使用不同的信号来评估代码覆盖率:边覆盖率、边计数器、值配置文件、间接调用方/被调用方对等。这些信号组合称为特征ft:)。

corp

当前内存中测试语料库中的条目数及其大小(以字节为单位)。

lim

语料库中新条目的当前长度限制。随着时间的推移而增加,直到达到最大长度(-max_len)。

exec/s

每秒模糊器迭代次数。

rss

当前内存消耗。

对于NEWREDUCE事件,输出行还包含有关生成新输入的变异操作的信息

L

新输入的大小(以字节为单位)。

MS: <n> <operations>

用于生成输入的变异操作的计数和列表。

示例

玩具示例

一个简单的函数,如果收到输入“HI!”则执行一些有趣的操作。

cat << EOF > test_fuzzer.cc
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size > 0 && data[0] == 'H')
    if (size > 1 && data[1] == 'I')
       if (size > 2 && data[2] == '!')
       __builtin_trap();
  return 0;
}
EOF
# Build test_fuzzer.cc with asan and link against libFuzzer.
clang++ -fsanitize=address,fuzzer test_fuzzer.cc
# Run the fuzzer with no corpus.
./a.out

您应该很快就会收到错误

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
==31511== ERROR: libFuzzer: deadly signal
...
artifact_prefix='./'; Test unit written to ./crash-b13e8756b13a00cf168300179061fb4b91fefbed

更多示例

可以在http://tutorial.libfuzzer.info找到实际模糊目标和它们发现的错误的示例。除其他事项外,您还可以了解如何在 1 秒内检测心脏出血

高级功能

字典

LibFuzzer 支持用户提供的字典,其中包含输入语言关键字或其他有趣的字节序列(例如,多字节魔术值)。使用-dict=DICTIONARY_FILE。对于某些输入语言,使用字典可能会显着提高搜索速度。字典语法类似于AFL在其-x选项中使用的语法。

# Lines starting with '#' and empty lines are ignored.

# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"

跟踪 CMP 指令

使用额外的编译器标志-fsanitize-coverage=trace-cmp(作为-fsanitize=fuzzer的一部分默认启用,请参阅SanitizerCoverageTraceDataFlow),libFuzzer 将拦截 CMP 指令并根据拦截的 CMP 指令的参数指导变异。这可能会降低模糊速度,但很可能改善结果。

值配置文件

使用-fsanitize-coverage=trace-cmp(使用-fsanitize=fuzzer时默认启用)和额外的运行时标志-use_value_profile=1,模糊器将为比较指令的参数收集值配置文件,并将某些新值视为新覆盖率。

当前实现大致执行以下操作

  • 编译器使用接收两个 CMP 参数的回调来检测所有 CMP 指令。

  • 回调计算(caller_pc&4095) | (popcnt(Arg1 ^ Arg2) << 12),并使用此值在位集中设置一个位。

  • 位集中观察到的每个新位都将被视为新覆盖率。

此功能有可能发现许多有趣的输入,但有两个缺点。首先,额外的检测可能会导致多达 2 倍的额外减速。其次,语料库的大小可能会增加数倍。

易于模糊测试的构建模式

有时被测代码不适合模糊测试。示例

  • 目标代码使用例如系统时间播种的 PRNG,因此即使最终结果相同,两次连续调用也可能执行不同的代码路径。这将导致模糊测试器将两个类似的输入视为显着不同,并导致测试语料库膨胀。例如,libxml 在其哈希表内部使用rand()

  • 目标代码使用校验和来防止无效输入。例如,png 会检查每个块的 CRC。

在许多情况下,构建一个特殊的易于模糊测试的版本,禁用某些不适合模糊测试的功能是有意义的。我们建议为所有此类情况使用一个通用的构建宏以保持一致性:FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION

void MyInitPRNG() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
  // In fuzzing mode the behavior of the code should be deterministic.
  srand(0);
#else
  srand(time(0));
#endif
}

AFL 兼容性

LibFuzzer 可以与AFL 一起在相同的测试语料库上使用。这两个模糊测试器都期望测试语料库位于一个目录中,每个输入一个文件。您可以一个接一个地在同一个语料库上运行这两个模糊测试器

./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
./llvm-fuzz testcase_dir findings_dir  # Will write new tests to testcase_dir

定期重新启动这两个模糊测试器,以便它们可以使用彼此的发现。目前,没有简单的方法可以在共享相同语料库目录的同时并行运行这两个模糊测试引擎。

您也可以在目标函数LLVMFuzzerTestOneInput上使用 AFL:请参阅示例此处

我的模糊测试器有多好?

一旦您实现了目标函数LLVMFuzzerTestOneInput并对其进行了彻底的模糊测试,您将想知道该函数或语料库是否可以进一步改进。当然,一个易于使用的指标是代码覆盖率。

我们建议使用Clang 覆盖率来可视化和研究您的代码覆盖率(示例)。

用户提供的变异器

LibFuzzer 允许使用自定义(用户提供的)变异器,有关更多详细信息,请参阅结构感知模糊测试

启动初始化

如果要测试的库需要初始化,则有几个选项。

最简单的方法是在LLVMFuzzerTestOneInput(或在全局作用域中,如果这对您有效)中使用静态初始化的全局对象

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  static bool Initialized = DoInitialization();
  ...

或者,您可以定义一个可选的初始化函数,它将接收您可以读取和修改的程序参数。仅当您确实需要访问argv/argc时才执行此操作。

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {
 ReadAndMaybeModify(argc, argv);
 return 0;
}

将 libFuzzer 作为库使用

如果要模糊测试的代码必须提供自己的main,则可以将 libFuzzer 作为库调用。请确保在编译期间传递-fsanitize=fuzzer-no-link,并将您的二进制文件链接到 libFuzzer 的无 main 版本。在 Linux 安装中,它通常位于

/usr/lib/<llvm-version>/lib/clang/<clang-version>/lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

如果从源代码构建 libFuzzer,则它位于构建输出目录中的以下路径

lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

从此处,代码可以执行它所需的任何设置,并且当它准备好开始模糊测试时,它可以调用LLVMFuzzerRunDriver,传入程序参数和回调。此回调与LLVMFuzzerTestOneInput一样被调用,并且具有相同的签名。

extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv,
                  int (*UserCb)(const uint8_t *Data, size_t Size));

拒绝不需要的输入

可能需要拒绝某些输入,即不将它们添加到语料库中。

例如,在模糊测试由解析和其他逻辑组成的 API 时,可能只想将成功解析的输入添加到语料库中。

如果模糊测试目标在给定输入上返回 -1,则无论它触发什么覆盖范围,libFuzzer 都不会将该输入添加到语料库中。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  if (auto *Obj = ParseMe(Data, Size)) {
    Obj->DoSomethingInteresting();
    return 0;  // Accept. The input may be added to the corpus.
  }
  return -1;  // Reject; The input will not be added to the corpus.
}

内存泄漏

使用AddressSanitizerLeakSanitizer 构建的二进制文件将尝试在进程关闭时检测内存泄漏。对于进程内模糊测试,这很不方便,因为模糊测试器需要在发现有泄漏的变异后立即报告泄漏及其重现步骤。但是,在每次变异后运行完整的泄漏检测代价很高。

默认情况下(-detect_leaks=1),libFuzzer 会在执行每个变异时计算mallocfree 调用的次数。如果这两个数字不匹配(这本身并不意味着存在泄漏),libFuzzer 将调用更昂贵的LeakSanitizer 传递,如果发现实际的泄漏,则将报告泄漏及其重现步骤,并且进程将退出。

如果您的目标存在大量泄漏并且泄漏检测被禁用,您最终将耗尽 RAM(请参阅-rss_limit_mb 标志)。

开发 libFuzzer

LibFuzzer 默认情况下在 macOS 和 Linux 上作为 LLVM 项目的一部分构建。其他操作系统的用户可以使用-DCOMPILER_RT_BUILD_LIBFUZZER=ON 标志显式请求编译。测试使用配置了-DCOMPILER_RT_INCLUDE_TESTS=ON 标志的构建目录中的check-fuzzer 目标运行。

ninja check-fuzzer

常见问题解答

问:为什么 libFuzzer 不使用任何 LLVM 支持?

有两个原因。

首先,我们希望此库可以在 LLVM 之外使用,而无需用户构建其余的 LLVM。这对许多 LLVM 人员来说可能听起来不太令人信服,但在实践中,构建整个 LLVM 的需求吓坏了许多潜在用户——我们希望更多用户使用此代码。

其次,有一个微妙的技术原因不依赖于其余的 LLVM 或任何其他大型代码体(可能甚至不包括 STL)。启用覆盖率检测时,它也会检测 LLVM 支持代码,这将导致进程的覆盖率集膨胀(因为模糊测试器是进程内的)。换句话说,通过使用更多外部依赖项,我们将降低模糊测试器的速度,而它存在的主要原因是极快的速度。

问:libFuzzer 支持 Windows 吗?

是的,libFuzzer 现在支持 Windows。初始支持是在 r341082 中添加的。任何 Clang 9 的构建都支持它。您可以从LLVM 快照构建下载具有 libFuzzer 的 Clang for Windows 构建。

在没有 ASAN 的情况下在 Windows 上使用 libFuzzer 不受支持。使用/MD(动态运行时库)编译选项构建模糊测试器不受支持。将来可能会添加对这些的支持。链接使用/INCREMENTAL 链接选项(或隐含它的/DEBUG 选项)的模糊测试器也不受支持。

发送任何问题或意见到邮件列表:libfuzzer(#)googlegroups.com

问:什么时候 libFuzzer 不是解决问题的最佳方案?

  • 如果测试输入由目标库验证,并且验证器在无效输入上断言/崩溃,则进程内模糊测试不适用。

  • 目标库中的错误可能会累积而未被检测到。例如,一个最初未被检测到的内存损坏,然后在测试另一个输入时导致崩溃。这就是为什么强烈建议使用所有清理器运行此进程内模糊测试器,以便在现场检测大多数错误。

  • 更难保护进程内模糊测试器免受目标库中过度内存消耗和无限循环的影响(尽管仍然可以)。

  • 目标库不应该具有在运行之间未重置的重要全局状态。

  • 许多有趣的目标库的设计方式不支持进程内模糊测试器接口(例如,需要文件路径而不是字节数组)。

  • 如果单个测试运行需要相当一部分时间(或更长时间),则进程内模糊测试器带来的速度优势可以忽略不计。

  • 如果目标库运行持久线程(超出一次测试的执行时间),则模糊测试结果将不可靠。

问:那么,这个模糊测试器到底擅长什么?

对于测试输入相对较小、每个输入运行时间小于 10 毫秒且库代码预计不会在无效输入上崩溃的库,此模糊测试器可能是不错的选择。示例:正则表达式匹配器、文本或二进制格式解析器、压缩、网络、加密。

问:LibFuzzer 在我的复杂模糊测试目标上崩溃了(但在较小的目标上对我来说运行良好)。

检查您的模糊测试目标是否使用了dlclose。目前,libFuzzer 不支持调用dlclose 的目标,这可能会在将来得到修复。

战利品