文章目录
avc_has_perm的处理逻辑[部分]
针对avc_has_perm
函数,我们在《SElinux内核态的实现-SID的计算篇》提出了两个问题:
- 入参的sid 和class id是如何生成的
- SELinux如何通过ssid + tsid + class id 查询到其对应的权限的。
在《SID的计算篇》,我们解答了第一个问题。
现在我们开始分析第二个问题:SELinux 如何通过ssid + tsid + class id 查询到其对应的权限的。即avc_has_perm
的实现流程。
由于其流程较长内容偏多,本文将仅仅介绍avc_has_perm
函数中使用的avc、avd架构体其结构设计。
首先来看avc_has_perm
函数
int avc_has_perm(struct selinux_state *state, u32 ssid, u32 tsid, u16 tclass,
u32 requested, struct common_audit_data *auditdata)
{
struct av_decision avd;
int rc, rc2;
rc = avc_has_perm_noaudit(state, ssid, tsid, tclass, requested, 0, &avd);
rc2 = avc_audit(state, ssid, tsid, tclass, requested, &avd, rc, auditdata, 0);
if (rc2)
return rc2;
return rc;
}
avc_audit
是SELinux审计部分代码,本文忽略
av_decision 访问向量决策的设计
这里SELinux引入了一个新变量av_decision
,这里简称AVD,用于保存查询结果:
struct av_decision {
u32 allowed;
u32 auditallow;
u32 auditdeny;
u32 seqno;
u32 flags;
};
分别表示查询到的三种结果:
- 允许的权限
- 需要审计的允许的权限
- 需要审计的拒绝的权限。
allowed、auditallow、auditdeny
这些变量的类型为u32,一个class默认支持32个操作(permissions)。所以u32的每个bit所在的位即class拥有的操作(permissions)的permission id。
如果对class和permission的关系有点疑惑的推荐回头看下《SElinux内核态的实现-数据库部分-class篇》
中的<class与permission的关联–selinux_mapping>章节
举个实际的例子:
比如查询的av_decision->allowed
的值为000000000010010011
其第1位、第2位、第5位、第8位的位值为1。则表示,我们对这个class下的permission id为1、2、5、8的操作拥有权限。
同理,如果是av_decision->auditallow
或者 av_decision->auditdeny
的值是000000000010010011
,则表示对这些操作(permission)进行允许或者拒绝的操作是需要审计
此值的具体使用将会在后续单独解析
seqno
在系统运行阶段,SELinux权限检查非常频繁,为了提高检查效率,SELinux会缓存avc_has_perm的检查结果。也就是缓存AVD的结果。在缓存前,SELinux将通过检查seqno来判断此结果是否基于最新规则文件,如果不是则放弃缓存。
缓存机制将会在avc章节详细解析
flags
标记SELinux是否运行在permissive模式下,该模式下仅仅打印日志,并不执行拒绝没有权限的操作
现在回头继续看avc_has_perm
的函数中的第一个函数avc_has_perm_noaudit
avc_has_perm_noaudit 检查点函数
函数原型
inline int avc_has_perm_noaudit(struct selinux_state *state,
u32 ssid, u32 tsid,
u16 tclass, u32 requested,
unsigned int flags,
struct av_decision *avd)
{
......
node = avc_lookup(state->avc, ssid, tsid, tclass);
if (unlikely(!node))
node = avc_compute_av(state, ssid, tsid, tclass, avd, &xp_node);
else
memcpy(avd, &node->ae.avd, sizeof(*avd));
denied = requested & ~(avd->allowed);
if (unlikely(denied))
rc = avc_denied(state, ssid, tsid, tclass, requested, 0, 0,
flags, avd);
......
}
参数解释
入参类型 | 入参名 | 解析 |
---|---|---|
struct selinux_state* | state | SELinux状态结构体指针,包含SELinux相关的全局状态和配置信息。 |
u32 | ssid | 源安全标识符ID(SSID) |
u32 | tsid | 目标安全标识符ID(SSID) |
u16 | tclass | 安全类ID 定义访问请求的类型 |
u32 | requested | 请求的权限位图,即对本次检查期望的权限 |
unsigned int | flags | 控制函数行为的标志位, 主要用于判断是否开启审计 |
struct av_decision * | avd | 存储检查结果 |
函数逻辑
查找或计算访问决策:
node = avc_lookup(state->avc, ssid, tsid, tclass);
if (unlikely(!node))
node = avc_compute_av(state, ssid, tsid, tclass, avd, &xp_node);
else
memcpy(avd, &node->ae.avd, sizeof(*avd));
- 首先,尝试通过
avc_lookup
函数在AVC缓存中查找与给定SSID、TSID和TCLASS匹配的节点。 - 如果未找到(即缓存未命中),则通过
avc_compute_av
函数计算新的检查结果,并将其存储在avd
中。同时,这个函数可能会将新的结果添加到avc缓存中。 - 如果找到匹配的节点,则将其中的结果复制到
avd
中。
检查是否有被拒绝的权限:
denied = requested & ~(avd->allowed);
if (unlikely(denied))
rc = avc_denied(state, ssid, tsid, tclass, requested, 0, 0, flags, avd);
avd->allowed
表示允许的权限集合,那么 ~(avd->allowed)即表示不允许的权限集合。- 如果requested & ~(avd->allowed) 满足,则表示请求的权限中,有拒绝的权限。
- 如果有被拒绝的权限,调用
avc_denied
函数来处理拒绝情况,主要是判断SELinux是否处于permissive
模式,并设置返回值rc
。
avc_has_perm_noaudit
函数是SELinux中用于检查权限的核心函数之一。
它首先尝试从AVC缓存中查找决策,如果未找到,则计算新的决策。
然后检查请求的权限是否被允许,并根据结果返回相应的状态码。
这个函数在执行过程中不进行审计操作,因此适用于那些不需要记录访问历史的场景。
这里就到了本文的第二个重点:SELinux中查询结果的缓存AVC的设计与实现
selinux检查结果缓存AVC 的设计与实现
struct selinux_avc
首先看下struct selinux_avc
结构体
struct selinux_avc {
unsigned int avc_cache_threshold;
struct avc_cache avc_cache;
};
struct selinux_avc
保存在selinux_state
中
avc_cache_threshold
用来设定SELinux访问向量缓存(AVC Cache)的大小阈值。这个阈值是用来控制AVC缓存增长的界限,以防止缓存无限制地消耗系统内存资源, 在新建avc节点的函数avc_alloc_node
会对此值做检查
static struct avc_node *avc_alloc_node(struct selinux_avc *avc)
{
......
if (atomic_inc_return(&avc->avc_cache.active_nodes) > avc->avc_cache_threshold)
avc_reclaim_node(avc);
......
}
SElinux提供了/sys/fs/selinux/avc/cache_threshold
来供用户空间修改此值
struct avc_cache
struct avc_cache {
struct hlist_head slots[AVC_CACHE_SLOTS]; /* head for avc_node->list */
spinlock_t slots_lock[AVC_CACHE_SLOTS]; /* lock for writes */
atomic_t lru_hint; /* LRU hint for reclaim scan */
atomic_t active_nodes;
u32 latest_notif; /* latest revocation notification */
};
变量类型 | 变量名 | 解析 |
---|---|---|
struct hlist_head [] | slots | 保存avc缓存的哈希表数组 数组长度为AVC_CACHE_SLOTS 默认为512 |
spinlock_t | slots_lock | 对应哈希表的读写锁 |
atomic_t | lru_hint | 缓存回收时辅助决定回收的slots的编号,为原子量 |
atomic_t | active_nodes | 当前已经缓存的avc_node数量,用于判断是否当前缓存节点数量超过了设置的阈值 |
u32 | latest_notif | 当前SELinux规则库的版本号,用于避免过期的avc缓存加入 |
struct avc_xperms_node为扩展权限的缓存,将在扩展权限中单独解析
avc的初始化 avc_ss_reset
在首次启动或者重新加载规则文件时候,SElinux会初始化avc。
int avc_ss_reset(struct selinux_avc *avc, u32 seqno)
{
......
avc_flush(avc); // 清理avc缓存
......
for (c = avc_callbacks; c; c = c->next) {
if (c->events & AVC_CALLBACK_RESET) {
tmprc = c->callback(AVC_CALLBACK_RESET);
......
avc_latest_notif_update(avc, seqno, 0);
......
}
- 使用avc_flush清除缓存
- 遍历回调函数列表,如果其events设置监听
AVC_CALLBACK_RESET
事件,则表示需要调用此回调函数,稍后讲解其内容 - 更新avc中notif,即当前数据库的版本号(注意此版本号表示当前加载的规则文件的次数,用于判断需要缓存的avc信息是否基于最新的规则文件,非SELinux软件版本号)
avc的回调函数
在SELinux的实现中,avc_add_callback机制允许系统组件注册回调函数,以便在特定事件发生时执行特定操作,确保系统状态的一致性。当AVC(Access Vector Cache)缓存被刷新(通过avc_flush)后,注册的回调函数会被触发,以响应这一变化
当前SELinux在init阶段在AVC回调函数的AVC_CALLBACK_RESET
事件上注册了selinux_netcache_avc_callback
、selinux_lsm_notifier_avc_callback
、aurule_avc_callback
-
selinux_netcache_avc_callback
这个回调函数与网络缓存(netcache)相关,负责在AVC缓存重置时同步更新网络缓存中的安全上下文信息。网络缓存用于存储网络相关的安全决策,例如套接字的SELinux标签。当AVC发生变化时,确保网络缓存中的安全决策与最新的安全策略保持一致。 -
selinux_lsm_notifier_avc_callback
这个回调函数与Linux安全模块(LSM)通知机制相关。当AVC缓存重置时,这个回调函数可能用于通知LSM框架中的其他组件,告知它们AVC发生了变化。这可能涉及到通知内核中的其他安全模块,如AppArmor或Smack,告知它们有关安全决策缓存更新的情况,以便这些模块也能适时调整其内部状态,确保与SELinux策略变更的协调一致。 -
aurule_avc_callback
与SELinux策略规则(audit2allow生成的规则)的管理有关,这个回调函数在AVC缓存重置后执行,用于更新或重新评估与审计规则相关的缓存信息,避免因规则更新而产生的误报或漏报。
SELinux提供了avc_add_callback
向回调函数链表中注册回调函数
avc的更新 avc_insert
首先来看看入参
static struct avc_node *avc_insert(struct selinux_avc *avc,
u32 ssid, u32 tsid, u16 tclass,
struct av_decision *avd,
struct avc_xperms_node *xp_node)
入参类型 | 入参名 | 解析 |
---|---|---|
struct selinux_avc* | avc | AVC 缓存数据库的地址。 |
u32 | ssid | 源安全标识符(Source Security ID) |
u32 | tsid | 目标安全标识符(Target Security ID) |
u16 | tclass | 目标安全类(Target Class) |
struct av_decision * | avd | 请求的结果 |
struct avc_xperms_node * | xp_node | 请求的扩写权限结果 |
static struct avc_node *avc_insert(struct selinux_avc *avc,
u32 ssid, u32 tsid, u16 tclass,
struct av_decision *avd,
struct avc_xperms_node *xp_node)
{
...
// 函数检查 avd->seqno 是否小于或等于最新的版本号。如果是,则返回 NULL,因为这意味此条规则已经过时。
if (avc_latest_notif_update(avc, avd->seqno, 1))
return NULL;
// 使用 avc_alloc_node 函数为新的 AVC 节点分配内存。如果分配失败,则返回 NULL
node = avc_alloc_node(avc);
....
// 使用 avc_node_populate 函数填充新节点的信息,包括 ssid、tsid、tclass 和 avd 中的决策结果。
avc_node_populate(node, ssid, tsid, tclass, avd);
// 如果提供了 xp_node,则使用 avc_xperms_populate 函数填充节点的扩展权限信息
if (avc_xperms_populate(node, xp_node)) {
avc_node_kill(avc, node);
return NULL;
}
// 使用 ssid、tsid 和 tclass 计算哈希值,以确定新节点应该插入到 AVC 缓存中的哪个槽位。
hvalue = avc_hash(ssid, tsid, tclass);
head = &avc->avc_cache.slots[hvalue];
lock = &avc->avc_cache.slots_lock[hvalue];
spin_lock_irqsave(lock, flag);
// 检查是否已经有数据,有则更新
hlist_for_each_entry(pos, head, list) {
if (pos->ae.ssid == ssid &&
pos->ae.tsid == tsid &&
pos->ae.tclass == tclass) {
avc_node_replace(avc, node, pos);
goto found;
}
}
// 如果没有则添加新的node
hlist_add_head_rcu(&node->list, head);
found:
spin_unlock_irqrestore(lock, flag);
return node;
}
需要提及的是avc_alloc_node
将会检查缓存节点是否已经超过avc_cache_threshold
,如果是则需要回收部分node
if (atomic_inc_return(&avc->avc_cache.active_nodes) > avc->avc_cache_threshold)
avc_reclaim_node(avc);
avc的回收 avc_reclaim_node
avc回收的逻辑比较简单,每次回收的数量为AVC_CACHE_RECLAIM,默认为16个
回收链表由lru_hint ++ & AVC_CACHE_SLOTS - 1决定
回收前会尝试获取链表的锁,如果获取失败则重新选择回收链表并重试
获取成功则删除链表的第一个node,此时还会将当前缓存节点数减1(active_nodes --)
这里我有点疑问啊,缓存信息插入的时候是插入在链表头部,如果删除的也是头部将会导致最新插入的数据被删除。也就是说此时刚刚访问的节点信息需要重新缓存,那么当缓存节点耗尽的情况下,那么反复的访问AVC_CACHE_RECLAIM+1个不同的标签文件将会导致缓存功能失效么?待我验证下
还会将当前cpu的avc_cache_stats.reclaims++
reclaims为缓存回收的计数器,除此外还有查询、命中、未命中、分配、回收和释放计数器,这些计数器用于辅助判断AVC缓存信息工作状态。
SElinux提供了/sys/fs/selinux/avc/cache_stats
以便于每个cpu的这些信息[root@localhost avc]# cat cache_stats lookups hits misses allocations reclaims frees 12674533 12665690 8843 8843 8960 9040 22963722 22955041 8681 8681 8576 8650 19575545 19567435 8110 8110 7888 7950 13829997 13821298 8699 8699 9216 9321 18249934 18241429 8505 8505 8848 8982 26695095 26686635 8460 8460 8336 8430 21246872 21237141 9731 9731 9888 10017 24400640 24392236 8404 8404 8160 8300 24220502 24211471 9031 9031 8800 8892 19544996 19534958 10038 10038 10240 10344 22237723 22229452 8271 8271 8096 8234 26382553 26375240 7313 7313 6560 6695 24373692 24366741 6951 6951 6224 6354 18612079 18603477 8602 8602 8288 8383 20974684 20966586 8098 8098 7728 7820 27578961 27571304 7657 7657 7376 7474
然后重复AVC_CACHE_SLOTS次,直到删除数量等于AVC_CACHE_RECLAIM
static inline int avc_reclaim_node(struct selinux_avc *avc)
{
for (try = 0, ecx = 0; try < AVC_CACHE_SLOTS; try++) {
hvalue = atomic_inc_return(&avc->avc_cache.lru_hint) & (AVC_CACHE_SLOTS - 1);
head = &avc->avc_cache.slots[hvalue];
lock = &avc->avc_cache.slots_lock[hvalue];
if (!spin_trylock_irqsave(lock, flags))
continue;
rcu_read_lock();
hlist_for_each_entry(node, head, list) {
avc_node_delete(avc, node);
avc_cache_stats_incr(reclaims);
ecx++;
if (ecx >= AVC_CACHE_RECLAIM) {
rcu_read_unlock();
spin_unlock_irqrestore(lock, flags);
goto out;
}
}
rcu_read_unlock();
spin_unlock_irqrestore(lock, flags);
}
out:
return ecx;
}
avc 查找
查找代码比较简单avc_lookup
-> avc_search_node
static struct avc_node *avc_lookup(struct selinux_avc *avc,
u32 ssid, u32 tsid, u16 tclass)
{
struct avc_node *node;
// 增加当前
avc_cache_stats_incr(lookups);
node = avc_search_node(avc, ssid, tsid, tclass);
if (node)
return node;
avc_cache_stats_incr(misses);
return NULL;
}
代码比较简单,但是需要注意的是,在avc_lookup
中提供了一个 avc_cache_stats_incr
方法去增加lookups与misses的计数, 而通过#define avc_cache_stats_incr(field) this_cpu_inc(avc_cache_stats.field)
我们可以知晓,selinux在每个cpu上都保留了一个avc缓存命中计数器,缓存信息可以通过/sys/fs/selinux/avc/cache_stats
查看
也提供了相应的函数来获取avc命中信息
static struct avc_cache_stats *sel_avc_get_stat_idx(loff_t *idx)
{
int cpu;
for (cpu = *idx; cpu < nr_cpu_ids; ++cpu) {
if (!cpu_possible(cpu))
continue;
*idx = cpu + 1;
return &per_cpu(avc_cache_stats, cpu);
}
(*idx)++;
return NULL;
}
小结
通过上述分析,我们SELinux是如何通过AVD保存查询结果,深入了解了AVC缓存机制如何支撑这种高效权限检查,包括其初始化、插入、查找和回收的整个生命周期管理。这为我们提供了关于SELinux权限管理机制在内核层面实现的深度见解,特别是在保证系统安全性的同时,通过缓存策略优化了性能。通过这些机制,SELinux能够在保持严格安全策略的同时,确保系统操作的高效执行。
标签:node,缓存,struct,SElinux,avd,AVC,avc From: https://blog.csdn.net/m0_37923396/article/details/139843504