对 FIDL 进行模糊测试

在 Fuchsia 上使用 libFuzzer 对 FIDL 服务器进行模糊测试

Fuchsia 提供对以 FIDL 单元测试样式编写 FIDL 模糊测试工具的实验性支持。

快速入门指南

//examples/fuzzers/fidl 中定义了一个示例模糊测试工具。如果您不熟悉模糊测试工具,请参阅概览。要在 Fuchsia 上使用 libFuzzer 对 FIDL 服务器进行模糊测试,需要使用 GN 目标,这些目标将生成模糊测试目标,并编写一些代码以提供要进行模糊测试的服务器实例。

  1. fuzzers = {protocol = "fully.qualified.fidl.ProtocolName"} 添加到 fidl() GN 目标中。
  2. 根据您使用的语言,请按以下步骤操作:
    • 在 C++ 级别(简单模式):使用 //sdk/lib/fidl/cpp/fuzzing/server_provider.h 中的 FIDL_FUZZER_DEFINITION() 宏,为您的接口和服务器实现类定义服务器提供程序。这样就会自动定义下述 C 符号。如需查看参考示例,请参阅 //examples/fuzzers/fidl
    • 在 C 级别(硬模式):实现一个定义以下符号的库:
      • zx_status_t fuzzer_init():实例化服务器实现。
      • zx_status_t fuzzer_connect(zx_handle_t, async_dispatcher_t*):将服务器实现绑定到频道句柄。如果您的服务器可以与模糊测试工具客户端在同一线程上进行模糊测试,则也可以使用调度程序(请参阅关于线程的注意事项)。
      • zx_status_t fuzzer_disconnect(zx_handle_t, async_dispatcher_t*):解除服务器实现与频道句柄的绑定。
      • zx_status_t fuzzer_clean_up():清理服务器实现。 如果其中任何一项返回的状态不是 ZX_OK,模糊测试工具将会执行清理并停止运行。
  3. 定义 fidl_protocol_fuzzer() GN 目标。指定以下内容:
    • fidl = //path/to:fidl_gn_target(上述 fidl() 目标)。
    • protocol = "fully.qualified.fidl.ProtocolName"
    • deps = [... :your_library ...](上文提到的那个,用于定义 fuzzer_... 符号)。
    • 对于会对服务器进行模糊测试的 fuzzer GN 目标所需的任何其他内容。
  4. fidl_protocol_fuzzer() 目标添加到新的或现有的 fuzzer_package GN 目标中的 fuzzers = [ ... ]

实现细节

FIDL 服务器实现模糊测试目标的大部分是由 fidlgen 生成的 C++ 代码,需要一些 C 符号来为 FIDL 服务器实现提供 API。生成的代码包含一个绑定到其初始线程的全局 async::Loop,在每次模糊测试目标运行时,该全局都会重复用于 FIDL 连接的客户端。LibFuzzer 使用不同的输入反复调用同一模糊测试目标。生成的模糊测试目标代码将:

  • 调用 fuzzer_init(),初始化要用于模糊测试的服务器。
  • 实例化一对 zx::channel
  • 初始化相应类型的 fidl::InterfacePtr,该类型绑定到通道端和循环的调度程序。
  • 调用 fuzzer_connect(raw_server_channel_handle, loop->dispatcher()),与服务器建立连接,并允许服务器选择使用与客户端相同的调度程序(如果其 API 与此类方案兼容,请参阅关于线程的说明)。
  • 通过方法的 fidl::InterfacePtr 调用该方法。
  • 将其 async::Loop 设置为运行直至空闲。
  • 通过 zx::event 与方法的回调同步。
  • 调用 fuzzer_disconnect(raw_server_channel_handle, loop->dispatcher()),以允许服务器清理连接端。
  • 调用 fuzzer_clean_up() 以拆解服务器实例。

向 FIDL 消息分配模糊测试目标输入

大体而言,前两个字节用于从 FIDL 源文件定义的协议和方法对中选择。如果 FIDL 文件包含多个协议,但在模糊测试工具中只启用了一个协议,则发现有意义的输入依靠 LibFuzzer 的覆盖率引导引擎来推断出某种形式的前字节会导致模糊测试工具几乎不运行任何代码。

在确定协议和方法对后,剩余的字节按如下方式划分:

  • 每种类型都有一个 trait,用于定义其所需的最少字节数。
  • 如果输入的字节数不足,模糊测试目标会立即退出。
  • 否则,超出方法参数所需最小值的“slack”字节将在参数之间平均分配,并且系统会调用每种类型的分配特征,以使用最多 MinForParam + SlackForParam 个字节来构造适当类型的对象。

就分配特征详细信息而言,根据要转换为对象的字节集合,集合和数字类型具有相对自然的解释。句柄像数字类型一样处理,当服务器尝试执行句柄时,这可能导致错误。

关于线程处理的注意事项

{#a-note-about-threading}

非常需要使模糊测试目标保持单线程状态。也就是说,在 C++ 中使用 ServerProviderDispatcherMode::kFromCaller,或在 C 中使用传递到 fuzzer_connectasync_dispatcher_t*。这是首选方式,因为它可以提高模糊测试工具所发现 bug 持续重现的可能性。

使用 AFL 对 FIDL 主机工具进行模糊测试

此外,过往的实验工作已经使用 afl-fuzz 对 FIDL 编译器进行了模糊测试。

构建 afl-fuzz

下载并构建它,然后将其 export AFL_PATH 设为您下载和构建它时所用的任何路径。

修补解析器,以免捕获到无效的语法

afl-fuzz 会将崩溃视为值得关注,但解析器目前会在遇到语法无效时调用 __builtin_trap()。移除 parser.h 中的相应行 - 它位于 Parser::Fail() 方法中。

使用 afl-fuzz 的插桩构建 fidl 工具

清除所有现有构建,然后使用 afl-fuzz 编译器封装容器进行构建。

cd $ZIRCON_DIR
rm -fr build-x86
PATH=$PWD/prebuilt/downloads/clang+llvm-x86_64-linux/bin/:$PATH:$AFL_PATH make \
  build-x86/tools/fidl HOST_TOOLCHAIN_PREFIX=afl-

如果您的不是在 x86 Linux 等平台上构建,请进行调整。

运行模糊测试工具

解析器包含一些用作输入的示例。随着 FIDL 被采用,我们可以扩展输入,以包含树中声明的所有不同协议,但目前我们使用 tools/fidl/examples 中的内容。

$AFL_PATH/afl-fuzz -i tools/fidl/examples -o fidl-fuzz-out build-x86/tools/fidl dump '@@'

成果

从 2017 年 5 月初开始针对源代码运行,在相当快速的计算机上经过两天的模糊测试后,没有发生任何崩溃或挂起。它执行了超过 3 亿次。