SElinux内核态的实现-avc、avd的设计篇

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*stateSELinux状态结构体指针,包含SELinux相关的全局状态和配置信息。
u32ssid源安全标识符ID(SSID)
u32tsid目标安全标识符ID(SSID)
u16tclass安全类ID 定义访问请求的类型
u32requested请求的权限位图,即对本次检查期望的权限
unsigned intflags控制函数行为的标志位, 主要用于判断是否开启审计
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_tslots_lock对应哈希表的读写锁
atomic_tlru_hint缓存回收时辅助决定回收的slots的编号,为原子量
atomic_tactive_nodes当前已经缓存的avc_node数量,用于判断是否当前缓存节点数量超过了设置的阈值
u32latest_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_callbackselinux_lsm_notifier_avc_callbackaurule_avc_callback

  1. selinux_netcache_avc_callback
    这个回调函数与网络缓存(netcache)相关,负责在AVC缓存重置时同步更新网络缓存中的安全上下文信息。网络缓存用于存储网络相关的安全决策,例如套接字的SELinux标签。当AVC发生变化时,确保网络缓存中的安全决策与最新的安全策略保持一致。

  2. selinux_lsm_notifier_avc_callback
    这个回调函数与Linux安全模块(LSM)通知机制相关。当AVC缓存重置时,这个回调函数可能用于通知LSM框架中的其他组件,告知它们AVC发生了变化。这可能涉及到通知内核中的其他安全模块,如AppArmor或Smack,告知它们有关安全决策缓存更新的情况,以便这些模块也能适时调整其内部状态,确保与SELinux策略变更的协调一致。

  3. 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*avcAVC 缓存数据库的地址。
u32ssid源安全标识符(Source Security ID)
u32tsid目标安全标识符(Target Security ID)
u16tclass目标安全类(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能够在保持严格安全策略的同时,确保系统操作的高效执行。

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值