线程消毒器

什么是线程消毒器?

线程消毒器 (TSan) 是一种用于 C/C++ 和 Rust 程序的快速数据竞争检测器。它使用编译时插桩来在运行时检查所有非无竞争的内存访问。与其他工具不同,它理解编译器内置的原子操作和同步,因此可以提供非常准确的结果,并且没有误报(除非使用了不受支持的同步原语,例如内联汇编或内存屏障)。有关 TSan 工作原理的更多信息,请参阅线程消毒器维基

一个名为 tsan 的元错误 用于跟踪使用 TSan 发现的所有错误。

一篇关于 hacks.mozilla.org 的博文 描述了此项目。

请注意,与其他消毒器不同,TSan 当前**仅在 Linux 上受支持**。

下载制品构建

获取带有线程消毒器的 Firefox 构建的最简单方法是下载 mozilla-central 的持续集成 TSan 构建(至少每天更新一次)

  • mozilla-central 优化构建:linux

模糊测试团队还提供了一个名为 fuzzfetch 的工具来下载此构建以及许多其他 CI 构建。它使下载和解压缩这些构建变得更加容易,不仅可用于模糊测试,还可用于所有需要 CI 构建下载的目的。

您可以从Github通过 pip安装 fuzzfetch

之后,您可以运行

$ python -m fuzzfetch --tsan -n firefox-tsan

将上面提到的构建解压缩到名为 firefox-tsan 的目录中。

创建 Try 构建

如果由于某种原因您无法使用上一节中提到的预构建二进制文件(例如,您需要测试补丁),您可以自行构建 Firefox(请参阅下一节)或使用try 服务器为您创建自定义构建。推送到 try 需要 L1 提交权限。如果您还没有此权限,您可以请求访问权限(请参阅成为 Mozilla 提交者Mozilla 提交权限策略 以了解要求)。

使用 mach try fuzzy --full,您可以选择 build-linux64-tsan/opt 作业和相关测试(如果需要)。

在 Linux 上创建本地构建

构建先决条件

LLVM/Clang/Rust

TSan 插桩作为 LLVM 传递实现,并集成到 Clang 中。我们强烈建议您使用 mach bootstrap 过程中提供的 Clang 版本,因为我们为 Firefox 上的 TSan 反向移植了一些必要的修复。

Rust 中的消毒器支持是真正的实验性功能,因此我们的构建系统仅适用于我们在 CI 中构建的经过特殊修补的 Rust 版本。要安装该特定版本(或更新到较新版本),请在 mozilla-central 检出的根目录中运行以下命令

./mach artifact toolchain --from-build linux64-rust-dev
rm -rf ~/.mozbuild/rustc-sanitizers
mv rustc ~/.mozbuild/rustc-sanitizers
rustup toolchain link gecko-sanitizers ~/.mozbuild/rustc-sanitizers
rustup override set gecko-sanitizers

mach artifact 将始终下载与您已检出的当前 mozilla central 提交关联的 linux64-rust-dev 工具链。该工具链的行为应该与普通的 rust nightly 工具链大致相同,但我们不建议将其用于构建 gecko 以外的任何用途,以防万一。另请注意,~/.mozbuild/rustc-sanitizers 只是一个合理的默认位置 - 请随意将工具链“安装”到您喜欢的任何位置。

构建 Firefox

获取源代码

使用该版本或任何更高版本,您只需获取 mozilla-central 的克隆

调整构建配置

在您的 mozilla-central 目录中创建构建配置文件 mozconfig,其内容如下

# Combined .mozconfig file for TSan on Linux+Mac

mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/objdir-ff-tsan

# Enable ASan specific code and build workarounds
ac_add_options --enable-thread-sanitizer

# This ensures that we also instrument Rust code.
export RUSTFLAGS="-Zsanitizer=thread"

# rustfmt is currently missing in Rust nightly
unset RUSTFMT

# Current Rust Nightly has warnings
ac_add_options --disable-warnings-as-errors

# These are required by TSan
ac_add_options --disable-jemalloc
ac_add_options --disable-crashreporter
ac_add_options --disable-elf-hack
ac_add_options --disable-profiling

# The Thread Sanitizer is not compatible with sandboxing
# (see bug 1182565)
ac_add_options --disable-sandbox

# Keep symbols to symbolize TSan traces later
export MOZ_DEBUG_SYMBOLS=1
ac_add_options --enable-debug-symbols
ac_add_options --disable-install-strip

# Settings for an opt build (preferred)
# The -gline-tables-only ensures that all the necessary debug information for ASan
# is present, but the rest is stripped so the resulting binaries are smaller.
ac_add_options --enable-optimize="-O2 -gline-tables-only"
ac_add_options --disable-debug

# Settings for a debug+opt build
#ac_add_options --enable-optimize
#ac_add_options --enable-debug

开始构建过程

现在,您可以使用常规的 ./mach build 命令开始构建过程。

启动 Firefox

构建完成后,./mach run 以及在调试器中运行的常用选项(gdblldbrr 等)都可以正常工作,--disable-e10s 和其他选项也可以正常工作。

在运行 Firefox 时,请确保它没有处于安全模式,因为这可能会在启动期间导致一些 tsan 故障。您可以使用不同的配置文件或添加 --temp-profile 来使用临时配置文件。

如果您有带有专有驱动程序的 NVIDIA GPU,Firefox 可能会在启动时崩溃。要解决此问题,请通过更改以下首选项禁用图形加速

  • gfx.x11-egl.force-disabled=true

  • gfx.webrender.software.opengl=true

  • layers.acceleration.disabled=true

您可以通过将这些首选项传递给您的 ./mach run 命令来实现,例如:./mach run --setpref "gfx.x11-egl.force-disabled=true" --setpref "gfx.webrender.software.opengl=true" --setpref "layers.acceleration.disabled=true",或者您可以将它们添加到您的 machrc 文件中。在此处了解有关 mach 设置的更多信息此处

仅构建 JavaScript shell

如果您只想构建 JavaScript shell 而不是进行完整的 Firefox 构建,则以下构建脚本可能会帮助您做到这一点。在 js/src/ 子目录中执行此脚本,并将目录名称作为第一个参数传递。然后,构建将在具有该名称的新子目录中创建。

#! /bin/sh

if [ -z $1 ] ; then
     echo "usage: $0 <dirname>"
elif [ -d $1 ] ; then
     echo "directory $1 already exists"
else
     autoconf2.13
     mkdir $1
     cd $1
     CC="/path/to/mozbuild/clang" \
     CXX="/path/to/mozbuild/clang++" \
     ../configure --disable-debug --enable-optimize="-O2 -gline-tables-only" --enable-thread-sanitizer --disable-jemalloc
fi

线程消毒器和符号

与地址消毒器不同,TSan 首先需要进行进程内符号化才能正常工作,因为否则任何类型的运行时抑制都将无法工作。

因此,您需要在您的 PATH 中或由 TSAN_SYMBOLIZER_PATH 环境变量指向的副本中包含 llvm-symbolizer。此二进制文件包含在您的本地 mozbuild 目录中,可通过 ./mach bootstrap 获得。

运行时抑制

TSan 能够在运行时抑制竞争条件报告。这可用于在开发修复程序时使竞争条件静音,以及永久使无法修复的(良性)竞争条件静音。

警告

**警告**:许多竞争条件看起来是良性的,但实际上并非如此。请仔细阅读常见问题解答部分,并在尝试抑制竞争条件之前三思而后行。

运行时抑制列表直接烘焙到 Firefox 中,并在编译时位于mozglue/build/TsanOptions.cpp

警告

**重要**:添加抑制时,请务必包含错误编号。如果抑制应该永久有效,请在与错误编号相同的行中添加字符串 permanent

警告

**重要**:添加数据竞争抑制时,请务必包含来自**每个**竞争堆栈的堆栈帧。仅为一个堆栈添加一个抑制可能会导致后来难以跟踪的间歇性故障。此规则的一个例外是抑制全局变量上的竞争条件。在这种情况下,一个包含变量名称的单个竞争条件条目就足够了。

故障排除/已知问题

已知的误报来源

TSan 有许多可能导致误报的内容,即

  • 使用内存屏障(例如 Rust Arc)

  • 使用内联汇编进行同步

  • 使用编译器内置函数进行同步的未插桩代码(例如外部库)

  • 仅涉及单个线程的锁顺序反转可能会导致误报死锁报告(另请参阅https://github.com/google/sanitizers/issues/488)。

如果没有涉及这四项中的任何一项,则在没有咨询 TSan 同行的情况下,您**绝不应**假设 TSan 向您报告了误报。由于编译器优化和并行代码的性质,竞争条件可能非常复杂且完全不明显,因此很容易误判竞争条件为误报。

间歇性错误堆栈

如果您间歇性地看到缺少一个堆栈的竞态条件报告,并且出现 failed to restore the stack 消息,这可能表示抑制正在部分覆盖您看到的竞态条件。

任何只有一个堆栈与运行时抑制匹配的竞态条件,如果该特定堆栈由于某种原因无法符号化,则会显示出来。通常的解决方案是在抑制中搜索潜在的候选者,并暂时禁用它们以检查您的竞态条件报告现在是否变得更加一致。

但是,损坏的 TSan 堆栈还有其他原因,特别是如果它们不是间歇性的。另请参阅 TSan 标记 中的 history_size 参数。

间歇性竞态条件报告

不幸的是,TSan 算法不能保证竞态条件始终被检测到。TSan 的间歇性故障(在一定程度上)是预期的,并且应记录并修复所涉及的竞态条件以解决问题。

关于 TSan 的常见问题

为什么要修复数据竞态条件?

数据竞态条件是未定义的行为,可能导致崩溃以及正确性问题。编译器优化可能导致存在竞态条件的代码具有不可预测且难以重现的行为。

在 Mozilla,我们已经看到了一些危险的竞态条件,导致随机的 使用后释放崩溃间歇性测试失败挂起性能问题间歇性断言。此类问题不仅降低了我们代码的质量和用户体验,而且还浪费了无数的开发人员时间。

由于很难判断特定的竞态条件是否会导致这种情况,因此我们决定尽可能修复所有数据竞态条件,因为这样做通常比分析竞态条件更便宜。

我的竞态条件是良性的,我们可以忽略它吗?

虽然可以添加运行时抑制来忽略竞态条件,但我们 *强烈* 建议您不要这样做,原因有两个。

  1. 每个被抑制的竞态条件都会降低 TSan 构建的整体性能,因为每次发生竞态条件时都必须对其进行符号化。由于 TSan 本身就是一个缓慢的构建,因此我们需要尽可能减少被抑制的竞态条件的数量。

  2. 判断竞态条件是否真正良性非常困难。我们建议阅读 这篇博文这篇论文 <https://www.usenix.org/legacy/events/hotpar11/tech/final_files/Boehm.pdf>,了解看似良性的竞态条件的影响。

抑制已确认的良性竞态条件的有效原因包括修复竞态条件导致的性能问题,或者修复竞态条件需要不合理的工作量的情况。

请注意,使用原子操作通常不会产生开发人员倾向于与其关联的糟糕的性能影响。如果您假设例如使用原子操作进行同步会导致性能下降,我们建议您进行基准测试以确认这一点。在许多情况下,差异是无法测量的。

TSan 的工作原理是什么?

有关 TSan 工作原理的更多信息,请参阅 线程消毒程序 wiki