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

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

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

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

摘要

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

设计初衷

程序测试可用于显示 bug 的存在,但绝不能用于显示 bug 的不存在!

--Edsger W. Dijkstra

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

  1. 模糊测试工具会生成一些测试输入数据,并使用这些数据来测试目标软件。
  2. 如果测试结果为失败,模糊测试器会记录输入并退出。
  3. 目标软件会生成由模糊测试工具收集的反馈
  4. 模糊测试器会使用反馈生成其他测试输入,并重复此过程。

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

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

  • 引擎:与目标无关的反馈环。
    • 语料库管理:维护模糊测试输入(即语料库)的集合。记录新输入并修改现有输入(例如合并)。
      • 种子语料库是一组精心制作的初始输入。
      • 实时语料库是一组不断更新的生成输入。
    • 变异器:一组变异策略和确定性伪随机性的来源,用于从语料库创建新的输入。
    • 反馈分析:根据输入内容的反馈来确定其处置方式。
    • 管理界面:与用户互动以协调工作流:
      • 使用特定输入来执行目标,即执行一次模糊测试器运行。
      • 对目标进行模糊测试,即执行(可能无限期)一系列模糊测试器运行。
      • 分析或处理给定的语料库。
      • 对检测到的错误做出响应和/或处理导致该错误的制品。
  • 目标:要模糊测试的特定目标领域。
    • 输入处理:将单次运行的模糊测试器输入映射到被测代码,例如通过特定函数、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:更新模糊测试器语料库的 BUILD.gn 文件。

模糊测试管理器

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

  • 借助可自定义的测试运行程序,您可以轻松创建复杂但密封的测试领域并对其进行驱动。
  • 它提供了一种收集重要诊断信息(例如日志和回溯)的方法。

此外,一次模糊测试运行可以自然地用组件测试框架的术语来表达:代码通过给定的测试输入来执行,并且可以根据是否发生错误来判断是已通过还是已失败。

不过,模糊测试与其他形式的测试有所不同,当将持续模糊测试持续测试进行比较时,这种差异会更加明显:

  • 测试输入不是先验已知的。
    • 测试输入是通过模糊测试生成的。
    • ClusterFuzz 等持续模糊测试基础架构将包含多个模糊测试工具实例,并会在模糊测试正在进行时“交叉传播”其测试输入。
  • 模糊测试执行是开放式的。模糊测试永远不会真正“通过”,只会失败或提前停止。
    • 因此,我们需要提供按需状态,其中包含其他测试通常不提供的详细信息,例如执行速度、收集的总反馈、消耗的内存等。
    • 需要持续向监控模糊测试工具执行情况的人员或模糊测试基础设施机器人提供此状态。
  • 模糊测试结果比简单的通过/失败更丰富。
    • 如果出现故障,输出需要包含触发输入以及任何关联的日志和回溯。
    • 如果提前终止,输出可能包括累积的反馈和建议的参数(例如字典),以供将来进行模糊测试。
  • 模糊处理后的 realm 可用于多种不同的工作流,模糊处理基础架构会选择依次执行这些工作流,例如“模糊处理一段时间。如果发现错误,则进行清理;否则,合并并压缩语料库”。 将每个步骤表示为一个测试套件会导致大量工作,即从一个步骤中提取状态,然后将其恢复到下一个步骤。

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

  • 通过 ffx 向用户提供管理界面。
  • test_manager 交互,以在 测试运行程序框架中的模糊测试领域内启动模糊测试工具。
  • 为这些模糊测试器提供 fuchsia.fuzzer.manager.Harness 以便它们连接回来并处理用户请求。
  • 提供一种数据传输协议,以便于将数据注入模糊测试器或从模糊测试器提取数据。

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

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

模糊测试引擎

fuzzer_engine 是模糊处理后的 realm 的一个组成部分。就模糊测试工具分类而言,它:

  • 实现 fuchsia.fuzzer.Controller 协议以提供管理界面
  • 创建并使用存储功能来管理每个语料库
  • 语料库中的输入进行变异,以创建新的测试输入。(例如,针对 libMutagen 的链接)。
  • Uses 一种Adapter功能,用于发送要处理的新输入
  • Exposes 一种 fuchsia.fuzzer.ProcessProxy 功能,经过插桩的模糊测试领域中的远程进程可以使用该功能来提供收集的反馈报告错误
  • 分析反馈

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

因此,模糊测试器引擎以类似于用于测试复杂拓扑的测试驱动程序的方式包含在测试 realm 本身中。由 eventpair 协调的共享 VMO 用于以尽可能低的延迟时间将测试输入传输到 fuzz 目标适配器,并将反馈从插桩的远程进程传输到 fuzz 目标适配器。

模糊测试工具引擎由 fuzz_test_runner 启动。此 runner 与现有的 elf_test_runner 非常相似,但有一个重要的新增功能:它会为 fuchsia.fuzzer.Controller 协议创建一对渠道。它会将此配对的一端作为启动句柄安装在 fuzzer_engine 中。它使用 test_manager 路由到 fuzz_managerfuchsia.fuzz.manager.Harness 功能将另一个传递给 fuzz_manager。这样一来,test_manager 就可以仅向 fuzz_test_runner 及其启动的模糊测试器提供 Harness 功能,而不是向所有测试提供该功能。

目标适配器

模糊测试目标适配器在模糊测试器分类中扮演输入处理角色。使用上述共享 VMO 和 eventpair,它会获取模糊测试引擎生成的测试输入,并将这些输入映射到被模糊测试的目标 realm 的插桩远程进程的特定交互。

这些特定互动由模糊测试器作者提供,通常是所谓的“编写模糊测试器”的贡献。

模糊测试器作者可以提供自己的模糊测试目标适配器自定义实现,也可以使用提供的某个框架。

可能的适配器支架示例包括:

  • 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)来构建模糊测试的目标 realm 中的所有进程。对于在树内构建的模糊测试工具,可以通过将 flagsdeps 传播到 GN 目标的依赖项的工具链变体来实现。必需的标志(例如 -fsanitize-coverage=inline-8bit-counters)将记录在案,以便进行树外编译。

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

fuzzing 分类而言,远程库提供:

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

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

最后,所需的编译时修改仅是对 LLVM IR 的转换。所有其他修改仅在链接时进行。这样一来,服务提供商就可以向愿意为其组件提供 LLVM 字节码的 SDK 使用者提供“模糊测试即服务”,而无需提供源代码。

组件拓扑

综合上述所有内容,模糊测试器组件拓扑包括:

  • core:系统根组件。
  • fuzz_manager:根 Realm 中模糊测试器与宿主工具之间的桥梁。
  • test_manager:与测试运行程序框架中相同。
  • target_fuzzer:模糊测试的 realm 入口点。
  • fuzzer_engine:与目标无关的模糊测试驱动程序。
  • target_adapter:包含用户提供的输入处理代码的特定于目标平台的组件。
  • instrumented_target:正在模糊测试的组件。

adaptertarget 组件可能具有其他子级,例如模拟对象和正在模糊测试的目标 realm。

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

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。包含一种用于注册协调 eventpair 和用于发送测试输入的共享 VMO 的方法。
  • fuchsia.fuzzer.ProcessProxy:由 fuzzer_engine 提供给模糊测试领域中的每个插桩进程。包括用于注册协调 eventpair 的方法,以及用于注册提供反馈的共享 VMO 的方法。

构建实用程序

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

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

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

  • 默认模糊测试器分片。
  • 指向目标适配器组件的网址。
  • 要模糊测试的组件的清单网址。这通常应可从相关的集成测试中重复使用。

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

实现

实现计划非常简单:在一系列更改中开发和单元测试各个类,然后组装从 libFuzzer 派生的集成测试,如测试部分中所述。

语言

fuzzer_engineremote_library 是用 C++ 实现的,以方便处理它们的特异性:

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

其他部分(例如 realm_builder_adapter)则使用 Rust 编写。

数据传输协议

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

  • 提供要执行、清理或最小化的特定测试输入。
  • 将模糊测试语料库与开发者主机上的语料库或多个 ClusterFuzz 实例上的语料库同步。
  • 提取触发错误的测试输入。

为了最大限度地减少维护负担,最好使用 overnet 传输这些数据。不过,任何单次转移都可能超出 Zircon 通道上单个 FIDL 消息的大小。相反,Controller 协议包含多种方法,这些方法可提供 zx_socket 对象,模糊测试引擎可使用这些对象将数据流式传输到 VMO 和/或本地存储的文件,或从 VMO 和/或本地存储的文件流式传输数据。

数据通过一种用于读取或写入命名字节序列的极简协议进行流式传输。该协议不是 FIDL,因为发送的数据可能会超出 FIDL 消息的最大长度。不过,这些命名字节序列在概念上等同于以下 FIDL 结构:

struct NamedByteSequence {
  uint32 name_length;
  uint32 size;
  bytes:name_length name;
  bytes:size data;
};

堆栈展开

目前,libFuzzer 使用 LLVM 中的一个解绕器,该解绕器假定它是从在触发信号的线程上执行的 POSIX 信号处理程序中调用的。对于 Fuchsia,这需要一种复杂的异常处理方法,包括修改崩溃线程的堆栈并注入保留回溯信息的汇编 trampoline,以在 unwinder 中“复活”线程。

如果 libFuzzer 不处理错误,则无需执行上述任何操作。相反,系统会以最方便有效的方式处理不同类型的错误,例如:

  • 异常由 fuzzer 引擎处理,该引擎从模糊测试运行程序接收异常通道,该运行程序是根据其对测试作业的句柄创建的。
  • 模糊测试器引擎还会管理超时。
  • 清理器回调和 OOM 由远程库处理,该库会通知模糊测试器引擎。

性能

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

同样,在未插桩的 build 上从模糊测试器生成单元测试与当前方法类似,预计不会比当前方法增加任何显著的模糊测试器测试成本。

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

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

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

工效学设计

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

此框架尝试通过多种方式尽可能简化模糊测试。它可让开发者:

  • 以熟悉且灵活的方式编写模糊测试器,如目标适配器部分中所述。
  • 使用现有的 GN fuzzing 模板系列构建模糊测试器。
  • 使用熟悉的工作流运行模糊测试器。ffx fuzz 的用法有意与 fx fuzz 类似。
  • 获得可作为行动依据的结果。通过与 ClusterFuzz 集成,系统会自动提交包含符号化回溯和重现说明的 bug。

向后兼容性

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

安全注意事项

此框架不会用于发布产品配置。对于以模糊测试配置构建的设备,与该设备的通信将使用 overnetffx 提供的现有身份验证和安全通信功能。

模糊测试器的输出可能存在安全注意事项,例如,测试输入可能会导致可利用的内存损坏。模糊测试人员(人工或模糊测试基础设施)必须像处理任何其他可利用的 bug 报告一样处理这些问题(例如,正确标记、防止未经授权的披露等)。

隐私注意事项

在考虑隐私权影响时,我们不会对模糊测试器运算符如何处理模糊测试器输出做出任何假设。这些输出包括符号化日志、导致错误的输入、生成的字典和生成的语料库。假设日志中已不包含用户数据,因为这是一个单独且受到密切监控的隐私问题。其余输出均直接派生自测试输入。因此,使模糊测试器的输入不含用户数据是使模糊测试器的输出不含用户数据的必要条件和充分条件。

向模糊测试器的语料库添加输入内容的方式有三种:

  • 作为种子输入。种子语料库应签入到源代码库中。 通常情况下,禁止在源代码库中包含用户数据。
  • 作为对实时语料库的手动添加项。
    • 这通常由模糊测试基础架构(例如 ClusterFuzz)完成,因为它会使用其他实例生成的输入来“交叉传播”模糊测试工具。在这种情况下,其他实例将不包含用户数据,添加的输入也不会包含用户数据。
    • 人工操作员也可以通过 ffx 添加输入。 以这种方式添加人工输入内容时,该工具会显示有关用户数据的警告。
  • 作为对实时语料库的生成性添加。这些输入是从现有输入变异而来的。由于这些输入不包含用户数据,因此生成的输入也不包含用户数据。有些输入可能纯粹是偶然匹配到了一些用户数据,例如模糊测试器设法生成了有效的用户名。不过,在这种情况下,没有明确的用户数据关联。

即使模糊测试器是非密封的(并且是非确定性的!),并且使用测试 realm 公开的来源中的数据,语料库中也不会包含任何其他数据。框架不会将该数据视为测试输入的一部分,也不会保存该数据。

最糟糕的情况是,模糊测试工具故意设计成非封闭式,并使用公开的功能将数据发送到测试领域之外的某个其他服务,该服务会验证 PII,例如返回用户名是否有效。这需要付出相当大的努力才能规避模糊测试和测试框架为鼓励密封性而做出的尝试。而且,由于外部服务未进行插桩,因此这种方法与随机猜测并无太大区别。

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

测试

模糊测试器引擎、目标适配器库和远程库使用常规方法(例如 GoogleTest#[cfg(test)] 等)进行单元测试。此外,集成测试使用默认 ELF 测试运行程序来运行一组模糊测试工作流,这些工作流具有专门构建的示例目标,基于 compiler-rt 中的适用子集

对于使用该框架编写的模糊测试工具,该框架将采用与 GN 模糊测试工具模板目前支持的相同方法:在未插桩的 build 中构建模糊测试工具时,引擎将被一个测试驱动程序替换,该驱动程序只会执行种子语料库中的每个输入。这可确保所有模糊测试工具都能构建和运行,从而缓解“位腐烂”问题。它还可作为回归测试,尤其是在模糊测试器作者通过在修复模糊测试发现的缺陷时添加输入来维护其种子语料库的情况下。

文档

需要使用新 GN 模板的具体示例来更新 fuzzing 文档树。任何其他计划的文档更改(例如 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 实现,信号通过 eventpair 实现,交换通过 FIDL 消息实现,方式与此提案中的设计类似。

遗憾的是,在长时间的审核过程中,这种方法因流程原因(而非技术原因)变得不可行:随着时间的推移,libFuzzer 维护人员越来越担心需要进行大量更改才能使 libFuzzer 以其最初设计之外的方式运行。最终,团队决定无限期推迟落实建议的更改。


单服务 FIDL 模糊测试

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(与当前版本 6.0.0 的 qemu 相比)。期望使用较新版模拟功能的代码不太可能正常运行。
    • 对不透明二进制模糊测试的需求并不大。事实上,该设计仅要求对目标代码进行插桩并与远程库进行链接;LLVM 字节码足以实现这一点。
  • TriforceAFL 在完整的插桩 QEMU 实例上使用 AFL。这样一来,您就可以通过从 QEMU 本身收集覆盖率来模糊测试没有源代码的不透明二进制文件。它不适合,原因与 afl-unicorn 类似:
    • 再次重申,没有必要进行不透明二进制模糊测试。
    • 此外,由于收集的覆盖率是整个实例的覆盖率,因此使用 TriforceAFL 进行模糊测试往往会产生大量噪声,尤其是在运行许多组件时。它通常仅适用于模糊测试极受限的配置,例如启动后立即测试 USB 驱动程序。