[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结构,而是会:
    1. 创建一个全新的cred结构的副本。
    2. 在新副本中修改所需的字段。
    3. 用一个原子操作,将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管理的核心是基于不可变性、写时复制和引用计数

  1. 数据结构 (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)。
  2. 写时复制 (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)。
  3. 无锁读取 (current_cred, get_current_cred)
    • 任何需要进行权限检查的代码,都可以调用current_cred()宏。
    • 这个宏在一个RCU读端临界区内,直接读取current->cred指针。因为cred本身是不可变的,所以即使在读取期间指针被其他CPU修改,我们读取到的旧指针所指向的cred内容也是一致和有效的。RCU保证了这个旧cred结构体直到所有读者都离开临界区后才会被真正释放。
  4. 引用计数 (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(),并将其中的fsuidfsgid与文件的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* get_cred_many - 获取一组凭据的引用
* @cred: 要引用的凭据
* @nr: 要获取的引用数量
*
* 获取指定的一组凭据的引用。调用者必须释放所有获取的引用。如果传入 %NULL,则返回且不执行任何操作。
*
* 此函数用于处理已提交的一组凭据。尽管指针是 const 类型,但此函数会暂时丢弃 const 并增加使用计数。
* 这样做的目的是尝试在编译时捕获对应被视为不可变的一组凭据的意外更改。
*/
static inline const struct cred *get_cred_many(const struct cred *cred, int nr)
{
struct cred *nonconst_cred = (struct cred *) cred;
if (!cred)
return cred;
nonconst_cred->non_rcu = 0;
atomic_long_add(nr, &nonconst_cred->usage);
return cred;
}

get_cred 获取凭据集的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* get_cred - 获取凭据集的引用
* @cred: 要引用的凭据
*
* 获取指定凭据集的引用。调用者必须释放该引用。如果传入 %NULL,则直接返回,不执行任何操作。
*
* 这用于处理已提交的凭据集。
*/
static inline const struct cred *get_cred(const struct cred *cred)
{
return get_cred_many(cred, 1);
}

static inline const struct cred *get_cred_rcu(const struct cred *cred)
{
struct cred *nonconst_cred = (struct cred *) cred;
if (!cred)
return NULL;
if (!atomic_long_inc_not_zero(&nonconst_cred->usage))
return NULL;
nonconst_cred->non_rcu = 0;
return cred;
}

kernel/cred.c

cred_init

1
2
3
4
5
6
7
8
9
/*
* 初始化凭据
*/
void __init cred_init(void)
{
/* 分配一个 slab,我们可以在其中存储凭证 */
cred_jar = KMEM_CACHE(cred,
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT);
}

prepare_creds 准备一组新的凭据以进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* prepare_creds - 准备一组新的凭据以进行修改
*
* 准备一组新的任务凭据以进行修改。任务的凭据通常不应直接修改,因此使用此函数
* 来准备一个新的副本,调用者随后修改该副本并通过调用 commit_creds() 提交修改。
*
* 准备过程包括复制目标凭据以进行修改。
*
* 如果成功,返回指向新的待修改凭据的指针,否则返回 NULL。
*
* 调用 commit_creds() 或 abort_creds() 进行清理。
*/
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;

new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;

kdebug("prepare_creds() alloc %p", new);

old = task->cred;
memcpy(new, old, sizeof(struct cred));

new->non_rcu = 0;
atomic_long_set(&new->usage, 1);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
key_get(new->session_keyring);
key_get(new->process_keyring);
key_get(new->thread_keyring);
key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
new->security = NULL;
#endif

new->ucounts = get_ucounts(new->ucounts);
if (!new->ucounts)
goto error;

if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
goto error;

return new;

error:
abort_creds(new);
return NULL;
}
EXPORT_SYMBOL(prepare_creds);

set_cred_ucounts 设置凭据的用户计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int set_cred_ucounts(struct cred *new)
{
struct ucounts *new_ucounts, *old_ucounts = new->ucounts;

/*
* 由于 alloc_ucounts() 在进行表查找时使用了锁,
* 因此需要进行此优化。
*/
if (old_ucounts->ns == new->user_ns && uid_eq(old_ucounts->uid, new->uid))
return 0;

if (!(new_ucounts = alloc_ucounts(new->user_ns, new->uid)))
return -EAGAIN;

new->ucounts = new_ucounts;
put_ucounts(old_ucounts);

return 0;
}

copy_creds 为由 fork() 创建的新进程复制凭据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* 为由 fork() 创建的新进程复制凭据
*
* 如果可以,我们会共享,但在某些情况下,我们必须生成一套新的。
*
* 新进程将当前进程的主观凭据作为其客观和主观凭据。
*/
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
struct cred *new;
int ret;
/* 如果 clone_flags 包含 CLONE_THREAD 标志,表示新进程是线程,与父进程共享资源 */
if (clone_flags & CLONE_THREAD) {
/* 增加父进程凭据的引用计数,确保凭据在多线程环境中安全共享 */
p->real_cred = get_cred_many(p->cred, 2);
kdebug("share_creds(%p{%ld})",
p->cred, atomic_long_read(&p->cred->usage));
/* 增加任务计数(UCOUNT_RLIMIT_NPROC),表示系统中任务数量的变化 */
inc_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
return 0;
}

/* 分配并初始化新的凭据结构 */
new = prepare_creds();
if (!new)
return -ENOMEM;

/* 为新进程创建用户命名空间,并设置相关计数 */
if (clone_flags & CLONE_NEWUSER) {
/* return -EINVAL; */
ret = create_user_ns(new);
if (ret < 0)
goto error_put;
ret = set_cred_ucounts(new);
if (ret < 0)
goto error_put;
}

p->cred = p->real_cred = get_cred(new);
inc_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
return 0;

error_put:
put_cred(new);
return ret;
}