RFC-0117:组件模糊测试框架 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 一个 Fuchsia 原生跨进程模糊测试框架。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2021-05-24 |
审核日期(年-月-日) | 2021-07-28 |
摘要
引导式模糊测试是一种有效减少 bug 并提高对平台信心的方法,但目前还没有可跨多个进程边界进行模糊测试的模糊测试框架,如 Fuchsia 组件拓扑中所示。本文档提出了这样一个框架的设计,该框架可在进程之间以及在测试环境内共享覆盖率和测试输入,从而允许在最典型的配置中对组件进行模糊测试。
设计初衷
程序测试可以用于证明存在 bug,但绝不能用于证明不存在 bug!
引导式模糊测试是一种使用生成的数据在反馈环中测试软件的过程:
- fuzz 工具会生成一些测试输入数据,并使用这些数据来测试目标软件。
- 如果测试失败,模糊测试工具会记录输入内容并退出。
- 目标软件会产生由模糊测试工具收集的反馈。
- 模糊测试工具会使用反馈生成其他测试输入,然后重复此过程。
引导式模糊测试对于发现与项目要求无关(因此通常未经过测试)的软件错误非常有用。通过自动化测试覆盖率,还可以提高开发者对系统中具有安全、正确性和/或稳定性考量的关键部分的信心。
引导式模糊测试框架可以使用以下分类进行描述:
- 引擎:与目标无关的反馈环。
- 语料库管理:维护一组模糊测试输入(语料库)。记录新输入并修改现有输入(例如合并)。
- 种子语料库是一组手工制作的初始输入。
- 实时语料库是一组不断更新的生成输入。
- 更改器:一组更改策略和一个确定性伪随机源,用于从语料库中创建新输入。
- 反馈分析:根据反馈处理输入。
- 管理界面:与用户互动以协调工作流:
- 使用特定输入对目标进行演练,即执行单次模糊测试运行。
- 对目标进行模糊测试,即执行一系列(可能无限)的模糊测试运行。
- 分析或处理给定语料库。
- 响应检测到的错误和/或处理导致该错误的工件。
- 语料库管理:维护一组模糊测试输入(语料库)。记录新输入并修改现有输入(例如合并)。
- 目标:要进行模糊处理的特定目标领域。
- 输入处理:将单次运行的模糊测试输入映射到被测代码,例如通过特定函数、I/O 管道等。
- 反馈收集:观察输入所导致的行为。可以收集硬件或软件轨迹、代码覆盖率数据、时间等。
- 错误检测:确定输入何时导致错误。收集和记录测试工件,例如输入、日志、回溯等。
其中一些方面可能需要操作系统和/或其工具链提供特定支持,例如反馈收集和错误检测。目前,在 Fuchsia 上,最受支持的模糊测试框架是 libFuzzer,它通过预构建的 clang 工具链作为编译器运行时提供。我们在用于收集代码覆盖率反馈的 sanitizer_common 运行时和用于检测异常的 libFuzzer 本身中添加了相应支持。借助这些工具以及一组 GN 模板和主机工具,开发者可以快速为 Fuchsia 上的库开发模糊测试工具。
不过,与 Linux 不同,在 Fuchsia 上,软件的基本可执行单元是组件,而不是库。使用现有的引导式模糊测试框架对组件进行模糊测试非常繁琐,因为其反馈的粒度要么过窄(例如,单个进程中的 libFuzzer),要么过宽(例如,在 qemu 实例上使用 TriforceAFL)。
适用于在 Fuchsia 中对组件进行模糊测试的理想框架具有以下特点:
- 与现有的持续模糊测试基础架构(例如 ClusterFuzz)集成。
- 一种模块化方法,可利用其他模糊测试框架的平台无关部分(例如突变策略)。
- 高性能的跨进程代码覆盖机制。
- 与现有 Fuchsia 工作流(例如
ffx
)集成。 - 一个密封环境,可隔离待测组件和/或为其依赖项提供模拟组件。
- 目标组件的未修改源代码。
- 一种可靠且灵活的方法,用于分析执行情况和检测错误。
- 与 Fuchsia 中的其他测试方式类似的开发者故事。
设计
此设计旨在:
- 遵循 Fuchsia 惯用法。
- 重复使用现有实现。
概括来讲,该设计利用测试运行程序框架,并添加了以下内容:
- 用于启动模糊测试的
fuzzer_engine
。 ffx
插件和模糊测试管理器,用于与模糊测试工具互动和管理模糊测试工具。- 用于将
fuzzer_engine
连接到模糊测试管理器的fuzz_test_runner
。
本文档的这一部分大致按控制流程组织;即,从希望执行模糊测试任务的人员或聊天机器人开始,逐步向要进行模糊测试的目标领域推进。读者应注意,部分部分会提及后续部分中详细介绍的概念。
ffx fuzz
主机工具
用户(包括人类用户和聊天机器人)通过 ffx
插件与该框架进行交互。此插件将能够通过以下方式与 fuzz_manager
服务通信:
ffx fuzz
的子命令与 fx fuzz
的子命令类似,例如:
analyze
:报告给定语料库和/或字典的覆盖率信息。check
:检查一个或多个模糊测试工具的状态。coverage
:为测试生成覆盖率报告。list
:列出当前 build 中可用的模糊测试工具。repro
:通过重放测试单元来重现模糊测试结果。start
:启动特定的模糊测试工具。stop
:停止特定模糊测试工具。update
:更新了 fuzzer 语料库的 BUILD.gn 文件。
模糊测试管理器
测试运行程序框架提供了两项重要功能:
- 借助它,您可以轻松创建复杂且密封的测试 Realm,并使用可自定义的测试运行程序来驱动这些测试。
- 它提供了收集日志和回溯等重要诊断信息的方法。
此外,单次模糊测试运行可以自然地用组件测试框架的术语表达:使用给定的测试输入运行代码,并可视为已通过或失败,具体取决于是否发生了错误。
不过,模糊测试与其他形式的测试确实有所不同,而将持续模糊测试与持续测试进行比较时,这种差异会更加明显:
- 测试输入无法事先知晓。
- 测试输入是通过模糊测试生成的。
- 持续模糊测试基础架构(例如 ClusterFuzz)将包含许多模糊测试工具实例,并且会在模糊测试进行期间“交叉授粉”其测试输入。
- 模糊测试执行是开放式的。模糊测试从来不会真正“通过”,它们只会失败或提前停止。
- 因此,需要提供按需状态,其中包含其他测试通常不会提供的详细信息,例如执行速度、收集的总反馈、消耗的内存等。
- 此状态需要持续提供给监控 fuzz 工具执行情况的人员或 fuzz 基础架构聊天机器人。
- 模糊测试结果比简单的“通过/失败”更丰富。
- 失败时,输出需要包含触发输入以及任何关联的日志和回溯。
- 在提前终止时,输出可能包含累积的反馈和建议的参数(例如字典),以供日后进行模糊测试。
- 模糊化令牌网域可用于模糊化基础架构选择依次执行的多种不同工作流,例如“模糊化一段时间。If an error is found, cleanse it, otherwise, merge and compact the corpus". 如果将每个步骤都表示为一个测试套件,则需要从一个步骤中提取状态,仅仅为了在下一个步骤中恢复该状态,这会带来大量工作。
其中一些问题可以通过扩展测试运行程序框架来解决,例如,它可以提供结构化输出。不过,如果对所有模糊测试需求都使用此方法,则会为不需要这些功能的其他测试添加大量功能。因此,该设计添加了一个新的 fuzz_manager
,其作用如下:
- 通过
ffx
向用户提供管理界面。 - 与
test_manager
交互,以便在测试运行程序框架中的模糊化 realm 中启动模糊测试工具。 - 提供
fuchsia.fuzzer.manager.Harness
,供这些模糊测试工具重新连接并处理用户请求。 - 提供数据传输协议,以便于向模糊测试工具注入数据或从模糊测试工具中提取数据。
然后,修改测试运行程序框架,如下所示:
- 添加了新的
fuzz_test_runner
。此运行程序基于现有的elf_test_runner
启动fuzzer_engine
,并将模糊测试工具网址传递给它。 - 修改了
test_manager
,以将fuchsia.fuzzer.manager.Harness
capability 路由到fuzz_test_runner
。此功能不会路由到测试,并且非模糊测试工具的密封性不会受到影响。 fuzz_test_runner
会为fuchsia.fuzzer.Controller
协议创建一个通道对。它会将一端安装为fuzzer_engine
中的启动句柄,并使用fuchsia.fuzzer.manager.Harness
将另一端传递给fuzz_manager
。
fuzzer 引擎
fuzzer_engine
是模糊化 Realm 的组成部分。从模糊测试工具分类的角度来看,它:
- 实现
fuchsia.fuzzer.Controller
协议以提供管理界面。 - 创建并使用存储功能来管理每个语料库。
- 更改语料库中的输入,以创建新的测试输入。(例如,与 libMutagen 建立链接)。
Uses
一个Adapter
capability,用于发送要处理的新输入。Exposes
一种fuchsia.fuzzer.ProcessProxy
功能,用于在模糊化领域中插桩的远程进程可以使用该功能提供收集的反馈和报告错误。- 分析反馈。
如果将模糊测试视为一系列具有不同输入的测试,那么一种方法是让模糊测试引擎为每个输入实例化一个新的测试 realm,即让测试运行程序依次执行每个模糊测试运行。这种方法的主要问题是反馈分析和变异循环的性能。fuzz 工具质量与吞吐量直接相关,主循环必须非常快:“更改、处理输入、收集反馈和分析反馈”的开销应为微秒级。
因此,与用于测试复杂拓扑的测试驱动程序类似,模糊测试引擎会包含在测试环境本身中。由 eventpairs 协调的共享 VMO 用于以尽可能低的延迟将测试输入传输到模糊测试目标适配器,以及从插桩远程进程传输反馈。
模糊测试引擎由 fuzz_test_runner
启动。此运行程序与现有的 elf_test_runner
非常相似,但有一个重要的补充:它会为 fuchsia.fuzzer.Controller
协议创建一个通道对。它会将此对的一端安装为 fuzzer_engine
中的启动句柄。它使用 test_manager
路由给它的 fuchsia.fuzz.manager.Harness
capability 将另一个 capability 传递给 fuzz_manager
。这样一来,test_manager
便可仅向 fuzz_test_runner
和它启动的模糊测试提供 Harness
功能,而不是向所有测试提供。
目标适配器
模糊测试目标适配器在模糊测试分类中执行输入处理角色。它使用上述共享 VMO 和事件对,获取 fuzzer 引擎生成的测试输入,并将其映射到与要进行 fuzz 测试的目标领域的插桩远程进程的特定互动。
这些特定互动由模糊测试程序作者提供,通常被称为“编写模糊测试程序”的贡献。
fuzz 工具作者可以提供自己的 fuzz 目标适配器自定义实现,也可以使用提供的某个架构。
可能的适配器架构示例包括:
llvm_fuzzer_adapter
:预计作者会实现 LLVM 的模糊测试目标函数。- 对于 C/C++,作者需要实现:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
- 对于 Rust,作者可以使用
#[fuzz]
proc_macro
属性实现方法。 - 对于 Go,作者需要实现以下内容:
func Fuzz(s []byte);
realm_builder_adapter
:除了 LLVM fuzz 目标函数之外,作者还实现了用于修改所提供RealmBuilder
的方法。适配器会向此函数提供默认构建器,并使用结果构建要进行模糊测试的组件领域。作者可以通过添加其他路由、功能、模拟等来修改它:pub trait FuzzedRealmBuilder { fn extend(builder : &mut RealmBuilder); }
libfuzzer_adapter
:与llvm_fuzzer_adapter
类似,但其组件清单会省略模糊测试引擎,自行公开Controller
功能,并直接与 libFuzzer 相关联。这种截然不同的组件拓扑结构允许在此框架中使用 libFuzzer 进行传统库模糊测试。honggfuzz-persistent-adapter
:预计模糊测试工具作者会实现以下内容:extern HF_ITER(uint8_t** buf, size_t* len);
目前不支持
honggfuzz
本身,但为其编写的模糊测试目标函数仍可与此框架集成。
请注意,目标适配器也可以且应与远程库关联,并与插桩目标中的插桩进程一起充当插桩远程进程。
插桩的远程进程
为了收集反馈和检测错误,需要使用额外的插桩(例如 SanitizerCoverage)构建要进行模糊测试的目标领域中的所有进程。对于在树中构建的 fuzz 工具,可以通过将 flags
和 deps
传播到 GN 目标的依赖项的工具链变体来实现此目的。我们将记录必需的标志(例如 -fsanitize-coverage=inline-8bit-counters
),以允许在树外进行编译。
此外,这些进程还需要 fuchsia.fuzzer.ProcessProxy
客户端实现。上述相同的工具链变体可以自动添加一个依赖项,以将树内模糊测试程序的进程与远程库关联起来。
从模糊测试分类的角度来看,远程库提供以下功能:
- 通过回调(例如
__sanitizer_cov_inline_8bit_counters_init
)收集反馈。 - 与
fuzzer_engine
的ProcessProxy
的早期启动连接。 - 可检测错误的后台线程,例如通过监控异常、内存用量等。
外部模糊测试工具可以提供自己的客户端实现。将 fuchsia.fuzzer.ProcessProxy
FIDL 接口和远程库实现添加到 SDK 中,有助于更轻松地编写外部模糊测试工具。
最后,所需的编译时修改仅是对 LLVM IR 的转换。所有其他修改仅在链接时进行。这样,服务提供商就可以向愿意为其组件提供 LLVM 字节码的 SDK 使用方提供“模糊测试即服务”,而无需提供源代码。
组件拓扑
综合上述所有内容,模糊测试组件拓扑包括:
core
:系统根组件。fuzz_manager
:在根 Realm 中建立 fuzzer 与宿主工具之间的桥接。test_manager
:如测试运行程序框架中所述。target_fuzzer
:模糊化 Realm 入口点。fuzzer_engine
:与目标无关的模糊测试驱动程序。target_adapter
:包含用户提供的输入处理代码的目标专用组件。instrumented_target
:正在模糊测试的组件。
adapter
和 target
组件可能还有其他子级,例如模拟对象和要进行模糊测试的目标 Realm。
上述各部分之间的交互可以如下图所示:
FIDL 接口
该框架添加了两个 FIDL 库:一个用于与 fuzz_manager
交互,另一个用于与 fuzzer 本身交互。
fuchsia.fuzzer.manager
fuchsia.fuzzer.manager
定义的类型包括:
LaunchError
:一个可扩展的enum
,用于列出与查找和启动模糊测试工具相关的错误。
fuchsia.fuzzer.manager
定义的协议包括:
fuchsia.fuzzer.manager.Coordinator
:由fuzz_manager
通过ffx
向用户提供。包含用于启动模糊测试工具和连接fuchsia.fuzzer.Controller
的方法,以及用于停止模糊测试工具的方法。fuchsia.fuzzer.manager.Harness
:由fuzz_manager
通过core
和test_manager
的静态路由传送到fuzz_test_runner
。运行程序使用此协议将通道的任一端传递给可用于fuchsia.fuzzer.Controller
协议的管理器。
fuchsia.fuzzer
fuchsia.fuzzer
定义的类型包括:
Options
:可扩展的table
,包含用于配置执行、错误检测等的参数。Feedback
:一个灵活的union
,表示目标反馈,例如代码覆盖率、轨迹、时间等。Status
:一个可扩展的table
,包含各种模糊测试指标,例如总覆盖率、速度等FuzzerError
:一个可扩展的enum
,用于列出错误类别,例如 ClusterFuzz 识别的错误类别。
fuchsia.fuzzer
定义的协议包括:
fuchsia.fuzzer.Controller
:由fuzzer_engine
提供,并通过fuzz_test_runner
传递给fuzz_manager
。由fuzz_manager
代理到用户。包含用于将输入传输到模糊测试工具或从模糊测试工具传输输入的方法,以及在模糊测试工具上执行工作流程(例如输入最小化、语料库合并和常规模糊测试)。fuchsia.fuzzer.CorpusReader
:从fuchsia.fuzzer.Controller
请求。用于从特定种子或实时语料库获取输入。fuchsia.fuzzer.CorpusWriter
:从fuchsia.fuzzer.Controller
请求。用于向特定种子或实时语料库添加输入。fuchsia.fuzzer.Adapter
:由开发者提供的target_adapter
提供给fuzzer_engine
。包含用于注册协调事件对和用于发送测试输入的共享 VMO 的方法。fuchsia.fuzzer.ProcessProxy
:fuzzer_engine
向模糊化 realm 中的每个插桩进程提供的标记。包含用于注册协调事件对和用于注册用于提供反馈的共享 VMO 的方法。
构建实用程序
该框架为开发者提供了 fuchsia_fuzzer_package
GN 模板。这样,他们就可以:
- 自动添加 fuzzer_engine。
- 生成可供工具使用的元数据,例如种子语料库的位置。
- 选择非模糊测试工具链变体时,构建集成测试,而不是模糊测试工具,如测试部分所述。
- 重复使用相关集成测试中被测组件的构建规则。
该框架还包含一个组件清单分片,其中包含模糊测试工具所需的常见元素,例如 fuzzer_engine
及其功能、fuzz_test_runner
等。模糊测试工具的组件清单由以下部分组成:
- 默认的 fuzzer 分片。
- 指向目标适配器组件的网址。
- 指向要进行模糊测试的组件清单的网址。这通常应可从相关集成测试中重复使用。
这些构建实用程序旨在让模糊测试开发体验类似于集成测试开发体验。对比:
实现
实现计划非常简单:在一系列更改中开发和单元测试各个类,然后组装从 libFuzzer 派生的集成测试(如测试部分所述)。
语言
fuzzer_engine
和 remote_library
采用 C++ 实现,以便于处理其自身的特性:
fuzzer_engine
和remote_library
都必须与其他 C ABI(例如 libMutagen、SanitizerCoverage 等)集成。- 大多数
remote_library
功能都会在“main
之前和exit
之后”发生,即在构建和/或加载 LLVM 模块时、运行atexit
处理程序时或引发严重异常时。因此,该框架需要对 ELF 可执行文件生命周期的细微细节进行明确控制。
其他部分(例如 realm_builder_adapter
)则是用 Rust 编写的。
数据传输协议
在以下几种情况下,用户需要能够提供或检索任意数量的数据,包括:
- 提供要执行、清理或最小化处理的特定测试输入。
- 将 fuzzer 词库与开发者主机上的词库同步,或跨多个 ClusterFuzz 实例同步。
- 提取触发错误的测试输入。
为了最大限度地减轻维护负担,建议使用通过网络传输此类数据。不过,任何单次传输都可能超出通过 Zircon 通道传输的单个 FIDL 消息的大小。相反,Controller
协议包含多种方法,用于提供 zx_socket
对象,模糊测试引擎会使用这些对象将数据从 VMO 和/或本地存储的文件流式传输到或从这些对象。
系统使用最小协议流式传输数据,以读取或写入命名字节序列。该协议不是 FIDL,因为发送的数据可能会超过 FIDL 消息的最大长度。不过,命名字节序列在概念上等同于以下 FIDL 结构体:
struct NamedByteSequence {
uint32 name_length;
uint32 size;
bytes:name_length name;
bytes:size data;
};
堆栈展开
目前,libFuzzer 使用的是 LLVM 中的 unwinder,该 unwinder 假定它是从触发信号的线程上执行的 POSIX 信号处理程序调用的。对于 Fuchsia,这需要采用复杂的方法来处理异常,包括修改崩溃线程的堆栈并注入可保留回溯的汇编跳板,以便在解除器中“复活”线程。
如果 libFuzzer 不处理错误,则无需执行上述任何操作。而是会以最方便、最有效的方式处理不同类型的错误,例如:
- 异常由模糊测试工具引擎处理,该引擎会从模糊测试运行程序接收异常通道,该通道是通过其对测试作业的句柄创建的。
- 超时也由 fuzzer 引擎管理。
- Sanitizer 回调和 OOM 由远程库处理,该库会通知 fuzzer 引擎。
性能
模糊测试不会在生产系统上执行,因此不会影响任何正式版代码的性能。虽然添加模糊测试工具链变体确实会对构建 Fuchsia 的性能产生轻微影响,但此框架将重复使用现有变体,并且不会产生任何新影响。
同样,在未插桩 build 上通过 fuzzer 生成单元测试与当前方法类似,并且预计不会比当前方法增加任何显著的每个 fuzzer 测试费用。
对于 fuzz 工具本身,用于确定 fuzz 工具质量的最重要指标是单位时间内的覆盖率,可通过衡量另外两个指标派生得出:
- 在固定时间段内运行的模糊测试工具的总覆盖率。
- 在固定时间内执行的运行总次数。
ClusterFuzz 已在其信息中心内监控并发布了每个 fuzz 工具的这些指标。
工效学设计
人体工学是此设计的一个重要方面,因为其影响取决于开发者是否采用。
此框架尝试通过多种方式尽可能简化模糊测试。借助该工具,开发者可以:
- 如目标适配器部分所述,以熟悉且灵活的方式编写模糊测试工具。
- 使用现有的 GN 模糊测试模板系列构建模糊测试工具。
- 使用熟悉的工作流运行 fuzz 工具。
ffx fuzz
的用法有意与fx fuzz
类似。 - 获取富有实用价值的结果。通过与 ClusterFuzz 集成,系统会自动提交 bug,并附上符号化回溯和重现说明。
向后兼容性
现有的基于 libFuzzer 的模糊测试工具会实现模糊测试目标函数。通过提供 libFuzzer 专用的模糊测试目标适配器,这些模糊测试工具将能够在该框架中运行,而无需进行任何源代码修改。
安全注意事项
此框架不会用于配送商品配置。对于采用模糊测试配置的内置设备,与设备之间的通信将使用 overnet
和 ffx
提供的现有身份验证和安全通信功能。
模糊测试输出可能存在安全注意事项,例如测试输入可能会导致可利用的内存损坏。这些问题必须由模糊测试操作者(人工或模糊测试基础架构)处理,处理方式与处理任何其他可利用的 bug 报告相同(例如正确标记、防止未经授权的披露等)。
隐私注意事项
在考虑隐私权影响时,我们不会对模糊测试运算符如何处理模糊测试输出做出任何假设。这些输出包括符号化日志、导致错误的输入、生成的字典和生成的语料库。我们假定日志中已不含用户数据,因为这是一个单独且受到密切监控的隐私问题。其余输出均直接派生自测试输入。因此,为了确保 fuzzer 输出不含用户数据,必须确保 fuzzer 输入不含用户数据。
有三种方法可以将输入添加到 fuzz 工具的语料库中:
- 作为种子输入。种子语料库应签入到源代码库中。适用针对在源代码库中包含用户数据的常规限制。
- 作为对实时语料库的手动添加内容。
- 这通常由模糊测试基础架构(例如 ClusterFuzz)完成,因为它会将其他实例生成的输入“交叉授粉”给模糊测试工具。在这种情况下,其他实例不会包含用户数据,添加的输入也不会。
- 人工操作员还可以通过
ffx
添加输入。以这种方式添加手动输入内容时,该工具会显示有关用户数据的警告。
- 作为对实时语料库的生成式补充。这些输入是从现有输入中变异而来。由于这些输入不含用户数据,因此生成的数据也不含用户数据。某些输入可能纯粹是出于偶然因素而与某些用户数据匹配,例如,模糊测试工具设法生成了有效的用户名。不过,在本例中,与用户数据之间没有明确的关联。
该语料库不包含任何其他数据,即使模糊测试工具是非密封的(并且非确定性的!)并且使用测试领域公开的来源中的数据也是如此。该框架不会将这些数据视为测试输入的一部分,也不会将其保存下来。
最糟糕的情况是,模糊测试工具故意设计为非密封的,并使用公开的功能将数据发送到用于验证个人身份信息(例如返回用户名是否有效)的某些其他服务。这需要付出大量精力来规避模糊测试和测试框架,以期提高密封性。由于外部服务未插桩,因此这与随机猜测没有什么区别。
此外,在实践中,模糊测试工具将完全密封。这些测试不会在包含用户数据的产品配置上运行,只会在开发模糊测试工具时在本地运行,以及在 ClusterFuzz 上运行。
测试
模糊测试引擎、目标适配器库和远程库使用常规方法(例如 GoogleTest、#[cfg(test)]
等)进行单元测试。此外,集成测试会使用默认的 ELF 测试运行程序,根据 compiler-rt 中的适用子集,使用专用示例目标运行一组模糊测试工作流。
对于使用该框架编写的 fuzz 工具,该框架将采用与 GN fuzz 工具模板目前支持的方法相同的方法:在未插桩 build 中构建 fuzz 工具时,引擎将被替换为仅执行种子语料库中的每个输入的测试驱动程序。这通过确保所有模糊测试工具都可以构建和运行来缓解“位腐烂”问题。它还可用作回归测试,尤其是在模糊测试作者在修复模糊测试中发现的缺陷时通过添加输入来维护其种子语料库时。
文档
需要更新模糊测试文档树,在其中添加使用新 GN 模板的具体示例。任何其他计划中的文档更改(例如 Codelab 等)也应反映此框架。
缺点和替代方案
所提方法的潜在缺点包括:
- 性能下降的风险,通过实现紧密模仿高度优化模糊测试工具的性能关键部分来缓解。
- 维护负担,但无需维护不便的集成(例如 POSIX 模拟)可抵消这一点。
- 耦合风险,例如,测试运行器框架日后可能会以破坏此设计的方式发生变化,或者可能无法因此设计而发生变化。如果将来出现此问题,可以通过将更多
test_manager
功能直接纳入fuzz_manager
来解决,例如让后者直接创建隔离的测试领域。
这些缺点不如我们探讨过的其他替代方案的缺点严重:
仅使用 libFuzzer 进行库模糊测试。
我们在 libFuzzer 中添加了足够的 Fuchsia 支持,以便在 Fuchsia 上使用它构建 fuzzer。在过去几年里,这些工具成功发现了数百个 bug。
同时,它们仅限于结构为库的单个进程。由于组件是 Fuchsia 上可执行软件的单元,并且组件通过 FIDL 进行广泛通信,因此通过这种方法,越来越多的 Fuchsia 代码无法“模糊化”。
进程内 FIDL 模糊测试。
Chrome 等项目尝试通过在单个进程中运行客户端和服务器线程来解决 RPC 模糊测试问题。这需要修改客户端和服务器,以便在新的非标准配置中运行。这可以在服务之间重复使用,但往往会对组件生命周期和/或每种语言绑定重新实现做出不灵活的假设。
更根本的是,对互动组件的闭包进行模糊测试变得越来越困难。许多组件具有非琐碎的拓扑。从复杂性、开销和性能方面来看,运行或模拟整个闭包很快就会变得不可持续。
这种方法已在 Fuchsia 上推出,但至少在一定程度上,由于存在这些限制,尚未得到广泛采用。
单服务 FIDL 模糊测试。
在设计跨进程 FIDL 模糊测试框架的初始尝试中,我们考虑了单个客户端和服务。在此设计中,libFuzzer 与服务相关联,客户端则作为简单的代理进行维护。通过保留客户端和服务器之间的 FIDL 接口,它可以使目标保持在更典型的配置中,从而实现更灵活的服务生命周期,并减少需要重新实现的代码。
不过,它无法解决组件闭包模糊测试的问题,因此相较于进程内 FIDL 模糊测试,其优势非常有限。
支持跨进程模糊测试的 LibFuzzer。
一般而言,与重新实现代码相比,重复使用代码有几个优势:代码通常更“成熟”,性能更好,错误更少,并且共享的维护成本更低。出于这些原因,之前的另一项尝试是尝试扩展 libFuzzer,而不是设计和实现新的模糊测试框架。新的编译器运行时 clang_rt.fuzzer-remote.a
将充当上述远程库,而 libFuzzer 本身可用作引擎。这两个编译器运行时都会使用一对操作系统专用 IPC 传输库来代理对另一个进程的方法调用。
我们与 libFuzzer 的维护者协作,对这两个运行时实施了一系列更改,并将其发布以供审核。此外,我们还针对 Linux 和 Fuchsia 开发了 IPC 传输库的实现。维护者明确请求了 Linux 支持,以便进行持续测试,因此我们再次将其送审。
- 在 Linux 上,共享内存是作为匿名映射文件(即通过
memfd_create
)创建的,而信号只是通过 Unix 域套接字传递的消息。这些套接字还用于传输共享内存文件描述符,即通过sendmsg
和recvmsg
。 - 在 Fuchsia 上,共享内存是使用 VMO、通过 eventpair 发送信号以及通过 FIDL 消息进行交换实现的,实现方式与此提案中的设计类似。
遗憾的是,在延长审核期期间,这种方法变得不可行,原因并非技术方面,而是流程方面:随着时间的推移,libFuzzer 维护者越来越担心,为了让 libFuzzer 以其最初未设计的方式运行,所需的必要更改范围。最终,该团队决定无限期推迟发布提议的更改。
AFL
LibFuzzer 绝不是唯一的模糊测试框架。有些工具(例如 AFL)从一开始就明确设计为跨进程。不过,AFL 需要的投资可能比人们想象的要多,原因有以下几点:
- AFL 假定它正在对单个进程进行模糊测试,因此它仍然面临关闭问题。
- AFL 会大量使用某些 Linux 和/或 POSIX 功能来提供反馈和检测错误。这些信号包括 POSIX 信号,但更重要的是,大量使用
/proc
文件系统,而 Fuchsia 上没有类似的文件系统(这是正确的)。 - AFL 使用经过修改的 GCC 对代码进行插桩,而该工具并非 Fuchsia 工具链的一部分。
AFLplusplus 是 AFL 的改进分支,由一组安全研究人员和 CTF 竞赛选手维护。它在 FuzzBench 上表现出色,并且具有模块化 AFL。遗憾的是,第一个版本已废弃,第二个版本尚未准备就绪(或者至少还不够成熟,无法强制更改上述设计)。不过,仍有几个部分与此提案的设计保持一致,未来有机会将它们集成以提高框架的覆盖率和/或速度。
结合使用 AFL 和 QEMU
此外,还有一些项目将 AFL 与 qemu 结合使用:
- afl-unicorn 将 AFL 与 Unicorn 结合使用,后者是一个项目,可通过相当简洁的界面公开 qemu 的 CPU 模拟核心。这样,您就可以通过从 CPU 模拟中收集覆盖率反馈,对没有源代码的不透明二进制文件进行模糊测试。它不适合组件框架,原因如下:
- 与 qemu 的核心 CPU 模拟集成非常复杂,因此 Unicorn 决定不再跟踪 qemu 开发,并锁定到 v2.1.2(与 qemu 的当前版本 6.0.0 相比)。预期使用较新模拟功能的代码不太可能正常运行。
- 没有明显的需求来进行不透明二进制模糊测试。事实上,该设计只要求对目标代码进行插桩并与远程库相关联;LLVM 字节码就足以实现这一点。
- TriforceAFL 会对完整的插桩 qemu 实例使用 AFL。这样一来,我们就可以通过从 qemu 本身收集覆盖率,在不使用源代码的情况下对不透明二进制文件进行模糊测试。与 afl-unicorn 类似,它不适合以下原因:
- 再次重申,没有必要对不透明二进制文件进行模糊测试。
- 此外,由于收集的覆盖率是整个实例的覆盖率,因此使用 TriforceAFL 进行模糊测试时噪声往往很大,尤其是在运行许多组件时。它通常仅适用于对极其受限的配置进行模糊测试,例如刚启动后的 USB 驱动程序。