消除Firefox中的数据比赛

2021-04-06 23:54:10

我们成功部署了Firefox项目中的ThreadSanitizer,以消除剩余的C / C ++组件中的数据比赛。在此过程中,我们发现了几个有影响力的错误,并且可以安全地说数据种族通常在对计划正确性的影响方面低估。我们建议所有多线程C / C ++项目采用ThreadSanitizer工具来增强代码质量。

ThreadSanitizer(TSAN)是编译时仪器,可根据Linux上的C / C ++内存模型检测数据比赛。值得注意的是,这些数据比赛被认为是C / C ++规范中的未定义行为。因此,编译器可以自由地假设数据比赛不会发生并在该假设下执行优化。检测由此类优化产生的错误可能是硬,并且数据竞争通常具有由于线程调度而具有间歇性的性质。如果没有像帖子化器这样的工具,即使是最经验丰富的开发人员也可以花费时间在找到这样的错误。使用ThreadSanitizer,您可以获得一个全面的数据竞争报告,通常包含解决问题所需的所有信息。

TSAN的一个重要属性是,当正确部署时,数据竞争检测不会产生误报。这对工具采用非常重要,因为开发人员在产生不确定结果的工具中迅速失去信心。

与其他消毒者一样,Tsan建于铿cl声,可以与最近的任何Clang / LLVM Toolchain一起使用。如果您的C / C ++项目已经使用了例如addresssanitizer(我们也强烈推荐),部署ThreadSanitizer从工具箱角度将非常简单。

尽管Threadsanitizer是一个非常精心设计的工具,但我们必须在部署阶段克服Mozilla的各种挑战。我们面临的最重要的问题是,难以证明数据种族实际上是有害的,并且它们影响了Firefox的日常使用。特别是,术语“良性”经常出现。良性数据种族承认特定数据种族实际上是一个种族,但假设它没有任何负面影响。

虽然存在良性数据种族,我们发现(与之前的这个主题的工作协议[1] [2]),数据种族非常容易被错误分类为良性。对此的原因很清楚:很难推理编译器可以并将优化,并确认某些“良性”数据播放需要您查看编译器最终产生的汇编代码。毋庸置疑,这个程序通常比固定实际数据竞争和未来的未来更耗时。因此,我们决定最终目标应该是“无数据比赛”政策,甚至宣布良性数据种族因其错误分类风险而导致的不良,调查所需的时间和未来编制者的潜在风险(具有更好的优化)或未来的平台(例如ARM)。

但是,很明显,建立这样的政策需要在技术方面都需要大量工作以及令人信服的开发人员和管理。特别是,我们无法指望大量资源致力于修复数据比赛,没有明确的产品影响。这就是Tsan的抑制列表派上用场:我们知道我们不得不停止新数据种族的涌入,但同时可以获得工具可用的工具,而无需修复所有遗留问题。抑制列表(特别是编译到Firefox的版本)允许我们暂时忽略一旦我们在文件上暂时忽略数据比赛,并最终将在CI中提取的TSAN构建,以自动避免进一步的回归。当然,安全臭虫需要专门处理,但通常很容易识别(例如,在非线线安全指针上赛车),并且在没有抑制的情况下快速固定。

为了帮助我们了解我们的工作的影响,我们维持了一个最严重的种族的内部清单,即Tsan检测到的所有最严重的种族(有副作用或可能导致崩溃)。这些数据有助于说服开发人员,该工具使他们的生活更容易,同时也明确证明了管理工作。除了这种定性数据外,我们还决定了更加定量的方法:我们看着一年多的所有虫子以及如何分类。在我们看过的64个错误中,34%被归类为“良性”,22%是“影响”(其余的尚未被分类)。

我们知道有一定数量的错误分类是预期的,但我们真正想知道的是:良性问题对项目带来了风险吗?假设所有这些问题都没有对该产品产生影响,我们是否浪费了很多关于修复它们的资源?值得庆幸的是,我们发现这些修复的大多数是微不足道的和/或改善的代码质量。

琐碎的修复主要将非原子变量转化为原子学(20%),为上游问题增加了永久抑制,我们无法立即解决(15%),或删除过于复杂的代码(20%)。只有45%的良性修复实际上需要某种更多的更精细的补丁(如同,差异大于几行代码,而不只是删除代码)。我们得出结论,作为主要资源汇的良性问题的风险不是一个问题,并且可以为项目提供的整体收益可接受。

如开始,Tsan在适当部署时不会产生假正数据竞争报告,其中包括将加载到过程中的所有代码进行了解,并避免了Tsan不理解的原语(例如原子围栏)。对于大多数项目,这些条件是微不足道的,但像Firefox这样的更大项目需要更多的工作。值得庆幸的是,这项工作大大于Tsan的强大抑制系统中的几行。

在Firefox中检测所有代码目前无法使用,因为它需要使用像GTK和X11等共享系统库。幸运的是,TSAN提供了“调用_from_lib”功能,可以在抑制列表中使用,以忽略源自共享库的任何呼叫。我们的其他超级守则的主要来源是构建标志未正确传递,这对于生锈代码特别有问题(请参阅下面的锈部分)。

至于不受支持的原语,我们遇到的唯一问题是缺乏对围栏的支持。大多数围栏是标准原子参考计数成语的结果,其可以在Tsan构建中的原子负荷替换。不幸的是,围栏是对设计的横梁板条箱(生锈的基础并发库)的基础,而唯一的解决方案是抑制。

我们还发现,在死锁检测中存在(众所周知的)假阳性,但是非常容易发现,并且根本不会影响数据种族检测/报告。简而言之,任何仅涉及单个线程的死锁报告可能会对单个线程呈现出误报。

我们发现的唯一真正的假阳性是Tsan的罕见错误,并在工具本身固定。但是,开发人员在各种情况下宣称特定报告必须是假阳性的。在所有这些情况下,证明Tsan确实是正确的,问题只是非常微妙,很难理解。这再次确认我们需要Tsan等工具来帮助我们消除这类错误。

目前,TSAN错误O-RAMA包含大约20个错误。我们仍在为一些这些错误进行修复,并希望指出几个特别有趣/有影响力的错误。

位菲尔德是一种方便的便利,可以节省空间,以存储大量不同的小值。例如,而不是拥有30个BOOL占用240个字节,它们都可以将其包装为4个字节。在大多数情况下,这效果很好,但它有一个令人讨厌的后果:现在别名的不同数据。这意味着访问“邻居”位域实际上是访问相同的内存,因此是潜在的数据竞争。

实际上,这意味着如果两个线程写入两个相邻位字段,则其中一个写入可能会丢失,因为这两种写入都是读取所有位字段的写入操作:

如果您熟悉位域并积极思考它们,这可能是显而易见的,但是当您只是说Myval.Isinitialized = True时,您可能不会考虑或甚至意识到您正在访问位域。

我们有很多这个问题的实例,但让我们看看错误的1601940及其(修剪)的赛事报告:

当我们第一次看到这份报告时,它很令人难以置疑,因为有问题的两个线程触摸不同的字段(MasynctRansformappliedTocontent与MtestattributeAppliers)。但是,事实证明,这两个字段都是类中的相邻位字段。

这导致我们的CI中的间歇性失败,并花费了这个代码的维护者有价值的时间。我们发现这个错误特别有趣,因为它展示了在没有适当的工具的情况下诊断数据比赛的困难,我们在Codebase中找到了更多类型的错误(Racy Bitfield Write / Write)的更多实例。其中一个实例甚至可能有可能导致网络负载来提供无效的缓存内容,另一个难以调试的情况,尤其是当它间歇性并且因此不容易可再现时。

我们遇到了这一点,我们最终推出了一个Moz_atomic_bitFields宏,它生成具有原子负载/存储方法的位域。这允许我们快速修复每个组件的维护者的问题位域,而无需重新设计它们的类型。

我们还发现了几种组件实例,该组件被明确地设计为单线程被意外使用多个线程使用,例如错误1681950:

比赛本身在这里是相当简单的,我们通过Stat64赛车在同一个文件上,并理解报告这次不是问题。但是,从帧10中可以看出,此呼叫源自偏好要求,该呼叫负责将更改写入Prefs.js文件,是Firefox偏好的中央存储。

它永远不会在多个线程上同时调用它,我们认为这有可能破坏prefs.js文件。因此,在下次启动期间,文件将无法加载并丢弃(重置为默认Prefs)。多年来,我们有很多错误报告与此文件有关,神奇地丢失其自定义首选项,但我们从未找到根本原因。我们现在认为这个错误至少部分负责这些损失。

我们认为这是一个特别好的失败的例子,原因有两个:这是一个比崩溃更有害影响的种族,它捕获了在其原始设计参数之外使用的东西的更大逻辑错误。

在几个场合,我们遇到了一种在良性的边界上遇到的模式,我们认为有些额外的注意力:故意读取价值,但后来稍后做正确验证的检查。例如,代码:

请不要这样做。这些模式非常脆弱,即使它们通常工作,它们最终是未定义的行为。只需编写适当的原子代码 - 您通常会发现性能完全正常。

在TSAN部署期间我们必须解决的另一个困难是由于我们的代码库的一部分是现在被锈写的,这对消毒者来说具有更大的成熟支持。这意味着我们在仍在开发的情况下,我们花了很大程度上的所有锈蚀代码。

我们并不特别关注我们的生锈代码,其中有很多比赛,而是通过通过Rust来混淆C ++代码中的比赛。事实上,我们强烈建议完全锈的新项目,以避免数据竞争。

特别是最困难的部分是需要使用Tsan仪器重建锈标准图书馆。在夜间有一个不稳定的功能-zbuild-std,让我们确切地做到这一点,但它仍然有很多粗糙的边缘。

我们与Build-STD的最大障碍是它目前与助手建立环境不兼容,Firefox使用。修复这并不简单,因为货物用于在依赖项中修补的工具不是用于仅影响子图(即,STD而不是您自己的代码)。到目前为止,我们通过维持鲁道科/货物上的一小套补丁来减轻了这一点,这对Firefox提供了这一点,但需要进一步的工作来上游。

但是通过Build-STD被攻击为我们为我们工作,我们能够为我们的锈蚀代码进行炼制,并且很乐意发现问题很少!我们发现的大多数事情是C ++竞赛,恰好通过一些铁锈代码,因此被我们的毯子抑制所隐藏。

第一个是Bug 1674770,这是Park_lot库中的一个错误。该RUST库提供同步基元和其他并发工具,并由专家编写和维护。我们没有调查影响,但问题是夫妇原子排序太弱,并由作者迅速修复。这是又一个示例,证明了写无码并发代码是多么困难。

第二个是错误1686158,这是WebRender软件OpenGL Shim中的一些代码。他们使用原始原子来维持一些手工卷起的共享变形状态,以实现部分实施,但忘了制作一个原子的原子。这很容易修复。

整体rust似乎符合其原始设计目标之一:允许我们安全地编写更多并发代码。 WebRender和Stylo都非常大而普遍地多线程,但具有最小的线程问题。我们发现的问题是在低级和明确不安全的多线程抽象中的实现中的错误 - 这些错误很简单。

这与我们的许多C ++比赛相反,这通常涉及在不同的线程上随机访问的语义上的不同线程中,需要代码的非琐碎重构。

数据种族是一个低估的问题。由于他们的复杂性和间歇性,我们经常努力识别它们,找到他们的原因并正确地判断他们的影响。在许多情况下,这也是一种耗时的过程,浪费了宝贵的资源。 ThreadSanitizer已被证明不仅有效地定位数据种族并提供足够的调试信息,而且即使在作为Firefox的项目中也是实用的。

我们要感谢ThreadSanitizer的作者提供工具,特别是DMitry Vyukov(Google),帮助我们在部署期间帮助我们使用一些复杂的Firefox特定边缘案例。