[TOC]
include/linux/refcount.h 安全引用计数器(Safe Reference Counter) 防止Use-After-Free的专用原子计数器
历史与背景
这项技术是为了解决什么特定问题而诞生的?
refcount_t
的诞生是为了解决一个在内核并发编程中长期存在的、非常严重的安全问题:使用通用的原子操作 (atomic_t
) 来实现引用计数时存在的固有风险。
在 refcount_t
出现之前,内核中广泛使用 atomic_t
来管理共享对象的生命周期。这种做法虽然可行,但非常脆弱,是导致 use-after-free (UAF) 和 double-free 等严重安全漏洞的常见原因:
- 整数溢出 (Overflow):如果一个对象的引用计数持续增加,达到
atomic_t
能表示的最大值(UINT_MAX
)时,下一次atomic_inc()
会使其值回绕(wrap around)到 0。这会造成灾难性后果:一个实际上被大量使用的对象,其引用计数突然变为0,从而被错误地释放,导致所有其他引用者都在访问一块已被释放的内存。 - 零值增加 (Increment from Zero):如果一个对象的引用计数已经变为0(意味着它已经被释放或即将被释放),此时若有代码错误地对它再次增加引用,
atomic_inc()
会成功地将其从0变为1。这相当于将一个“僵尸”对象复活,极易导致 use-after-free。 - 重复减少 (Use-after-free on decrement):当引用计数为1时,如果两个线程同时执行
atomic_dec_and_test()
,可能会出现竞争,导致对象被释放两次(double-free),或者一个线程释放了对象后,另一个线程仍在操作这个垂悬的指针。
refcount_t
被设计为一个专用的、安全的引用计数器,其唯一目标就是通过在API层面内建保护机制,从根本上杜绝上述所有问题。
它的发展经历了哪些重要的里程碑或版本迭代?
refcount_t
的发展是一个典型的内核安全加固(Hardening)过程:
- 引入内核:由内核安全专家 Kees Cook 主导,在 Linux 4.11 内核中被正式引入。它的出现本身就是一个重要的里程碑,标志着内核社区开始系统性地解决由不安全引用计数带来的安全问题。
- 大规模迁移:自引入以来,内核社区进行了一项庞大而持续的努力,即在整个内核代码树中,用
refcount_t
逐步替换所有用于引用计数的atomic_t
。这个过程跨越了多个内核版本,涉及到对数千个代码点的修改。 - 成为标准:如今,
refcount_t
已经成为内核中实现引用计数的唯一标准和推荐方法。所有新编写的代码都必须使用它,而对老代码的迁移工作也在不断推进。
目前该技术的社区活跃度和主流应用情况如何?
refcount_t
是一个极其核心、稳定且强制使用的内核安全特性。
- 绝对主流:它是所有关键内核子系统(如
kobject
核心、文件系统、网络栈、驱动模型)中对象生命周期管理的基础。 - 强制使用:代码审查者会严格检查新的代码,确保引用计数场景没有误用
atomic_t
。 - 持续维护:虽然核心API已经稳定,但社区仍在不断进行代码审计,将剩余的
atomic_t
用法迁移过来,并可能根据新的编译器特性或CPU架构对其底层实现进行微调。
核心原理与设计
它的核心工作原理是什么?
refcount_t
本质上是对 atomic_t
的一个封装,其“魔法”在于其API函数中添加了严格的状态检查逻辑。
refcount_set(r, n)
: 将计数器设置为一个初始值(通常是1)。refcount_inc(r)
:- 它不会直接执行
atomic_inc()
。 - 它会先原子地读取当前值。
- 检查1:是否为0? 如果当前值为0,意味着对象已死,再增加引用是典型的use-after-free。此时,
refcount_inc
会触发一个WARN_ON(true)
,让系统崩溃或打印警告,从而在开发测试阶段就暴露这个bug。 - 检查2:是否饱和? 如果当前值已达到最大值(
UINT_MAX
),它会保持在最大值,而不会回绕。这被称为饱和(Saturation)。
- 它不会直接执行
refcount_dec_and_test(r)
:- 它不会直接执行
atomic_dec_and_test()
。 - 它会先原子地读取当前值。
- 检查1:是否为0? 如果当前值为0,意味着出现了逻辑错误(过度释放),会触发警告。
- 它会安全地执行原子递减操作。如果递减后结果为0,则返回
true
,表示调用者是最后一个使用者,可以安全地释放对象。
- 它不会直接执行
通过这些检查,refcount_t
将不安全的行为从“静默地发生,导致后期难以追踪的崩溃”转变为“在发生时立即、大声地报告出来”。
它的主要优势体现在哪些方面?
- 安全性:从根本上防止了因引用计数溢出和误用导致的use-after-free等严重安全漏洞。
- 主动的错误检测:通过
WARN_ON
机制,它能帮助开发者在开发和测试阶段就发现并修复生命周期管理相关的bug。 - 代码意图清晰:当代码中使用
refcount_t
时,阅读者可以清晰地知道这是一个用于对象生命周期管理的引用计数器,而不是一个通用的原子变量。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 性能开销:由于增加了检查步骤,
refcount_t
的操作会比原始的atomic_t
操作稍微慢一点。但在几乎所有情况下,这点微小的性能开销与它带来的巨大安全性收益相比,都是完全值得的。 - 专用性:它是一个高度专用的工具,绝对不能用作通用的原子计数器。它的语义是为“引用计数”量身定做的。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
在内核中,任何需要管理一个可能被多处代码共享的对象的生命周期时,refcount_t
都是唯一且标准的解决方案。
- 例一:
kobject
和设备模型struct kobject
(所有设备和驱动的基础)的核心就是kref
,而kref
的现代实现就是基于refcount_t
。当一个驱动程序获得一个设备的引用时,它会增加kobject
的引用计数。当它用完后,就减少引用计数。最后一个减少计数的调用者负责释放设备对象。 - 例二:文件结构 (
struct file
)
当多个进程通过fork()
或dup()
共享同一个打开的文件时,它们都指向同一个struct file
对象。这个对象的生命周期由一个refcount_t
(f_count
)管理。只有当f_count
降为0时,内核才会真正地关闭文件并释放该结构。
是否有不推荐使用该技术的场景?为什么?
- 统计计数器:如果你需要一个计数器来统计事件发生的次数(如收到了多少网络包),绝对不能使用
refcount_t
。这种计数器可能会合法地溢出(应使用atomic64_t
),并且没有“减到0就销毁”的语义。应使用atomic_t
或atomic64_t
。 - 资源池管理:如果你用一个计数器来表示资源池中可用资源的数量,也不能使用
refcount_t
。因为这个计数器会上下浮动,减到0只表示资源暂时用完,而不是要销毁资源池本身。
对比分析
请将其 与 其他相似技术 进行详细对比。
特性 | refcount_t (安全引用计数器) |
atomic_t (通用原子整数) |
---|---|---|
核心功能 | 专用的、安全的对象生命周期管理。 | 通用的、高性能的32位原子操作。 |
溢出行为 | 饱和 (Saturates)。达到最大值后保持不变,并可能警告。 | 回绕 (Wraps around)。从UINT_MAX 变为0,静默地产生bug。 |
零值增加行为 | 禁止。从0增加会触发警告,防止UAF。 | 允许。从0变为1,可能导致“复活”已死对象。 |
性能 | 高,但比atomic_t 有轻微开销(因增加了安全检查)。 |
极高,直接映射到CPU的原子指令。 |
安全性 | 高。设计目标就是为了防止引用计数相关的漏洞。 | 低(当用于引用计数时)。其通用性使其非常容易被误用。 |
典型用途 | 对象引用计数 (kobject , struct file , sk_buff )。 |
统计,简单的自旋锁实现,位掩码操作,通用原子变量。 |
代码意图 | 清晰:这是一个引用计数。 | 模糊:可能用于任何需要原子操作的整数。 |
include/linux/refcount.h
refcount_set 设置 refCount 的值
1 | /** |
refcount_dec_and_test 递减 refCount 并测试它是否为 0
1 | static inline __must_check __signed_wrap |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 wdfk-prog的个人博客!
评论