[TOC]

include/linux/security.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief security_capable - 检查凭证是否拥有特定能力(LSM钩子包装)。
* @param cred 待检查的进程凭证。
* @param ns 目标资源所属的用户命名空间。
* @param cap 要检查的能力编号。
* @param opts 附加选项。
* @return 0 表示拥有能力,-EPERM 表示没有。
*/
static inline int security_capable(const struct cred *cred,
struct user_namespace *ns,
int cap,
unsigned int opts)
{
// 这是一个内联包装函数,直接调用底层的能力检查核心函数。
return cap_capable(cred, ns, cap, opts);
}

security/commoncap.c

cap_capable: 跨用户命名空间的能力检查机制

本代码片段是 Linux 内核安全子系统的基石之一,提供了核心函数 cap_capable,用于精确地判断一个进程(由其凭证 cred 代表)是否拥有某个特定的能力(Capability),并且这个判断过程完全支持并正确处理了复杂的用户命名空间(User Namespace)层次结构。当内核的其他部分需要进行权限检查时(例如,判断一个进程是否可以重启系统),它们最终都会调用这个函数。

实现原理分析

此机制的核心原理是基于用户命名空间(User Namespace)的层次化能力模型。一个进程的能力不再是一个简单的全局属性,而是与其所属的用户命名空间相关联。cap_capable_helper 函数中的 for(;;) 循环通过向上遍历命名空间树,实现了这一复杂的检查逻辑。

  1. 向上遍历(Upward Traversal): 检查的起点是目标资源所属的命名空间 target_ns。循环通过 ns = ns->parent 不断向上移动,直至根命名空间(init_user_ns)。

  2. 能力检查的三个关键规则: 在循环的每一层,代码会依次应用以下规则:

    • 规则一:同命名空间内的直接检查if (likely(ns == cred_ns)):这是最常见和最高效的情况。如果当前检查的命名空间 ns 正是进程凭证所属的命名空间 cred_ns,那么就直接检查该进程的有效能力集(cred->cap_effective)中是否包含所需的能力位。这是通过位掩码操作 cap_raised 实现的。
    • 规则二:命名空间所有者特权if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid)):这是用户命名空间的一个核心特性。一个用户在一个父命名空间中创建了一个新的子命名空间,那么该用户(ns->owner)在这个新的子命名空间内自动获得全部能力。此规则检查的就是这种情况:如果当前检查的命名空间 ns 的父命名空间正好是进程所在的命名空间,并且该进程的有效用户ID(cred->euid)与 ns 的所有者ID 匹配,那么就授予权限。
    • 规则三:父命名空间能力继承。循环本身 ns = ns->parent 体现了继承原则。如果一个进程在某个父命名空间中拥有某项能力,那么它自动对该父命名空间下的所有子孙命名空间都拥有该项能力。循环会持续向上,直到在某个父命名空间中通过了规则一的直接检查。
  3. 提前终止优化: if (ns->level <= cred_ns->level) 检查是一个重要的优化。level 代表命名空间的嵌套深度。如果向上遍历的过程中,ns 的深度已经小于或等于进程凭证 cred_ns 的深度,但 ns 却不等于 cred_ns,这说明它们处于不同的分支, 沿着这条路继续前进是没有意义的。

代码分析

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* @brief cap_capable - 判断一个任务是否拥有特定的有效能力。
* @param cred 要使用的凭证。
* @param target_ns 被访问资源所属的用户命名空间。
* @param cap 要检查的能力。
* @param opts 在 include/linux/security.h 中定义的选项位掩码(在此函数中未使用)。
* @return 成功(拥有能力)返回0,失败(没有能力)返回负数错误码。
*
* @note 与 capable() 等函数相反,cap_capable() 在任务拥有能力时返回0,
* 而 capable() 及其变体在这种情况下返回布尔值true(1)。
*/
int cap_capable(const struct cred *cred, struct user_namespace *target_ns,
int cap, unsigned int opts)
{
// 获取凭证自身所属的用户命名空间。
const struct user_namespace *cred_ns = cred->user_ns;
// 调用辅助函数执行实际的检查逻辑。
int ret = cap_capable_helper(cred, target_ns, cred_ns, cap);

// 记录一次能力检查的追踪事件,用于调试和审计。
trace_cap_capable(cred, target_ns, cred_ns, cap, ret);
return ret;
}

/**
* @brief cap_capable_helper - 判断任务是否拥有特定有效能力的辅助函数。
* @param cred 要使用的凭证。
* @param target_ns 被访问资源所属的用户命名空间。
* @param cred_ns 凭证所属的用户命名空间。
* @param cap 要检查的能力。
* @return 成功返回0,失败返回负数错误码。
*/
static inline int cap_capable_helper(const struct cred *cred,
struct user_namespace *target_ns,
const struct user_namespace *cred_ns,
int cap)
{
// 从目标命名空间开始向上遍历。
struct user_namespace *ns = target_ns;

/*
* 通过检查目标用户命名空间及其所有父命名空间,
* 来判断cred是否在目标用户命名空间中拥有该能力。
*/
for (;;) {
// 我们是否拥有必要的能力?
// [规则一] 如果当前检查的命名空间就是凭证自己的命名空间...
if (likely(ns == cred_ns))
// ...那么直接检查凭证的有效能力集中是否包含该能力。
return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;

/*
* 如果我们已经到达了比我们凭证所在层级更低或相同的层级
* (但ns != cred_ns),那么搜索结束,肯定没有权限。
*/
if (ns->level <= cred_ns->level)
return -EPERM;

/*
* [规则二] 用户命名空间的创建者在其父命名空间中,
* 对其创建的子命名空间拥有全部能力。
*/
if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid))
return 0;

/*
* [规则三] 如果你在一个父用户命名空间中拥有某项能力,
* 那么你对它所有的子用户命名空间也拥有该能力。
*/
// 移动到父命名空间,继续下一轮检查。
ns = ns->parent;
}

/* 我们永远不会执行到这里 */
}