Hotdry.
security

Chromium 禁用 C++ 特性的安全工程权衡

深入解析 Chromium 项目为提升安全性而禁用特定 C++ 特性的工程决策,涵盖禁用列表、替代方案与遗留代码迁移策略。

作为全球使用量最大的浏览器引擎之一,Chromium 的代码库承载着数十亿用户的上网安全责任。其 C++ 风格指南不仅遵循 Google C++ 风格指南,更在此基础上根据自身安全需求增加了大量禁用特性。这些禁用决策背后隐藏着深刻的工程考量,理解这些决策对于任何从事高安全性 C++ 开发的人员都具有重要参考价值。

禁用机制背后的安全哲学

Chromium 的安全模型建立在「Rule of 2」这一核心原则之上:当代码处理来自互联网的不可信输入时,必须在以下三个条件中最多选择两个 —— 不可信输入、不安全的实现语言、高权限。这意味着在浏览器这样处理大量不可信输入的高权限应用中,必须通过语言层面的限制来降低内存安全漏洞的风险。

Chromium 采用渐进式的特性支持策略。当新的 C++ 标准发布时,新特性最初会被禁止使用,等待团队评估其安全性、工具链支持情况和与现有代码的兼容性。只有在完成全面评估后,特性才会被移入允许列表或禁止列表。如果某个特性在「待定」列表中停留超过两年,风格仲裁者必须明确决定其去留。这种机制确保了技术债务不会在禁用列表中无限积累。

禁用特性的原因可以归纳为几类:与现有基础设施重叠且 Chromium 的替代方案更优、存在已知的未定义行为风险、工具链支持不完善、或者与 Chromium 的安全模型存在根本性冲突。理解这些分类有助于开发者在遇到类似决策时做出正确的判断。

内存安全与智能指针策略

在所有禁用特性中,最引人注目的可能是对标准智能指针的全面禁用。std::shared_ptrstd::weak_ptr 被禁止使用,相关的 std::enable_shared_from_this::weak_from_this 和透明比较器 std::owner_less 也随之被禁用。这并非因为标准智能指针本身有缺陷,而是因为 Chromium 使用了自研的引用计数机制。

Chromium 的 scoped_refptrbase::WeakPtr 与标准的智能指针有着不同的内存管理语义。标准库的 shared_ptr 采用通用的引用计数实现,而 Chromium 的智能指针针对浏览器的特殊需求进行了优化,包括与 Chromium 的组件系统更好集成、与消息循环的生命周期管理协同工作,以及更精细的控制能力。虽然从功能上来说两者都能实现引用计数,但混用两种不同的引用计数实现会导致难以追踪的内存管理错误和潜在的竞态条件。

这种禁用决定也带来了一些实际成本。新加入 Chromium 的开发者需要学习一套不同于标准库的惯用写法,代码在与其他 C++ 代码库交互时需要额外的适配层。但考虑到浏览器安全漏洞的严重性,Chromium 团队认为这种统一性带来的收益远超这些成本。

异常处理与错误报告机制

Chromium 完全禁止使用 C++ 异常,这是一个影响深远的决策。与之相关的 <exception> 头文件、std::uncaught_exceptions 以及所有基于异常的编程模式都被禁止。这一决策的历史背景是 Chromium 项目早期在全面启用异常支持后发现编译后的二进制文件体积显著增加,且异常处理路径的代码难以进行静态分析。

在禁用异常的背景下,Chromium 发展出了一套独特的错误处理模式。返回值形式的错误码通过 base::expected 类型传递,这种模式将错误处理显式化,要求调用者必须处理可能的失败情况。与异常的隐式传播相比,这种方式虽然写代码时需要更多的手动处理,但使得代码路径更容易追踪,静态分析工具能够更准确地识别未处理的错误情况。

对于需要传播错误的异步操作,Chromium 使用 base::OnceCallbackbase::RepeatingCallback 来封装可能的失败。这种设计将错误处理与异步任务的生命周期管理统一起来,避免了异常在异步边界上传播时可能导致的复杂问题。

字符串处理与区域设置风险

<cctype><ctype.h><cwctype><wctype.h> 这几个来自 C 标准库的头文件被完全禁止使用。这一决定基于两个技术原因:首先,这些函数依赖于 C 区域设置,而 Chromium 的行为不应该因用户或系统的区域设置而改变;其次,当传入的值不能表示为 unsigned charwchar_t 时,这些函数会触发未定义行为。

Chromium 要求开发者使用 third_party/abseil-cpp/absl/strings/ascii.h 中提供的替代函数。这些函数明确指定了行为边界,不会因为区域设置的改变而产生不同的处理结果。在处理网络数据时,这种确定性尤为重要,因为来自互联网的输入不应该受到客户端区域设置的影响。

同样被禁止的还有 std::regex,理由是与 Chromium 中已有的多个正则表达式库存在重叠。在性能敏感的浏览器代码中,third_party/re2 库通常是更好的选择,它提供了更好的编译时性能和更可预测的运行时行为。对于需要正则表达式匹配的场景,Chromium 鼓励开发者评估是否真的需要正则表达式的灵活性,或者简单的字符串操作是否已经足够。

并发原语与线程模型

整个线程支持库被禁用,包括 <thread><mutex><condition_variable><future><barrier><latch><semaphore><stop_token>。这看起来是一个激进的决定,但其背后有深刻的原因。

Chromium 的线程模型建立在 base::Threadbase::MessageLoop 之上,这套系统提供了比标准线程库更丰富的功能,包括任务调度、消息循环集成、性能追踪和优雅关闭机制。标准库的线程原语虽然功能完备,但无法与 Chromium 的消息循环系统无缝集成。如果混用两套系统,可能会导致死锁、资源泄漏或者难以复现的竞态条件。

对于需要互斥锁的场景,Chromium 要求使用 base::Lock 或更轻量的 base::SpinLock。这些类型针对 Chromium 的使用模式进行了优化,并且与现有的追踪和诊断工具集成。对于条件变量,base::ConditionVariable 提供了与消息循环配合使用的特殊版本,能够在等待期间正确处理任务队列中的工作。

并行算法库 <algorithm> 中的并行执行策略同样被禁止。官方解释是 libc++ 的支持尚不完整,且其线程实现与 Chrome 的交互方式不够清晰。Chromium 鼓励开发者对于需要显式并行化的长运行算法使用 Chromium 自有的线程 API,这样能够确保调度器控制、关闭策略和追踪等机制在整个代码库中保持一致。

动态多态与类型安全

std::functionstd::bind 和 C++20 的 std::bind_front 被禁止使用,取而代之的是 Chromium 的 base::Bindbase::Callback 机制。这三组 API 在功能上有明显的重叠,但 Chromium 的实现具有一些独特的优势。

base::Bind 支持弱绑定的 base::Unretainedbase::ConstRef 等扩展,这些在标准 std::bind 中需要更复杂的语法才能实现。更重要的是,base::Callback 与 Chromium 的任务系统深度集成,能够正确地处理回调对象在异步操作执行前就已经被销毁的情况,这是标准 std::function 无法保证的。

std::any 被禁止使用,而 Abseil 的 absl::any 也同样被禁止。Chromium 的代码库中很少有需要存储任意类型值的场景,如果确实需要类似的功能,通常意味着设计存在问题。对于需要类型安全联合的场景,std::variant 是允许使用的,因为它要求类型列表是已知的,比 any 的完全动态类型安全得多。

文件系统与时间处理的替代方案

std::filesystem<chrono> 这两个看似无辜的库也被禁止使用,理由是与 Chromium 自有的 base::FilePathbase::Time 和相关类型存在重叠。这些 Chromium 原生类型在跨平台一致性、性能优化和与浏览器其他部分的集成方面都有特殊的考量。

<chrono> 的禁止还涉及一个更深层的问题:C++ 标准库的时间设施与区域设置存在复杂的交互,而 Chromium 需要其时间处理行为在所有平台上保持一致。base::Timebase::TimeDelta 提供了更直接的控制能力,开发者可以明确知道时间值的解释方式,不受任何区域设置的影响。

std::timespec_get 被禁止的原因是其行为在实现之间存在差异,且规范不够清晰。对于需要获取当前时间的场景,Chromium 提供了 base::Time::Now()base::TimeTicks::Now() 等函数,这些函数的行为经过充分测试,能够在所有目标平台上提供一致的结果。

字符编码与字符串类型

对 UTF-8 字面量和 char8_t 的禁用是 Chromium 风格指南中最具争议的决定之一。char8_t 作为 C++20 引入的独立类型,本意是提供更精确的 UTF-8 字符表示,但 Chromium 团队认为这种精确性带来的收益不足以弥补其带来的兼容性成本。

几乎所有 Chromium 的 API、STL 实现和平台特定代码都使用 char* 而不是 char8_t*。如果使用 u8 前缀的字面量或 char8_t 类型,就需要在几乎所有地方插入类型转换。这不仅增加了代码的冗余,还可能在转换过程中引入微妙的错误。

Chromium 的建议是直接使用未加前缀的字符或字符串字面量,它们在所有支持的平台上都会以 UTF-8 编码存储在二进制中。对于需要在类型层面区分「这是字符串」和「这是任意二进制数据」的场景,Chromium 鼓励使用 std::string_view 这样的视图类型,而不是通过字符类型来区分。

新的 C++ 标准支持状态

Chromium 对 C++ 标准的支持采用目标机制。目前 C++23 已被宣布为「初始支持」状态,这意味着工具链已经准备好支持新的标准,但新特性仍需经过评估才能决定是否允许使用。C++26 尚未开始支持。

在 C++20 中,已允许使用的特性包括简写函数模板、consteval、约束和概念、默认比较运算、指定初始化符、constinit[[likely]][[unlikely]]、三路比较运算符、using enum 声明等。库层面的 <bit><compare><concepts>、范围算法、范围访问原语等也已开放使用。

C++20 中被禁止的特性包括 char8_t、模块、[[no_unique_address]](在 Windows 上无效,且对联合体成员使用可能导致内存安全问题)、std::bind_frontstd::bit_cast(标准版本允许转换指针类型,这既无用又危险)、范围工厂和适配器、std::ranges::view_interface<span>std::to_address<syncstream>

处于待定状态的 C++20 特性包括括号聚合初始化、协程、<format><source_location>std::u8string。协程的支持需要大量的支持代码和 API 规划工作,Chromium 团队仍在评估最佳的集成方式。

遗留代码迁移的工程实践

对于已有代码库需要遵循 Chromium 风格的团队,迁移过程需要系统性的规划和执行。建议的策略是分阶段进行:首先识别代码中使用的所有禁用特性,然后为每个特性找到等效的替代方案,最后在持续集成系统中添加静态检查以防止新代码引入禁用特性。

对于智能指针的迁移,std::shared_ptr 应替换为 scoped_refptrstd::weak_ptr 应替换为 base::WeakPtr。对于回调函数,std::function 应替换为 base::Callbackbase::OnceCallbackstd::bind 应替换为 base::Bind。这些迁移工作虽然繁琐,但能确保代码与 Chromium 的其他部分保持一致的内存管理语义。

在迁移过程中,可能会遇到一些边界情况,例如与第三方库接口交互时需要传递禁用类型的指针。Chromium 的风格指南对此给出了明确的建议:被禁止的类型如果仅用于接口边界,且能够在 Chromium 端尽快转换为等效的允许类型,那么这种使用是被允许的。但如果禁用原因是安全问题或工具链支持问题,则必须寻找替代方案或与 Chromium 团队讨论。

决策机制与社区参与

Chromium 的 C++ 风格决策并非一成不变。团队维护了一个开放的讨论渠道,开发者可以通过发送邮件至 cxx@chromium.org 来提议改变某个特性的状态。提议应包含对特性的简要说明、认为应该允许或禁止的理由,以及相关讨论的链接。

如果讨论达成共识,发起者可以提交代码审查来修改风格指南文件。风格指南文件位于 src/styleguide/c++/ 目录下,修改这些文件需要获得相应目录所有者的批准。这种开放的决策机制确保了风格指南能够随着 C++ 语言的发展和 Chromium 需求的变化而持续演进。

对于正在考虑采用类似限制策略的团队,Chromium 的经验表明,最重要的不是完全复制其禁用列表,而是理解每个禁用决策背后的原因。不同的项目有不同的安全需求和性能约束,盲目复制禁用列表可能带来不必要的成本。关键是建立一个评估机制,能够系统性地根据项目的具体需求决定哪些语言特性应该被限制使用。

资料来源

本文主要参考了 Chromium 官方 C++ 风格指南中的「Modern C++ use in Chromium」文档,该文档详细列出了各 C++ 标准版本中特性的允许、禁止和待定状态。决策过程和理由说明均可从官方文档中获取。

查看归档