大约一年前,Mozilla 启动了一项提升 Linux 上 Firefox 稳定性的工作。这项工作很快成为开源项目之间良好协同效应的典范。
每次 Firefox 崩溃时,用户都可以向我们发送崩溃报告,我们使用这些报告来分析问题并希望能够修复它。
该报告包含一个小型转储,其中包含崩溃时进程内存的小快照。这包括处理器寄存器的内容以及每个线程堆栈的数据。
下面是它的典型外观
如果您熟悉核心转储,那么小型转储本质上是它们的缩小版本。小型转储格式最初是在微软设计,Windows 有一个原生方式来写入小型转储。在 Linux 上,我们使用 Breakpad 来完成此任务。Breakpad 最初起源于 Google 用于其软件(Picasa、Google Earth 等),但我们已经分叉了它,并针对我们的目的进行了大量修改,最近还用 Rust 部分重写了它。
一旦用户提交了崩溃报告,我们就会有一个服务器端组件(称为 Socorro)来处理它,并从小型转储中提取堆栈跟踪。然后,根据崩溃线程堆栈跟踪的顶层方法名称对报告进行聚类。当发现新的崩溃时,我们会为其分配一个错误并开始着手解决它。请参见下面的图片,了解崩溃是如何分组的示例
要从小型转储中提取有意义的堆栈跟踪,还需要两件事:展开信息和符号。展开信息是一组指令,它们描述了在给定指令指针的情况下,如何找到堆栈中的各个帧。符号信息包含与给定地址范围相对应的函数名称,以及它们来自的源文件以及给定指令对应的行号。
在常规的 Firefox 版本中,我们从构建文件中提取此信息,并将其存储到 Breakpad 标准格式的符号文件中。有了这些信息,Socorro 就可以生成人类可读的堆栈跟踪。整个流程如下图所示
下面是一个正确的堆栈跟踪示例
如果 Socorro 无法访问崩溃的适当符号文件,则生成的跟踪将只包含地址,这并没有什么帮助
说到 Linux,它的工作方式与其他平台不同:我们的大多数用户都不会安装我们的构建,他们会安装适合其最喜爱的发行版的 Firefox 版本。
这在处理 Linux 上的稳定性问题时带来了一个重大问题:对于我们的大多数崩溃报告,我们无法生成高质量的堆栈跟踪,因为我们没有所需的符号信息。提交报告的 Firefox 构建不是我们完成的。更糟糕的是,Firefox 依赖于许多第三方软件包(如 GTK、Mesa、FFmpeg、SQLite 等)。如果崩溃发生在这些软件包中而不是 Firefox 本身,我们也无法获得良好的堆栈跟踪,因为我们没有它们的符号。
为了解决这个问题,我们开始从多个发行版(Arch、Debian、Fedora、OpenSUSE 和 Ubuntu)的软件包存储库中提取 Firefox 构建及其依赖项的调试信息。由于每个发行版都略有不同,因此我们必须编写特定于发行版的脚本,这些脚本会遍历其存储库中的软件包列表并查找关联的调试信息(这些脚本可以在 这里 获取)。然后,将这些数据输入一个工具,该工具从调试信息中提取符号文件并将其上传到我们的符号服务器。
有了这些信息,我们能够分析 99% 以上我们从 Linux 用户那里收到的崩溃报告,而之前只有不到 20%。下面是一个从发行版打包的 Firefox 版本中提取的高质量跟踪示例。我们还没有构建任何相关的库,但函数名称存在,受影响代码的文件和行号也存在
这非常重要:Linux 用户往往更精通技术,更有可能帮助我们解决问题,因此所有这些报告对于提高其他操作系统(Windows、Mac、Android 等)的稳定性都是宝贵的财富。特别是,我们通常首先在 Linux 上发现 Fission 错误。
这项新获得的检查 Linux 崩溃能力的第一个影响是,它极大地加快了我们对特定于 Linux 的问题的响应时间,并且通常允许我们在 Nightly 和 Beta 版本的 Firefox 进入发布通道之前识别出问题。
我们还可以快速识别尖端组件(如 WebRender、WebGPU、Wayland 和 VA-API 视频加速)中的问题和回归;通常在导致问题的更改后几天内提供修复。
我们并没有止步于此:我们现在可以识别特定于发行版的问题和回归。这使我们能够通知软件包维护人员这些问题,并让他们快速解决。例如,我们能够在一个问题引入后仅两周就识别出一个特定于 Debian 的问题并立即修复它。这个崩溃是由 Debian 对 Firefox 依赖项之一进行的修改造成的,该修改会导致启动时崩溃,如果您对细节感兴趣,它已在错误 1679430 中记录。
另一个很好的例子来自 Fedora:他们一直使用自己的崩溃报告系统(ABRT)来捕获其 Firefox 构建中的 Firefox 崩溃,但鉴于我们方面的改进,他们开始改为向我们发送 Firefox 崩溃。
我们还可以最终识别出我们依赖项中的回归和问题。这使我们能够向上游沟通这些问题,有时甚至贡献修复,使我们的用户和他们的用户都受益。
例如,在某个时候,Debian 更新了 fontconfig 软件包,通过反向移植上游针对内存泄漏的修复。不幸的是,该修复中包含一个错误,该错误会 导致 Firefox 崩溃,甚至可能导致其他软件崩溃。我们在更改落地到 Debian 源代码后仅六天就发现了新的崩溃,并且仅几周后,该问题在上游和 Debian 中都得到了修复。我们还向其他项目发送了报告和修复,包括 Mesa、GTK、glib、PCSC、SQLite 等等。
Firefox 的 Nightly 版本还包含一个用于发现安全敏感问题的工具:概率堆检查器。此工具随机填充少量内存分配,以检测缓冲区溢出和使用后释放访问。当它检测到其中之一时,它会向我们发送一个非常详细的崩溃报告。鉴于 Firefox 在 Linux 上拥有庞大的用户群,这使我们能够在 upstream 项目中发现一些难以捉摸的问题并报告它们。
这也暴露出我们用于崩溃分析的工具中的一些限制,因此我们决定用 Rust 重写它们,主要依靠 Sentry 开发的优秀板条箱。生成的工具比我们旧的工具快得多,使用的内存少得多,并且产生了更准确的结果。代码双向流动:我们为他们的板条箱(及其依赖项)做出了改进,而他们则扩展了他们的 API 来解决我们的新用例并修复我们发现的问题。
这项工作的另一个令人愉快的副作用是,Thunderbird 现在也受益于我们为 Firefox 做出的改进。
这表明开源项目之间的协作不仅有利于其用户,最终还有利于整个生态系统以及依赖它的更广泛的社区。
特别感谢 Calixte Denizet、Nicholas Nethercote、Jan Auer 和所有为这项工作做出贡献的人!