首页 > 系统相关 >SElinux内核态的实现-avc、avd的设计篇

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

时间:2024-06-22 22:32:36浏览次数:29  
标签:node 缓存 struct SElinux avd AVC avc

文章目录

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能够在保持严格安全策略的同时,确保系统操作的高效执行。

标签:node,缓存,struct,SElinux,avd,AVC,avc
From: https://blog.csdn.net/m0_37923396/article/details/139843504

相关文章

  • SELinux 安全模型——MLS
    首发公号:Rand_csSELinux安全模型——MLSBLP模型:于1973年被提出,是一种模拟军事安全策略的计算机访问控制模型,它是最早也是最常用的一种多级访问控制模型,主要用于保证系统信息的机密性,是第一个严格形式化的安全模型暂时无法在飞书文档外展示此内容多层安全的核心:“数据流向......
  • SELinux策略语法以及示例策略
    首发公号:Rand_csSELinux策略语法以及示例策略本文来讲述SELinux策略常用的语法,然后解读一下SELinux这个项目中给出的示例策略安全上下文首先来看一下安全上下文的格式:user:role:type:level每一个主体和客体都有一个安全上下文,通常也称安全标签、标签,由4部分......
  • SELinux 安全模型——TE
    首发公号:Rand_csSELinux安全模型——TE通过前面的示例策略,大家对SELinux应该有那么点感觉认识了,从这篇开始的三篇文章讲述SELinux的三种安全模型,会涉及一些代码,旨在叙述SELinux内部的原理SELinux提供了3种安全模型:RBAC:RoleBasedAccessControl<基于角色的权限访......
  • SELinux 基本原理
    首发公号:Rand_csSELinux基本原理本文讲述SELinux保护安全的基本原理安全检查顺序不废话,直接先来看张图当我们执行系统调用的时候,会首先对某些错误情况进行检查,如果失败通常会得到一些error信息,通过查看全局变量errno可以知道到底是哪一类错误随后进行DAC检查,简......
  • ffmpeg结构体解析-AVClass 和 AVOption
    AVClass先来看AVClass的结构如下:/***DescribetheclassofanAVClasscontextstructure.Thatisan*arbitrarystructofwhichthefirstfieldisapointertoan*AVClassstruct(e.g.AVCodecContext,AVFormatContextetc.).*/typedefstructAVClass{......
  • ffmpeg之视频(avc+aac)无损转mp4(批处理,拖放)
    很多能够无损转视频的工具都来自命令行的ffmpeg版本,本文将介绍如何简单的批处理方法(直接拖放到bat文件上)来实现无损转视频。工具/原料ffmpeg(默认的static版本)方法/步骤 1.桌面左下角开始菜单,点Windows附件→记事本。 2.复制本步骤以下全部内......
  • 在Linux中,SELinux是什么?
    SELinux(Security-EnhancedLinux)是一个强大的安全模块,它为Linux操作系统提供安全策略机制。SELinux是NSA(美国国家安全局)开发并开源的,目的是增强系统的安全性,通过强制访问控制(MandatoryAccessControl,MAC)来限制对资源的访问,从而保护系统免受未授权访问和潜在威胁。1.SELinux的主......
  • H264/AVC-帧内预测相邻像素推导过程
    帧内预测过程会以相邻块的像素值做参考,来预测当前块的像素值。以Intra_4x4为例,如下图所示,需要用到的13个相邻像素值,那么如何获取这13个像素值?本文要主要说明如何获取帧内预测所用到的相邻像素。获取相邻像素的流程如下:找到当前块(可以为4x4、8x8、16x16大小)的左、上、......
  • AVFrame(avcodec.h)
    AVFrameAVFrame是包含码流参数较多的结构体/***AudioVideoFrame.*NewfieldscanbeaddedtotheendofAVFRAMEwithminorversion*bumps.Similarlyfieldsthataremarkedastobeonlyaccessedby*av_opt_ptr()canbereordered.Thisallows2forks......
  • AVCodec(avcodec.h)
    AVCodecAVCodec是存储编解码器信息的结构体。/***AVCodec.*/typedefstructAVCodec{/***Nameofthecodecimplementation.*Thenameisgloballyuniqueamongencodersandamongdecoders(butan*encoderandadecodercansharethe......