RFC-0117 - 组件模糊测试框架

RFC-0117:组件模糊测试框架
状态已接受
领域
  • 测试
说明

Fuchsia 原生跨进程模糊测试框架。

问题
  • 24740
Gerrit 更改
  • 529348
作者
审核人
提交日期(年-月-日)2021-05-24
审核日期(年-月-日)2021-07-28

总结

引导式模糊测试可有效减少 bug 并提高平台信心,但目前还没有一种模糊框架可以跨多个进程边界进行模糊测试,如 Fuchsia 组件拓扑中所述。本文档针对这样一种框架提出了一种设计,该框架可跨进程和测试领域共享覆盖率测试输入,从而允许组件在其最典型的配置中进行模糊测试。

设计初衷

程序测试可用于显示是否存在 bug,但绝不能表明不存在 bug!

--Edsger W. Dijkstra

引导式模糊测试是使用生成的数据在反馈环中测试软件的过程:

  1. 模糊测试工具会生成一些测试输入数据,并使用这些数据来测试目标软件。
  2. 如果测试失败,模糊测试工具会记录输入并退出。
  3. 目标软件会生成由模糊测试工具收集的反馈
  4. 模糊测试工具会根据反馈生成额外的测试输入,然后重复输入。

引导式模糊测试对于查找与项目要求无关(因此通常未经测试)的软件错误非常有用。通过自动执行测试覆盖范围,还可以提高开发者对系统中具有安全性正确性和/或稳定性注意事项的关键部分的信心。

可以使用以下分类来描述引导式模糊测试框架:
模糊测试分类

  • 引擎:与目标无关的反馈循环。
    • 语料库管理:维护一组模糊测试输入(即语料库)。记录新的输入并修改现有输入(例如合并)。
      • 种子语料库是一组手工制作的初始输入。
      • 实时语料库是一组不断更新的生成的输入。
    • Mutator:一组突变策略和确定性伪随机性来源,用于从语料库创建新输入。
    • 反馈分析:根据输入内容的反馈对其进行处理。
    • 管理界面:与用户互动以协调工作流程:
      • 使用特定的输入来运行目标,即执行一次模糊测试工具运行
      • 对目标进行模糊测试,即执行一系列(可能无限期)的模糊测试工具运行。
      • 分析或操纵给定语料库。
      • 响应检测到的错误和/或处理导致错误的工件。
  • Target:要进行模糊测试的特定目标领域。
    • 输入处理:将单次运行的模糊测试工具输入映射到被测代码,例如通过特定函数、I/O 管道等。
    • 反馈收集:观察由输入引起的行为。可能会收集硬件或软件跟踪记录、代码覆盖率数据、计时等。
    • 错误检测:确定输入何时导致了错误。收集并记录测试工件,例如输入、日志、回溯等。

其中某些方面可能需要操作系统和/或其工具链提供特定支持,例如反馈收集和错误检测。目前在 Fuchsia 上,最完全受支持的模糊测试框架是 libFuzzer,它通过预构建的 clang 工具链作为编译器运行时提供。已向用于收集代码覆盖率反馈的 sanitizer_common 运行时以及 libFuzzer 本身提供相关支持,以检测异常。除了一组 GN 模板主机工具之外,开发者还可利用这些工具快速为 Fuchsia 上的库开发模糊测试工具。

不过,与 Linux 不同,在 Fuchsia 上,软件的基本可执行单元是组件,而不是库。使用现有的引导式模糊测试框架对组件进行模糊测试非常麻烦,因为其反馈的粒度太窄(例如单个进程中的 libFuzzer)或过于宽泛(例如对 qemu 实例的 TriforceAFL)。

对 Fuchsia 中的组件进行模糊测试的理想框架具有以下特征:

  • 与现有持续模糊测试基础架构集成,例如 ClusterFuzz
  • 一种模块化方法,可以利用其他模糊框架中与平台无关的部分,例如突变策略。
  • 一种高性能的跨进程代码覆盖率机制。
  • 与现有 Fuchsia 工作流集成,例如 ffx
  • 一个封闭环境,可隔离受测组件和/或为其依赖项提供模拟组件。
  • 目标组件的来源未经修改。
  • 一种强大且灵活的方法来分析执行情况和检测错误。
  • 一个开发者故事,类似于 Fuchsia 中其他测试方式。

设计

此设计试图:

  • 使用 Fuchsia 的惯用语言。
  • 重复使用现有实现。

概括来讲,该设计利用了测试运行程序框架,并添加了以下内容:

  • 一个用于推动模糊测试的 fuzzer_engine
  • ffx 插件和模糊测试管理器,用于与模糊测试工具进行交互和管理。
  • 一个 fuzz_test_runner,用于将 fuzzer_engine 连接到模糊测试管理器。


组件模糊测试框架设计

本文档的这一部分按照控制流进行大致结构;也就是说,文档首先以想要执行模糊测试任务的人类或机器人为起点,然后面向进行模糊测试的目标领域。读者应注意,某些部分会引用后续部分中详细介绍的概念。

ffx fuzz 托管工具

用户(包括人类和机器人)通过 ffx 插件与框架进行交互。此插件将可以通过以下方式与 fuzz_manager 服务通信:

ffx fuzz 的子命令与 fx fuzz 的子命令相同,例如:

  • analyze:报告指定语料库和/或字典的覆盖范围信息。
  • check:检查一个或多个模糊测试工具的状态。
  • coverage:为测试生成覆盖率报告。
  • list:列出当前 build 中可用的模糊测试工具。
  • repro:通过重放测试单元来重现模糊测试工具发现结果。
  • start:启动特定的模糊测试工具。
  • stop:停止特定的模糊测试工具。
  • update:更新模糊测试工具语料库的 BUILD.gn 文件。

模糊测试管理器

测试运行程序框架提供了两个重要功能:

  • 借助该工具,您可以轻松创建复杂而封闭的测试领域,并通过可自定义的测试运行程序来驱动这些领域。
  • 它提供了收集重要诊断信息(例如日志和回溯)的方法。

此外,单次模糊测试运行在组件测试框架的术语中可以自然地表达:代码使用给定的测试输入执行,并且可根据是否出现错误来被视为通过或失败。

但是,模糊测试与其他测试形式不同,在将连续模糊测试与连续测试进行比较时,这种差异会加以放大:

  • 测试输入事先未知。
    • 测试输入是通过模糊测试生成的。
    • 诸如 ClusterFuzz 的持续模糊测试基础架构将具有一个模糊测试工具的许多实例,并且在进行模糊测试时会“交叉轮询”其测试输入。
  • 执行模糊测试是开放式的。模糊测试永远不会真正“通过”,它们只会失败或提前停止。
    • 因此,需要提供按需状态,其中包括其他测试通常不会提供的详细信息,例如执行速度、收集的总反馈、消耗的内存等。
    • 需要持续向监控模糊测试工具执行情况的人工或模糊基础架构机器人提供此状态。
  • 模糊测试的结果比简单的通过/失败测试结果要更丰富。
    • 如果失败,输出需要包含触发输入以及所有关联的日志和回溯。
    • 提前终止阶段,输出可能包括累计反馈和推荐参数(例如字典),以用于将来的模糊测试。
  • 模糊领域可用于模糊测试基础架构选择连续执行的若干不同工作流,例如“一段时间模糊。如果发现错误,请进行清理,否则,合并和压缩语料库”。如果将每个步骤表示为测试套件,则会导致从一个步骤提取状态的繁重工作,而只在下一个步骤中恢复状态。

其中一些问题可以通过扩展测试运行程序框架来解决,例如,它可以提供结构化输出。不过,如果针对所有模糊测试需求使用此方法,则会为其他不需要这些功能的测试增加重要的功能。因此,设计添加了一个新的 fuzz_manager,它具有以下特点:

  • 通过 ffx 为用户提供管理界面。
  • test_manager 交互,以在测试运行程序框架中的模糊领域内启动模糊测试工具。
  • 为这些模糊测试工具提供 fuchsia.fuzzer.manager.Harness,以便重新连接和服务用户请求。
  • 提供数据传输协议,有助于将数据注入到模糊测试工具中或从模糊测试工具中提取数据。

然后,测试运行程序框架按如下方式修改:

  1. 添加了新的 fuzz_test_runner。此运行程序基于现有 elf_test_runner 进行构建,以启动 fuzzer_engine 并向其传递模糊测试工具网址。
  2. test_manager 已修改为将 fuchsia.fuzzer.manager.Harness capability 路由到 fuzz_test_runner。此功能不会送交测试,并且非模糊测试工具的封闭性不受影响。
  3. fuzz_test_runner 用于为 fuchsia.fuzzer.Controller 协议创建通道对。它会将一端作为启动句柄安装在 fuzzer_engine 中,并使用 fuchsia.fuzzer.manager.Harness 将另一端传递给 fuzz_manager

模糊测试工具引擎

fuzzer_engine 是模糊领域的一个组件。就模糊测试工具分类而言,它:

  • 实现 fuchsia.fuzzer.Controller 协议以提供管理接口
  • 创建并使用存储空间容量来管理每个语料库
  • 从语料库 Mutate 输入以创建新的测试输入。(例如,指向 libMutagen 的链接)。
  • UsesAdapter 功能,用于发送要处理的新输入
  • Exposes 是对模糊领域中的远程进程进行插桩的 fuchsia.fuzzer.ProcessProxy 功能,可用于提供收集的反馈报告错误
  • 分析反馈

如果将模糊测试视为具有不同输入的一系列测试,一种方法是让模糊测试引擎为每个输入实例化一个新的测试领域,即让测试运行程序连续执行每个模糊测试runnerrunner这种方法的主要问题在于反馈分析和变更循环的性能。模糊测试工具的质量与吞吐量直接相关,而主循环必须非常快:“更改、处理输入、收集反馈和分析反馈”的开销应该大约为微秒。

因此,模糊测试工具引擎纳入测试领域本身的方式与用于测试复杂拓扑的测试驱动程序类似。由 eventpair 协调的共享 VMO 用于将测试输入传输到模糊测试目标适配器,以及来自插桩远程进程的反馈,从而尽可能缩短延迟时间。

模糊测试工具引擎由 fuzz_test_runner 启动。此运行程序与现有的 elf_test_runner 非常相似,但主要添加了一个:为 fuchsia.fuzzer.Controller 协议创建一个通道对。它会将此键值对的一端作为启动句柄安装在 fuzzer_engine 中。它会使用由 test_manager 路由到它的 fuchsia.fuzz.manager.Harness 功能将另一个请求传递给 fuzz_manager。这样,test_manager 就可以仅向 fuzz_test_runner 及其启动的模糊测试工具提供 Harness 功能,而不是为所有测试提供。

目标适配器

模糊测试目标适配器在模糊测试工具分类中充当输入处理角色。使用上述共享 VMO 和事件对,它会接受模糊测试工具引擎生成的测试输入,并将它们映射到与要进行模糊测试的目标领域插桩远程进程的特定互动。

这些特定交互由模糊测试工具作者提供,通常称为“编写模糊测试工具”。

模糊测试工具作者可以自行提供模糊测试目标适配器的自定义实现,也可以使用所提供的其中一个 Scaffold。

可能的适配器 Scaffold 示例包括:

  • 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 模糊测试目标函数之外,作者还实现了用于修改所提供的 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)进行构建。对于树内构建的模糊测试工具,这可通过将 flagsdeps 传播到 GN 目标的依赖项的工具链变体来实现。将记录必需的标志(例如 -fsanitize-coverage=inline-8bit-counters),以便也允许进行树外编译。

此外,这些进程还需要一个 fuchsia.fuzzer.ProcessProxy 客户端实现。上述工具链变体可自动添加依赖项,以便将树内模糊测试工具的进程与远程库相关联。

模糊测试分类的角度来看,远程库提供以下功能:

  • 通过回调(例如 __sanitizer_cov_inline_8bit_counters_init收集反馈
  • fuzzer_engineProcessProxy 的早期启动连接。
  • 后台线程,可以检测错误,例如通过监控异常、内存使用情况等。

树外模糊测试工具可以提供自己的客户端实现。向 SDK 添加 fuchsia.fuzzer.ProcessProxy FIDL 接口和远程库实现可更轻松地编写树外模糊测试工具。

最后,所需的编译时修改只是 LLVM IR 上的转换。所有其他修改都仅限于链接时。这样,服务提供商便可以为愿意为其组件提供 LLVM 字节码的 SDK 使用者提供“模糊测试即服务”,而无需提供源代码。

组件拓扑

综上所述,模糊测试工具组件的拓扑包括:

  • core:系统根组件。
  • fuzz_manager:在模糊测试工具与主机工具之间的根领域。
  • test_manager:与测试运行程序框架中一样。
  • target_fuzzer:模糊领域入口点。
  • fuzzer_engine:与目标无关的模糊测试驱动程序。
  • target_adapter:包含用户提供输入处理代码的目标专用组件。
  • instrumented_target:对组件进行模糊测试。

adaptertarget 组件可能会有额外的子项,例如模拟和被模糊测试的目标领域。

上述部分之间的交互可如下图所示:
模糊测试框架拓扑

FIDL 接口

该框架添加了两个 FIDL 库:一个用于与 fuzz_manager 交互,另一个用于与模糊测试工具本身交互。

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 通过 coretest_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 提供给模糊领域的每个插桩进程。包括用于注册协调事件对和注册用于提供反馈的共享 VMO 的方法。

构建实用程序

框架为开发者提供了一个 fuchsia_fuzzer_package GN 模板。 这样,他们就可以:

  • 自动添加模糊测试引擎。
  • 生成可供工具使用的元数据,例如,种子语料库的位置。
  • 测试部分所述,选择非模糊工具链变体时,构建集成测试,而不是模糊测试工具。
  • 重复使用相关集成测试中被测组件的构建规则。

该框架还包含一个组件清单分片,其中包含模糊测试工具所需的常见元素,例如 fuzzer_engine 及其功能、fuzz_test_runner 等。模糊测试工具的组件清单包含以下内容:

  • 默认模糊测试工具分片。
  • 目标适配器组件的网址。
  • 要进行模糊测试的组件的清单的网址。该 ID 通常应该可以从相关集成测试中重复使用。

这些构建实用程序共同,旨在打造类似于集成测试开发体验的模糊测试工具开发体验。比较:
测试和模糊测试工具开发流程

实现

实现计划非常简单:开发一系列更改中的各个类并对其进行单元测试,然后组合派生自 libFuzzer 的集成测试(如测试部分所述)。

语言

fuzzer_engineremote_library 采用 C++ 实现,以简化其特性:

  • fuzzer_engineremote_library 都必须与其他 C ABI 集成,例如 libMutagenSanitizerCoverage
  • 大多数 remote_library 功能发生在“main 之前和 exit 之后”,即在构建和/或加载 LLVM 模块时、运行 atexit 处理程序时或引发严重异常时。因此,框架需要明确控制 ELF 可执行文件生命周期的细微细节。

其他部分(例如 realm_builder_adapter)是用 Rust 编写的。

数据传输协议

在以下几种情况下,用户需要提供或检索任意数量的数据,包括:

  • 提供特定的测试输入,以执行、清理或最小化。
  • 将模糊测试工具语料库与开发者主机上的文档库进行同步,或者跨多个 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 的展开程序,该程序假定由在触发信号的线程上执行的 POSIX 信号处理程序调用。对于 Fuchsia,这需要采用一种复杂的方法来处理异常,包括修改崩溃线程的堆栈和注入保留回溯的汇编 trampoline,以在展开程序中“恢复”线程。

如果 libFuzzer 未处理错误,就不需要这些信息。而是以最方便、最有效的方式处理不同类型的错误,例如:

  • 异常由模糊测试工具引擎处理,该引擎会从其创建的模糊测试运行程序接收异常渠道(通过其句柄发送到测试作业)。
  • 超时也由模糊测试工具引擎管理。
  • 排错程序回调和 OOM 由远程库处理,远程库会通知模糊测试工具引擎。

性能

模糊测试不是在生产系统上执行,因此不会影响任何交付代码的性能。虽然添加模糊测试工具链变体确实会对构建 Fuchsia 的性能产生轻微的影响,但此框架将重复使用现有变体,并且不会添加新的影响。

同样,在非插桩 build 上通过模糊测试工具生成单元测试也是当前方法的镜像,预计使用当前方法时不会增加每个模糊测试工具的测试成本。

对于模糊测试工具本身,确定模糊测试工具质量的最关键指标是每单位时间的覆盖率,该指标可以通过测量另外两个指标得出:

  1. 在固定时间内运行的模糊测试工具的总覆盖率。
  2. 在固定时间内执行的运行总数。

ClusterFuzz 已在其信息中心监控并发布每个模糊测试工具的这些指标。

工效学设计

人体工学是此设计的一个重要方面,因为其影响取决于开发者的采用情况。

该框架尝试以几种方式尽可能简化模糊测试。它使开发者能够:

  • 目标适配器部分所述,以熟悉的方式和灵活的方式编写模糊测试工具。
  • 使用现有的 GN 模糊测试模板系列构建模糊测试工具。
  • 使用熟悉的工作流运行模糊测试工具。ffx fuzz 的用法特意类似于 fx fuzz
  • 获取切实可行的结果。与 ClusterFuzz 集成后,系统会自动提交 bug 以及符号化的回溯和重现指令。

向后兼容性

基于 libFuzzer 的现有模糊测试工具会实现模糊测试目标函数。通过提供特定于 libFuzzer 的模糊目标适配器,这些模糊测试工具将无需任何源代码修改即可在此框架中运行。

安全注意事项

此框架不会用于运费商品配置。对于内置模糊配置的设备,与设备之间的通信将使用由 overnetffx 提供的现有身份验证和安全通信功能。

模糊测试工具输出可能需要考虑安全因素,例如,测试输入可能会导致可利用的内存损坏。这些问题必须由模糊测试工具运算符(人工或模糊测试基础架构)处理,处理方式必须与任何其他可利用的 bug 报告相同(例如正确标记、防止未经授权的披露等)。

隐私注意事项

在考虑隐私影响时,我们不会对模糊测试工具运算符如何处理模糊测试工具输出进行任何假设。这些输出包括符号化日志、导致错误的输入、生成的字典和生成的语料库。系统会假定日志中不含用户数据,因为这是另一个受到密切监控的隐私权问题。其余输出都直接由测试输入派生而来。因此,使模糊测试工具输入中不含用户数据是必要且足够的事情,以使模糊测试工具输出中没有用户数据。

您可以通过以下三种方式向模糊测试工具的语料库添加输入:

  • 作为种子输入。种子语料库应签入源代码库。针对在源代码库中包含用户数据的常规限制同样适用。
  • 手动添加到实时语料库。
    • 这通常由模糊测试基础架构(例如 ClusterFuzz)完成,因为它与其他实例生成的输入“交错”模糊测试工具。在这种情况下,其他实例不会包含用户数据,添加的输入也不会包含用户数据。
    • 人类操作员也可以通过 ffx 添加输入。以这种方式添加手动输入时,该工具将显示有关用户数据的警告。
  • 以生成的形式添加到实时语料库。这些输入会从现有输入中发生改变。由于这些输入不含用户数据,因此生成的输入也不含数据。某些输入可能会纯粹地匹配某些用户数据,例如模糊测试工具成功生成了一个有效的用户名。但在这种情况下,与用户数据没有明确的关联。

语料库中不包含任何其他数据,即使模糊测试工具为非封闭式(也非确定性!),并使用来自测试领域所公开来源的数据,也是如此。框架不会将该数据视为测试输入的一部分,也不会保存该数据。

最糟糕的场景是使用模糊测试工具,其设计意图是有意实现非封闭性,并使用公开功能将数据从测试领域发送到验证个人身份信息的其他服务,例如返回用户名是否有效。这需要投入大量精力来规避模糊测试和测试框架(旨在提升封闭性)。此外,由于外部服务是无插桩的,因此这不比随机猜测更好。

此外,在实践中,模糊测试工具是完全封闭的。它们不会针对包含用户数据的产品配置运行,而是仅在开发模糊测试工具和 ClusterFuzz 时在本地运行。

测试

使用常规方法(例如 GoogleTest#[cfg(test)] 等)对模糊测试工具引擎、目标适配器库以及远程库进行单元测试。此外,集成测试使用默认 ELF 测试运行程序,根据 compile-rt 的适用子集,使用专为特定目标构建的示例目标运行一组模糊测试工作流。

对于使用框架编写的模糊测试工具,框架将采用 GN 模糊测试工具模板当前支持的方法:在非插桩 build 中构建模糊测试工具时,引擎将被替换为一个测试驱动程序,该驱动程序仅执行种子语料库中的每个输入。这样可以确保所有模糊测试工具都可以构建和运行,从而减少“位衰减”问题。这也是一项回归测试,尤其是如果模糊测试工具作者在修复通过模糊测试发现的缺陷时,通过添加输入来维护其种子语料库。

文档

模糊测试文档树需要更新,以提供使用新 GN 模板的具体示例。计划的任何其他文档变更(例如 Codelab 等)也应体现此框架。

缺点和替代方案

上述方法的潜在缺点包括:

  • 通过紧密模拟高度优化的模糊测试工具的性能关键部分,实现降低性能下降的风险。
  • 维护负担,由于无需维护尴尬的集成(例如 POSIX 模拟)而节省的费用。
  • 耦合风险(例如测试运行程序框架)将来可能会以某种方式破坏此设计,或者由于这种设计而无法更改。如果此问题在将来成为了问题,可以通过将 test_manager 的更多功能直接集成到 fuzz_manager 中来解决此问题,例如让后者直接创建隔离的测试领域。

与已探索过的其他替代方案相比,这些缺点没有那么严重:

仅使用 libFuzzer 进行库模糊测试。

为 libFuzzer 添加了足够的 Fuchsia 支持,以便在 Fuchsia 上构建模糊测试工具。在过去几年中,我们成功发现了数百个 bug。

同时,它们仅限于以库结构构建的单个进程。由于组件是 Fuchsia 上的可执行软件单元,并且组件通过 FIDL 进行广泛通信,因此这种方法能够让大量越来越多的 Fuchsia 代码变得“易混淆”。


传统 libFuzzer

进程内 FIDL 模糊测试。

Chrome 等项目已尝试通过在单个进程中运行客户端和服务器线程来解决 RPC 模糊测试。这需要修改客户端和服务器,以在新的非标准配置中运行。这可以在服务之间重复使用,但往往倾向于对组件生命周期和/或每种语言绑定重新实现做出不灵活的假设。

从根本上说,很难对交互组件的“闭包”进行模糊化处理。许多组件的拓扑非常重要。运行或模拟整个闭包会导致复杂性、开销和性能迅速变得不可持续。

这种方法在 Fuchsia 上已可用,但至少在一定程度上由于存在这些限制,尚未得到广泛采用。


进程内 FIDL 模糊测试

单服务 FIDL 模糊测试。

在最初尝试设计跨进程 FIDL 模糊测试框架时,我们考虑了使用单一客户端和服务。在此设计中,libFuzzer 与服务相关联,客户端作为简单代理进行维护。通过在客户端和服务器之间保留 FIDL 接口,可使目标保持更典型的配置,从而实现更灵活的服务生命周期,减少需要重新实现的代码。

不过,该方法并不能解决对组件关闭进行模糊测试的问题,因此与进程内 FIDL 模糊测试相比,效果非常有限。


单服务 FIDL 模糊测试

支持跨进程模糊测试的 LibFuzzer。

一般而言,重复使用代码比重新实现代码有几个优势:代码通常更“成熟”,性能更好但 bug 更少,并且维护成本更低且需要共同承担的维护成本。出于这些原因,先前的另一项尝试是尝试扩展 libFuzzer,而不是设计和实现一个新的模糊测试框架。新的编译器运行时 clang_rt.fuzzer-remote.a 将充当上述远程库,而 libFuzzer 本身可以用作引擎。这两个编译器运行时都会使用一对操作系统专用的 IPC 传输库来代理对其他进程的方法调用。

与 libFuzzer 的维护人员协作后,我们进行了一系列更改,同时实施了这两个运行时并进行了发布以供审核。此外,还针对 Linux 和 Fuchsia 开发了 IPC 传输库的实现。维护者明确请求 Linux 支持以便持续测试,并且系统再次将其送交审核

  • 在 Linux 上,共享内存是作为匿名映射文件(即通过 memfd_create)创建的,信号只是通过 Unix 网域套接字传递的消息。这些套接字还用于传输共享内存文件描述符(即通过 sendmsgrecvmsg)。
  • 在 Fuchsia 上,共享内存通过 VMO、通过事件对的信号以及通过 FIDL 消息进行交换的方式实现,方式类似于此方案中的设计。

遗憾的是,在长时间的审核中,这种方法不再可行,不是出于技术原因,而是由于流程原因:随着时间的推移,libFuzzer 维护人员越来越关注使 libFuzzer 以并非最初设计的方式运行所需的必要更改的范围。最终,该团队决定无限期推迟实施提议的更改。


单服务 FIDL 模糊测试

美国橄榄球联盟

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 模拟的集成非常复杂,因此 Uncorn 决定放弃后续 qemu 开发,并锁定到 v2.1.2(与当前版本的 qemu 6.0.0 相比)。需要使用较新模拟功能的代码可能无法正常运行。
    • 没有明显需要不透明二进制模糊测试。事实上,该设计只要求对目标代码进行插桩和关联;LLVM 字节码足以实现这一点。
  • TriforceAFL 在完整的插桩 qemu 实例上使用 AFL。同样,这也可以通过从 qemu 本身收集覆盖率,对无源代码的不透明二进制文件进行模糊测试。它由于与 afl-unicorn 类似的原因而不适合:
    • 同样,也没有明显需要不透明二元模糊测试。
    • 此外,由于收集的覆盖率是整个实例的覆盖率,因此使用 TriforceAFL 进行模糊测试往往非常嘈杂,尤其是在运行许多组件时。它通常仅用于对极为受限的配置进行模糊测试,例如在启动后立即对 USB 驱动程序进行模糊测试。