chromium base的rawptr.h引起了我的注意,常见的ptr封装有refed_ptr、weak_ptr等。raw_ptr还要封装吗?那一定是有特殊的意义。
果然,raw_ptr.md引入眼帘:
原文地址:https://chromium.googlesource.com/chromium/src/+/HEAD/base/memory/raw_ptr.md
raw_ptr简介
raw_ptr与原始指针相比,它的内存安全性有所提高。在USE_RAW_PTR_BACKUP_REF_IMPL 关闭的平台上,它的行为与原始指针一样,而在 USE_RAW_PTR_BACKUP_REF_IMPL 打开时,它几乎与原始指针一样。主要区别在于,当启用 USE_RAW_PTR_BACKUP_REF_IMPL 时,raw_ptr对安全性有益,因为它可以防止大量释放后使用 (UaF) 漏洞被利用。它通过
隔离释放的内存(只要 存在raw_ptr指向它的悬空指针),并污染它内存(使用0xEF…EF 模式)来实现这一点。
raw_ptr的使用场景:
当在类或结构体中定义成员变量时,如果可能的话,推荐使用raw_ptr而不是直接使用T*。raw_ptr是一种专门设计的指针类型,它具有比原始指针更明确的语义和规则,有助于减少内存管理和生命周期管理中的错误。
说明中指出,Renderer-only代码是一个例外。这意味着在渲染器(renderer)特有的代码中,可能出于性能或其他特殊原因,继续使用传统的T*原始指针是合理的。渲染器代码通常与图形处理、渲染管线紧密相关,可能需要更细粒度的控制和优化,因此可能不适合或不必要强制使用raw_ptr。
什么是 (UaF) 漏洞
(UaF) 漏洞是Chromium最常见的漏洞之一,而且危害很大。
“释放后使用”(Use-after-Free,简称UaF)是一种常见的内存管理错误,通常出现在程序设计中对动态分配的内存处理不当的情况下。这种类型的漏洞发生在程序员释放了一块内存后,但在释放后仍然尝试访问或使用这块内存。当这种情况发生时,可能会引发几种不同的问题:
-
未定义行为:由于内存已经被释放,其内容可能已被操作系统重用或清空。因此,任何试图读取或写入这块内存的操作都将导致不可预测的结果。
-
数据损坏:如果释放后的内存被重用并分配给了另一个对象,那么对原对象的引用可能无意中修改新对象的数据,从而导致数据损坏。
-
程序崩溃:如果尝试访问的内存不再属于程序,操作系统可能会检测到非法访问并终止程序。
-
安全漏洞:攻击者可能利用UaF漏洞来执行任意代码或控制程序的执行流程。例如,攻击者可以通过精心构造的输入来控制释放后内存的地址,然后在程序再次使用这块内存时注入恶意代码。
UaF漏洞之所以危险,是因为它们往往难以检测和调试。程序可能在某些情况下运行正常,而在其他情况下则会失败,这取决于释放后的内存是否被重用以及何时被重用。因此,这类漏洞经常被黑客利用来进行攻击,尤其是在网络应用和操作系统内核中。
为了防止UaF漏洞,开发者应该遵循良好的编程实践,比如:
- 在释放内存后立即置空指向它的指针,避免悬挂指针的产生。
- 使用智能指针或RAII(Resource Acquisition Is Initialization)资源管理技术,以自动化的方式管理内存生命周期。
- 使用现代编程语言或库,它们提供了内置的内存管理机制,如垃圾回收。
- 运行时检查和调试工具,如AddressSanitizer(ASan)、Valgrind等,可以帮助识别潜在的UaF问题。
在修复已知的UaF漏洞时,通常需要仔细审查代码,确保在释放内存后不再有任何对该内存的引用,并更新相关的数据结构和指针状态。
raw_ptr如何防止 (UaF) 漏洞
主要是在raw_ptr析构时,尝试访问自己指向的内存,如果目标内存是无效值,那么就及时出发崩溃。
这段代码体现如下:
// 在raw_ptr析构或赋值时,会调用:
Impl::ReleaseWrappedPtr(wrapped_ptr_);
// 其实现的核心代码如下:
template <typename T>
static void ProbeForLowSeverityLifetimeIssue(T* wrapped_ptr) {
if (!MayDangle && wrapped_ptr) {
const volatile void* probe_ptr =
reinterpret_cast<const volatile void*>(wrapped_ptr);
if (!LikelySmuggledScalar(probe_ptr) &&
!EndOfAliveAllocation(probe_ptr, IsAdjustablePtr)) {
reinterpret_cast<const volatile uint8_t*>(probe_ptr)[0];
}
}
}
名为ProbeForLowSeverityLifetimeIssue
的目标函数目的是检测一个给定指针(T* wrapped_ptr
)是否可能指向一个过早释放或生命周期管理不当的对象,从而可能导致Use-after-Free(UaF)等问题。让我们逐行解析这段代码:
-
函数签名:
template <typename T> static void ProbeForLowSeverityLifetimeIssue(T* wrapped_ptr) {
这是一个静态成员函数,使用模板参数
T
,意味着它可以接受任何类型的指针作为参数。 -
条件检查:
if (!MayDangle && wrapped_ptr) {
这里有两个条件:
!MayDangle
: 这个条件检查是否允许指针悬空(即指向已释放内存)。如果MayDangle
为真,那么这段代码将不会执行进一步的检查,因为它认为指针可能已经无效。wrapped_ptr
: 检查传入的指针是否非空,即是否指向一个有效的内存地址。
-
创建探针指针:
const volatile void* probe_ptr = reinterpret_cast<const volatile void*>(wrapped_ptr);
创建一个新的
void*
类型的指针probe_ptr
,它被声明为const volatile
,这表示该指针指向的内存内容不会被修改,但可能在任何时候发生变化(例如,在多线程环境中)。reinterpret_cast
用于将wrapped_ptr
转换为void*
类型,使其可以用于后续的通用内存操作。 -
检查是否可能是走私的标量:
if (!LikelySmuggledScalar(probe_ptr) && !EndOfAliveAllocation(probe_ptr, IsAdjustablePtr)) {
这里有两个函数调用:
LikelySmuggledScalar(probe_ptr)
: 这个函数检查probe_ptr
是否可能指向一个被非法移动或复制的标量值。EndOfAliveAllocation(probe_ptr, IsAdjustablePtr)
: 这个函数检查probe_ptr
是否指向一个活动内存分配的末尾,IsAdjustablePtr
可能是一个标识符或函数,用于判断probe_ptr
是否是一个可调整的指针。
-
访问内存:
reinterpret_cast<const volatile uint8_t*>(probe_ptr)[0];
如果上述两个条件都不满足,即
probe_ptr
既不是走私的标量子对象,也不是活动内存分配的末尾,那么这里尝试访问probe_ptr
指向的内存的第一个字节。这通常用于触发潜在的未定义行为,如访问已释放内存,以便在运行时捕捉到错误。
这段代码的主要意图是探测wrapped_ptr
指向的内存是否有潜在的生命周期问题,例如是否指向一个已经释放的内存区域,或是否指向一个可能被不当移动的对象。如果存在这样的问题,上述代码可能会触发运行时错误,帮助开发者定位和修复潜在的UaF漏洞。需要注意的是,volatile
关键字的使用在这里可能更多是为了防止编译器优化掉不必要的内存访问,以确保代码能正确地触发潜在的错误。
了解其他预防 (UaF) 漏洞的方法
防止Use-after-Free(UaF)漏洞被利用的策略并不单一,而是采用多种技术和方法相结合。下面是一些关键的原理和方法:
-
分区内存分配(Partitioned Memory Allocation):
- 这种方法将内存分成多个独立的区域(或分区),每个分区有自己的分配器。当一个对象被释放时,其地址空间只在相应的分区中被标记为可用,而不是在整个堆中。这样可以减少UaF漏洞的利用机会,因为攻击者即使控制了某个分区的内存分配,也很难影响到其他分区中的对象。
-
随机化内存布局(Address Space Layout Randomization, ASLR):
- ASLR是一种安全机制,它随机化程序的内存布局,包括代码段、数据段、堆和栈的位置。这增加了攻击者预测特定内存位置的难度,从而降低了UaF漏洞被利用的可能性。
-
内存标签扩展(Memory Tagging Extensions, MTE):
- MTE是现代处理器的一种特性,它为内存块添加了额外的标签信息。当内存被释放后,其标签会被清除或更改。随后,如果尝试访问带有不同标签的内存,则会触发异常。这可以有效地检测UaF情况。
-
智能指针(Smart Pointers):
- 在现代编程语言中,如C++,智能指针(如
std::unique_ptr
和std::shared_ptr
)的使用可以自动管理内存的生命周期。当最后一个指向某个对象的智能指针超出作用域或被显式销毁时,对象的内存会被自动释放,从而避免UaF漏洞。
- 在现代编程语言中,如C++,智能指针(如
-
监控和验证(Monitoring and Validation):
- 一些工具和库在运行时监控内存访问模式,当检测到UaF行为时会抛出异常或错误。例如,AddressSanitizer(ASan)是一个用于检测内存错误的工具,包括UaF。
-
内存屏障(Memory Barriers):
- 在并发环境中,内存屏障可以确保内存操作的顺序性和可见性,防止因并发操作导致的UaF漏洞。
-
代码审查和静态分析:
- 代码审查和使用静态分析工具可以帮助开发者在代码投入生产前发现潜在的UaF漏洞。
防止UaF漏洞的核心在于确保在释放内存后,没有任何代码路径可以继续访问这块内存,同时确保新的内存分配不会重用这块已释放的内存,除非它已经经过适当的重置或初始化。通过上述方法和技术的综合运用,可以极大地降低UaF漏洞被利用的风险。
raw_ptr的实现还依赖了partition_alloc库。由此可见raw_ptr避免 (UaF) 漏洞不止上一节提到的方法,应该是多管齐下的。
言归正传,raw_ptr使用原则如下
应将raw_ptr<T>
将其视为一个原始的C++指针。具体而言:
-
自行初始化它,不要假设构造函数会默认初始化它(可能初始化也可能不初始化)。(始终使用
raw_ptr<T> member_ = nullptr;
的初始化形式,而不是所谓的统一初始化形式(空括号)raw_ptr<T> member_{};
,后者的意义会随实现的不同而变化。) -
不要假设移动操作会清空指针(可能清空也可能不清空)。
-
内存的所有者必须在适当的时候释放内存,不要假设
raw_ptr<T>
会替你释放它(它不会)。与std::unique_ptr<T>
、base::scoped_refptr<T>
等不同,它不会管理分配对象的所有权或生命周期。- 如果指针是内存的所有者,应考虑使用替代的智能指针。
-
不要假设
raw_ptr<T>
会保护你免于过早释放内存(它可能能做到,但存在陷阱;其中一个陷阱是,解引用会导致其他类型的未定义行为)。 -
不要赋值无效的、非空地址(这包括曾有效现已释放的内存、Win32句柄等)。你只能为
raw_ptr<T>
赋值在赋值时刻仍有效的内存地址。例外情况包括:- 指向有效分配末尾的指针(但不能超出末尾的1字节)
- 指向地址空间最后一页的指针,例如用作哨兵的
reinterpret_cast<void*>(-1)
-
不要直接初始化或赋值
raw_ptr<T>
所指向的内存(例如reinterpret_cast<ClassWithRawPtr*>(buffer)
或memcpy(reinterpret_cast<void*>(&obj_with_raw_ptr), buffer)
)。 -
不要在多个线程中并发地赋值给
raw_ptr<T>
,即便赋值的是相同的值。 -
不要依赖移动操作后指针的旧值。与原始指针不同,
raw_ptr<T>
在移动操作后可能会被清空。 -
不要在
raw_ptr<T>
被析构后继续使用它。与原始指针不同,raw_ptr<T>
在析构时可能会被清空。例如,当字段排序导致指针字段在使用该指针字段的类字段析构前就被析构时(参见[奇异问题])。 -
不要在构造函数运行前就对
raw_ptr<T>
进行赋值。这可能发生在基类的构造函数使用了派生类中尚未初始化的字段时(参见[MiraclePtr的应用])。
其中一些操作即使在没有raw_ptr<T>
的世界里也会导致未定义行为(UB),但你可能侥幸不会遇到任何后果(例如参见[字段析构顺序])。然而,在raw_ptr<T>
的世界里,可能会发生隐晦的崩溃。这些崩溃经常表现为SEGV(段错误)或在RawPtrBackupRefImpl::AcquireInternal()
或RawPtrBackupRefImpl::ReleaseInternal()
中的CHECK错误,你也可能遭遇内存破坏或无声地丧失UaF(释放后使用)保护。
结语
理解raw_ptr的引入背景和实现细节,对阅读chromium源码很有帮助。
标签:11,漏洞,raw,源码,内存,UaF,Chromium,ptr,指针 From: https://blog.csdn.net/hebhljdx/article/details/140554549