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. 规则输入
type_transition source_type target_type:object_class default_type;
type_transition source_type target_type:object_class default_type "object_name";| 字段 | Process transition | File/object transition |
|---|---|---|
| source type | exec 调用者 domain | 创建者 domain |
| target type | executable file type | 父目录或 related object type |
| class | process | file、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
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
define(`domain_auto_trans', `
domain_trans($1,$2,$3)
type_transition $1 $2:process $3;
')相关宏:init_daemon_domain
源码文件:system/sepolicy/public/te_macros
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
/* 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
/* 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
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
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
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_sid | task credentials | selinux_bprm_creds_for_exec() | 下一次 exec | exec 消费后清零,或写空值 |
create_sid | task credentials | selinux_determine_inode_label() | 每次文件创建 | 写空值;exec 后新 credentials 清零 |
| 自动 transition | loaded policydb | security_compute_sid() | exec/create 计算时 | 替换策略或修改规则 |
| inode SID | inode 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
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
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
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 transition | init SID | transition、entrypoint |
system_server exec perfetto_exec | 普通 process transition | perfetto SID | transition、entrypoint |
vold 在 storage_file 下 mkdir | 普通 dir transition | storage_stub_file SID | add_name/search、create、associate |
system_server 创建 ndebugsocket | filename transition | system_ndebug_socket SID | add_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_trans | SID 保持不变,但执行被拒绝 | process transition 与 execute 权限 |
已算出 new SID,但缺 process transition | exec 被拒绝 | source→new domain allow |
缺 entrypoint | new domain 不能以该文件为入口 | new domain→executable allow |
显式 exec_sid 无 setexec | attribute 设置阶段失败 | 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
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_name | may_create() 第一次 AVC | 无法在目录增加名字 |
| transition 计算出无效 Context | security_compute_sid() | invalid_context audit,enforcing 下拒绝 |
新 type 缺 create | 第二次 AVC | 标签能算出,但对象不能创建 |
新 type 缺 filesystem associate | 第三次 AVC | type 不能用于该 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
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
if (!enforcing_enabled())
return 0;
return -EACCES;不要把 permissive 理解为“不计算 Context”。计算、validity 检查和 audit 仍发生,只是最终拒绝被放行。
9. 反向验证
9.1 策略查询
在已有 Android 编译输出的环境中,可以用 SETools 查询编译后的二进制策略。查询编译产物比只搜索 .te 更可靠,因为宏、条件分支和平台/vendor 合并都已经处理完成。
# 将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。
# 查看主体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
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 源码搜索
没有编译策略时,可以先用只读搜索重建规则与消费者,但应明确这只是源码层输入,不等同于产品最终合并策略。
# 找到规则声明和权限消费者,避免只看到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_macros | domain_trans、domain_auto_trans、file_type_auto_trans |
| Android 有哪些真实规则 | system/sepolicy/private/*.te | type_transition、宏调用与配套 allow |
| exec 如何选择 new SID | kernel/common/security/selinux/hooks.c | selinux_bprm_creds_for_exec |
| create 如何选择并检查 SID | kernel/common/security/selinux/hooks.c | selinux_determine_inode_label、may_create |
| filename rule 如何存储 | kernel/common/security/selinux/ss/policydb.h | filename_trans_key、filename_trans_datum |
| 完整 Context 如何合成 | kernel/common/security/selinux/ss/services.c | security_compute_sid、filename_compute_type |
| 显式 Context 保存在哪里 | kernel/common/security/selinux/hooks.c | selinux_lsm_setattr、exec_sid、create_sid |
阅读一条陌生规则时,建议按“策略输入 → operation hook → SID 计算 → AVC 权限 → 状态提交”的顺序走,而不是从宏名猜行为。宏库专题会继续整理参数约定与组合方式;本篇只展开与 transition 直接相关的四个宏。
11. 闭环复述
看到下面两条规则时,应能分别回答完整输入和结果,而不是只说“它们会自动切换标签”。
源码文件:system/sepolicy/private/vold.te
源码文件:system/sepolicy/private/system_server.te
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。
