模糊测试接口¶
模糊测试接口是 mozilla-central 中的粘合代码,旨在使开发人员和安全研究人员能够更轻松地使用 libFuzzer 或 afl-fuzz 测试 C/C++ 代码。
这些模糊测试工具基于编译时检测来衡量分支覆盖率等内容以及每个模糊测试的更高级启发式方法。这样做允许这些工具在几乎无需在模糊测试本身中实现任何自定义逻辑/知识的情况下遍历代码。通常,这些工具唯一需要的是一个代码“垫片”,它为模糊测试提供进入被测代码的入口点。我们将此附加代码称为模糊测试目标,本手册的其余部分描述了如何实现和使用这些目标。
至于与这些目标一起使用的工具,我们目前建议使用 libFuzzer 而不是 afl-fuzz,因为后者不再维护,而 libFuzzer 正在积极开发中。此外,libFuzzer 有一些高级检测功能(例如,值分析来处理代码中复杂的比较),使其整体上更有效。
可以测试什么?¶
该接口可用于测试最终进入 libxul
(更准确地说,libxul
的 gtest 版本)或是 JS 引擎一部分的所有 C/C++ 代码。
请注意,这不是测试整个浏览器的正确方法。它更适合于基于组件的测试(尤其是一些组件不容易从完整构建中分离出来)。
注意
注意:如果您正在处理 JS 引擎(尝试重现 bug 或寻求开发新的模糊测试目标),那么请务必阅读本文档末尾的JS 引擎细节部分,因为 JS 引擎提供了用于实现和运行模糊测试目标的其他选项。
重现现有模糊测试目标的 bug¶
如果您正在处理涉及现有模糊测试接口目标的 bug,则可以使用两种方法重现该问题
使用现有构建¶
我们在 CI 中有几个模糊测试构建,您可以直接下载。我们建议为此目的使用 fuzzfetch
,因为它使下载和解压缩这些构建变得更加容易。
您可以从Github 或通过 pip安装 fuzzfetch
。
之后,您可以运行
$ python -m fuzzfetch -a --fuzzing --target firefox gtest -n firefox-fuzzing
以获取最新的优化构建。或者,我们提供非 ASan 调试构建,您可以使用以下命令下载
$ python -m fuzzfetch -d --fuzzing --target firefox gtest -n firefox-fuzzing
在这两个命令中,firefox-fuzzing
指示将为下载创建的目录的名称。
之后,您可以使用以下命令重现 bug
$ FUZZER=TargetName firefox-fuzzing/firefox test.bin
假设 TargetName
是您正在处理的 bug 中指定的模糊测试目标的名称,而 test.bin
是附加的测试用例。
注意
注意:您不应该在 shell 中永久导出 FUZZER
变量,尤其是在您计划进行本地构建的情况下。如果导出了 FUZZER
变量,它将影响构建过程。
如果 CI 构建不满足您的要求,并且您需要本地构建,则可以按照以下步骤创建一个
本地构建要求和标志¶
您需要一个具有最新 Clang 的 Linux 环境。建议使用 ./mach bootstrap
下载的 Clang 或更新版本。
启用模糊测试目标所需的唯一构建标志是 --enable-fuzzing
,因此将
ac_add_options --enable-fuzzing
添加到您的 .mozconfig
中已经足以生成模糊测试构建。但是,为了提高崩溃处理能力并检测其他错误,强烈建议将 libFuzzer 与AddressSanitizer 结合使用,至少对于优化构建和需要 ASan 才能重现的 bug(例如,您正在处理 ASan 报告某种内存安全违规的 bug)。
构建完成后,如果您想运行 gtests,则**必须**另外运行
$ ./mach gtest dontruntests
以强制构建 gtest libxul。
如果您遇到错误 error while loading shared libraries: libxul.so: cannot open shared object file: No such file or directory
,则需要在调用模糊测试的命令之前将 LD_LIBRARY_PATH
显式设置为您的构建目录 obj-dir
。例如,IPC 模糊测试调用
$ LD_LIBRARY_PATH=/path/to/obj-dir/dist/bin/ MOZ_FUZZ_TESTFILE=/path/to/test.bin NYX_FUZZER="IPC_Generic" /path/to/obj-dir/dist/bin/firefox /path/to/testcase.html
注意
注意:如果您修改了任何代码,请确保运行**两个**构建命令以确保 gtest libxul 也重新构建。只运行 ./mach build
并错过第二个命令是一个常见的错误。
完成这些步骤后,您可以使用上面为下载的构建描述的相同步骤在本地重现 bug。
开发新的模糊测试目标¶
使用模糊测试接口开发新的模糊测试目标只需要几个步骤。
确定模糊测试接口是否为合适的工具¶
模糊测试接口不适合所有类型的测试。特别是如果您的测试需要运行完整的浏览器,那么您可能需要考虑其他测试方法。
该接口使用 ScopedXPCOM
实现来提供一个 XPCOM 可用并已初始化的环境。您可以初始化可能需要的其他子系统,但您自己负责任何类型的初始化步骤。
理论上,您可以在浏览器初始化方面走多远都没有限制。但是,涉及的子系统越多,由于不确定性和性能下降而可能发生的问题就越多。
如果您不确定模糊测试接口是否适合您,或者您需要帮助评估可以为您的特定任务做些什么,请不要犹豫联系我们。
开发模糊测试代码¶
将模糊测试代码放在哪里¶
使用模糊测试接口的代码通常位于名为 fuzztest
的单独目录中,该目录与 gtests 位于同一级别。如果您的组件没有 gtests,那么 tests 或主目录中的子目录都可以。如果您的组件中尚不存在此类目录,则需要使用合适的 moz.build
创建一个。请参阅transport 目标的示例
为了将新子目录包含到构建过程中,您还必须相应地修改顶级 moz.build
文件。为此,您应该仅在设置了 FUZZING_INTERFACES
时将您的目录添加到 TEST_DIRS
。再次参见transport 目标的示例。
您的代码应该是什么样子¶
为了定义您的模糊测试目标 MyTarget
,您只需要实现 2 个函数
一次性初始化函数。
在启动时,模糊测试接口**一次**调用此函数,因此它可用于执行一次性操作,例如初始化子系统或解析额外的模糊测试选项。
此函数等效于LLVMFuzzerInitialize 函数,并具有相同的签名。但是,使用我们的模糊测试接口,它不会通过其名称解析,因此可以将其定义为
static
并根据您的喜好命名。请注意,该函数应始终return 0
,并且(除了返回之外)可以保持为空。为了本文档的目的,我们假设您有
static int FuzzingInitMyTarget(int* argc, char*** argv);
模糊测试迭代函数。
这是实际模糊测试发生的地方,此函数等效于LLVMFuzzerTestOneInput。同样,与模糊测试接口的区别在于该函数不会通过其名称解析。此外,我们为该函数提供了两种不同的可能签名,分别是
static int FuzzingRunMyTarget(const uint8_t* data, size_t size);
或
static int FuzzingRunMyTarget(nsCOMPtr<nsIInputStream> inputStream);
后者只是第一个函数的包装器,用于通常使用流的实现。无论您选择使用哪种签名,在函数内部唯一需要实现的是使用提供的 data 与您的目标实现。这可能意味着简单地将数据馈送到您的目标,使用数据驱动目标 API 上的操作,或两者兼而有之。
在执行此操作时,应避免以永久方式更改全局状态,使用其他数据/随机性来源或使代码在迭代函数的生命周期之外运行(例如,在另一个线程上),原因很简单:覆盖引导模糊测试工具依赖于迭代函数的**确定性**。如果对该函数的相同输入在运行两次时没有导致相同的执行(例如,因为结果状态取决于多次连续调用或由于其他外部影响),则该工具将无法重现其模糊测试进度并表现不佳。处理此限制可能具有挑战性,例如,在处理运行多线程的异步目标时,但通常可以通过在迭代函数结束时同步所有线程上的执行来管理。对于累积全局状态的实现,可能需要在每次迭代中(重新)初始化此全局状态,而不是在初始化函数中执行一次,即使这会带来额外的性能成本。
请注意,与普通的 libFuzzer 方法不同,您可以在此函数中
return 1
以指示输入“无效”。这样做会导致 libFuzzer 丢弃输入,无论它是否生成新的覆盖率。如果您有方法可以内部检测和捕获错误测试用例的行为(例如超时/过度资源使用等),以避免这些测试最终进入您的语料库,这将特别有用。
一旦您实现了这两个函数,唯一剩下的就是将它们注册到模糊测试接口。为此,我们提供了两个宏,具体取决于您使用的迭代函数签名。如果您坚持使用缓冲区和大小的经典签名,则可以简单地使用
#include "FuzzingInterface.h"
// Your includes and code
MOZ_FUZZING_INTERFACE_RAW(FuzzingInitMyTarget, FuzzingRunMyTarget, MyTarget);
其中MyTarget
是目标的名称,稍后将用于在运行时确定应使用哪个目标。
如果您改为使用流接口,则需要不同的包含文件,但宏调用非常相似
#include "FuzzingInterfaceStream.h"
// Your includes and code
MOZ_FUZZING_INTERFACE_STREAM(FuzzingInitMyTarget, FuzzingRunMyTarget, MyTarget);
有关实时示例,另请参阅STUN 模糊测试目标的实现。
向被测代码添加检测¶
libFuzzer 要求您尝试测试的代码使用特殊的编译器标志进行检测。幸运的是,只需在每个构建被测代码的moz.build
文件中包含以下指令,就可以在每个目录的基础上添加这些标志。
# Add libFuzzer configuration directives
include('/tools/fuzzing/libfuzzer-config.mozbuild')
该包含文件已执行适当的配置检查,仅在模糊测试构建中处于活动状态,因此您无需以任何方式保护它。
注意
注意:此包含文件相应地修改了CFLAGS和CXXFLAGS,但这仅适用于在此特定目录中定义的源文件。这些标志**不会**自动传播到子目录,您必须确保构建目标源文件的每个目录都将其包含文件添加到其moz.build
文件中。
通过将检测限制在使用此工具实际正在测试的部分,您不仅可以提高性能,还可以潜在地减少 libFuzzer 看到的噪声量。
构建您的代码¶
有关如何修改您的.mozconfig
以创建适当的构建的说明,请参阅上面的构建说明。
运行您的代码并构建语料库¶
您需要设置以下环境变量以启用在 Firefox 内部而不是常规浏览器中运行模糊测试代码。
FUZZER=name
其中name
是您在调用MOZ_FUZZING_INTERFACE_RAW
宏时指定的模糊测试模块的名称。对于上面的示例,这将是MyTarget
或实时示例中的StunParser
。
现在,当您使用-help=1
参数在构建目录中调用 Firefox 二进制文件时,您应该会看到常规的 libFuzzer 帮助。例如,在 Linux 上
$ FUZZER=StunParser obj-asan/dist/bin/firefox -help=1
您应该会看到类似于此的输出
Running Fuzzer tests...
Usage:
To run fuzzing pass 0 or more directories.
obj-asan/dist/bin/firefox [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]
To run individual tests without fuzzing pass 1 or more files:
obj-asan/dist/bin/firefox [-flag1=val1 [-flag2=val2 ...] ] file1 [file2 ...]
Flags: (strictly in form -flag=value)
verbosity 1 Verbosity level.
seed 0 Random seed. If 0, seed is generated.
runs -1 Number of individual test runs (-1 for infinite runs).
max_len 0 Maximum length of the test input. If 0, libFuzzer tries to guess a good value based on the corpus and reports it.
...
重现崩溃¶
为了从给定的测试文件重现崩溃,只需将该文件作为命令行上的唯一参数,例如
$ FUZZER=StunParser obj-asan/dist/bin/firefox test.bin
这应该会重现给定的问题。
FuzzManager 和 libFuzzer¶
我们的 FuzzManager 项目附带一个用于运行 libFuzzer 的工具,并可以选择连接到 FuzzManager 服务器实例。请注意,此连接不是强制性的,即使没有服务器,您也可以使用本地工具。
您可以在此处找到该工具。
与 StunParser 一起使用的工具的示例调用可能如下所示
FUZZER=StunParser python /path/to/afl-libfuzzer-daemon.py --fuzzmanager \
--stats libfuzzer-stunparser.stats --libfuzzer-auto-reduce-min 500 --libfuzzer-auto-reduce 30 \
--tool libfuzzer-stunparser --libfuzzer --libfuzzer-instances 6 obj-asan/dist/bin/firefox \
-max_len=256 -use_value_profile=1 -rss_limit_mb=3000 corpus-stunparser
此操作将执行以下操作:
使用 6 个并行实例在
StunParser
目标上运行 libFuzzer,使用corpus-stunparser
目录中的语料库(使用指定的 libFuzzer 选项,例如-max_len
和-use_value_profile
)如果语料库增长了 30%(并且至少有 500 个文件),则自动减少语料库并重新启动
使用 FuzzManager(需要一个本地
.fuzzmanagerconf
和一个firefox.fuzzmanagerconf
二进制配置,如 FuzzManager 手册中所述)并将崩溃提交为libfuzzer-stunparser
工具将统计信息写入
libfuzzer-stunparser.stats
文件
JS 引擎细节¶
模糊测试接口也可用于测试 JS 引擎,实际上有两种独立的选项来实现和运行模糊测试目标
使用 C++ 实现¶
与 Firefox 中的模糊测试接口类似,您可以使用完全相同的 C++ 实现您的目标,其接口与之前描述的非常相似。
不过,也有一些细微的差别
所有模糊测试目标都位于js/src/fuzz-tests中。
所有代码都链接到一个名为fuzz-tests的单独二进制文件,类似于所有 JSAPI 测试最终都位于jsapi-tests中。为了构建此二进制文件,您必须使用
--enable-fuzzing
**和**--enable-tests
构建 JS shell。同样,这可以并且应该与 AddressSanitizer 结合使用以获得最大的有效性。这也意味着在处理 JS 模糊测试目标并使用 shell 作为完整浏览器构建的一部分时,无需(重新)构建 gtest。围绕 JS 实现的工具已为您提供一个已初始化的
JSContext
和全局对象。您可以通过声明以下内容来访问您的目标中的这些内容:extern JS::PersistentRootedObject gGlobal;
和
extern JSContext* gCx;
但您没有义务使用它们。
有关实时示例,另请参阅StructuredCloneReader 目标的实现。
使用 JS 实现¶
除了 C++ 目标之外,您还可以使用 JavaScript 运行时 (JSRT) 模糊测试方法在 JavaScript 中实现目标。使用这种方法不仅简单得多(因为您不需要了解任何关于 JSAPI 或引擎内部的信息),而且还让您可以完全访问 JS shell 中定义的所有内容,包括timeout()
等便捷函数。
当然,这种方法也存在缺点:调用 JS 并在那里执行模糊测试操作会影响性能。此外,与相当隔离的 C++ 目标相比,更有可能导致全局副作用或不确定性。
根据经验,如果以下情况,您应该使用 JS 实现目标:
您不了解 C++ 和/或如何使用 JSAPI(毕竟,JS 模糊测试目标总比没有好),
您的目标预计会出现大量挂起/超时(您可以内部捕获这些情况),
或者您的目标不够隔离,不适合 C++ 目标和/或您需要特定的 JS shell 函数。
树中有一个示例目标,大致展示了如何实现这样的模糊测试目标。
要运行此类目标,您必须运行js
(shell)二进制文件而不是fuzz-tests
二进制文件,并将FUZZER
变量指向包含模糊测试目标的文件,例如
$ FUZZER=/path/to/jsrtfuzzing-example.js obj-asan/dist/bin/js --fuzzing-safe --no-threads -- <libFuzzer options here>
更详细的目标可以在js/src/fuzz-tests/中找到。
故障排除¶
模糊测试接口:错误:未找到测试回调¶
此错误意味着使用FUZZER
环境变量指定的名称的模糊测试回调未找到。原因通常是名称拼写错误或您的代码未构建(检查您的moz.build
文件和构建日志)。
mach build
似乎没有更新我的模糊测试代码¶
请记住,您始终需要运行mach build
和mach gtest dontruntests
命令才能更新您的模糊测试代码。后者会重新构建包含您的代码的libxul
的 gtest 版本。