消毒用品

动机

排错程序是用于检测代码中特定类 bug 的工具。运作原理各不相同,但清理工具通常(但并不总是)依赖于某种形式的编译时插桩,以便以在运行时揭示 bug 的方式更改代码。Fuchsia 使用各种清理程序来发现和诊断其他情况下难以发现的危险 bug。

通过构建时标志启用清理程序。Sanitizer build 会在 Fuchsia 的持续集成 (CI) 和提交队列 (CQ) 上持续运行,并为 Fuchsia C/C++ 和 Rust 开发者提供服务。

开发者通常无需执行任何特殊操作即可受益于清理程序,并且只需在检测到 bug 时关注清理程序。不过,这项功能存在一些限制。请继续阅读,了解支持哪些清理程序以及如何使用它们。

支持的清理程序

Fuchsia 目前支持以下清理程序:

  • AddressSanitizer (ASan) 可检测出界访问、释放 / 返回 / 作用域后使用和双重释放的示例。
  • LeakSanitizer (LSan) 用于检测内存泄漏。在检查是否存在内存泄漏时,LeakSanitizer 的运作方式与保守型垃圾回收器类似。从引用根(线程堆栈、线程寄存器、全局变量和线程本地变量)无法访问的任何分配都将被视为泄露。
  • ThreadSanitizer (TSan) 可检测数据争用(仅限主机)。
  • UndefinedBehaviorSanitizer (UBSan) 可检测依赖于未定义程序行为的特定问题。

Zircon 内核中提供了以下清理程序行为:

  • 实体内存管理器 (PMM) 检查器 (pmm_checker) 可检测释放后使用 bug 和孤岛 DMA。
  • Kernel AddressSanitizer (KASan) 与 PMM 协作,将 AddressSanitizer 扩展到内核代码。
  • Lockdep 是一个运行时锁定验证器,用于检测死锁等锁定危险。

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

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

最后,Fuchsia 使用 libFuzzersyzkaller 执行以覆盖率为导向的模糊测试。fuzz 工具与清理工具类似,它们会尝试在运行时揭露代码中的 bug,并且通常会结合使用。fuzz 工具与清理工具不同,因为 fuzz 工具会尝试强制将生产代码的执行路径转到可能存在 bug 的路径。

支持的配置

目前,在本地 build 和 CI/CQ 中,在以下配置下支持使用清理程序:

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

此外,sanitizer 也适用于宿主工具。

上述所有测试均在 qemuIntel NUC 上的 CI/CQ 上运行。由于资源和容量问题,我们未使用清理器对其他平台进行测试,但您可以使用下文中的构建工作流在本地对这些平台进行测试。

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

上述清理程序会应用于 C/C++ 代码。此外,LSan 会应用于 Rust 代码,以检测 Rust 内存泄漏

排查清理程序问题

构建

Fuchsia 平台 build(in-tree)

如需重现 sanitizer build,您可以使用build 变体启用 sanitizer:

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

或者,您也可以选择仅插桩特定二进制文件:

fx set product --variant asan-ubsan/executable_name

选择性插桩工作流非常适合在本地硬件上进行测试,因为在这种情况下,完全插桩的 build 不适用于设备。

具体而言,如需检测内核代码中的“释放后使用”bug,您需要启用内核 PMM 检查器

树外 build

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

创建包含插桩组件的 Fuchsia 软件包时,您需要确保该软件包包含所有运行时依赖项,包括作为 Clang 工具链的一部分分发的 sanitizer 运行时,以及作为 sysroot 下的 Fuchsia SDK 的一部分分发的插桩 C 库。

测试

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

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

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

请注意,触发过净化程序的测试可能仍会显示为通过。sanitize 问题不会显示为测试失败。

由清理程序检测到的问题通常具有类似的根本原因。您可以搜索 Fuchsia 错误,查找与您在清理程序输出中看到的某些关键字相同的错误,以便找到之前的工作的参考信息。

已知问题

#[should_panic]

Fuchsia 的 Rust build 会在 panic! 上终止。这会显著缩减二进制文件的大小。一个不幸的副作用是,使用 #[should_panic] 属性的测试可能会误检测内存泄漏。这些测试会发出预期的 panic,然后退出而不解开,这意味着它们不会释放其堆分配。对 LeakSanitizer 而言,这与真正的内存泄漏无法区分。

如果此问题影响到您的测试,您可以执行以下操作:

  • 请按照此示例切换到 #[fuchsia::test] 属性。这是首选属性,因为 #[fuchsia::test] 只会停用 LeakSanitizer,但会保留其他 sanitizer。

  • 如果难以切换到 #[fuchsia::test],请按照此示例在沙盒化 build 中停用该测试。

请参阅:问题 88496:should_panic 触发 leaksanitizer 的 Rust 测试

最佳做法

确保代码经过测试

排错程序会在运行时揭示 bug。除非您的代码在运行(例如在测试中或在 Fuchsia 上以 CI/CQ 所执行的方式运行),否则清理器将无法发现代码中的 bug。

确保代码的清理器覆盖率的最佳方法是在相同配置下确保测试覆盖率。请参阅测试覆盖率指南。

请勿在代码中抑制清理程序

系统可能会抑制特定 build 目标的清理程序。最常用于在引入 sanitizer 支持之前出现的问题,尤其是 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" ]
}

上述两个示例都是在整个可执行文件级别进行抑制。如需更精细地抑制,您可以检测代码中是否存在排错程序。例如,这对于在特定测试用例中抑制清理程序非常有用,但在更广泛的范围内则不太实用。例如,会故意引入内存错误并测试 sanitizer 运行时本身的测试会使用此方法。

对于 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:blobfs 中的 ASAN 用完作用域

bug 报告包含以下内容:

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

路线图

正在进行的工作:

未来的工作领域:

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

另请参阅:2021 年路线图中的清理程序