Skip to content

Type Transition

从 Android transition 宏、exec 与文件创建 hook、security_compute_sid 和命名转换追踪新进程与对象类型的计算。

基于android-17.0.0_r1
AndroidSELinuxtype_transitionDomainTransitionFileTransition源码阅读

Type Transition ​

本文面向已经读过 类型强制、安全上下文 和 Type与Attribute 的读者。前文介绍过 domain_auto_trans 和文件 type transition,本篇把它们展开为真实内核路径:exec 如何从旧 task SID 与 executable SID 计算新 process SID,文件创建如何从 creator、父目录、class 和名字计算 inode SID,命名 transition 为什么能覆盖普通 transition,以及转换规则为何永远不能替代 allow 权限。

本文不把 type_transition source target:class new_type; 当成一个魔法赋值。读完后,你应能从一条规则找到触发 operation、source/target context、默认 type、权限检查和最终 SID;还能区分自动 transition、显式 exec_sid/fscreate_sid、保持原 type 和创建失败四种结果。

1. 规则输入 ​

text
type_transition source_type target_type:object_class default_type;
type_transition source_type target_type:object_class default_type "object_name";
字段Process transitionFile/object transition
source typeexec 调用者 domain创建者 domain
target typeexecutable file type父目录或 related object type
classprocessfile、dir、sock_file 等
default type新进程 domain新 inode/object type
object name通常不使用可按最后一个路径组件精确覆盖

type transition 只决定候选新 type。操作仍需通过 allow、constraint、role/MLS、filesystem associate 等检查。没有 transition 时也不一定失败:process/socket 通常沿用 source type,普通对象通常沿用 related target type。

2. Process转换 ​

2.1 domain_trans ​

domain_trans 只生成转换所需 allow,不生成自动选择规则。

源码文件:system/sepolicy/public/te_macros

text
define(`domain_trans', `
allow $1 $2:file { getattr open read execute map };
allow $1 $3:process transition;
allow $3 $2:file { entrypoint open read execute getattr map };
ifelse($1, `init', `', `allow $3 $1:process sigchld;')
dontaudit $1 $3:process noatsecure;
allow $1 $3:process { siginh rlimitinh };
')

如果程序通过 setexeccon/LSM exec attribute 显式指定 new context,domain_trans 提供执行、process transition 和 entrypoint 权限;没有显式目标或其他 transition 规则时,仅有这些 allow 不会自动选择 $3。

2.2 自动域转换 ​

domain_auto_trans 先展开所有 allow,再追加自动 process transition。

源码文件:system/sepolicy/public/te_macros

text
define(`domain_auto_trans', `
domain_trans($1,$2,$3)
type_transition $1 $2:process $3;
')

相关宏:init_daemon_domain

源码文件:system/sepolicy/public/te_macros

text
define(`init_daemon_domain', `
domain_auto_trans(init, $1_exec, $1)
')

init_daemon_domain(servicemanager) 最终建立 init + servicemanager_exec:process → servicemanager,并补齐三组权限。宏参数命名约定要求存在 $1_exec type 与对应 file context。

2.3 Android案例 ​

源码文件:system/sepolicy/private/kernel.te

text
domain_auto_trans(kernel, init_exec, init)
domain_auto_trans(kernel, snapuserd_exec, snapuserd)

kernel initial domain 执行带 init_exec 标签的文件时进入 init domain。first-stage init 对 /system/bin/init restorecon 后重新 exec,正是这条转换的用户空间触发点。

源码文件:system/sepolicy/private/servicemanager.te

text
typeattribute servicemanager coredomain;
init_daemon_domain(servicemanager)

servicemanager 由 init exec 启动,自动进入 servicemanager。如果 binary 标签错误为普通 system_file,transition key 不匹配,后续可能走 execute_no_trans 或被 entrypoint/execute 权限拒绝。

2.4 Exec Hook ​

SELinux exec hook 默认继承旧 SID;若 task 设置了 exec_sid,优先使用显式 SID,否则调用 security_transition_sid() 查自动 transition。

源码文件:kernel/common/security/selinux/hooks.c

相关函数:selinux_bprm_creds_for_exec

c
old_crsec = selinux_cred(current_cred());
new_crsec = selinux_cred(bprm->cred);
isec = inode_security(inode);

/* Default to the current task SID. */
new_crsec->sid = old_crsec->sid;
new_crsec->osid = old_crsec->sid;

/* Reset fs, key, and sock SIDs on execve. */
new_crsec->create_sid = 0;
new_crsec->keycreate_sid = 0;
new_crsec->sockcreate_sid = 0;

if (old_crsec->exec_sid) {
	new_crsec->sid = old_crsec->exec_sid;
	/* Reset exec SID on execve. */
	new_crsec->exec_sid = 0;

	/* Fail on NNP or nosuid if not an allowed transition. */
	rc = check_nnp_nosuid(bprm, old_crsec, new_crsec);
	if (rc)
		return rc;
} else {
	/* Check for a default transition on this program. */
	rc = security_transition_sid(old_crsec->sid,
				     isec->sid, SECCLASS_PROCESS, NULL,
				     &new_crsec->sid);
	if (rc)
		return rc;

	/*
	 * Fallback to old SID on NNP or nosuid if not an allowed
	 * transition.
	 */
	rc = check_nnp_nosuid(bprm, old_crsec, new_crsec);
	if (rc)
		new_crsec->sid = old_crsec->sid;
}

自动 transition 的 object name 为 null;命名 file transition 不参与 process exec。显式 exec SID 用完后会清空,避免影响下一次 exec。

2.5 权限验证 ​

新 SID 等于旧 SID 时,hook 检查 executable 的 execute_no_trans;发生转换时检查 source→new process transition 和 new domain→executable entrypoint。

源码文件:kernel/common/security/selinux/hooks.c

c
if (new_crsec->sid == old_crsec->sid) {
	rc = avc_has_perm(old_crsec->sid, isec->sid, isec->sclass,
			  FILE__EXECUTE_NO_TRANS, &ad);
	if (rc)
		return rc;
} else {
	/* Check permissions for the transition. */
	rc = avc_has_perm(old_crsec->sid, new_crsec->sid,
			  SECCLASS_PROCESS, PROCESS__TRANSITION, &ad);
	if (rc)
		return rc;

	rc = avc_has_perm(new_crsec->sid, isec->sid, isec->sclass,
			  FILE__ENTRYPOINT, &ad);
	if (rc)
		return rc;
}

因此 transition 规则命中只产生 new SID,allow/constraint 仍决定 exec 能否提交 credentials。

3. 文件转换 ​

Process transition 的 target 是 executable inode;文件 transition 的 target 则是父目录。创建 /data/.../child 时,内核不会拿完整路径匹配 type_transition,而是使用创建者 SID、父目录 SID、待创建对象 class,以及最后一个路径组件 child 计算新 SID。

3.1 权限宏 ​

file_type_trans 的名字容易造成误解:它只补齐“能够在该目录创建指定 type”的 allow,不生成 transition 规则。

源码文件:system/sepolicy/public/te_macros

text
define(`file_type_trans', `
allow $1 $2:dir ra_dir_perms;
allow $1 $3:notdevfile_class_set create_file_perms;
allow $1 $3:dir create_dir_perms;
')

这与 domain_trans 的角色相同:宏解决权限前提,不决定默认标签。调用方若通过 setfscreatecon() 显式选择 $3,这些权限可以让创建成功;若希望普通 open(O_CREAT) 或 mkdir() 自动选择 $3,还需要 file_type_auto_trans。

3.2 自动规则 ​

file_type_auto_trans 先展开权限宏,再分别为 dir 与 notdevfile_class_set 生成 transition。之所以拆成两条,是因为 SELinux 规则的 class 是匹配键的一部分。

源码文件:system/sepolicy/public/te_macros

text
define(`file_type_auto_trans', `
file_type_trans($1, $2, $3)
type_transition $1 $2:dir $3;
type_transition $1 $2:notdevfile_class_set $3;
')

例如参数是 (demo, parent_data, demo_data),宏并不是“把 parent_data 改成 demo_data”,而是声明:demo 在 parent_data 目录下创建新对象时,新对象默认采用 demo_data。父目录本身的 SID 不变。

3.3 标签入口 ​

VFS 在创建前调用 SELinux hook。selinux_determine_inode_label() 按三层优先级选择 new SID:mountpoint 固定标签优先;支持扩展标签的挂载上,显式 create_sid 次之;其余情况才进入策略 transition 计算。

源码文件:kernel/common/security/selinux/hooks.c

c
static int
selinux_determine_inode_label(const struct cred_security_struct *crsec,
				 struct inode *dir,
				 const struct qstr *name, u16 tclass,
				 u32 *_new_isid)
{
	const struct superblock_security_struct *sbsec =
						selinux_superblock(dir->i_sb);

	if ((sbsec->flags & SE_SBINITIALIZED) &&
	    (sbsec->behavior == SECURITY_FS_USE_MNTPOINT)) {
		*_new_isid = sbsec->mntpoint_sid;
	} else if ((sbsec->flags & SBLABEL_MNT) &&
		   crsec->create_sid) {
		*_new_isid = crsec->create_sid;
	} else {
		const struct inode_security_struct *dsec = inode_security(dir);
		return security_transition_sid(crsec->sid,
					       dsec->sid, tclass,
					       name, _new_isid);
	}

	return 0;
}

这里的状态 owner 是当前 task credentials:crsec->sid 表示创建者,crsec->create_sid 保存显式创建标签。父目录 SID 由 inode security blob 持有,filesystem SID 由 superblock security blob 持有。三个 owner 分离,正是后续要做三组权限检查的原因。

3.4 创建检查 ​

may_create() 展示了 transition 与 allow 的真实先后关系。内核先确认调用者能修改父目录,再计算新 SID,然后检查调用者能创建该新 type,最后确认该 type 可以关联到当前 filesystem。

源码文件:kernel/common/security/selinux/hooks.c

c
static int may_create(struct inode *dir,
		      struct dentry *dentry,
		      u16 tclass)
{
	const struct cred_security_struct *crsec = selinux_cred(current_cred());
	struct inode_security_struct *dsec;
	struct superblock_security_struct *sbsec;
	u32 sid, newsid;
	struct common_audit_data ad;
	int rc;

	dsec = inode_security(dir);
	sbsec = selinux_superblock(dir->i_sb);

	sid = crsec->sid;

	ad.type = LSM_AUDIT_DATA_DENTRY;
	ad.u.dentry = dentry;

	rc = avc_has_perm(sid, dsec->sid, SECCLASS_DIR,
			  DIR__ADD_NAME | DIR__SEARCH,
			  &ad);
	if (rc)
		return rc;

	rc = selinux_determine_inode_label(crsec, dir, &dentry->d_name, tclass,
					   &newsid);
	if (rc)
		return rc;

	rc = avc_has_perm(sid, newsid, tclass, FILE__CREATE, &ad);
	if (rc)
		return rc;

	return avc_has_perm(newsid, sbsec->sid,
			    SECCLASS_FILESYSTEM,
			    FILESYSTEM__ASSOCIATE, &ad);
}

newsid 只是局部候选值。前三步任一步失败,VFS 创建操作都不会提交;只有权限全部通过,随后 inode 初始化 hook 才把 SID 写入 inode security blob,并在支持 xattr 的文件系统上生成 security.selinux。

3.5 标签提交 ​

创建检查与标签写入是两个阶段。selinux_inode_init_security() 再次确定 new SID,把它写入新 inode 的 SELinux 状态,并按文件系统能力返回 xattr。

源码文件:kernel/common/security/selinux/hooks.c

c
static int selinux_inode_init_security(struct inode *inode, struct inode *dir,
				       const struct qstr *qstr,
				       struct xattr *xattrs, int *xattr_count)
{
	const struct cred_security_struct *crsec = selinux_cred(current_cred());
	struct superblock_security_struct *sbsec;
	struct xattr *xattr = lsm_get_xattr_slot(xattrs, xattr_count);
	u32 newsid, clen;
	u16 newsclass;
	int rc;
	char *context;

	sbsec = selinux_superblock(dir->i_sb);

	newsid = crsec->create_sid;
	newsclass = inode_mode_to_security_class(inode->i_mode);
	rc = selinux_determine_inode_label(crsec, dir, qstr, newsclass, &newsid);
	if (rc)
		return rc;

	/* Possibly defer initialization to selinux_complete_init. */
	if (sbsec->flags & SE_SBINITIALIZED) {
		struct inode_security_struct *isec = selinux_inode(inode);
		isec->sclass = newsclass;
		isec->sid = newsid;
		isec->initialized = LABEL_INITIALIZED;
	}

	if (!selinux_initialized() ||
	    !(sbsec->flags & SBLABEL_MNT))
		return -EOPNOTSUPP;

	if (xattr) {
		rc = security_sid_to_context_force(newsid,
						   &context, &clen);
		if (rc)
			return rc;
		xattr->value = context;
		xattr->value_len = clen;
		xattr->name = XATTR_SELINUX_SUFFIX;
	}

	return 0;
}

下面的时序图把策略计算、权限检查和标签提交分开。transition 是中间计算,不是最终授权,也不是磁盘写入动作。

4. 命名转换 ​

普通 transition 的匹配键只有 source type、target type 和 class。同一目录内若只有少数固定名字需要专用标签,给整个目录增加普通 transition 会扩大影响范围;带 "object_name" 的规则为此增加最后一个路径组件。

4.1 数据结构 ​

内核没有把 source type 放进 filename transition 的哈希 key。key 使用父目录 type、对象 class 和名字;同一 key 下的 datum 用 source type bitmap 区分来源,并保存结果 type。

源码文件:kernel/common/security/selinux/ss/policydb.h

c
struct filename_trans_key {
	u32 ttype; /* parent dir context */
	u16 tclass; /* class of new object */
	const char *name; /* last path component */
};

struct filename_trans_datum {
	struct ebitmap stypes; /* bitmap of source types for this otype */
	u32 otype; /* resulting type of new object */
	struct filename_trans_datum *next; /* record for next otype*/
};

这种布局让多个 source type 共享 (parent type, class, name) key,避免为每个 source 重复存储字符串。next 允许同一 key 对应不同结果 type,每个 datum 再用 bitmap 表示哪些 source type 选择它。

4.2 策略加载 ​

读取二进制策略时,内核建立上述 key/datum,并把所有出现过 filename transition 的 target type 记录在 filename_trans_ttypes bitmap 中。

源码文件:kernel/common/security/selinux/ss/policydb.c

c
ft->ttype = ttype;
ft->tclass = tclass;
ft->name = name;

rc = hashtab_insert(&p->filename_trans, ft, first,
		    filenametr_key_params);
if (rc == -EEXIST)
	pr_err("SELinux:  Duplicate filename transition key\n");
if (rc)
	goto out;

return ebitmap_set_bit(&p->filename_trans_ttypes, ttype, 1);

这个 bitmap 是快速否定索引,而不是规则结果缓存。若父目录 type 从未出现在任何 filename transition 中,查询立即返回;若出现过,仍需使用 class 和 name 查哈希表,再用 source bitmap 确认。

4.3 名字查询 ​

filename_compute_type() 只修改 newcontext.type,user、role 和 MLS 仍由通用 SID 计算路径处理。

源码文件:kernel/common/security/selinux/ss/services.c

c
static void filename_compute_type(struct policydb *policydb,
				  struct context *newcontext,
				  u32 stype, u32 ttype, u16 tclass,
				  const char *objname)
{
	struct filename_trans_key ft;
	struct filename_trans_datum *datum;

	/*
	 * Most filename trans rules are going to live in specific directories
	 * like /dev or /var/run.  This bitmap will quickly skip rule searches
	 * if the ttype does not contain any rules.
	 */
	if (!ebitmap_get_bit(&policydb->filename_trans_ttypes, ttype))
		return;

	ft.ttype = ttype;
	ft.tclass = tclass;
	ft.name = objname;

	datum = policydb_filenametr_search(policydb, &ft);
	while (datum) {
		if (ebitmap_get_bit(&datum->stypes, stype - 1)) {
			newcontext->type = datum->otype;
			return;
		}
		datum = datum->next;
	}
}

stype - 1 来自策略 type 编号从 1 开始、bitmap 位从 0 开始的表示差异。名字必须精确等于 dentry 最后一个组件;父路径、正则和 glob 都不参与这里的比较。

4.4 Android实例 ​

system_server 在 system_data_file 目录下创建两个固定名字的 Unix socket 节点时,分别获得独立 type。

源码文件:system/sepolicy/private/system_server.te

text
type_transition system_server system_data_file:sock_file system_ndebug_socket "ndebugsocket";
type_transition system_server system_data_file:sock_file system_unsolzygote_socket "unsolzygotesocket";

allow system_server system_ndebug_socket:sock_file create_file_perms;
allow system_server system_unsolzygote_socket:sock_file create_file_perms;

名字选择 type,后续 allow 决定 system_server 能否创建。策略还用 neverallow 限制哪些 domain 能写这些 socket 节点,因此两个固定名字不只是标签美化,而是把不同消费者隔离到不同访问边界。

5. SID计算 ​

宏和策略文件最终只提供 type 规则。真正把 source SID、target SID、class 和 object name 合成为完整安全上下文的是 security_compute_sid()。

5.1 未加载策略 ​

通用函数在策略尚未初始化时不查询 policydb:process class 沿用 source SID,其他对象沿用 target SID。

源码文件:kernel/common/security/selinux/ss/services.c

c
if (!selinux_initialized()) {
	switch (orig_tclass) {
	case SECCLASS_PROCESS: /* kernel value */
		*out_sid = ssid;
		break;
	default:
		*out_sid = tsid;
		break;
	}
	goto out;
}

这是 security_compute_sid() 的默认值,不应与 exec hook 的 early-boot 特例混为一谈:selinux_bprm_creds_for_exec() 在策略未加载时会直接把用户空间 task 标成 SECINITSID_INIT,不会进入这里的 process 默认分支。

5.2 Context骨架 ​

策略加载后,函数先把 source/target SID 映射为 struct context,然后为新 context 选择 user 和 role 默认值。process/socket 通常沿用 source role,普通对象通常使用 object_r。

源码文件:kernel/common/security/selinux/ss/services.c

c
sentry = sidtab_search_entry(sidtab, ssid);
if (!sentry) {
	pr_err("SELinux: %s:  unrecognized SID %d\n",
	       __func__, ssid);
	rc = -EINVAL;
	goto out_unlock;
}
tentry = sidtab_search_entry(sidtab, tsid);
if (!tentry) {
	pr_err("SELinux: %s:  unrecognized SID %d\n",
	       __func__, tsid);
	rc = -EINVAL;
	goto out_unlock;
}

scontext = &sentry->context;
tcontext = &tentry->context;

if (tclass && tclass <= policydb->p_classes.nprim)
	cladatum = policydb->class_val_to_struct[tclass - 1];

/* Set the user identity. */
switch (specified) {
case AVTAB_TRANSITION:
case AVTAB_CHANGE:
	if (cladatum && cladatum->default_user == DEFAULT_TARGET) {
		newcontext.user = tcontext->user;
	} else {
		/* notice this gets both DEFAULT_SOURCE and unset */
		/* Use the process user identity. */
		newcontext.user = scontext->user;
	}
	break;
case AVTAB_MEMBER:
	/* Use the related object owner. */
	newcontext.user = tcontext->user;
	break;
}

/* Set the role to default values. */
if (cladatum && cladatum->default_role == DEFAULT_SOURCE) {
	newcontext.role = scontext->role;
} else if (cladatum && cladatum->default_role == DEFAULT_TARGET) {
	newcontext.role = tcontext->role;
} else {
	if ((tclass == policydb->process_class) || sock)
		newcontext.role = scontext->role;
	else
		newcontext.role = OBJECT_R_VAL;
}

这里说明 type_transition 语句只携带结果 type,却仍能得到完整 user:role:type:level:其余字段由 class 默认、role transition 和 MLS 计算补齐。

5.3 Type选择 ​

type 的选择顺序是永久 TE avtab、已启用的条件 avtab、class 默认值,最后才是 filename override。

源码文件:kernel/common/security/selinux/ss/services.c

c
/* Set the type.
 * Look for a type transition/member/change rule.
 */
avkey.source_type = scontext->type;
avkey.target_type = tcontext->type;
avkey.target_class = tclass;
avkey.specified = specified;
avnode = avtab_search_node(&policydb->te_avtab, &avkey);

/* If no permanent rule, also check for enabled conditional rules */
if (!avnode) {
	node = avtab_search_node(&policydb->te_cond_avtab, &avkey);
	for (; node; node = avtab_search_node_next(node, specified)) {
		if (node->key.specified & AVTAB_ENABLED) {
			avnode = node;
			break;
		}
	}
}

/* If a permanent rule is found, use the type from
 * the type transition/member/change rule. Otherwise,
 * set the type to its default values.
 */
if (avnode) {
	newcontext.type = avnode->datum.u.data;
} else if (cladatum && cladatum->default_type == DEFAULT_SOURCE) {
	newcontext.type = scontext->type;
} else if (cladatum && cladatum->default_type == DEFAULT_TARGET) {
	newcontext.type = tcontext->type;
} else {
	if ((tclass == policydb->process_class) || sock) {
		/* Use the type of process. */
		newcontext.type = scontext->type;
	} else {
		/* Use the type of the related object. */
		newcontext.type = tcontext->type;
	}
}

/* if we have a objname this is a file trans check so check those rules */
if (objname)
	filename_compute_type(policydb, &newcontext, scontext->type,
			      tcontext->type, tclass, objname);

因此带名字的规则不是只在“普通规则未命中”时使用。它在普通/default type 已确定后执行,命中时覆盖 newcontext.type。这解释了同一目录可以有一个普通默认 type,再为少数名字指定更窄的 type。

5.4 完整Context ​

type 决定后,函数还要处理 role transition、MLS 与 context constraint。最终 context 若等于 source 或 target 就复用已有 SID,否则在 sidtab 中查找或分配 SID。

源码文件:kernel/common/security/selinux/ss/services.c

c
/* Check for class-specific changes. */
if (specified & AVTAB_TRANSITION) {
	/* Look for a role transition rule. */
	struct role_trans_datum *rtd;
	struct role_trans_key rtk = {
		.role = scontext->role,
		.type = tcontext->type,
		.tclass = tclass,
	};

	rtd = policydb_roletr_search(policydb, &rtk);
	if (rtd)
		newcontext.role = rtd->new_role;
}

/* Set the MLS attributes.
   This is done last because it may allocate memory. */
rc = mls_compute_sid(policydb, scontext, tcontext,
			     tclass, specified,
			     &newcontext, sock);
if (rc)
	goto out_unlock;

/* Check the validity of the context. */
if (!policydb_context_isvalid(policydb, &newcontext)) {
	rc = compute_sid_handle_invalid_context(policy, sentry,
						tentry, tclass,
						&newcontext);
	if (rc)
		goto out_unlock;
}

/* Obtain the sid for the context. */
if (context_equal(scontext, &newcontext))
	*out_sid = ssid;
else if (context_equal(tcontext, &newcontext))
	*out_sid = tsid;
else {
	rc = sidtab_context_to_sid(sidtab, &newcontext, out_sid);
	if (rc == -ESTALE) {
		rcu_read_unlock();
		context_destroy(&newcontext);
		goto retry;
	}
}

SID 是 context 的内核索引,不是策略规则直接写出的常量。策略热替换期间出现 -ESTALE 时,函数销毁临时 context 并重试,避免把旧 policydb 计算结果提交到新 sidtab。

6. 显式Context ​

自动 transition 由策略根据 operation 输入选择新 type;显式 Context 则由进程先写入 exec 或 fscreate attribute。两者最终汇入相同的 exec/create 权限检查,显式设置不会绕过策略。

6.1 设置权限 ​

内核先检查进程是否拥有 setexec 或 setfscreate,再把用户传入的 Context 字符串解析成 SID。空字符串得到 SID 0,用于清除显式设置。

源码文件:kernel/common/security/selinux/hooks.c

c
struct cred_security_struct *crsec;
struct cred *new;
u32 mysid = current_sid(), sid = 0, ptsid;
int error;
char *str = value;

	/*
	 * Basic control over ability to set these attributes at all.
	 */
	switch (attr) {
	case LSM_ATTR_EXEC:
		error = avc_has_perm(mysid, mysid, SECCLASS_PROCESS,
				     PROCESS__SETEXEC, NULL);
		break;
	case LSM_ATTR_FSCREATE:
		error = avc_has_perm(mysid, mysid, SECCLASS_PROCESS,
				     PROCESS__SETFSCREATE, NULL);
		break;
	case LSM_ATTR_KEYCREATE:
		error = avc_has_perm(mysid, mysid, SECCLASS_PROCESS,
				     PROCESS__SETKEYCREATE, NULL);
		break;
	case LSM_ATTR_SOCKCREATE:
		error = avc_has_perm(mysid, mysid, SECCLASS_PROCESS,
				     PROCESS__SETSOCKCREATE, NULL);
		break;
	case LSM_ATTR_CURRENT:
		error = avc_has_perm(mysid, mysid, SECCLASS_PROCESS,
				     PROCESS__SETCURRENT, NULL);
		break;
	default:
		error = -EOPNOTSUPP;
		break;
	}
	if (error)
		return error;

	/* Obtain a SID for the context, if one was specified. */
	if (size && str[0] && str[0] != '\n') {
		if (str[size-1] == '\n') {
			str[size-1] = 0;
			size--;
		}
		error = security_context_to_sid(value, size,
						&sid, GFP_KERNEL);
		if (error == -EINVAL && attr == LSM_ATTR_FSCREATE) {
			if (!has_cap_mac_admin(true)) {
				struct audit_buffer *ab;
				size_t audit_size;

				/* We strip a nul only if it is at the end,
				 * otherwise the context contains a nul and
				 * we should audit that */
				if (str[size - 1] == '\0')
					audit_size = size - 1;
				else
					audit_size = size;
				ab = audit_log_start(audit_context(),
						     GFP_ATOMIC,
						     AUDIT_SELINUX_ERR);
				if (!ab)
					return error;
				audit_log_format(ab, "op=fscreate invalid_context=");
				audit_log_n_untrustedstring(ab, value,
							    audit_size);
				audit_log_end(ab);

				return error;
			}
			error = security_context_to_sid_force(value, size,
								&sid);
		}
		if (error)
			return error;
	}

security_context_to_sid() 会拒绝无法解析的 Context,并为无 MAC admin 能力的 FSCREATE 调用记录 invalid_context audit。拥有管理能力的调用方才进入 force 转换路径,但这项能力不改变后续创建权限检查。

6.2 状态提交 ​

解析成功后,内核复制 credentials,在副本中写 exec_sid 或 create_sid,再用 commit_creds() 原子替换当前 task 的 credentials。

源码文件:kernel/common/security/selinux/hooks.c

c
new = prepare_creds();
if (!new)
	return -ENOMEM;

/* Permission checking based on the specified context is
   performed during the actual operation (execve,
   open/mkdir/...), when we know the full context of the
   operation.  See selinux_bprm_creds_for_exec for the execve
   checks and may_create for the file creation checks. The
   operation will then fail if the context is not permitted. */
crsec = selinux_cred(new);
if (attr == LSM_ATTR_EXEC) {
	crsec->exec_sid = sid;
} else if (attr == LSM_ATTR_FSCREATE) {
	crsec->create_sid = sid;
}

源码文件:kernel/common/security/selinux/hooks.c

c
commit_creds(new);
return size;

abort_change:
	abort_creds(new);
	return error;

exec_sid 是一次性状态:下一次 exec 读取后清零。create_sid 则持续影响后续创建,直到调用方清除、修改,或发生 exec;exec hook 会把新 credentials 的 create_sid 重置为 0。这个生命周期差异决定了封装 API 时必须成对设置和恢复 fscreate Context,避免给后续无关文件留下错误标签。

6.3 生效边界 ​

状态owner消费者生效时机清理方式
exec_sidtask credentialsselinux_bprm_creds_for_exec()下一次 execexec 消费后清零,或写空值
create_sidtask credentialsselinux_determine_inode_label()每次文件创建写空值;exec 后新 credentials 清零
自动 transitionloaded policydbsecurity_compute_sid()exec/create 计算时替换策略或修改规则
inode SIDinode security blob/xattr后续访问控制创建提交后relabel/restorecon 等显式操作

显式 create_sid 只在支持 per-inode label 的挂载上优先于自动 transition;采用 mountpoint label 行为的文件系统仍由 mntpoint_sid 决定。换言之,“调用成功设置 fscreate Context”不保证所有文件系统都会用它。

7. 策略案例 ​

7.1 Kernel到Init ​

kernel → init 是 process transition。输入是 kernel domain、init_exec executable type 与 process class;结果是 init domain。宏同时生成 execute、transition 和 entrypoint 权限。

源码文件:system/sepolicy/private/kernel.te

text
domain_auto_trans(kernel, init_exec, init)

实际排查时应同时验证三件事:/system/bin/init 或对应 first-stage 路径最终是否为 init_exec,编译策略是否存在 transition,kernel 与 init 对应的 allow 是否存在。只看到宏文本不能证明运行时 executable 已获得正确标签。

7.2 Perfetto转换 ​

system_server 执行 Perfetto 命令行客户端时进入 perfetto,而不是把工具继续留在庞大的 system_server domain。

源码文件:system/sepolicy/private/system_server.te

text
domain_auto_trans(system_server, perfetto_exec, perfetto);
allow system_server perfetto:fifo_file { read write };

domain_auto_trans(system_server, trace_redactor_exec, trace_redactor);
allow system_server trace_redactor:process signal;

这里的消费者不只是 exec hook。转换后的 perfetto domain 决定子进程后续能访问哪些 trace 文件、fd 和 Binder 资源;system_server 只保留向其 FIFO 写入或控制进程所需的窄权限。

7.3 Vold挂载桩 ​

vold 创建 storage mountpoint 目录时,用普通 file transition 把新目录标成 stub type。注释给出了设计原因:挂载尚未出现时,专用 type 可阻止其他代码误把 mountpoint 当普通存储目录写入。

源码文件:system/sepolicy/private/vold.te

text
type_transition vold storage_file:dir storage_stub_file;
type_transition vold mnt_media_rw_file:dir mnt_media_rw_stub_file;

allow vold {
    mnt_media_rw_stub_file storage_stub_file
}:dir { mounton create rmdir getattr setattr };

这两条规则没有 object name,因此同一 source、parent type 和 dir class 下的所有新目录都会采用 stub type。若只想限制某个固定目录名,应改用命名 transition,而不是继续扩大普通规则。

7.4 四类结果 ​

操作规则/状态计算结果后续关键权限
kernel exec init_exec普通 process transitioninit SIDtransition、entrypoint
system_server exec perfetto_exec普通 process transitionperfetto SIDtransition、entrypoint
vold 在 storage_file 下 mkdir普通 dir transitionstorage_stub_file SIDadd_name/search、create、associate
system_server 创建 ndebugsocketfilename transitionsystem_ndebug_socket SIDadd_name/search、create、associate

四个案例共用 security_transition_sid(),差异来自 class、target 对象和 object name。理解这组输入比记忆宏名更重要,因为 AVC denial 中出现的正是 source context、target context、class 和 permission。

8. 失败边界 ​

8.1 Exec失败 ​

失败条件实际结果优先检查
executable type 错误transition key 不命中file contexts、ls -Z
无 transition 且无 execute_no_transSID 保持不变,但执行被拒绝process transition 与 execute 权限
已算出 new SID,但缺 process transitionexec 被拒绝source→new domain allow
缺 entrypointnew domain 不能以该文件为入口new domain→executable allow
显式 exec_sid 无 setexecattribute 设置阶段失败process setexec
Context 无效字符串转 SID 失败user/role/type/MLS 是否存在且合法

no_new_privs 和 nosuid 不是统一的“禁止 transition”。内核先允许 policy capability 明确授权的 NNP/nosuid transition,也允许 bounded transition;都不满足时,显式 exec SID 直接失败,自动 transition 则回退到旧 SID,再接受 execute_no_trans 检查。

源码文件:kernel/common/security/selinux/hooks.c

c
if (!nnp && !nosuid)
	return 0; /* neither NNP nor nosuid */

if (new_crsec->sid == old_crsec->sid)
	return 0; /* No change in credentials */

/*
 * If the policy enables the nnp_nosuid_transition policy capability,
 * then we permit transitions under NNP or nosuid if the
 * policy allows the corresponding permission between
 * the old and new contexts.
 */
if (selinux_policycap_nnp_nosuid_transition()) {
	av = 0;
	if (nnp)
		av |= PROCESS2__NNP_TRANSITION;
	if (nosuid)
		av |= PROCESS2__NOSUID_TRANSITION;
	rc = avc_has_perm(old_crsec->sid, new_crsec->sid,
			  SECCLASS_PROCESS2, av, NULL);
	if (!rc)
		return 0;
}

/*
 * We also permit NNP or nosuid transitions to bounded SIDs,
 * i.e. SIDs that are guaranteed to only be allowed a subset
 * of the permissions of the current SID.
 */
rc = security_bounded_transition(old_crsec->sid,
				 new_crsec->sid);
if (!rc)
	return 0;

/*
 * On failure, preserve the errno values for NNP vs nosuid.
 * NNP:  Operation not permitted for caller.
 * nosuid:  Permission denied to file.
 */
if (nnp)
	return -EPERM;
return -EACCES;

这种差异保持了显式 API 的确定性:调用方要求进入某个 Context 时,内核不会悄悄以旧 Context 执行;自动规则则允许在安全边界内降级,但降级后的普通执行仍必须被策略允许。

8.2 Create失败 ​

失败条件停止位置可见现象
父目录缺 search/add_namemay_create() 第一次 AVC无法在目录增加名字
transition 计算出无效 Contextsecurity_compute_sid()invalid_context audit,enforcing 下拒绝
新 type 缺 create第二次 AVC标签能算出,但对象不能创建
新 type 缺 filesystem associate第三次 AVCtype 不能用于该 filesystem
filename 名字不匹配不一定失败,回落普通/default type创建成功但标签与预期不同
显式 create_sid 未清理后续无关对象沿用该 SID连续文件被错误标注

命名不匹配特别容易误判:type_transition ... "ndebugsocket" 不会要求“只能创建这个名字”,它只规定该名字的结果 type。其他名字仍可能依靠普通 transition 或 class 默认值成功创建,因此应检查实际 ls -Z,而不是只找 denial。

8.3 Context有效性 ​

SID 计算出的 user、role、type、MLS 组合还要通过 policydb_context_isvalid()。enforcing 下无效组合返回 -EACCES;非 enforcing 下记录警告后允许继续,这也是 permissive 设备上能看到 invalid context 日志但操作未失败的原因。

源码文件:kernel/common/security/selinux/ss/services.c

c
if (!policydb_context_isvalid(policydb, &newcontext)) {
	rc = compute_sid_handle_invalid_context(policy, sentry,
						tentry, tclass,
						&newcontext);
	if (rc)
		goto out_unlock;
}

源码文件:kernel/common/security/selinux/ss/services.c

c
if (!enforcing_enabled())
	return 0;
return -EACCES;

不要把 permissive 理解为“不计算 Context”。计算、validity 检查和 audit 仍发生,只是最终拒绝被放行。

9. 反向验证 ​

9.1 策略查询 ​

在已有 Android 编译输出的环境中,可以用 SETools 查询编译后的二进制策略。查询编译产物比只搜索 .te 更可靠,因为宏、条件分支和平台/vendor 合并都已经处理完成。

bash
# 将PRODUCT替换为实际产品名;引号避免占位内容被shell解释。
ANDROID_SEPOLICY='out/target/product/PRODUCT/root/sepolicy'

# 查 process transition:输入source、target executable type和process class。
sesearch -T -s system_server -t perfetto_exec \
  -c process "$ANDROID_SEPOLICY"

# 查 vold 创建目录时的普通type transition。
sesearch -T -s vold -t storage_file -c dir \
  "$ANDROID_SEPOLICY"

# 查询命名transition;不同SETools版本的显示格式可能不同。
sesearch -T -s system_server -t system_data_file \
  -c sock_file "$ANDROID_SEPOLICY"

输入是最终策略、source type、target type 和 class。关键断言分别是结果包含 perfetto、storage_stub_file,以及两个 socket 名字对应的专用 type。它能说明规则已进入最终策略,却不能说明设备上的 executable、父目录或文件名在运行时确实提供了相同输入。

9.2 运行标签 ​

运行时验证要同时看调用进程、触发对象和结果对象。只看其中一个标签无法重建 transition key。

bash
# 查看主体domain;-A显示所有进程,-Z显示安全上下文。
adb shell ps -AZ | grep -E 'system_server|perfetto|servicemanager'

# 查看executable与父目录标签,它们分别是process/file transition的target。
adb shell ls -Z /system/bin/servicemanager
adb shell ls -Zd /data/system

# 查看命名socket创建后的结果type。
adb shell ls -Z /data/system/ndebugsocket \
  /data/system/unsolzygotesocket

对 process 案例,断言是父进程 domain、executable type 与子进程 domain 构成策略中的三元关系;对命名对象,断言是相同 parent type 下不同名字获得不同结果 type。该实验受服务是否启动、路径是否由设备配置改变以及 shell 是否有目录搜索权限影响,不能把“文件不存在”直接解释为 transition 失败。

9.3 Attribute测试 ​

Linux LSM selftest 会枚举已启用的 LSM,读取 LSM_ATTR_EXEC 与 LSM_ATTR_FSCREATE,并在旧 /proc/self/attr/* 接口可读时比较两条接口返回的 Context。

源码文件:kernel/common/tools/testing/selftests/lsm/lsm_get_self_attr_test.c

c
if (cnt_exec) {
	size = page_size;
	count = lsm_get_self_attr(LSM_ATTR_EXEC, ctx, &size, 0);
	ASSERT_GE(cnt_exec, count);
	if (count > 0) {
		tctx = ctx;
		if (read_proc_attr("exec", attr, page_size) == 0)
			ASSERT_EQ(0, strcmp((char *)tctx->ctx, attr));
	}
}
if (cnt_fscreate) {
	size = page_size;
	count = lsm_get_self_attr(LSM_ATTR_FSCREATE, ctx, &size, 0);
	ASSERT_GE(cnt_fscreate, count);
	if (count > 0) {
		tctx = ctx;
		if (read_proc_attr("fscreate", attr, page_size) == 0)
			ASSERT_EQ(0, strcmp((char *)tctx->ctx, attr));
	}
}

测试输入是当前 task 与系统启用的 LSM 集合。断言关注两点:新 syscall 能返回受支持 attribute;SELinux 的新旧读取接口在都有结果时返回相同 Context。它验证的是 attribute 暴露和状态读取一致性,不验证某个 Context 是否拥有 exec/create 权限,也不覆盖 transition 规则的 type 选择。

9.4 源码搜索 ​

没有编译策略时,可以先用只读搜索重建规则与消费者,但应明确这只是源码层输入,不等同于产品最终合并策略。

bash
# 找到规则声明和权限消费者,避免只看到type_transition一行。
rg -n 'domain_auto_trans\(system_server, perfetto_exec' \
  system/sepolicy/private/system_server.te
rg -n 'system_ndebug_socket|system_unsolzygote_socket' \
  system/sepolicy/private/system_server.te

# 找到内核中的计算入口、权限检查和标签提交点。
rg -n 'security_transition_sid|may_create|inode_init_security' \
  kernel/common/security/selinux/hooks.c

第一组搜索应同时找到 transition、create_file_perms 和消费者限制;第二组应找到计算、检查、提交三个阶段。若只找到规则却找不到 allow,通常意味着宏在别处展开、条件未覆盖当前构建,或策略尚未写完整,需要继续沿 attribute 与宏定义追踪。

10. 源码导航 ​

问题首选文件关键符号
domain/file 宏展开了什么system/sepolicy/public/te_macrosdomain_trans、domain_auto_trans、file_type_auto_trans
Android 有哪些真实规则system/sepolicy/private/*.tetype_transition、宏调用与配套 allow
exec 如何选择 new SIDkernel/common/security/selinux/hooks.cselinux_bprm_creds_for_exec
create 如何选择并检查 SIDkernel/common/security/selinux/hooks.cselinux_determine_inode_label、may_create
filename rule 如何存储kernel/common/security/selinux/ss/policydb.hfilename_trans_key、filename_trans_datum
完整 Context 如何合成kernel/common/security/selinux/ss/services.csecurity_compute_sid、filename_compute_type
显式 Context 保存在哪里kernel/common/security/selinux/hooks.cselinux_lsm_setattr、exec_sid、create_sid

阅读一条陌生规则时,建议按“策略输入 → operation hook → SID 计算 → AVC 权限 → 状态提交”的顺序走,而不是从宏名猜行为。宏库专题会继续整理参数约定与组合方式;本篇只展开与 transition 直接相关的四个宏。

11. 闭环复述 ​

看到下面两条规则时,应能分别回答完整输入和结果,而不是只说“它们会自动切换标签”。

源码文件:system/sepolicy/private/vold.te

源码文件:system/sepolicy/private/system_server.te

text
type_transition vold storage_file:dir storage_stub_file;
type_transition system_server system_data_file:sock_file system_ndebug_socket "ndebugsocket";

第一条在 vold 创建目录时,以 storage_file 父目录和 dir class 为 key,得到 storage_stub_file;随后还要通过父目录 search/add_name、新目录 create 和 filesystem associate,创建提交后 inode 才持有该 SID。

第二条额外把最后一个组件 ndebugsocket 加入 key。security_compute_sid() 先算普通/default type,再由 filename_compute_type() 精确覆盖;名字不匹配时不会使用 system_ndebug_socket。显式 create_sid 又位于自动计算之前,但它仍不能跳过同一组创建权限。

能够从 AVC 中的 scontext、tcontext、tclass 和 permission 反推失败发生在“输入标签、SID 计算、权限检查、Context validity、标签提交”中的哪一层,才算真正掌握 type_transition。