[TOC]
kernel/cred.c 凭证管理(Credential Management) 内核中任务身份与权限的核心
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术以及其核心数据结构struct cred
,是为了解决在现代多用户、多进程操作系统中一个根本性的安全问题:如何安全、高效、无竞争地管理和访问一个任务(进程或线程)的身份和权限集合。
- 集中化身份信息:一个任务的“身份”是复杂的,它包含了用户ID(UID)、组ID(GID)、补充组列表、安全标签(如SELinux上下文)、权能(Capabilities)等一系列信息。在
struct cred
出现之前,这些信息分散地存储在task_struct
(进程描述符)中。 - 解决竞态条件与锁争用:直接修改
task_struct
中的权限字段是一个巨大的安全隐患。例如,如果一个线程正在修改自己的UID,而另一个线程同时在检查这个UID以决定是否允许某个操作,就会产生严重的竞态条件。为了保护这些字段,task_struct
需要一个锁,但这在高并发系统(如大型Web服务器)中会成为严重的性能瓶颈,因为权限检查是内核中最频繁的操作之一。 - 实现权限的不可变性(Immutability):
struct cred
模型的核心思想是**“写时复制”(Copy-on-Write)。一个cred
结构一旦被创建,就是只读的、不可变的**。当一个任务需要改变其任何权限时(例如,通过setuid()
系统调用),内核不会去修改当前的cred
结构,而是会:- 创建一个全新的
cred
结构的副本。 - 在新副本中修改所需的字段。
- 用一个原子操作,将
task_struct
中指向旧cred
的指针,替换为指向新cred
的指针。
- 创建一个全新的
- 高效共享:由于
cred
是不可变的,多个共享相同权限的任务(例如,一个进程中的所有线程,或者fork()
出的子进程)可以安全地共享同一个struct cred
实例,只需增加其引用计数即可。这极大地节省了内存,并简化了权限管理。
它的发展经历了哪些重要的里程碑或版本迭代?
struct cred
的引入是Linux安全模型的一次重大重构。
- 从
task_struct
分离:最重要的里程碑就是将所有与权限相关的字段从task_struct
中剥离出来,整合到独立的struct cred
中。task_struct
中只保留一个指向当前有效cred
的指针(task->cred
)。 - 引入RCU保护:为了实现对当前
cred
指针的无锁读取,内核采用了RCU(Read-Copy-Update)机制来保护它。这意味着内核中任何地方的代码都可以通过current_cred()
宏,在无锁的情况下,安全地获取当前任务的凭证指针并进行权限检查。这极大地提升了系统性能。 - 凭证缓存:为了避免在每次权限变更时都重新分配和初始化一个完整的
cred
结构,内核实现了一个凭证缓存(cred_jar
)。它会缓存最近使用过的cred
结构。当需要一个新的cred
时,内核会先尝试在缓存中查找一个匹配的,如果找到就直接重用,进一步提升了性能。
目前该技术的社区活跃度和主流应用情况如何?
cred
管理是Linux内核安全子系统的绝对核心,其架构非常稳定。
- 社区活跃度:其核心代码和架构几乎没有大的变动。社区活动主要集中在:1) 当内核添加新的安全特性时(如新的权能、新的LSM钩子),会向
struct cred
中添加新的字段;2) 对cred
缓存的性能进行微调。 - 主流应用:它是所有安全和权限检查的基础。
- 系统调用:每个系统调用在执行前,都会使用
current_cred()
来获取当前任务的凭证,并基于其中的UID, GID, capabilities等进行权限检查。 - 文件系统:在访问文件时,VFS层会比较文件的所有者/权限(来自inode)和当前进程的
cred
,以决定是否允许访问。 - LSM(Linux Security Modules):SELinux, AppArmor等安全模块将其安全上下文(SID)存储在
struct cred
中,并在内核的各个关键点(钩子)上,通过cred
来实施强制访问控制(MAC)。
- 系统调用:每个系统调用在执行前,都会使用
核心原理与设计
它的核心工作原理是什么?
cred
管理的核心是基于不可变性、写时复制和引用计数。
- 数据结构 (
struct cred
):它是一个包含了任务所有安全相关属性的集合体。usage
:一个原子引用计数器。uid
,gid
,euid
,egid
,suid
,sgid
,fsuid
,fsgid
:各种用户和组ID。cap_effective
,cap_permitted
,cap_inheritable
,cap_bset
:POSIX权能(Capabilities)集合。security
:一个指针,用于LSM存放其安全数据(如SELinux SID)。
- 写时复制 (
prepare_creds
,commit_creds
):- 当一个进程调用
setuid()
时,内核会调用prepare_creds()
。 prepare_creds()
会创建一个当前cred
的副本。如果当前cred
的引用计数为1(即只有当前任务在使用它),则可以直接修改;否则,必须分配一个新的cred
结构并拷贝所有内容。- 内核在新的
cred
副本上修改uid
,euid
等字段。 - 最后,调用
commit_creds()
,这个函数会用一个受RCU保护的原子操作,将task_struct->cred
指针指向这个新的cred
结构。之后,旧的cred
结构的引用计数会被减少(通过put_cred
)。
- 当一个进程调用
- 无锁读取 (
current_cred
,get_current_cred
):- 任何需要进行权限检查的代码,都可以调用
current_cred()
宏。 - 这个宏在一个RCU读端临界区内,直接读取
current->cred
指针。因为cred
本身是不可变的,所以即使在读取期间指针被其他CPU修改,我们读取到的旧指针所指向的cred
内容也是一致和有效的。RCU保证了这个旧cred
结构体直到所有读者都离开临界区后才会被真正释放。
- 任何需要进行权限检查的代码,都可以调用
- 引用计数 (
get_cred
,put_cred
):get_cred()
:增加cred->usage
的引用计数。put_cred()
:减少引用计数。当计数降为0时,cred
结构体被释放回slab缓存,其占用的所有资源(如补充组列表、LSM数据)也会被释放。
它的主要优势体现在哪些方面?
- 极高的并发性能:通过RCU实现的无锁读取,使得权限检查这个内核中最频繁的操作几乎没有性能开销,具有极佳的扩展性。
- 无竞态条件:写时复制的模式从根本上杜绝了在修改和读取权限时的竞态条件。
- 内存高效:不可变性使得凭证可以在进程和线程间安全共享,节省了内存。
- 安全性:将所有安全属性集中管理,并采用严格的“分配-修改-提交”流程,使得权限管理更加清晰和安全。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 写操作开销:每次权限的改变(即使是很小的改变)都可能需要分配和拷贝整个
struct cred
(大约200字节)。虽然有缓存机制来缓解,但这仍然比直接修改一个字段要慢。然而,这是一个被普遍接受的权衡,因为权限的改变远没有权限的检查频繁。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
cred
是内核内部的、强制性的权限管理机制,不是一个可选方案。
sudo
命令执行:当你在shell中执行sudo ls
时,sudo
程序会调用setuid(0)
。这会触发内核的prepare_creds
/commit_creds
流程,为sudo
进程创建一个新的、UID为0的cred
。之后,当sudo
执行ls
时,ls
进程会继承这个root
权限的cred
。- 文件访问检查:当任何进程尝试打开一个文件时,VFS中的
inode_permission()
函数会被调用。它会获取current_cred()
,并将其中的fsuid
和fsgid
与文件的inode
中的所有者和权限位进行比较。 - 网络端口绑定:当一个进程尝试绑定到一个小于1024的端口时,网络栈会检查
current_cred()
是否具有CAP_NET_BIND_SERVICE
权能。
是否有不推荐使用该技术的场景?为什么?
不存在。在Linux内核态,所有与任务身份和权限相关的操作都必须通过cred
框架进行。
对比分析
请将其 与 其他相似技术 进行详细对比。
对比 cred
模型 vs. “前cred时代” (直接在task_struct
中管理)
特性 | cred 模型 |
“前cred时代” (Old Model) |
---|---|---|
数据位置 | 独立的、不可变的struct cred 。 |
分散的字段,直接位于可变的struct task_struct 中。 |
并发控制 | 无锁读取 (RCU) + 写时复制。 | 全局锁 (tasklist_lock) 或 per-task锁。 |
读性能 (检查) | 极高,无锁,扩展性好。 | 差,需要获取锁,在高并发下是瓶颈。 |
写性能 (修改) | 中等,涉及内存分配和拷贝。 | 高,直接修改字段。 |
安全性 | 高。从设计上避免了竞态条件。 | 低。容易引入竞态条件和安全漏洞。 |
内存使用 | 高效。通过引用计数实现共享。 | 较高。每个task_struct 都有一份完整的拷贝。 |
总体设计 | 现代、健壮、高性能。 | 过时、脆弱、性能差。 |
include/linux/cred.h
get_cred_many 获取一组凭据的引用
1 | /** |
get_cred 获取凭据集的引用
1 | /* |
kernel/cred.c
cred_init
1 | /* |
prepare_creds 准备一组新的凭据以进行修改
1 | /** |
set_cred_ucounts 设置凭据的用户计数
1 | int set_cred_ucounts(struct cred *new) |
copy_creds 为由 fork() 创建的新进程复制凭据
1 | /* |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 wdfk-prog的个人博客!
评论