消毒用品

设计初衷

排错程序是用于检测代码中某些类别的 bug 的工具。操作原则各不相同,尽管排错程序通常(但并非总是)依赖于某种形式的编译时插桩更改代码,以在运行时公开 bug。 Fuchsia 使用各种排错程序来发现和诊断难以发现的危险 bug。

排错程序由构建时标志启用。我们会在 Fuchsia 的持续集成 (CI) 和提交队列 (CQ) 上持续执行排错程序 build,并为 Fuchsia C/C++ 和 Rust 开发者提供服务。

开发者通常无需采取特殊操作即可受益于排错程序,并且只有在检测到 bug 时才需要关注排错程序。但存在一定限制。继续阅读,了解支持哪些排错程序以及如何使用它们。

支持的排错程序

Fuchsia 目前支持以下排错程序:

  • AddressSanitizer (ASan) 可检测出界访问实例,在释放 / 返回 / 作用域之后使用,以及双重释放。
  • LeakSanitizer (LSan) 会检测内存泄漏。 LeakSanitizer 在检查泄漏方面就像一个保守的垃圾回收器。任何无法从引用根(线程堆栈、线程寄存器、全局变量和线程局部变量)访问的任何分配都被视为泄露。
  • ThreadSanitizer (TSan) 会检测数据争用(仅限主机)。
  • UndefineBehaviorSanitizer (UBSan) 可检测依赖于未定义程序行为的具体问题。

Zircon 内核中提供了以下排错程序行为:

  • 物理内存管理器 (PMM) 检查工具 (pmm_checker) 可检测释放后使用 bug 和散布的 DMA。
  • 内核 AddressSanitizer (KASan) 与 PMM 协作将 AddressSanitizer 扩展至内核代码。
  • Lockdep 是一个运行时锁验证器,可检测锁屏危险,如死锁。

默认情况下,系统会添加以下 C/C++ 编译选项,以在运行时检测或防止 bug:

  • -ftrivial-auto-var-init=pattern(请参阅 RFC)会将自动变量初始化为非零模式,以发现与从未初始化内存中读取数据相关的 bug。
  • ShadowCallStackSafeStack 可针对堆栈溢出强化生成的代码。

最后,Fuchsia 使用 libFuzzersyzkaller 执行覆盖率导向型模糊测试。模糊测试工具与排错程序类似,因为它们会尝试在运行时公开代码中的 bug,并且通常一起使用。模糊测试工具与排错程序的不同之处在于,模糊测试工具会尝试强制将生产代码执行到可能会暴露 bug 的路径中。

支持的配置

目前,通过以下配置,本地 build 和 CI/CQ 中支持排错程序:

  • bringup.x64
  • bringup.arm64
  • core.x64
  • core.arm64
  • zbi_tests.x64
  • zbi_tests.arm64

此外,清理程序也适用于主机工具。

以上所有测试都是在 qemuIntel NUC 上使用 CI/CQ 进行的。由于资源和容量问题,其他平台不会使用排错程序进行测试,但您可以使用以下构建工作流在这些平台上在本地进行测试。

对于在 //vendor 下定义的配置,系统可能会针对某些已登录的用户在 Gerrit 和 CI 控制台中显示其他 tryjobs。查找名称中包含 -asan 的配置。

上面列出的排错程序适用于 C/C++ 代码。此外,LSan 还应用于 Rust 代码,用于检测 Rust 内存泄漏

排查排错程序问题

Build

Fuchsia 平台 build(树内)

如需重现排错程序 build,您可以使用 build 变体来启用排错程序:

fx set product --variant asan-ubsan --variant host_asan-ubsan

或者,您也可以选择仅对某些二进制文件进行插桩:

fx set product --variant asan-ubsan/executable_name

当完全插桩 build 不适合于设备时,选择性插桩工作流对于在硬件上进行本地测试非常有用。

具体而言,为了检测内核代码中的“释放后使用”错误,您需要启用内核 PMM 检查工具

树外构建

使用 Fuchsia 工具链进行编译时,只需传递 -fsanitize= 标志即可指示要使用的排错程序。请参阅编译器文档

使用插桩组件创建 Fuchsia 软件包时,您需要确保软件包包含所有运行时依赖项,包括排错程序运行时(作为 Clang 工具链的一部分分发)和插桩 C 库(作为 sysroot 下的 Fuchsia SDK 的一部分分发)。

测试

像往常一样在本地工作流中或在已启用排错程序的 CQ 构建器上进行测试(tryjob 名称中含有 asan)。如果排错程序检测到问题,则消息将输出到包含以下字符串之一的日志中:

  • ERROR: AddressSanitizer
  • ERROR: LeakSanitizer
  • SUMMARY: UndefinedBehaviorSanitizer
  • WARNING: ThreadSanitizer

在这些消息之后,您会看到确定问题性质并指向根本原因的堆栈轨迹。您可以在 fx log 中找到这些消息。

请注意,触发排错程序的测试可能仍会显示为通过。Sanitizer 问题不会表现为测试失败。

排错程序检测到的问题通常具有类似的根本原因。您可以通过搜索 Fuchsia bug,找到对之前工作的引用,查找 bug,其中包含一些您在排错程序输出中看到的关键字。

已知问题

#[should_panic]

Fuchsia 的 Rust 在panic! 上中止构建流程。这会大幅缩减二进制文件的大小。遗憾的是,使用 #[should_panic] 属性的测试可能会错误地检测内存泄漏。这些测试会发出预期的 panic,然后在没有展开的情况下退出,这意味着它们不会释放其堆分配。对于 LeakSanitizer 来说,这与真正的内存泄漏没有区别。

如果此问题会影响您的测试,您可以按照此示例在排错程序 build 中停用它。

请参阅:问题 88496:Rust 测试 should_panic trigger leaksanitizer

最佳实践

确保您的代码由测试执行

排错程序会在运行时公开 bug。除非您的代码以 CI/CQ 的执行方式(例如在测试中或通常在 Fuchsia 上)运行,否则排错程序将无法显示代码中的 bug。

确保排错程序覆盖代码的最佳方法是确保同一配置下的测试覆盖率。请参阅有关测试覆盖率的指南。

不要在代码中抑制排错程序

对于某些构建目标,可能会禁用排错程序。它最常用于引入排错程序支持之前的问题,尤其是并非由 Fuchsia 项目拥有的第三方代码中的问题。

抑制排错程序应被视为技术债务,因为它们不仅会隐藏旧 bug,还会让您在引入代码时无法发现新 bug。理想情况下,不应添加新的抑制操作,并应移除现有的抑制操作,并修复底层 bug。

您可以通过修改定义可执行目标的 BUILD.gn 文件来实现抑制排错程序,如下所示:

executable("please_fix_the_bugs") {
  ...
  # TODO(https://fxbug.dev/42074368): delete the below and fix the memory bug.
  deps += [ "//build/config/sanitizers:suppress-asan-stack-use-after-return" ]
  # TODO(https://fxbug.dev/42074368): delete the below and fix the memory bug.
  deps += [ "//build/config/sanitizers:suppress-asan-container-overflow" ]
  # TODO(https://fxbug.dev/42074368): delete the below and fix the memory leak.
  deps += [ "//build/config/sanitizers:suppress-lsan.DO-NOT-USE-THIS" ]
}

上面的示例演示了如何禁止所有排错程序。不过,您最多应该抑制导致故障的排错程序。请通过提交 bug 并在注释中引用该 bug 来跟踪抑制情况,如上所示。

停用排错程序的另一种常见方法是:

executable("too_slow_when_built_with_asan") {
  ...
  exclude_toolchain_tags = [ "asan" ]
}

上述两个示例都是按整个可执行文件的粒度进行抑制。如需实现更精细的抑制,您可以检测代码中是否存在排错程序。例如,这对于在特定测试用例中抑制排错程序非常有用,但其范围不是更广。例如,此类测试用于故意引入内存错误以及测试排错程序运行时本身的测试。

对于 C/C++,请参阅:

对于 Rust,您可以遵循以下模式:

#[cfg(test)]
mod tests {
    #[test]
    // TODO(https://fxbug.dev/42074368): delete the below and fix the leak
    #[cfg_attr(feature = "variant_asan", ignore)]
    fn test_that_leaks() {
        // ...
    }
}

测试不稳定

如果被测代码的行为不确定,排错程序错误可能会不稳定。例如,内存泄漏可能仅在某些竞态条件下发生。如果排错程序错误看起来不稳定,请参阅测试 CQ 中的不稳定指南。

报告良好 bug

如果遇到排错程序问题,请提交包含所有可用问题排查信息的 bug。

示例:问题 73214:ASAN use-after-scope in blobfs

bug 报告包含:

  • 排错程序提供的错误(在本例中为 ASan)。
  • 关于如何构建和测试以重现错误的说明。
  • 后续调查的详细信息,并根据需要提供特定代码指针。
  • 引用相关更改,例如在本例中为修复 bug 的根本原因的更改。

路线图

日常工作:

今后的工作领域:

  • ThreadSanitizer (TSan):检测数据争用。
  • 用于检测并发 bug 的内核支持。
  • 扩展了对 Rust 排错程序的支持,例如检测 Rust unsafe {} 代码块或 FFI 调用中的内存安全 bug,或检测未定义的行为 bug。
  • MemorySanitizer (MSan):检测从未初始化内存中读取的数据。

另请参阅:2021 年路线图中的排错程序