网络字体的内存安全

Dominik Röttsches
Dominik Röttsches
Rod Sheeter
Rod Sheeter
Chad Brokaw
Chad Brokaw

发布时间:2025 年 3 月 19 日

Skrifa 采用 Rust 编写,旨在替代 FreeType,以便为所有用户确保 Chrome 中的字体处理安全。Skifra 利用了 Rust 的内存安全特性,让我们能够更快地迭代 Chrome 中的字体技术改进。从 FreeType 迁移到 Skrifa 后,我们在更改字体代码时可以灵活且无所畏惧。现在,我们在修复安全 bug 上所花的时间大大减少了,从而加快了更新速度并提高了代码质量。

本文将介绍 Chrome 为何弃用 FreeType,以及此举带来的一些有趣的技术细节改进。

为什么要替换 FreeType?

网络的独特之处在于,它允许用户从各种不受信任的来源提取不受信任的资源,并希望一切都能正常运行且安全无虞。这种假设通常是正确的,但要向用户兑现这一承诺,需要付出代价。例如,为了安全地使用 Web 字体(通过网络传送的字体),Chrome 会采用多种安全防范措施:

  • 根据两条规则,系统会对字体处理进行沙盒化处理:字体不可信,使用字体的代码也不安全。
  • 在处理之前,系统会将字体传递给 OpenType Sanitizer
  • 与解压缩和处理字体相关的所有库都经过了模糊测试

Chrome 附带 FreeType,并将其用作 Android、ChromeOS 和 Linux 上的主字体处理库。也就是说,如果 FreeType 存在漏洞,大量用户都会受到影响。

Chrome 使用 FreeType 库来计算指标并从字体加载提示轮廓。总体而言,使用 FreeType 对 Google 来说是一次巨大的胜利。它执行复杂的工作,并且做得很好,我们非常依赖它,并为其做出贡献。不过,它是使用不安全代码编写的,起源于恶意输入可能性较低的时代。仅仅跟上通过模糊测试发现的问题,Google 就需要至少 0.25 名全职软件工程师。更糟糕的是,我们显然无法找到所有问题,或者只能在代码分发给用户后发现问题。

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

为什么问题会不断出现

在评估 FreeType 的安全性时,我们发现了三类主要问题(并非详尽无遗):

使用不安全的语言

模式/问题 示例
手动内存管理
未经检查的数组访问 CVE-2022-27404
整数溢出 在执行嵌入式虚拟机以对 CFF 绘制和提示进行 TrueType 提示期间
https://issues.oss-fuzz.com/issues?q=FreeType%20Integer-overflow
对零化分配和非零化分配的使用不当 https://gitlab.freedesktop.org/freetype/freetype/-/merge_requests/94 中的讨论,之后发现了 8 个模糊测试问题
类型转换无效 请参阅以下行,了解宏用量

项目专用问题

模式/问题 示例
宏掩盖了缺少显式大小类型的事实
  • FT_READ_*FT_PEEK_* 等宏会掩盖所使用的整数类型,隐藏了未使用具有显式大小的 C99 类型(int16_t 等)
新代码总是会引入 bug,即使采用防御性编码方式也是如此。
  • COLRv1 和 OT-SVG 支持都产生了问题
  • 模糊测试会发现一些(但不一定是所有)#32421#52404
缺少测试
  • 制作测试字体既耗时又困难

依赖项问题

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

模糊测试还不够

模糊测试是一种使用各种输入(包括随机无效输入)进行自动化测试的方法,旨在发现 Chrome 稳定版中存在的许多类型的问题。我们在 Google 的 oss-fuzz 项目中对 FreeType 进行了模糊测试。它确实可以发现问题,但由于以下原因,字体已被证明对模糊测试具有一定抵抗力。

字体文件非常复杂,与视频文件类似,因为它们包含多种不同类型的信息。字体文件是多个表格的容器格式,其中每个表格都有不同的用途,用于共同处理文本和字体,以便在屏幕上生成正确定位的字形。在字体文件中,您会看到:

  • 静态元数据,例如可变字体的字体名称和参数。
  • 从 Unicode 字符到字体的映射。
  • 用于字形屏幕布局的复杂规则集和语法。
  • 视觉信息:描述放置在屏幕上的字符的外观的字符形状和图片信息。
    • 视觉表格反过来可以包含 TrueType 提示程序,这些程序是执行以更改字形形状的小程序。
    • CFF 或 CFF2 表中的字符串,它们是 CFF 渲染引擎中执行的强制性曲线绘制和提示指令。

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

由于格式较为复杂,模糊测试在查找字体文件中的问题时存在缺点。

由于以下原因,很难实现良好的代码覆盖率或模糊测试工具进度:

  • 使用简单的位翻转/移位/插入/删除式修饰符对 TrueType 提示程序、CFF 字符串和 OpenType 布局进行模糊处理时,很难覆盖所有状态组合。
  • 模糊测试至少需要生成部分有效的结构。而随机突变很少会这样做,这使得很难实现良好的覆盖率,尤其是对于更深层级的代码。
  • ClusterFuzz 和 oss-fuzz 目前的模糊测试工作尚未使用结构感知型变异。使用语法或结构感知型修饰符可能会有助于避免生成在早期被拒绝的变体,但代价是需要花费更多时间进行开发,并且可能会错过搜索空间的部分内容。

多个表中的数据需要保持同步,才能推进模糊测试:

  • 模糊测试工具的常规变异模式不会生成部分有效的数据,因此许多迭代会被拒绝,进度会变慢。
  • 字形映射、OpenType 布局表和字形绘制相互关联且相互依赖,形成一个组合空间,其角落很难通过模糊处理来触及。
  • 例如,高严重程度的 tt_face_get_paintCOLRv1 漏洞就花了 10 多个月才被发现。

尽管我们已尽最大努力,但字体安全问题仍屡次出现在最终用户面前。将 FreeType 替换为 Rust 替代方案可防止多种类型的漏洞。

Chrome 中的 Skrifa

Skia 是 Chrome 使用的图形库。Skia 依赖于 FreeType 从字体加载元数据和字体样式。Skrifa 是一个 Rust 库,属于 Fontations 库系列,可为 Skia 使用的 FreeType 部分提供安全替代方案。

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

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

未来,我们还将针对操作系统字体改用 Fontation,首先是 Linux 和 ChromeOS,然后是 Android。

安全第一

我们的首要目标是减少(最好是消除!)由对内存超出边界的访问导致的安全漏洞。Rust 开箱即用即可提供此功能,前提是您避免使用任何不安全代码块。

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

为了避免出现不安全的代码,我们选择将此责任外包给 bytemuck,这是一个专为此目的而设计的 Rust 库,在整个生态系统中得到了广泛的测试和使用。将原始数据重新解释集中在 bytemuck 中可确保我们将此功能集中到一个位置并进行审核,并避免重复使用不安全代码来实现此目的。安全转换项目旨在将此功能直接纳入 Rust 编译器,我们将在该功能可用后立即进行切换。

正确性至关重要

Skrifa 由独立的组件构建而成,其中大多数数据结构都被设计为不可变。这有助于提高可读性、可维护性和多线程处理能力。这也让代码更易于单元测试。我们抓住了这个机会,编写了一组大约 700 个单元测试,涵盖了从低级解析例程到高级提示虚拟机的整个堆栈。

正确性还意味着保真度,FreeType 因其能够生成高质量的轮廓而备受推崇。我们必须达到此质量才能成为合适的替代品。为此,我们构建了一款名为 fauntlet 的专用工具,用于比较 Skrifa 和 FreeType 在各种配置下对批量字体文件的输出。这让我们能够确保避免质量回归。

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

模糊测试是一项重要的工具,可用于确定软件对格式错误和恶意输入的响应方式。自 2024 年 6 月以来,我们一直在不断对新代码进行模糊测试。这涵盖了 Rust 库本身和集成代码。虽然 fuzzer 在撰写本文时发现了 39 个 bug,但值得注意的是,其中没有任何 bug 属于严重的安全问题。它们可能会导致不期望的视觉结果,甚至导致受控崩溃,但不会导致可利用的漏洞。

继续!

我们对使用 Rust 处理文本所取得的成效非常满意。 向用户提供更安全的代码提高开发者的工作效率对我们来说是巨大的胜利。我们计划继续寻找机会在文本堆栈中使用 Rust。如需了解详情,请参阅 Oxidize,其中概述了 Google Fonts 的部分未来计划。