[中文正文内容]

Web Fonts的内存安全

使用集合保持井井有条 根据您的喜好保存内容并对其进行分类。 Dominik Röttsches Dominik Röttsches GitHub Mastodon Rod Sheeter Rod Sheeter GitHub Chad Brokaw Chad Brokaw GitHub 发布时间:2025 年 3 月 19 日

Skrifa 是用 Rust 编写的,旨在替代 FreeType,从而使 Chrome 中的字体处理对所有用户都安全。Skrifa 利用了 Rust 的内存安全性,让我们能够在 Chrome 中更快地迭代字体技术改进。从 FreeType 迁移到 Skrifa 使我们在更改字体代码时既敏捷又无所畏惧。现在,我们花在修复安全漏洞上的时间大大减少,从而加快了更新速度并提高了代码质量。

这篇文章分享了 Chrome 为什么放弃 FreeType,以及这项举措所带来的改进的一些有趣的技术细节。

为什么替换 FreeType?

Web 的独特之处在于,它允许用户从各种不受信任的来源获取不受信任的资源,并期望一切都能正常运行,并且这样做是安全的。这个假设通常是正确的,但是兑现对用户的承诺是有代价的。例如,为了安全地使用 Web Font(通过网络传送的字体),Chrome 采用了多种安全缓解措施:

Chrome 附带 FreeType,并在 Android、ChromeOS 和 Linux 上使用它作为主要的字体处理库。这意味着如果 FreeType 中存在漏洞,则会暴露 大量 用户。

Chrome 使用 FreeType 库来计算指标并从字体加载提示轮廓。总的来说,使用 FreeType 对 Google 来说是一个巨大的胜利。它完成了一项复杂的工作,并且做得很好,我们广泛地依赖它并为其做出贡献。但是,它是用不安全的代码编写的,并且起源于恶意输入不太可能出现的时代。仅仅跟上 fuzzing 发现的问题流就需要 Google 至少 0.25 名全职软件工程师。更糟糕的是,我们显然没有发现所有内容,或者仅在代码发布给用户后才发现内容。

这种问题模式并非 FreeType 独有,我们观察到,即使我们使用我们能找到的最好的软件工程师,代码审查每个更改并要求进行测试,其他不安全的库也会出现问题。

为什么问题不断潜入?

当我们评估 FreeType 的安全性时,我们观察到发生了三个主要类别的问题(非详尽):

使用不安全的语言

模式/问题 | 示例 ---|--- 手动内存管理 |

未检查的数组访问 | CVE-2022-27404 整数溢出 | 在执行用于 TrueType 提示 CFF 绘图和提示的嵌入式虚拟机期间 https://issues.oss-fuzz.com/issues?q=FreeType%20Integer-overflow 错误地使用零填充与非零填充分配 | 在 https://gitlab.freedesktop.org/freetype/freetype/-/merge_requests/94 中讨论,之后发现了 8 个 fuzzer 问题 无效的强制转换 | 请参见下面有关宏用法的行

项目特定问题

模式/问题 | 示例 ---|--- 宏掩盖了缺少显式大小类型的事实 |

即使编写防御性代码,新代码也会持续添加错误。 |

缺少测试 |

依赖关系问题

Fuzzing 反复发现了 FreeType 依赖的库(例如 bzip2、libpng 和 zlib)中的问题。例如,比较 freetype_bdf_fuzzer: Use-of-uninitialized-value in inflate

Fuzzing 还不够

Fuzzing (使用各种输入(包括随机无效输入)进行的自动化测试) 旨在查找进入 Chrome 稳定版本的许多类型的问题。我们作为 Google 的 oss-fuzz 项目的一部分对 FreeType 进行 fuzzing。它的确发现了问题,但是字体已被证明对 fuzzing 有一定的抵抗力,原因如下。

字体文件很复杂,与视频文件相当,因为它们包含多种不同类型的信息。字体文件是多个表的容器格式,其中每个表在将文本和字体一起处理以在屏幕上产生正确定位的字形方面起着不同的作用。在字体文件中,您将找到:

字体文件中的复杂性相当于拥有自己的编程语言和状态机处理,需要特定的虚拟机来执行它们。

由于格式的复杂性,fuzzing 在查找字体文件中的问题方面存在缺点。

由于以下原因,很难实现良好的代码覆盖率或 fuzzer 进度:

多个表中的数据需要同步才能使 fuzzing 取得进展:

尽管我们尽了最大的努力,字体安全问题还是反复影响到最终用户。用 Rust 替代品替换 FreeType 将可以防止多种类型的漏洞。

Chrome 中的 Skrifa

Skia 是 Chrome 使用的图形库。Skia 依靠 FreeType 从字体加载元数据和字母形式。Skrifa 是一个 Rust 库,是 Fontations 系列库的一部分,它为 Skia 使用的 FreeType 部分提供了安全的替代品。

为了将 FreeType 过渡到 Skia,Chrome 团队开发了一个新的基于 Skrifa 的 Skia 字体后端,并逐步向用户推广了这一更改:

为了集成到 Chrome 中,我们依赖于 Chrome 安全团队 引入的 Rust 与代码库的顺利集成。

将来,我们还将切换到 Fontations 来处理操作系统字体,首先是 Linux 和 ChromeOS,然后是 Android。

安全第一

我们的主要目标是减少(或理想情况下,消除!)由越界访问内存引起的安全漏洞。只要您避免任何 unsafe 代码块,Rust 就可以开箱即用地提供此功能。

我们的性能目标要求我们执行当前不安全的一项操作:将任意字节重新解释为强类型数据结构。这使我们无需执行 不必要的副本 即可从字体文件中读取数据,这对于生成快速字体解析器至关重要。

为了避免我们自己的不安全代码,我们选择将此责任外包给 bytemuck,它是一个专门为此目的而设计的 Rust 库,并在整个生态系统中得到广泛的测试和使用。将原始数据重新解释集中在 bytemuck 中,可确保我们在一个地方具有此功能并经过审核,并避免出于目的而重复不安全的代码。safe transmute project 旨在将此功能直接集成到 Rust 编译器中,我们将在可用后立即进行切换。

正确性很重要

Skrifa 由独立的组件构建,其中大多数数据结构都设计为不可变的。这提高了可读性、可维护性和多线程处理能力。它还使代码更适合于单元测试。我们利用了这个机会,并生成了一套大约 700 个单元测试,涵盖了从低级解析例程到高级提示虚拟机的完整堆栈。

正确性也意味着保真度,而 FreeType 因其高质量轮廓的生成而备受推崇。我们必须匹配此质量才能成为合适的替代品。为此,我们构建了一个名为 fauntlet 的定制工具,该工具比较了 Skrifa 和 FreeType 在各种配置下的一批字体文件的输出。这为我们提供了一些保证,我们可以避免质量下降。

此外,在集成到 Chromium 之前,我们对 Skia 进行了 广泛的像素比较,比较了 FreeType 渲染与 Skrifa 以及 Skia 渲染,以确保在所有必需的渲染模式(跨不同的抗锯齿和提示模式)下,像素差异绝对最小。

Fuzz 测试 是确定软件如何对畸形和恶意输入做出反应的重要工具。自 2024 年 6 月以来,我们一直在不断地对新代码进行 fuzzing。这涵盖了 Rust 库本身和集成代码。虽然 fuzzer 已经发现(截至撰写本文时)39 个错误,但值得注意的是 这些错误都不是对安全至关重要的。它们可能会导致不良的视觉结果,甚至导致受控的崩溃,但不会导致可利用的漏洞。

前进!

我们对使用 Rust 进行文本处理所取得的成果感到非常满意。向用户交付更安全的代码 提高开发人员的工作效率对我们来说是一个巨大的胜利。我们计划继续寻找在文本堆栈中使用 Rust 的机会。如果您想了解更多信息,Oxidize 概述了 Google Fonts 的未来计划。

除非另有说明,否则此页面的内容均根据 Creative Commons Attribution 4.0 License 获得许可,并且代码示例根据 Apache 2.0 License 获得许可。有关详细信息,请参见 Google Developers Site Policies。Java 是 Oracle 和/或其关联公司的注册商标。

上次更新时间 2025-03-19 UTC。