WeakPtr 与 Raw 指针:UAF 如何识别、如何处理、以及 Chromium 的设计哲学
本文聚焦:Use-After-Free 在底层如何被「看见」 、看见之后怎么办 、以及 Weak / Raw / RefPtr 为何并存。
1. 先选契约,再选指针
Chromium 并不是用一种智能指针解决所有生命周期问题,而是按语义分工:
| 契约 | 典型 API | 你在说什么 | 设计目标 |
|---|---|---|---|
| 回调执行时对象必须还在 | scoped_refptr、WrapRefCounted(this) |
「任务跑完前别析构」 | 引用计数延长生命 |
| 对象可能提前没了 | weak_factory_.GetWeakPtr() |
「关了窗口/Shutdown 就别再调我」 | 不拥有;失效当 null |
| 我保证异步触发前对象仍有效 | Unretained(raw)、裸 T* |
「信我,不会先 free」 | 零开销;错了就 UAF |
没有「万能指针」:
- WeakPtr 故意不靠 refcount 保活------否则与
scoped_refptr重复。 - Unretained 故意不在 Release 里做有效性检查------否则与 WeakPtr 重复。
raw_ptr<T>主要服务类成员字段 的内存安全(BackupRefPtr),不能 当成 PostTask 里Unretained(this)的替代品。
业务侧 CloudModuleUpdater::CollectAllDownloadInfo 崩溃,本质是:用了 Unretained 的契约,却处在对象可能先销毁的时序里。
2. WeakPtr:UAF 如何被「识别」
WeakPtr 不检测 「这块内存是否已被 free」,也 不阻止 free。它维护的是逻辑状态:factory 关联的 Flag 是否已被 invalidate。
2.1 内部是什么
每个 WeakPtr<T> 大致包含:
cpp
// base/memory/weak_ptr.h(概念结构)
internal::WeakReference ref_; // 指向共享 Flag(ref-counted)
T* ptr_; // 创建 WeakPtr 时记下的地址,不拥有对象
GetWeakPtr() 时:
cpp
// base/memory/weak_ptr.h
WeakPtr<T> GetWeakPtr() {
return WeakPtr<T>(weak_reference_owner_.GetRef(),
reinterpret_cast<T*>(ptr_));
}
所有从同一 WeakPtrFactory 发出的 WeakPtr 共享同一个 Flag。
2.2 「有效」判定的源码
cpp
// base/memory/weak_ptr.h
T* get() const { return ref_.IsValid() ? ptr_ : nullptr; }
explicit operator bool() const { return get() != nullptr; }
T* operator->() const {
CHECK(ref_.IsValid());
return ptr_;
}
cpp
// base/memory/weak_ptr.cc
bool WeakReference::Flag::IsValid() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return !invalidated_.IsSet();
}
bool WeakReference::Flag::MaybeValid() const {
return !invalidated_.IsSet(); // 无 sequence DCHECK,可跨线程「偷看」
}
识别 UAF 的方式 :不是分析 T* 指向的堆块,而是 invalidated_ 是否被置位。
2.3 Flag 何时 invalidate
cpp
// factory / 对象析构
WeakReferenceOwner::~WeakReferenceOwner() {
flag_->Invalidate();
}
// 显式 ShutDown
void WeakPtrFactory<T>::InvalidateWeakPtrs() {
weak_reference_owner_.Invalidate();
// 并换一个新 Flag,之后 GetWeakPtr() 走新的一代
}
对象析构时,weak_factory_(通常放在成员列表最后 )先析构 → 所有已发出的 WeakPtr 同时失效 。
注意:ptr_ 里可能仍是旧地址,但 get() 已返回 nullptr,不会通过 WeakPtr 再去访问。
2.4 发现「无效」后怎么处理
| 用法 | 行为 |
|---|---|
if (weak) / weak.get() |
无效 → nullptr,业务 return |
BindOnce(..., weak) 作 receiver |
运行前 get(),无效 → 整段回调不执行 |
weak->Method() |
CHECK(IsValid()),Debug 下 主动崩溃(误用检测) |
哲学 :在 WeakPtr 路径上,UAF 被 语义化成「指针已失效」 ------Prefer 跳过 ,而不是 硬解引用。
2.5 跨线程:传递 vs 解引用
base/memory/weak_ptr.h 原文要点:
text
Weak pointers may be passed safely between sequences, but must always be
dereferenced and invalidated on the same SequencedTaskRunner ...
| 操作 | 跨 UI ↔ file |
|---|---|
拷贝、放进 PostTask / BindOnce |
✅ |
if (weak) / weak-> / weak.get() |
⚠️ 应在 factory 所在 sequence |
单测 NonOwnerThreadDereferencesWeakPtrAfterReference:主线程先解引用绑定 sequence 后,后台线程再 get() → DCHECK death。
推荐 :file 线程只 传递 WeakPtr,回 UI 再 BindOnce(..., weak) 或 if (weak)。
2.6 业务示例:EmbedWebView
cpp
// embed_web_view.h
base::WeakPtr<EmbedWebView> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
// embed_web_delegate.cc
HandleGetSearchEngineList(web_view->GetWeakPtr(), invoke_id);
void EmbedWebDelegate::HandleGetSearchEngineList(
base::WeakPtr<EmbedWebView> web_view,
int invoke_id) {
if (!web_view || !web_view->GetWebContents())
return;
// TemplateURLService 未 loaded 时再 BindOnce(..., web_view, ...)
}
不延长 EmbedWebView 生命;关窗后 if (!web_view) 直接 return------这是 WeakPtr 的典型契约。
3. Raw / Unretained:UAF 如何被「识别」
3.1 裸指针本身:无识别
T*、Unretained(this) 在闭包里只存地址。对象 free 后继续用 → 经典 UAF,Release 下可能静默踩毒内存。
cpp
// 崩溃链示例(cloud_module_updater.cc)
file_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&CloudModuleUpdater::CollectAllDownloadInfoOnFileThread,
base::Unretained(this), // receiver:与 WeakPtr 无关
weak_factory_.GetWeakPtr()));
对非 static 成员 的 BindOnce:第一个参数是 receiver (当 this),第二个才是 weakptr 形参。
崩溃常在 Unretained receiver ,不是 WeakPtr 参数;CollectDownloadInfo(i) 仍走 receiver 的 this。
3.2 Debug:UnretainedDanglingRawPtrDetectedCrash
Chromium 在 Debug(及启用 BackupRefPtr 检测时)对 Unretained 包装的 raw 指针 挂钩 PartitionAlloc dangling 检测:
识别流程(简化):
- 对象释放时,若仍有 Unretained/
raw_ptr包装指着该地址 → 标记为 dangling; - 异步任务执行、解包 Unretained 解引用 →
UnretainedDanglingRawPtrDetectedCrash。
哲学 :Unretained 仍是「我保证它还活着」;Debug 用 crash 报告 惩罚违反契约------不是替你做生命周期管理。
3.3 raw_ptr<T>:成员字段层,不是 PostTask 万能盾
摘自 base/memory/raw_ptr.md:
text
BackupRefPtr:只要有 dangling raw_ptr 指着,释放的内存会被 quarantine + 0xEF 毒化
解引用 dangling 不必然立刻崩,但提高后续崩溃概率
仍是 Undefined Behavior
| 维度 | WeakPtr | raw_ptr 成员 | Unretained |
|---|---|---|---|
| 层次 | 逻辑失效标记 | 内存 quarantine / 毒化 | 无(Debug dangling 钩子) |
| 主要场景 | 异步可能取消 | 类/struct 字段 | PostTask receiver |
| Release 安全 | 跳过回调 | 缓解 exploit | 仍可能 UAF |
4. scoped_refptr:第三种哲学------用 refcount 避免 free
RefCountedThreadSafe + WrapRefCounted(this):
cpp
file_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&CloudModuleUpdater::CollectAllDownloadInfoOnFileThread,
base::WrapRefCounted(this), weak_factory_.GetWeakPtr()));
| 时机 | 引用计数 |
|---|---|
WrapRefCounted(this) 进闭包 |
+1 |
| file 任务结束、闭包析构 | -1 (scoped_refptr 析构,业务代码无显式 Release) |
ModuleHost::cloud_module_ 长期持有 |
始终 ≥1,直到 owner 析构 |
识别 UAF 的方式 :不是 Flag,而是 refcount > 0 就不析构------从根上避免「对象已 free 仍回调」。
哲学 :回调执行期间 对象必须活着 时,用 refptr 比 WeakPtr 更直接;WeakPtr 只绑 Done 管不到 file 段 receiver 的 this。
5. 三层对照:谁在哪抓 UAF
┌─────────────────────────────────────┐
逻辑层 │ WeakPtr: Flag invalidate │
(是否允许访问) │ → if (!weak) skip / CHECK │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
所有权层 │ scoped_refptr: refcount > 0 │
(对象能否析构) │ → 有 ref 就不 destroy │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
内存层 │ raw_ptr BRP / Unretained dangling │
(free 后是否踩) │ → quarantine + DetectedCrash │
└─────────────────────────────────────┘
| WeakPtr | Unretained | raw_ptr 成员 | scoped_refptr | |
|---|---|---|---|---|
| 如何「识别」 | Flag invalid | Debug dangling 钩子 | BRP quarantine | refcount |
| 如何处理 | 当 null / 跳过 | Debug crash | 毒化/延迟回收 | 延长生命 |
| 是否阻止析构 | 否 | 否 | 否(仅延迟回收块) | 是 |
| Release 误用 | 跳过回调 | 可能 UAF | 缓解利用 | 有 ref 则安全 |
6. 选型决策表(写代码时用)
| 你的目标 | 建议 |
|---|---|
| 只修 PostTask dangling,类已是 RefCounted,file 段必须跑完 | Unretained → WrapRefCounted(this) |
| 窗口/组件可能已销毁(Widget、Delegate) | GetWeakPtr() + if (!weak),receiver 勿 Unretained |
| 严格跨线程 + file 不写成员 | snapshot 或 PostTaskAndReply |
| 同步代码、生命周期清晰 | raw / 引用;跨异步慎用 Unretained |
| 类成员裸指针 | 优先 raw_ptr<T>(非 Renderer 进程规范) |
反模式 :Unretained(this) + GetWeakPtr() 混在同一 BindOnce------看起来像双保险,crash 往往在 receiver 上,WeakPtr 管不到。
7. 业务侧两个典型案例
7.1 CloudModuleUpdater(RefCounted + file PostTask)
- 问题 :
Unretained(this)作 receiver,Shutdown 后 file 任务仍跑 → dangling crash。 - 最小修复 :
WrapRefCounted(this);GetWeakPtr()仍可只给DoneOnUIThread。 - 教训:RefCounted 类 + 「任务期间必须活着」→ refptr,不是 WeakPtr alone。
7.2 EmbedWebDelegate(EmbedWebView + 异步 TemplateURLService)
- 模式 :
web_view->GetWeakPtr(),入口与RegisterOnLoadedCallback均if (!web_view)。 - 教训 :对象可能先没 → WeakPtr;不要 Unretained web_view。
8. 结语:三句话
- WeakPtr 用共享 Flag 表达「对象/logical owner 已失效」------不拥有、不阻止析构 ;UAF 在 API 层变成 null。
- Raw / Unretained 默认 零检查 ;Debug 靠 dangling 检测 抓违规,不替你做异步生命周期。
- scoped_refptr 用 refcount 保证「有引用就不 free」------适合 RefCounted + 回调期间必须执行 的链路。
先想清楚「回调跑的时候,对象是否还必须活着」,再选 Weak、Raw 还是 RefPtr------这比背 API 名字更重要。
9. 延伸阅读
- 团队笔记:<weak_ptr_unretained_refptr_async_callbacks.md>
- 可选 demo:
chrome/browser/<product>/component_updater/*_weak_ptr_demo_unittest.cc(仓库内路径,对外分享时可删) - 上游:
base/memory/weak_ptr.h、base/memory/raw_ptr.md