用 Rust 重写浏览器组件的意义

此前的 无所畏惧的安全系列文章 探讨了 Rust 中的 内存安全线程安全。这篇收尾文章以 Quantum CSS 项目 为案例研究,探讨了用 Rust 重写代码的现实影响。

样式组件是浏览器中将 CSS 规则应用于页面的部分。这是一个在 DOM 树上自上而下的过程:在给定父级样式的情况下,可以独立计算子级的样式,这非常适合并行计算。到 2017 年,Mozilla 已经尝试过两次使用 C++ 并行化样式系统,但都失败了。

Quantum CSS 源于提高页面性能的需求,而提升安全则是额外的收获。

Rewrites code to make it faster; also makes it more secure

内存安全违规与安全相关错误之间有很大重叠,因此我们预计这次重写将减少 Firefox 中的攻击面。在这篇文章中,我将总结自 Firefox 于 2002 年首次发布 以来,样式代码中出现的潜在安全漏洞。然后,我将分析使用 Rust 能否以及如何防止这些漏洞。

在 Firefox 的样式组件的生命周期中,共出现了 69 个安全漏洞。如果我们有一台时间机器,可以从一开始就用 Rust 编写这个组件,其中 51 个(73.9%)漏洞就不会出现。虽然 Rust 使得编写更优质的代码变得更容易,但它并非万无一失。

Rust

Rust 是一种现代系统编程语言,具有类型安全和内存安全。作为这些安全保证的副作用,Rust 程序也以在编译时线程安全而闻名。因此,当

✅ 安全地处理不可信输入。
✅ 引入并行性以提高性能。
✅ 将隔离的组件集成到现有代码库中。

时,Rust 可能是特别好的选择。然而,有一些类型的错误是 Rust 明确不处理的,特别是正确性错误。事实上,在 Quantum CSS 重写过程中,工程师意外地重新引入了 C++ 代码中先前已修复的一个关键安全错误,导致 错误 641731 的修复失效。这允许通过 SVG 图像文档泄露全局历史记录,导致出现 错误 1420001。作为一个简单的历史窃取错误,其安全等级被评为高。原始修复是在使用 SVG 文档作为图像之前进行额外检查。不幸的是,在重写过程中忽略了此检查。

虽然我们有旨在捕获类似此类 :visited 规则违规的自动化测试,但实际上它们并没有检测到此错误。为了加快自动化测试速度,我们暂时关闭了测试此功能的机制,如果测试没有运行,就没什么用。重新实现逻辑错误的风险可以通过良好的测试覆盖率(以及实际运行测试)来降低。仍然存在引入新的逻辑错误的风险。

随着开发人员对 Rust 语言的熟悉程度提高,最佳实践也将得到改进。用 Rust 编写的代码将变得更加安全。虽然它可能无法防止所有可能的漏洞,但 Rust 消除了最严重错误的整个类别。

Quantum CSS 安全漏洞

总的来说,与内存、边界、空/未初始化变量或整数溢出相关的错误在 Rust 中默认会被阻止。我上面提到的杂项错误不会被阻止,它是由分配失败导致的崩溃。

按类别划分的安全漏洞

本分析中所有的漏洞都与安全相关,但只有 43 个获得了官方安全分类。(这些分类由 Mozilla 的安全工程师根据对“可利用性”的判断而分配。)普通的错误可能表明缺少功能或问题,例如崩溃。虽然不可取,但崩溃不会导致数据泄露或行为修改。官方的安全漏洞的严重程度从低(范围非常有限)到严重漏洞(可能允许攻击者在用户的平台上运行任意代码)不等。

内存漏洞和严重的安全问题之间存在很大的重叠。在 34 个严重/高等级漏洞中,有 32 个与内存相关。

按安全等级分类的漏洞

比较 Rust 和 C++ 代码

错误 955914GetCustomPropertyNameAt 函数中的堆缓冲区溢出。代码使用了错误的变量进行索引,导致解释数组末尾之后的内存。这会导致在访问错误的指针时崩溃,或者将内存复制到传递给另一个组件的字符串中。

所有 CSS 属性(包括长属性和自定义属性)的顺序都存储在数组 mOrder 中。每个元素要么由其 CSS 属性值表示,要么在自定义属性的情况下,由从 eCSSProperty_COUNT(非自定义 CSS 属性的总数)开始的值表示。要检索自定义属性的名称,首先必须从 mOrder 中检索自定义属性值,然后访问 mVariableOrder 数组中对应索引处的名称,该数组按顺序存储自定义属性名称。

有漏洞的 C++ 代码

    void GetCustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const {
            MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT);

            aResult.Truncate();
            aResult.AppendLiteral("var-");
            aResult.Append(mVariableOrder[aIndex]);

问题出现在第 6 行,使用 aIndex 访问 mVariableOrder 数组的元素。aIndex 旨在与 mOrder 数组一起使用,而不是与 mVariableOrder 数组一起使用。mOrder 中由 aIndex 表示的自定义属性的对应元素实际上是 mOrder[aIndex] - eCSSProperty_COUNT

已修复的 C++ 代码

    void Get CustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const {
      MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT);

      uint32_t variableIndex = mOrder[aIndex] - eCSSProperty_COUNT;
      aResult.Truncate();
      aResult.AppendLiteral("var-");
      aResult.Append(mVariableOrder[variableIndex]);
    }

等效的 Rust 代码

虽然 Rust 在某些方面类似于 C++,但习惯性的 Rust 使用了不同的抽象和数据结构。Rust 代码与 C++ 代码看起来非常不同(有关详细信息,请参见下文)。首先,让我们考虑一下如果我们尽可能地直译有漏洞的代码会发生什么

    fn GetCustomPropertyNameAt(&self, aIndex: usize) -> String {
        assert!(self.mOrder[aIndex] >= self.eCSSProperty_COUNT);

        let mut result = "var-".to_string();
        result += &self.mVariableOrder[aIndex];
        result
    }

Rust 编译器会接受代码,因为没有办法在运行时确定向量的长度。与必须知道其长度的数组不同,Rust 中的 Vec 类型 是动态大小的。然而,标准库向量实现具有内置的边界检查功能。当使用无效索引时,程序会以受控的方式立即终止,防止任何非法访问。

Quantum CSS 中的 实际代码 使用了非常不同的数据结构,因此没有完全等效的代码。例如,我们使用 Rust 中强大的内置数据结构来统一排序和属性名称数据。这样一来,我们就不需要维护两个独立的数组。Rust 数据结构还改善了数据封装,并减少了这类逻辑错误的可能性。由于代码需要与浏览器引擎中其他部分的 C++ 代码进行交互,因此新的 GetCustomPropertyNameAt 函数看起来不像习惯性的 Rust 代码。它仍然提供所有安全保证,同时提供更易于理解的底层数据抽象。

tl;dr;

由于内存安全违规与安全相关错误之间存在重叠,我们可以说 Rust 代码应该会产生更少的关键 CVE(常见漏洞和披露)。然而,即使是 Rust 也并非万无一失。开发人员仍然需要意识到正确性错误和数据泄露攻击。代码审查、测试和模糊测试对于维护安全的库仍然至关重要。

编译器无法捕获程序员可能犯下的所有错误。但是,Rust 的设计消除了内存安全的负担,使我们能够专注于逻辑正确性和健壮性。

关于 Diane Hosfelt

Diane Hosfelt 的更多文章……


16 条评论

  1. stillDreaming1

    什么是模糊测试,它与安全有什么关系?

    2019 年 2 月 28 日 下午 12:29

    1. Diane Hosfelt

      模糊测试 是一种技术,它向程序提供随机输入,并查看任何输入是否会导致崩溃或其他问题。 浏览器尤其广泛地使用模糊测试来发现潜在的漏洞。

      2019 年 2 月 28 日 下午 12:57

  2. Aky

    好文章!Rust 的类型和内存安全优势听起来不错…… 但它似乎描述了更多发生在 Mozilla 实验室的事情,而不是实践中——在那里,让它在第一位工作是命中注定的。 Rust 最受欢迎的用途是在 Firefox 中,但它无法编译当前的 ESR (60.5.2)
    rustc[25334]: segfault at ffffffff ip 00000000ffffffff sp 00007f37697fcf70 error 14 in rustc[55db58dbc000+1000]

    由于 Firefox 的许多主要版本需要特定版本的 rustc,有时甚至这种特异性也不足以满足,因此 rust 似乎太不稳定,无法被归类为系统编程语言。

    当 Rust 真的能用时,我很想再试一次。 但在那之前,它仍然只是 Firefox 的一个附加依赖项。

    2019 年 2 月 28 日 下午 12:47

    1. D D

      可能是 Firefox 的构建系统试图以过于复杂和脆弱的方式或以某种错误的方式来完成事情——或者可能是 Firefox 的 Rust 代码过于依赖于实验性的 Rust 语言特性……

      但我认为代码导致 rustc 出错/段错误并不常见。 此外,还有很多底层代码,甚至操作系统内核也用 Rust 编写。 所以我认为对于系统编程语言来说,它运行得很好。 语言的开发者承认它是一种年轻的语言,还有成长的空间和成熟的空间。 但我认为 Rust 在此时可以“准备好投入生产”,特别是如果您在需要时向 Rust 社区寻求支持或故障排除。

      2019 年 3 月 6 日 下午 7:43

  3. tim

    那些 *正确性错误* 通常被称为逻辑错误。 好文章!

    2019 年 2 月 28 日 下午 1:20

    1. Rune K. Svendsen

      给出的例子听起来像是内存安全错误,而不是逻辑错误。

      2019 年 3 月 6 日 上午 8:47

  4. teki

    C++ 也能进行边界检查,只需使用 .at() 而不是 []。

    2019 年 2 月 28 日 下午 7:53

  5. Tom Gee

    几年前,我们将这种类型的测试命名为“五岁儿童测试套件”,这是为了纪念我们一位同事的五岁女儿,她在一个实时测试程序前被遗忘了。 当那位父亲回来时,她正在一台冻结的屏幕上快乐地打字(这是系统代码)。 当他问她在做什么时,她笑着说:“写信给奶奶”。 我们都笑了起来。 每当他带着她来时,我们都会让她给奶奶写一封信。 很多次,它要么产生意想不到的结果,要么崩溃。

    抱歉插嘴,但这太棒了,错过了这个机会。 如果你喜欢,请删除。

    此致敬礼,

    TGee

    2019 年 3 月 1 日 下午 5:28

  6. Carl Rash

    你见过 Rust hello world 应用程序的大小吗? 令人难以置信。

    2019 年 3 月 4 日 上午 4:38

    1. Jonathan Adams

      fn main() {
      println!(“Hello World!”);
      }

      这对我来说似乎是正常的。

      2019 年 3 月 7 日 下午 12:14

    2. D D

      上次我检查时,用 Rust 编译的 hello_world 在磁盘上大约为 2.4 MB(使用 cargo/rustc 1.33.0)。 这与用 C 编译的 hello_world 相比相当大(大约 16.5 KB,使用 gcc 8)

      几个月前,Reddit 上有一篇文章解释了背后的优先级:https://www.reddit.com/r/rust/comments/9m2dwo/noob_question_why_are_rust_binaries_so_big/

      正如线程中所提到的,默认情况下每个二进制文件都包含一些功能,这些功能会增加大小。(这可以关闭,显然。)

      我个人不觉得有资格知道我是否应该同意所做的权衡,但对于任何想知道为什么 Rust 中真正简单的应用程序的二进制文件比其他语言(使用 Rust 的默认设置)更大的人来说,这个线程解释了这一点。 并且至少有一些理由可以解释这种尺寸。

      2019 年 3 月 8 日 上午 9:39

    3. jcdyer

      其中大部分只是调试符号,这些符号默认情况下包含在 Rust 构建中。 在构建上运行 strip 会清除大部分这些内容。

      cliff@conakry:~$ cargo new /tmp/foo
      创建二进制文件(应用程序)`/tmp/foo` 包
      cliff@conakry:/tmp/foo$ cargo build
      正在编译 foo v0.1.0 (/tmp/foo)
      c 已完成开发 [未优化 + 调试信息] 目标在 0.96 秒内完成
      cliff@conakry:/tmp/foo$ cargo build –release
      正在编译 foo v0.1.0 (/tmp/foo)
      已完成发布 [优化] 目标在 0.21 秒内完成
      cliff@conakry:/tmp/foo$ ls -al target/*/foo
      -rwxr-xr-x 2 cliff cliff 2427528 2019 年 3 月 19 日 上午 9:53 target/debug/foo
      -rwxr-xr-x 2 cliff cliff 2415600 2019 年 3 月 19 日 上午 9:53 target/release/foo
      cliff@conakry:/tmp/foo$ strip target/*/foo
      cliff@conakry:/tmp/foo$ ls -al target/*/foo
      -rwxr-xr-x 2 cliff cliff 199080 2019 年 3 月 19 日 上午 9:54 target/debug/foo
      -rwxr-xr-x 2 cliff cliff 194904 2019 年 3 月 19 日 上午 9:54 target/release/foo

      200k 仍然比 16.5k 大,但对于大多数用途来说并不算太糟糕。 当您真正开始编写程序时,它就更不相关了。 使用 #[no_std] 构建,您可能会在任何受严格限制的环境中使用它,这可以使生成的二进制文件更小。

      2019 年 3 月 19 日 上午 6:57

  7. Dan Neely

    你的图表有问题。 它们显示为嵌入式 JSFiddle,而不是渲染; 而且当我尝试在新的标签中打开并运行它们时,它们会静默失败。(单击运行不会产生任何输出。)

    2019 年 3 月 4 日 上午 5:46

    1. Diane Hosfelt

      谢谢! 现在应该修复了。

      2019 年 3 月 5 日 上午 10:45

  8. Javier Sánchez

    我想试试。

    2019 年 3 月 7 日 上午 9:24

  9. Wellington Torrejais da SIlva

    不错! 谢谢!

    2019 年 3 月 8 日 上午 6:40

本文的评论已关闭。