Hotdry.
security

Chromium 明令禁止的 C++ 特性及其安全考量

深入解析 Chromium 项目禁止使用的 C++ 语言特性,剖析每项禁令背后的安全漏洞案例与替代方案设计。

作为全球装机量最大的浏览器项目之一,Chromium 的代码库承载着数以亿计用户的浏览安全。其 C++ 风格指南中有一份显眼的「禁止列表」,详细列出了数十项不允许在 Chromium 代码中使用的语言和库特性。这份列表不是凭空设定的,每一条禁令背后都有真实的漏洞案例、模糊的未定义行为或与 Chromium 架构的深层冲突。本文将系统梳理这些被禁止的特性,分析其被禁的技术根源,并给出工程上的替代方案参考。

1 禁令背后的核心原则

在深入具体特性之前,有必要理解 Chromium 为何要对 C++ 标准设限。Chromium 的 C++ 风格决策遵循一个明确的目标:产出更安全的产品、更高产的工程师、更少的 bug。这三个目标直接导向了几类典型的禁令动机。

第一类是内存安全相关的特性。Chromium 明令禁止 std::shared_ptrstd::weak_ptr 以及整个线程支持库,原因在于这些标准设施与 Chromium 自研的内存分配器 PartitionAlloc 存在深度耦合。标准智能指针的引用计数机制无法利用 PartitionAlloc 的原始分区隔离能力,在浏览器渲染进程频繁创建销毁的场景下,可能导致难以追踪的 Use-After-Free 漏洞。

第二类是与 Chromium 自有实现重叠的标准库。这类禁令占据名单的大部分篇幅:std::chrono 被禁是因为 base::Time 已经提供了跨平台的统一时间抽象;std::regex 被禁是因为项目要求统一使用 third_party/re2std::span 被禁是因为 base::span 提供了更丰富的功能集和更好的安全保证。使用标准库版本会破坏代码库的一致性,增加维护成本。

第三类是未定义行为风险或编译器支持不完善的特性。例如 <cctype> 相关头文件在 C locale 下行为未定义,而 Chromium 的字符串处理不应依赖 locale;std::aligned_alloc 在不同平台上的对齐支持参差不齐;C++20 的 Modules 在 Clang 和 GN 构建系统中的支持仍不充分。

2 内存安全与智能指针禁令

2.1 禁止 std::shared_ptr 与 std::weak_ptr

这两项禁令是 Chromium 代码审查列表中最受关注的条目之一。表面上,shared_ptr 提供了一套看起来很优雅的共享所有权机制,但 Chromium 团队在多个安全漏洞后决定全面禁止它。

问题出在 shared_ptr 与 Chromium 进程模型的不匹配上。Chromium 的渲染进程彼此隔离,单个渲染进程的崩溃不会影响浏览器主进程或其他渲染进程。为了实现这种隔离,Chromium 使用了 Mojo 接口进行进程间通信,并依赖 base::WeakPtr 来安全地处理跨异步操作的对象生命周期。shared_ptr 的引用计数语义与这种基于消息传递的生命周期管理存在根本冲突:当引用计数归零时,对象被立即销毁;但如果此时仍有 pending 的 Mojo 消息指向该对象,就会产生悬垂指针。

更棘手的是 shared_ptr 的线程安全性。它保证了引用计数的原子操作是线程安全的,但 shared_ptr 控制块本身的析构仍可能在任意线程执行。在 Chromium 的沙箱模型中,渲染进程被限制了某些系统调用权限,在受限环境中触发任意线程的析构函数可能触发安全检查失败。base::WeakPtr 的设计则考虑到了这一点:它总是从创建它的线程进行回调,保证了析构行为在预期线程上发生。

替代方案是使用 base::WeakPtr 处理跨异步边界的对象访问,使用 scoped_refptr(Chromium 内部的引用计数模板)处理进程内的对象共享。scoped_refptr 的优势在于它明确要求在构造时指定线程亲和性,并且与 Chromium 的任务调度器深度集成。

2.2 禁止 std::function 与 std::bind

这两项禁令同样与内存安全和生命周期管理相关。std::function 内部可能持有任意类型的可调用对象,包括 lambda 表达式、函数指针和成员函数指针。当 std::function 被拷贝时,它需要拷贝捕获的对象;如果捕获的对象通过裸指针持有其他资源,就可能在拷贝过程中出现对象已销毁但指针未更新的情况。

Chromium 内部提供的 base::BindOncebase::BindRepeating 通过强制的 std::move 语义和参数绑定检查避免了大部分问题。base::Bind* 的模板实现会在编译期检查参数是否可移动或可拷贝,并且强制要求捕获的对象通过 Owned()Passed() 明确表达所有权转移。此外,base::Bind* 支持在绑定时自动将 raw_ptr<T> 包装进去,避免裸指针在回调中悬挂。

3 字符串与字符处理的严格限制

3.1 禁止 char8_t 与 UTF-8 字符字面量

C++17 引入了 char8_t 作为 UTF-8 代码单元的专用类型,C++20 进一步扩展了其使用场景。然而 Chromium 决定全面禁止 char8_t,原因非常实际:Chromium 代码库中几乎所有的字符串 API—— 无论是平台层、系统层还是第三方库 —— 都使用 char*std::string_view。引入 char8_t 后,开发者需要在每一个 API 边界插入类型转换,这不仅增加了代码噪音,还可能在转换过程中丢失 const 正确性或产生意外的符号扩展。

Chromium 对字符串编码的立场是:默认所有 const char*std::string 都是 UTF-8 编码,不需要也不应该通过类型系统在编译期强制区分。这与 C++ 标准委员会希望通过 char8_t 解决「UTF-8 与字节数组混淆」问题的思路不同。Chromium 认为混淆的问题应该通过 API 设计(使用 std::string_view 明确表示字符串视图)而非类型系统来解决。

3.2 禁止 相关头文件

<cctype><ctype.h><cwctype><wctype.h> 被禁止使用,原因有两点。首先,这些函数的行为依赖 C locale,而 Chromium 作为一个全球化产品,必须保证无论用户设置何种 locale,其行为都保持一致。其次,当传入 signed charwchar_t 范围外的值时,这些函数的行为是未定义的。历史上曾出现过因为字符分类函数处理负数 char 值时产生崩溃的 bug。

推荐的替代是 absl::ascii 中的工具函数,或者直接使用 base::ContainsOnlyChars 等封装。这些替代实现不依赖 locale,并且对输入值有明确的定义域限制。

4 并发与线程库的全面封禁

Chromium 对标准线程库的封禁是全面且明确的。整个 <thread><mutex><condition_variable><future><atomic> 以外的标准并发设施都不被允许使用。这不是因为标准库实现有问题,而是因为 Chromium 已经有一套成熟的线程抽象层。

base::Threadbase::MessageLoop 的紧耦合是标准库无法替代的关键。Chromium 的任务调度器(Task Scheduler)需要在宏观上控制所有工作线程的优先级、生命周期和资源占用。如果允许直接使用 std::thread,开发者可能创建不受调度器管理的线程,这会破坏 Chrome 进程的资源统计和沙箱隔离机制。浏览器主进程中意外创建的后台线程可能绕过线程优先级策略,导致 UI 卡顿或功耗增加。

对于同步原语,Chromium 要求使用 base::Lockbase::ConditionVariable 等封装。这些封装在原生 mutex/condvar 基础上增加了死锁检测、日志记录和线程绑定的功能。对于原子操作,Chromium 使用 base::Atomic32base::Atomic64 等模板,或者直接使用 <atomic> 但必须通过 base::atomic 风格的封装来统一内存序策略。

5 文件系统与时间处理的专用抽象

5.1 禁止 std::filesystem

<filesystem> 在 C++17 中被引入,提供了一组看起来很方便的文件系统操作 API。然而 Chromium 禁止使用它,原因与线程库类似:Chromium 已经有了一套完整的文件操作抽象层 base::FilePathbase::File,这些抽象在 Windows、macOS、Linux、ChromeOS 等平台上都有统一的语义。标准库的 <filesystem> 在不同实现上的行为差异(尤其是符号链接处理和权限语义)可能导致跨平台回归。

更深层的问题是错误处理。std::filesystem 的 API 大多通过 std::error_code 报告错误,这与 Chromium 普遍使用的 base::File::Error 枚举不兼容。混用两种错误处理模式会让代码审查变得困难,也让错误追踪日志难以统一解析。

5.2 禁止 std::chrono

<chrono> 被禁的直接原因是与 base::Timebase::TimeDeltabase::TimeTicks 的功能重叠。但更隐蔽的问题是时区处理和闰秒。std::chrono 的时间点计算假设时间总是线性递增的,而现实世界存在闰秒、历史时区变更、夏令时切换等复杂情况。Chromium 的时间抽象层在这些边界情况上有明确的处理策略,混入标准库实现可能导致难以复现的时间相关 bug。

6 代码生成与编译期特性的取舍

6.1 禁止 C++20 Modules

尽管 Modules 被认为是 C++ 未来的重要特性,能够显著改善编译时间和解决头文件依赖问题,Chromium 仍然将其列入禁止列表。官方的理由是「Clang 和 GN 构建系统对 Modules 的支持尚不充分」。实际原因更为复杂:Chromium 的构建系统(GN + Ninja)在处理模块依赖时需要精确的依赖追踪,以确保增量编译的正确性。当前的模块实现无法保证 import 声明的变化能够被正确地传播到所有消费方,这会导致部分文件没有被重新编译,从而产生 ABI 兼容性问题。

Chromium 的代码库规模决定了他们无法承受「理论上有收益但工程上不成熟」的特性。Modules 何时能够被解禁,取决于 clang 工具链和 GN 构建设置的进一步成熟。

6.2 禁止 inline namespace

inline namespace 在 C++ 中用于版本控制,允许在命名空间外部使用时不指定版本后缀。Google 风格指南和 Chromium 都禁止使用它。原因在于 inline namespace 会让开发者无意中引入二进制兼容性问题:如果不同版本的库在 ABI 上有差异,而代码使用了 inline namespace,链接器可能将不同版本的对象混合链接,产生难以追踪的崩溃。

Chromium 对 ABI 稳定性有极高要求,尤其是作为插件宿主的浏览器进程。任何可能破坏 ABI 兼容性的特性都会被谨慎对待。

7 替代方案与工程实践

理解了禁令背后的原因后,实际开发中的替代方案可以归纳为几个类别。对于标准库中与 Chromium 自有实现重叠的部分,应当查阅 base/ 目录下的对应头文件:base/time/ 替代 <chrono>base/strings/ 替代字符串处理,base/memory/ 替代智能指针。base/ 中的实现往往经过了 Chrome 团队的专门优化和安全性审查,比通用标准库更适合浏览器场景。

对于因安全原因被禁的特性,Chromium 的内部封装提供了更安全的语义。例如,代替 std::function 应使用 base::BindOncebase::BindRepeating;代替 std::shared_ptr 应使用 scoped_refptr 结合 base::WeakPtr;代替 <thread> 应使用 base::Thread 或任务调度器。

对于因编译器或构建系统支持不完善而被禁的特性(如 Modules),开发者需要持续关注 cxx@ 邮件列表的讨论。Chromium 的 C++ 特性政策允许任何人通过发送邮件至 cxx@chromium.org 来提案解禁某个特性,讨论达成共识后会更新官方文档。

8 结语

Chromium 的 C++ 禁止列表是一份活文档,随着编译器支持状态、安全漏洞发现和架构演进而不断更新。这份列表背后的核心思想是:工程决策应当服务于实际的产品目标 —— 更少的 bug、更好的安全性、更高的开发效率,而非追求语言特性本身的先进性。理解这些禁令及其背后的原因,对于在 Chromium 代码库中工作的开发者来说是必要的背景知识,也对于任何需要在大规模 C++ 项目中制定编码规范的技术团队具有借鉴意义。


参考资料

查看归档