安全上下文
本文面向已经读过 SELinux架构、DAC与MAC 和 LSM框架 的读者。前文已经说明 SELinux 需要 source context、target context、object class 和 permission;本文继续回答这些 context 从哪里来、如何在内核中表示、何时附着到进程或 inode、怎样通过 /proc 与 ls -Z 读出来,以及 Android 的 seapp_contexts 为什么会把同一个 UID 的应用映射到不同 domain。
本文不把 u:r:domain:s0 当成一串静态字符串来背诵,也不展开完整 TE、RBAC、MLS 规则语法。文章沿四条可以回到源码的主线组织:内核用 SID 和 security blob 保存状态;init 用 file_contexts 与 restorecon 给文件对象定标签;Zygote 用 seapp_contexts 输入选择应用进程 domain;用户空间通过 proc 属性和 selabel_lookup() 读取或查询这些状态。读完后,你应能区分“字符串解析成功”“SID 已分配”“对象已附着标签”“策略裁决已消费”四个时刻。
1. 四个表示
同一个上下文在不同层次有不同表示,不能互相替换:
| 表示 | 示例 | 所有者 | 主要消费者 |
|---|---|---|---|
| 上下文字符串 | u:r:system_server:s0 | libselinux、proc 接口、日志 | 人和工具、配置文件 |
| SID | 内核整数标识 | SELinux sidtab/state | AVC、hook、security server |
| cred/task security blob | cred 中的 sid、osid、exec_sid,以及 task 的 AVC 目录缓存 | struct cred / task blob | exec、capable、Binder、进程查询 |
| inode security blob | inode 的 sid、sclass、初始化状态 | struct inode / inode blob | VFS、file hook、AVC |
上下文字符串是外部可读格式,SID 是内核快速查找键。security_context_to_sid() 把字符串转换为 SID,security_sid_to_context() 做反向转换;如果只看到日志中的 context 字符串,不能直接推断内核中 SID 数值或对象 blob 已经初始化。
2. 内核状态
2.1 task状态
SELinux 把进程身份附着在 struct cred 的 LSM 复合 blob 中;struct task_struct 另有 task-local AVC 目录缓存。两者都属于 SELinux 的 LSM blob,但字段职责不同。
源码文件:kernel/common/security/selinux/include/objsec.h
相关结构:cred_security_struct、task_security_struct
struct cred_security_struct {
u32 osid;
u32 sid;
u32 exec_sid;
u32 create_sid;
u32 keycreate_sid;
u32 sockcreate_sid;
};
struct task_security_struct {
struct {
u32 sid;
u32 seqno;
unsigned int dir_spot;
struct avdc_entry dir[TSEC_AVDC_DIR_SIZE];
bool permissive_neveraudit;
} avdcache;
};cred_security_struct.sid 是当前 task 身份;osid 记录 exec 前身份,便于审计和域转换;exec_sid 是显式 exec context 的候选值;create_sid、keycreate_sid、sockcreate_sid 分别影响新建文件、key 和 socket 的标签。task_security_struct.avdcache 则缓存当前 task 的目录访问决策,不是第二份进程 domain。它们不是同时生效的多个 domain,只有相应 hook 使用某个字段时才参与状态转换。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:cred_init_security、cred_sid
static void __init cred_init_security(void)
{
struct cred_security_struct *crsec;
crsec = selinux_cred(unrcu_pointer(current->real_cred));
crsec->osid = crsec->sid = SECINITSID_KERNEL;
}
static inline u32 cred_sid(const struct cred *cred)
{
const struct cred_security_struct *crsec;
crsec = selinux_cred(cred);
return crsec->sid;
}系统最初的 task 使用 SECINITSID_KERNEL,不是 Android 用户空间最终看到的 u:r:init:s0。first-stage init 运行在 kernel domain,策略加载、restorecon(/system/bin/init) 和重新 exec 后,用户空间 init 才进入 Android init domain。把启动早期的 kernel SID 与 second-stage init context 混为一谈,会误判策略加载时机。
2.2 inode状态
文件对象的标签附着在 inode security blob。inode 的 sclass 决定同一个 SID 在当前对象上按 file、dir、socket 等哪类权限解释。
源码文件:kernel/common/security/selinux/include/objsec.h
相关结构:inode_security_struct、file_security_struct
struct inode_security_struct {
struct inode *inode;
struct list_head list;
u32 task_sid;
u32 sid;
u16 sclass;
unsigned char initialized;
spinlock_t lock;
};
struct file_security_struct {
u32 sid;
u32 fown_sid;
u32 isid;
u32 pseqno;
};这里的 task_sid 是创建或初始化 inode 时关联的 task 身份,sid 才是该 inode 当前 target 标签。打开文件后,file_security_struct 保存打开者相关 SID、打开时 inode SID 和 policy sequence,用于后续 file permission 重验证。文件描述符继承不等于重新根据路径查标签。
2.3 sidtab
SID 到字符串的映射由 sidtab/security server 持有。访问控制路径只携带 SID,只有日志、proc 或用户空间 API 需要字符串时才转换。
源码文件:kernel/common/security/selinux/ss/services.c
相关函数:security_context_to_sid
int security_context_to_sid(
const char *scontext, u32 scontext_len,
u32 *sid, gfp_t gfp)
{
return security_context_to_sid_core(
scontext, scontext_len, sid,
SECSID_NULL, gfp, 0);
}策略未加载时,内部实现只接受初始 SID 字符串并回退到 SECINITSID_KERNEL;这不是任意字符串都已经成为有效 domain。策略加载完成后,sidtab 才按当前 policy 的 type/role/level 定义分配或查找 SID;字符串不合法、类型不存在或策略状态不允许时会返回错误。
3. 字段语义
3.1 user与role
Android 常见上下文的 user 和 role 字段通常是 u、r 或 object_r。它们参与完整 SELinux context 解析,但 Android 日常 TE 授权主要围绕 type/domain 展开。
| 上下文 | user | role | type/domain | level |
|---|---|---|---|---|
u:r:init:s0 | u | r | init | s0 |
u:r:system_server:s0 | u | r | system_server | s0 |
u:object_r:system_file:s0 | u | object_r | system_file | s0 |
u:object_r:activity_service:s0 | u | object_r | activity_service | s0 |
进程上下文使用进程 role,文件、服务名和属性等客体通常使用 object_r。role 不是 Linux UID,也不是 Android user id;u:r:untrusted_app:s0:c512,c768 中的 u 不表示该进程属于 Android user 0。
3.2 type与domain
同一个 type 语法在主体和客体上承担不同角色:进程 type 称为 domain,文件或服务 type 是 target type。allow 规则以 type 为主键,role/user/level 仍可能通过约束参与最终判断。
源码文件:system/sepolicy/private/init.te
相关规则:init domain 与 capability
typeattribute init coredomain;
init_daemon_domain(init)
allow init self:global_capability_class_set {
dac_override
dac_read_search
sys_admin
sys_chroot
};init_daemon_domain(init) 不只是声明字符串,它展开为 init 可执行文件、domain transition 和进程启动相关的多条规则。后续文章会专门展开 type、attribute 和宏,本文只需记住 domain 是运行中 task 的 type,而不是一个独立于 SID 的第二身份。
3.3 level与MCS
Android 通常使用 s0 sensitivity,并用 MCS categories 区分应用数据。level 的具体生成由 seapp_contexts 的 levelFrom 选择器决定:app 使用 app id,user 使用 Android user id,all 同时使用两者,none 不附加 level。
源码文件:system/sepolicy/private/seapp_contexts
相关规则:levelFrom 与应用 domain
user=_app seinfo=platform domain=platform_app \
type=app_data_file levelFrom=user
user=_app isEphemeralApp=true domain=ephemeral_app \
type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=37 domain=untrusted_app \
type=app_data_file levelFrom=alllevelFrom 不是标签字符串中的固定文字,而是 libselinux 根据应用输入和 UID 计算 level 的指令。不同 Android user 的相同 app id 可以得到不同 level;同一 user 中的不同 app id 也可以通过 all 产生不同 categories。不要用一个示例 s0:c... 推断所有设备的 category 分配算法。
4. 文件标签
4.1 规则来源
file_contexts 的左侧是路径或正则,右侧是期望安全上下文。它描述“某路径应该是什么标签”,不等于磁盘 inode 已经被写入该标签。
源码文件:system/sepolicy/private/file_contexts
相关规则:关键 Android 路径
/system/bin/init u:object_r:init_exec:s0
/system/bin/app_process32 u:object_r:zygote_exec:s0
/system/bin/app_process64 u:object_r:zygote_exec:s0
/system/bin/servicemanager u:object_r:servicemanager_exec:s0
/dev/socket/zygote u:object_r:zygote_socket:s0
/dev/socket/zygote_secondary u:object_r:zygote_socket:s0
/dev/socket/property_service u:object_r:property_socket:s0路径命中数据库后,init 或其他调用者还要执行 selinux_android_restorecon(),把期望 context 应用到文件系统对象。对于 procfs、sysfs 等伪文件系统,标签可能来自 genfs contexts 或内核 filesystem labeling,而不是 ext4 xattr。
4.2 查询与重标
init 为 file contexts 建立并缓存 selabel_handle,查询函数返回字符串;restorecon 则使用相同的数据库对对象执行重标。
源码文件:system/core/init/selabel.cpp
相关函数:SelabelInitialize、SelabelLookupFileContext
namespace {
selabel_handle* sehandle = nullptr;
}
void SelabelInitialize() {
sehandle = selinux_android_file_context_handle();
selinux_android_set_sehandle(sehandle);
}
bool SelabelLookupFileContext(
const std::string& key, int type,
std::string* result) {
result->clear();
if (!sehandle)
return true;
char* context;
if (selabel_lookup(
sehandle, &context, key.c_str(), type) != 0) {
return false;
}
*result = context;
free(context);
return true;
}selabel_lookup() 的 owner 是内存中的 label handle,消费者拿到的是期望字符串;它不会自动修改对象。selinux_android_restorecon() 由 init、ueventd、property service 等路径调用,才会把 lookup 结果与对象当前标签比较并修复。
4.3 初始化时机
SELinux 在 first-stage init 仍处于 kernel domain 时就会为 /system/bin/init 执行 restorecon,随后重新 exec second-stage init。这样第二阶段进程才能在正确的 init domain 中处理 rc、属性和服务。
源码文件:system/core/init/selinux.cpp
相关函数:SetupSelinux
if (IsMicrodroid()) {
LoadSelinuxPolicyMicrodroid();
} else {
LoadSelinuxPolicyAndroid();
}
SelinuxSetEnforcement();
if (selinux_android_restorecon(
"/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));生效时机是“策略已加载 + init 可执行文件有 init_exec 标签 + exec 完成”,不是 file_contexts 文件被打包进镜像的时刻。若 lookup 成功但 restorecon 失败,后续 domain transition 仍可能失败。
5. 应用上下文
5.1 输入字段
seapp_contexts 的匹配输入由 Android framework 和 Zygote 共同准备。Android 17 文件头明确列出 isSystemServer、user、seinfo、name、isPrivApp、minTargetSdkVersion、fromRunAs、isolated/sdk sandbox 等 selector。
源码文件:system/sepolicy/private/seapp_contexts
相关说明:selector 与输出字段
Input selectors: isSystemServer, isEphemeralApp, user, seinfo, name,
isPrivApp, minTargetSdkVersion, fromRunAs,
isIsolatedComputeApp, isSdkSandboxNext, isSdkSandboxAudit
Outputs: domain, type, levelFrom, level这些输入不是全部来自包名。user 可能由 UID 映射为 system、_app、_isolated 或 _sdksandbox;seinfo 来自签名和 mac_permissions.xml;isPrivApp、target SDK 和包名由 PackageManager 传给进程启动链。少一个输入,可能命中更宽泛的规则。
5.2 匹配优先级
文件注释给出严格优先级:system server 优先于普通进程;明确 selector 优先于未指定;更具体的 user/seinfo/name 优先于宽泛或前缀匹配;平台 contexts 文件优先于 vendor/odm 文件;最终按第一个匹配项输出。
源码文件:system/sepolicy/private/seapp_contexts
相关规则:system server、isolated 和普通应用
isSystemServer=true domain=system_server_startup
user=_isolated domain=isolated_app levelFrom=user
user=_isolated isIsolatedComputeApp=true \
domain=isolated_compute_app levelFrom=user
user=_app seinfo=platform minTargetSdkVersion=37 \
domain=platform_app type=app_data_file levelFrom=user
user=_app minTargetSdkVersion=37 \
domain=untrusted_app type=app_data_file levelFrom=all规则比较不是“文件中写在前面的永远优先”。解析器先按 selector specificity 排序,再寻找第一个匹配;因此新增一条更具体规则可能改变旧应用的 domain,即使它被追加在文件尾部。
5.3 neverallow
同一个 seapp_contexts 文件还包含 neverallow assertion,它们只在构建检查阶段使用,不会输出为设备运行时的匹配条目。
源码文件:system/sepolicy/private/seapp_contexts
相关规则:domain 归属约束
neverallow isSystemServer=false domain=system_server
neverallow isSystemServer=false domain=system_server_startup
neverallow user=((?!system).)* domain=system_app
neverallow user=_isolated domain=((?!isolated_app).)*
neverallow user=((?!_isolated).)* domain=isolated_app这解释了“规则能匹配”与“策略能构建”是两件事。一个 selector 组合即使语法正确,只要违反 neverallow,构建应失败;反过来,构建通过也只说明静态约束满足,不说明运行时每个包都命中了预期 selector。
5.4 Zygote消费
Zygote specialization 调用 selinux_android_setcontext(),把 UID、是否 system server、seInfo 和 nice name 交给 Android libselinux。system_server 还会显式设置固定 u:r:system_server:s0。
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
相关位置:SpecializeCommon
const char* se_info_ptr = se_info.has_value()
? se_info.value().c_str() : nullptr;
if (selinux_android_setcontext(
uid, is_system_server,
se_info_ptr, nice_name_ptr) == -1) {
fail_fn(CREATE_ERROR(
"selinux_android_setcontext(%d, %d, \"%s\", \"%s\") failed",
uid, is_system_server,
se_info_ptr, nice_name_ptr));
}
if (is_system_server) {
static const char* kSystemServerLabel =
"u:r:system_server:s0";
if (selinux_android_setcon(kSystemServerLabel) != 0) {
fail_fn(CREATE_ERROR(
"selinux_android_setcon(%s)",
kSystemServerLabel));
}
}selinux_android_setcontext() 的返回成功只表示 libselinux 完成进程 context 设置;Zygote 还要继续执行 capability、seccomp、线程名和 post-fork hook。任何 context 设置失败都不能让子进程进入 ART/Java 入口。
图中 Linux UID/GID 和 SELinux task SID 是两条状态写入路径;seInfo 只是选择上下文的输入,不等于最终 domain 字符串。
6. 进程属性
6.1 current属性
传统 SELinux proc 接口通过 /proc/<pid>/attr/current 暴露 task 当前 context 字符串。内核实现先由 proc 文件读取入口调用 security 层,再由 SELinux 将 task SID 转成 context。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:selinux_getprocattr
static int selinux_getprocattr(
struct task_struct *p, const char *name, char **value)
{
unsigned int attr = lsm_name_to_attr(name);
int rc;
if (attr) {
rc = selinux_lsm_getattr(attr, p, value);
if (rc != -EOPNOTSUPP)
return rc;
}
return -EINVAL;
}不同属性名由 lsm_name_to_attr() 映射后交给 selinux_lsm_getattr();current、exec 等具体字段读取发生在后者内部。它们都不是 Android package name,也不是 UID。工具读取 /proc/self/attr/current 时得到的字符串已经经过 SID 反向转换。
6.2 新属性API
LSM 通用系统调用还提供 lsm_get_self_attr/lsm_set_self_attr,调用统一 security 层。它与 SELinux 专有的 proc 属性接口有不同 ABI 和错误处理,不应混用。
源码文件:kernel/common/security/lsm_syscalls.c
相关函数:sys_lsm_get_self_attr
SYSCALL_DEFINE4(lsm_get_self_attr,
unsigned int, attr,
struct lsm_ctx __user *, ctx,
u32 __user *, size, u32, flags)
{
return security_getselfattr(
attr, ctx, size, flags);
}通用 API 返回的是一个或多个 LSM context 记录,调用方必须处理缓冲区大小和模块差异;id -Z 通常读取的是 SELinux 当前 context 的人类可读形式。看到同样的 u:r:... 字符串,不表示两个接口拥有相同的调用路径。
6.3 读取命令
# 查看当前 shell 的 SELinux context 与 Linux UID/GID。
adb shell 'id; id -Z'
# 对比进程当前 context、exec 候选 context 和安全属性文件。
adb shell 'cat /proc/self/attr/current'
adb shell 'cat /proc/self/attr/exec'
# 查看关键 Android 进程以及文件对象的标签。
adb shell 'ps -AZ | grep -E "init|servicemanager|zygote|system_server"'
adb shell 'ls -lZ /system/bin/init /system/bin/app_process64 /dev/socket/zygote'attr/exec 在没有显式 exec context 时可能为空或返回错误,不能把它当成当前 domain 的第二份副本。ls -Z 显示的是对象标签,ps -Z 显示的是 task 标签,两者的 type 可能同名,也可能完全不同。
7. 状态转换
7.1 Exec转换
execve 可能触发 domain transition。SELinux hook 在 bprm_* 生命周期中读取当前 task、可执行文件 inode 和候选 transition SID,最后提交新 credentials。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:selinux_bprm_committing_creds、selinux_bprm_committed_creds
static void selinux_bprm_committing_creds(
const struct linux_binprm *bprm)
{
struct cred_security_struct *new_crsec;
new_crsec = selinux_cred(bprm->cred);
if (new_crsec->sid == new_crsec->osid)
return;
// ... 根据新旧 SID 清理无权继承的文件与资源限制。
}
static void selinux_bprm_committed_creds(
const struct linux_binprm *bprm)
{
const struct cred_security_struct *crsec =
selinux_cred(current_cred());
if (crsec->sid == crsec->osid)
return;
// ... 根据新旧 SID 清理信号状态,并唤醒等待父进程。
}上面保留了真实函数签名和状态判断,并用注释概括较长的清理分支;真正的 transition SID 计算发生在更早的 bprm credentials hook。阅读 exec 问题时要把“查找新 domain”“提交 credentials”“进程开始执行新映像”分开,不要只搜 setcon。
7.2 创建对象
新 inode 的 SID 可能来自父目录、文件系统 mount 选项、task create_sid 或 security_transition_sid()。因此新建文件的标签不一定等于创建者当前 domain,也不一定等于父目录标签。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:selinux_determine_inode_label
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;
}这段代码是解释 type_transition 的入口之一:策略转换函数接收创建者 SID、父目录 SID、对象 class 和名字,计算新对象 SID。对象标签的 owner 是 inode security state,不是创建它的 Java API。
7.3 策略重载
策略序列改变后,旧 inode/file 状态需要重新验证。selinux_file_open() 保存 pseqno,后续访问可以发现 policy 已更新并重新做权限检查;因此 context 字符串相同也不代表旧的 AVC 决策仍然有效。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:selinux_file_open
static int selinux_file_open(struct file *file)
{
struct file_security_struct *fsec;
struct inode_security_struct *isec;
fsec = selinux_file(file);
isec = inode_security(file_inode(file));
fsec->isid = isec->sid;
fsec->pseqno = avc_policy_seqno();
/* Recheck after saving inode SID and policy sequence. */
return file_path_has_perm(
file->f_cred, file, open_file_to_av(file));
}8. 失败边界
| 现象 | 状态检查 | 可能 owner | 不应直接下的结论 |
|---|---|---|---|
id -Z 读取失败 | proc attr ABI、task SID、SELinux 是否启用 | kernel SELinux/proc | 不能说明文件标签错误 |
ls -Z 有值但访问被拒绝 | inode target SID/class、mode/ACL、source SID | VFS/SELinux | 有 context 不代表有 allow |
file_contexts 有条目但文件标签不对 | lookup 产物、restorecon 是否执行、伪文件系统类型 | init/ueventd/filesystem | 配置存在不代表 inode 已重标 |
| 应用 domain 与预期不同 | UID、seinfo、包名、privileged、target SDK、规则优先级 | PackageManager/Zygote/libselinux | 不能只改 domain allow |
| exec 后 domain 未切换 | 可执行文件 label、transition 规则、bprm hook、权限 | SELinux exec path | setcon 不是所有 transition 的入口 |
| policy 更新后行为仍旧 | policy sequence、AVC/cache、file state | SELinux kernel state | 不能假设旧 file descriptor 永远沿用旧决策 |
| 同一服务名不同进程看到不同结果 | servicemanager source SID、service_contexts target、LSM order | Binder/servicemanager | 不能只查服务实现类 |
排查顺序应是:先读主体 task context,再读客体 context;确认标签来源和生效时机;确认 object class 与 permission;最后才查 allow/neverallow。安全上下文是访问判断的输入,不是访问成功的证明。
9. 反向测试
9.1 LSM属性测试
内核 LSM selftest 通过错误指针、容量不足和非法 flags 验证通用属性 API 的 ABI 边界。
源码文件:kernel/common/tools/testing/selftests/lsm/lsm_list_modules_test.c
相关测试:size_null_lsm_list_modules、size_too_small_lsm_list_modules、flags_set_lsm_list_modules
TEST(size_null_lsm_list_modules)
{
const long page_size = sysconf(_SC_PAGESIZE);
__u64 *syscall_lsms = calloc(page_size, 1);
ASSERT_NE(NULL, syscall_lsms);
errno = 0;
ASSERT_EQ(-1, lsm_list_modules(
syscall_lsms, NULL, 0));
ASSERT_EQ(EFAULT, errno);
free(syscall_lsms);
}
TEST(flags_set_lsm_list_modules)
{
const long page_size = sysconf(_SC_PAGESIZE);
__u64 *syscall_lsms = calloc(page_size, 1);
__u32 size = page_size;
errno = 0;
ASSERT_EQ(-1, lsm_list_modules(
syscall_lsms, &size, 7));
ASSERT_EQ(EINVAL, errno);
free(syscall_lsms);
}第一个测试用空 size 指针验证内核不会写入非法用户地址;第二个用非零 flags 验证保留字段不会被静默接受。它们证明的是接口边界,不是 SELinux context 匹配规则。
9.2 活动列表测试
correct_lsm_list_modules 把 syscall 返回的 LSM ID 映射到名称,再与 /sys/kernel/security/lsm 文本逐项比较。这个测试直接反向验证“注册状态”和“用户可见模块列表”是否一致。
源码文件:kernel/common/tools/testing/selftests/lsm/lsm_list_modules_test.c
相关测试:correct_lsm_list_modules
count = lsm_list_modules(syscall_lsms, &size, 0);
ASSERT_LE(1, count);
cp = sysfs_lsms;
for (i = 0; i < count; i++) {
switch (syscall_lsms[i]) {
case LSM_ID_CAPABILITY:
name = "capability";
break;
case LSM_ID_SELINUX:
name = "selinux";
break;
default:
name = "INVALID";
break;
}
ASSERT_EQ(0, strncmp(cp, name, strlen(name)));
cp += strlen(name) + 1;
}测试没有要求模块列表必须固定为某个字符串;它根据当前内核返回值验证 ID/name 映射。因此不同 CONFIG_LSM 或 lsm= 顺序可以产生不同合法结果,文章中的静态模块清单不能替代设备检查。
9.3 UID测试
Android libcutils 单元测试验证 user id 与 app id 的数学合成,为理解应用 context 的 user 输入提供反向证据。
源码文件:system/core/libcutils/multiuser_test.cpp
相关测试:MultiuserTest.TestMerge
TEST(MultiuserTest, TestMerge) {
EXPECT_EQ(10000U, multiuser_get_uid(0, 10000));
EXPECT_EQ(1000000U, multiuser_get_uid(10, 0));
EXPECT_EQ(1010000U, multiuser_get_uid(10, 10000));
}该测试证明 seapp_contexts 看到的 Android user/UID 输入可以区分 user 0 与 user 10;它没有证明某个 app 一定匹配 untrusted_app,因为 seinfo、包名、target SDK 和 selector specificity 仍由进程启动链提供。
10. 源码导航
# 从 task SID 和 inode SID 开始,查找 context 的内核表示。
rg -n 'task_security_struct|inode_security_struct|cred_sid|security_context_to_sid|security_sid_to_context' \
kernel/common/security/selinux/include/objsec.h \
kernel/common/security/selinux/hooks.c \
kernel/common/security/selinux/ss/services.c确认 SID 的持有者后,再切换到文件标签的生产和生效路径:
# 查找文件路径如何映射到 context,以及 init 在何时 restorecon。
rg -n 'SelabelInitialize|SelabelLookupFileContext|selinux_android_restorecon|file_contexts' \
system/core/init/selabel.cpp \
system/core/init/selinux.cpp \
system/sepolicy/private/file_contexts如果问题只出现在应用进程,再检查 seapp selector 的优先级和 Zygote 写入点:
# 跟踪应用 seapp selector、优先级、neverallow 与 Zygote context 设置。
rg -n 'Input selectors|Precedence|neverallow|levelFrom|selinux_android_setcontext' \
system/sepolicy/private/seapp_contexts \
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp最后确认用户空间可见属性与内核通用 LSM 接口是否来自同一活动模块集合:
# 比较 proc 属性、LSM 通用属性和设备可见标签。
rg -n 'selinux_getprocattr|lsm_get_self_attr|procattr|securityfs/lsm' \
kernel/common/security/selinux/hooks.c \
kernel/common/security/lsm_syscalls.c \
kernel/common/tools/testing/selftests/lsm完成源码侧比对后,再用只读命令观察设备当前的主体、客体和活动模块状态:
# 设备侧同时观察主体、客体和活动模块;这些命令只读系统状态。
adb shell 'id -Z; cat /proc/self/attr/current'
adb shell 'cat /sys/kernel/security/lsm'
adb shell 'ps -AZ | grep -E "zygote|system_server"'
adb shell 'ls -lZ /system/bin/init /dev/socket/zygote'11. 闭环复述
- 从
u:r:system_server:s0开始,复述字符串如何对应 SID、task blob 和current_sid(),再指出哪些代码把它交给 AVC。 - 从
/system/bin/init的file_contexts条目开始,追到selabel_lookup()、restorecon()、execv()和 second-stage init domain,说明配置存在、对象已标注和进程已切换的区别。 - 给定一个普通应用的 UID、seinfo、包名和 target SDK,按
seapp_contextsprecedence 判断可能命中的 domain/type/levelFrom,并指出缺少哪个输入时无法下结论。 - 解释为什么
id -Z、ps -Z和ls -Z分别观察 task、进程列表和 inode/对象;再给出一个“标签正确但访问仍拒绝”的 class/permission 诊断路径。 - 使用
lsm_list_modules测试的EFAULT、EINVAL和 ID/name 对照断言,说明它验证的是框架 ABI 和活动模块状态,而不是某条 Android allow 规则。
安全上下文的核心不是四个字段本身,而是它们在生命周期中的交接:配置文件生产字符串,libselinux 把字符串映射为 SID,LSM blob 把 SID 附着到 task/inode,hook 再用 source SID、target SID、class 和 permission 做裁决。掌握这些交接点后,id -Z、seapp_contexts、restorecon 和 AVC 日志才会落回真实源码,而不是停留在标签格式的记忆。
