WTF、内存管理与核心原语
前置知识
- ›第 1 篇:架构概览与 WebKit 代码库导航
- ›扎实的 C++ 基础:移动语义、RAII、模板、CRTP
- ›理解引用计数与垃圾回收的区别
WTF、内存管理与核心原语
WebKit 的每一行代码都构建在两个基础层之上:bmalloc(内存分配器)和 WTF(Web Template Framework)。它们不是可选的工具库,而是整个代码库的基础词汇。不理解 Ref,就读不懂 WebCore 的类;不了解 protectedThis,就无法调试 DOM 变更;不搞清楚 IsoHeap,就无法评估 WebKit 的安全架构。
本文将系统拆解这两个基础层:先从 WebKit 为何要替换 STL 说起,再梳理其所有权模型,最后介绍那个让类型混淆漏洞利用难度大幅提升的安全加固分配器。
WTF:WebKit 为何替换 STL
Web Template Framework 这个名字并非随意为之——它是一个专为 Web 引擎工作负载设计的库的有意选择。WebKit 用 WTF::Vector 替换 std::vector,用 WTF::HashMap 替换 std::unordered_map,用 WTF::String 替换 std::string,用 WTF::Ref/WTF::RefPtr 替换 std::shared_ptr。
这背后有三个核心动机:
-
性能 — WTF 的容器针对 Web 工作负载进行了专项优化。
Vector支持内联存储(在编译期控制的小缓冲区优化);HashMap采用 Robin Hood 哈希的开放地址法;String在可能的情况下以 8-bit Latin-1 格式存储数据,避免不必要的 16-bit 内存占用。 -
安全 — 智能指针类型在编译期强制保证所有权的正确性。
Ref<T>不可为空,从根本上消除了一整类空指针解引用 bug。内存分配器按类型隔离,以防止漏洞利用。 -
平台抽象 — WTF 提供了一致的线程原语(
RunLoop、WorkQueue、Lock)、时间工具函数以及针对 macOS、Linux、Windows 的平台辅助功能。
graph LR
subgraph STL["C++ STL"]
SV["std::vector"]
SM["std::unordered_map"]
SS["std::shared_ptr"]
STR["std::string"]
end
subgraph WTF_LIB["WTF"]
WV["Vector<T, N>"]
WM["HashMap<K,V>"]
WR["Ref<T> / RefPtr<T>"]
WSTR["String (8/16-bit)"]
end
SV -.->|"replaced by"| WV
SM -.->|"replaced by"| WM
SS -.->|"replaced by"| WR
STR -.->|"replaced by"| WSTR
提示: 阅读 WebKit 代码时,应将 WTF 类型视为一等公民,而非对 STL 的简单封装。它们与 STL 并不兼容——API 不同,迭代器语义不同,性能特性也不同。请直接查阅头文件,不要预设它们与 STL 行为一致。
引用计数:Ref、RefPtr 与 RefCounted
WebKit 的主要所有权模型是侵入式引用计数。与 std::shared_ptr 需要单独分配控制块不同,WebKit 通过 RefCounted<T> 基类将引用计数直接内嵌到对象自身中。
类层次结构如下:
classDiagram
class RefCountedBase {
-uint32_t m_refCount = 1
+ref()
+hasOneRef() bool
+refCount() uint32_t
#derefBase() bool
}
class RefCounted~T~ {
+deref()
}
class Ref~T~ {
-T* m_ptr
+get() T&
+operator->() T*
}
class RefPtr~T~ {
-T* m_ptr
+get() T*
+operator->() T*
+operator bool()
}
RefCountedBase <|-- RefCounted
RefCounted <.. Ref : "points to"
RefCounted <.. RefPtr : "points to"
RefCountedBase 是持有 uint32_t m_refCount(初始值为 1)的非模板基类。ref() 方法递增计数,derefBase() 方法递减计数并在计数归零时返回 true,表示该对象应当被销毁。
RefCounted<T> 在此基础上增加了模板参数化的 deref() 方法,当计数归零时调用 delete const_cast<T*>(static_cast<const T*>(this)) 完成实际的析构。
两种智能指针提供不同的保证:
-
Ref<T>— 不可为空。始终持有有效引用,不能默认构造。从T&构造时会立即调用ref()。这是首选的所有权类型。 -
RefPtr<T>— 可为空。可以持有nullptr,适用于可选引用、容器成员值以及延迟初始化的字段。
侵入式方案相比 std::shared_ptr 有一个关键优势:无需额外分配控制块。这意味着对已有 DOM 节点创建 Ref<Node> 只需一次原子自增操作——没有堆分配,没有额外的 cache line 占用。对于一个每次会话都要创建和销毁数百万个 DOM 节点的系统来说,这一点至关重要。
protectedThis 模式与 Use-After-Free 防护
WebKit 中最重要的编程模式之一——也是你将反复看到的模式——就是 protectedThis:
Ref<PendingScript> protectedThis(*this);
这行代码出现在 Source/WebCore/dom/PendingScript.cpp 第 68 行。一个对象为什么要持有对自身的引用?
答案在于 DOM 树变更的工作方式。考虑以下场景:
sequenceDiagram
participant JS as JavaScript
participant N as Node::dispatchEvent()
participant DOM as DOM Tree
JS->>N: element.click()
N->>N: Begin dispatching event
Note over N: Handler may modify DOM
JS->>DOM: element.remove()
Note over DOM: Node ref count drops to 0
DOM-->>N: Node deleted!
N->>N: 💥 Use-after-free
当 DOM 节点派发事件时,事件处理器(JavaScript 代码)可能以某种方式修改 DOM 树,将正在派发事件的节点本身从树中移除。如果该节点的引用计数在派发过程中降至零,节点就会在其方法仍处于调用栈上时被销毁。
解决方案很简单,但必须一致地应用:在任何可能触发回调或 DOM 变更的方法起始处,创建一个临时的 Ref 指向 *this:
void Node::someMethod() {
Ref protectedThis { *this }; // bumps ref count
// Safe to call code that might remove us from the tree
dispatchEvent(...);
// protectedThis destructor decrements count when we return
}
这个模式遍布整个 WebCore,出现在事件派发、定时器回调、Promise 决议,以及所有对象的方法可能触发代码进而导致自身被销毁的场景中。栈上的 Ref 确保对象在方法返回之前始终存活。
提示: 如果你在 WebCore 中新增了调用 JavaScript 或派发事件的代码,务必追问自己:"这个回调有没有可能销毁我当前所在的对象?"如果答案是肯定的,就在方法开头加上
Ref protectedThis { *this };。
WeakPtr 与 CanMakeWeakPtr
非拥有性引用同样不可或缺。WeakPtr<T> 会在被引用对象销毁时自动变为空——无悬空指针,无需手动失效。
要使用 WeakPtr,类必须继承自 CanMakeWeakPtr:
classDiagram
class CanMakeWeakPtr~T~ {
-WeakPtrFactory m_weakPtrFactory
+weakImpl() WeakPtrImpl&
}
class WeakPtr~T~ {
-WeakPtrImpl* m_impl
+get() T*
+operator bool()
}
class WeakPtrImpl {
-void* m_ptr
+get() void*
+clear()
}
CanMakeWeakPtr --> WeakPtrImpl : "owns factory"
WeakPtr --> WeakPtrImpl : "observes"
其机制通过一个中间对象 WeakPtrImpl 实现。目标对象被销毁时,CanMakeWeakPtr 的析构函数会调用 impl 的 clear(),将内部指针置为空。所有现存的 WeakPtr 都指向同一个 impl,因此会同时变为空。
正如文档所指出的,WeakPtr 涉及一层额外的间接寻址(本质上是指向指针的指针),可能影响编译器优化。在性能敏感的代码中,RefPtr 或 CheckedPtr 往往是更好的选择。
WeakPtr 的常见使用场景包括:不希望延长目标对象生命周期的事件监听器、在被缓存对象销毁后应自动失效的缓存条目,以及观察者不应维持主体对象存活的观察者模式。
bmalloc 与 IsoHeap:安全加固的内存分配
WTF 之下是 bmalloc——WebKit 的自定义内存分配器。其最重要的安全特性是 IsoHeap(隔离堆),它按类型对内存分配进行隔离。
在传统分配器中,一个 Node 对象和一个 CSSStyleSheet 对象可能被分配在相邻的内存位置。如果攻击者在 Node 上触发了 use-after-free,就可以在同一内存槽中分配一个 CSSStyleSheet,进而实施类型混淆攻击——用 CSSStyleSheet 的数据去调用 Node 的方法。
IsoHeap 通过为每种类型分配独立的内存页来防止这一点:
flowchart TD
subgraph TRADITIONAL["Traditional Allocator"]
T1["Page: [Node][CSS][Node][Event]"]
end
subgraph ISO["IsoHeap (bmalloc)"]
I1["Page A: [Node][Node][Node]"]
I2["Page B: [CSS][CSS][CSS]"]
I3["Page C: [Event][Event]"]
end
TRADITIONAL -->|"type confusion possible"| VULN["🔓 Attacker can reuse Node slot with CSS object"]
ISO -->|"type confusion blocked"| SAFE["🔒 Node slot can only be reused by Node"]
在代码库中,可以通过 WTF_MAKE_TZONE_ALLOCATED 宏让类型选择加入隔离分配。以 Node.h 为例:
class Node : public EventTarget, public CanMakeCheckedPtr<Node> {
WTF_MAKE_PREFERABLY_COMPACT_TZONE_ALLOCATED(Node);
TZoneMalloc 系统构建于 bmalloc 的 IsoHeap 之上,提供按类型隔离的内存分配。WebContent 进程的入口点甚至会在初始化阶段配置 TZone 的 bucket 参数,如 WebContentServiceEntryPoint.mm 所示:
#if USE(TZONE_MALLOC)
bmalloc::api::TZoneHeapManager::setBucketParams(4);
#endif
这是纵深防御的体现:即便攻击者找到了 use-after-free 漏洞,类型隔离也能大幅限制其可利用的空间。
String 与线程原语
WebKit 的 String 类型针对 Web 工作负载进行了深度优化。其核心洞察是:Web 上的大多数文本都是 ASCII——因此 String 默认以 8-bit Latin-1 格式存储字符,只有当字符串包含非 Latin-1 字符时才升级为 16-bit UChar 存储。
字符串类型体系包含:
String— 主要的可空字符串类型,底层由引用计数的StringImpl支撑。AtomString— 经过内部化的字符串,用于低成本的相等性比较(指针比较,而非逐字符比较)。用于标签名、属性名和 CSS 属性名等场景。StringView— 对字符串数据的非拥有视图,类似于std::string_view。
线程方面,WTF 提供:
RunLoop— 平台抽象的事件循环(在 Apple 平台封装CFRunLoop,在 GTK 平台封装GMainLoop)。WorkQueue— 用于后台任务的并发派发队列。Lock/Condition— 轻量级同步原语。ThreadSafeRefCounted<T>—RefCounted的线程安全变体,使用原子操作进行引用计数。
注意,普通的 RefCounted 不是线程安全的——它使用非原子自增。如果需要跨线程共享对象,必须改为继承 ThreadSafeRefCounted。引用计数调试器会在违反此规则时触发断言。
下一步
理解了 WTF 和 bmalloc 的基础之后,我们就可以正式进入 WebCore——WebKit 中规模最大、也最为复杂的组件。第 3 篇将追踪 HTML 如何变成像素:DOM 树、渲染管线、CSS 样式解析(包括独特的 CSS 选择器 JIT 编译器),以及连接 JSC 垃圾回收器与 WebCore 引用计数世界的 Web IDL 绑定系统。本篇介绍的 Ref/RefPtr 模式将出现在几乎每一行代码中。