WeakPtr 与 Raw 指针:UAF 如何识别、如何处理、以及 Chromium 的设计哲学

WeakPtr 与 Raw 指针:UAF 如何识别、如何处理、以及 Chromium 的设计哲学

本文聚焦:Use-After-Free 在底层如何被「看见」看见之后怎么办 、以及 Weak / Raw / RefPtr 为何并存


1. 先选契约,再选指针

Chromium 并不是用一种智能指针解决所有生命周期问题,而是按语义分工:

契约 典型 API 你在说什么 设计目标
回调执行时对象必须还在 scoped_refptrWrapRefCounted(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 检测

识别流程(简化)

  1. 对象释放时,若仍有 Unretained/raw_ptr 包装指着该地址 → 标记为 dangling
  2. 异步任务执行、解包 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 任务结束、闭包析构 -1scoped_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 段必须跑完 UnretainedWrapRefCounted(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(),入口与 RegisterOnLoadedCallbackif (!web_view)
  • 教训 :对象可能先没 → WeakPtr;不要 Unretained web_view。

8. 结语:三句话

  1. WeakPtr 用共享 Flag 表达「对象/logical owner 已失效」------不拥有、不阻止析构 ;UAF 在 API 层变成 null
  2. Raw / Unretained 默认 零检查 ;Debug 靠 dangling 检测 抓违规,替你做异步生命周期。
  3. scoped_refptrrefcount 保证「有引用就不 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.hbase/memory/raw_ptr.md
相关推荐
袋鼠云数栈前端2 小时前
基于 superpowers 实现复杂前端改造
前端·ai
武子康2 小时前
调查研究-155 Open-LLM-VTuber 本地部署与互动实战指南
人工智能·python·深度学习·ai·数字人
装不满的克莱因瓶2 小时前
使用 PyTorch Tensor 的相关数据处理
人工智能·pytorch·python·深度学习·机器学习·ai
fqbqrr3 小时前
2606C++,方便的调试类
c++
我不是懒洋洋3 小时前
从零实现一个RPC框架:远程调用与服务治理
c++
小二·3 小时前
Dify + Ollama + DeepSeek:本地部署完全指南
ai·deepseek
困意少年3 小时前
从统一初始化到移动语义:C++11 为什么是现代 C++ 的起点
c++
stolentime3 小时前
CF2066D1 Club of Young Aircraft Builders (easy version)题解
c++·算法·动态规划·组合数学
Jun6263 小时前
QT(1)-C/C++库生成和调用
c语言·开发语言·c++·qt