SELinux架构
本文面向已经读过 FirstStageInit、SELinux启动、ServiceManager启动 和 ZygoteInit.main 的读者。前置文章解决“进程何时启动”,本文继续回答“安全策略在何时成为可执行状态,以及一次访问怎样得到允许或拒绝”。
这是一篇架构入口,不展开所有 TE 语法、LSM hook 或 Treble 兼容规则。重点是建立四条可以沿源码追踪的主线:策略源码怎样变成设备产物,init 怎样把策略交给内核,进程与对象怎样获得安全上下文,以及 servicemanager 怎样把调用方、服务名和权限组合成一次裁决。读完后,你应能区分策略、标签、访问请求和审计结果的所有者,而不是把它们都笼统地称为“SELinux 配置”。
1. 安全边界
SELinux 的最小判断单位不是 Java 类或 Linux 文件路径,而是下面这组输入:
| 输入 | 含义 | 典型来源 |
|---|---|---|
| source context | 主体安全上下文 | 调用进程 SID、Binder calling SID |
| target context | 客体安全上下文 | inode 标签、服务名标签、属性标签 |
| object class | 客体类别 | file、binder、service_manager、property_service |
| permission | 请求动作 | read、write、find、add、set |
| policy state | 当前策略与 enforcing 状态 | init 加载到内核的二进制策略 |
安全上下文常写成 user:role:type:level,例如 u:r:system_server:s0。Android 策略中,最常决定访问关系的是 type;但 role、MLS/MCS level 仍属于完整上下文,不能在解析日志时丢掉。更重要的是,路径和服务名本身不是最终权限主体:它们先通过 file_contexts、service_contexts 或其他上下文数据库映射为 target context,策略再判断访问。
一次允许结果也不代表“SELinux 负责了全部安全”。Linux DAC、capability、Binder UID 检查、Android permission 和 SELinux 可以共同约束同一操作。本文只追踪 SELinux 所拥有的状态,不把其他检查合并成一个虚构的总开关。
2. 四层架构
Android SELinux 可以按状态所有权拆成四层:构建层生产策略与标签数据库;启动层选择并加载策略;标注层把名称或对象转换为安全上下文;裁决层消费 source、target、class 和 permission。
这四层有两个容易忽略的边界。第一,二进制策略和上下文数据库是不同产物:前者回答“某种上下文组合能否访问”,后者回答“某个名字或对象应该使用什么上下文”。第二,selinux_check_access() 既可由内核路径间接触发,也可由 servicemanager 这类用户空间组件主动查询;用户空间查询不等于绕过策略,它只是把特定名字空间的权限检查放在资源管理者处执行。
3. 策略产物
3.1 模块边界
Android 17 的策略构建不是一条 Makefile 文本替换命令。system/sepolicy/build/soong/policy.go 注册了三个不同职责的模块:se_policy_conf 聚合并预处理策略源,se_policy_cil 把 conf 编译成 CIL,se_policy_binary 再把 CIL 编译成内核可加载的二进制策略。
源码文件:system/sepolicy/build/soong/policy.go
相关位置:模块注册与工厂说明
func init() {
android.RegisterModuleType("se_policy_conf", policyConfFactory)
android.RegisterModuleType("se_policy_conf_defaults", policyConfDefaultFactory)
android.RegisterModuleType("se_policy_cil", policyCilFactory)
android.RegisterModuleType("se_policy_binary", policyBinaryFactory)
}
// se_policy_conf merges collection of policy files into a policy.conf file to be processed by
// checkpolicy.
func policyConfFactory() android.Module {
c := &policyConf{}
c.AddProperties(&c.properties)
initFlaggableModule(c)
android.InitAndroidArchModule(c, android.DeviceSupported, android.MultilibCommon)
android.InitDefaultableModule(c)
return c
}模块拆分让中间产物可以被单独检查、版本化和安装。policy.conf 仍保留 m4 展开后的策略语言,CIL 是模块化中间表示,binary policy 才是 init 最终交给内核的内容。把三者都称作“sepolicy 文件”会导致调试时看错产物。
3.2 conf生成
se_policy_conf 会先按策略文件种类排序,再在每个输入之间插入换行,最后把构建变体、Treble、ASAN、MLS 类别等条件作为 m4 宏传入。
源码文件:system/sepolicy/build/soong/policy.go
相关函数:policyConf.transformPolicyToConf
srcs := android.PathsForModuleSrc(ctx, c.properties.Srcs)
sort.SliceStable(srcs, func(x, y int) bool {
return findPolicyConfOrder(srcs[x].Base()) < findPolicyConfOrder(srcs[y].Base())
})
newlineFile := android.PathForModuleOut(ctx, "newline")
rule.Command().Text("echo").FlagWithOutput("> ", newlineFile)
rule.Temporary(newlineFile)
var srcsWithNewline android.Paths
for _, src := range srcs {
srcsWithNewline = append(srcsWithNewline, src, newlineFile)
}
rule.Command().Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Flag("--fatal-warnings").
FlagWithArg("-D mls_num_sens=", strconv.Itoa(MlsSens)).
FlagWithArg("-D mls_num_cats=", strconv.Itoa(c.mlsCats())).
FlagWithArg("-D target_arch=", ctx.DeviceConfig().DeviceArch()).
FlagWithArg("-D target_build_variant=", c.buildVariant(ctx)).
FlagWithArg("-D target_full_treble=", c.sepolicySplit(ctx)).
Inputs(srcsWithNewline).
Text("> ").Output(conf)输入顺序是构建状态的一部分,而不是排版偏好;文件缺少尾换行可能把相邻规则粘在一起,所以构建器显式插入 newline 临时文件。target_build_variant 等宏决定条件策略是否进入最终 conf,同一份 .te 在 user 与 userdebug 构建中不一定产生相同规则。
3.3 CIL与二进制
conf 到 CIL 使用 checkpolicy -C,CIL 到 binary policy 使用 secilc。两步都启用 MLS,且 secilc_check 默认会对中间 CIL 再做一次完整检查。
源码文件:system/sepolicy/build/soong/policy.go
相关函数:policyCil.compileConfToCil、policyBinary.GenerateAndroidBuildActions
checkpolicyCmd := rule.Command().BuiltTool("checkpolicy").
Flag("-C"). // Write CIL
Flag("-M"). // Enable MLS
Flag("-L"). // Line markers for allow rules
FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
FlagWithOutput("-o ", cil).
Input(conf)
if proptools.BoolDefault(c.properties.Secilc_check, true) {
rule.Command().BuiltTool("secilc").
Flag("-m").
FlagWithArg("-M ", "true").
Flag("-G").
FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
Text(cil.String()).
FlagWithArg("-o ", os.DevNull).
FlagWithArg("-f ", os.DevNull).
Flag("-v")
}真正生成二进制策略时,输入变为多个 CIL 文件,输出写到临时 binary;user 构建还会用 sepolicy-analyze permissive 检查不允许存在的 permissive domain。
secilcCmd := rule.Command().BuiltTool("secilc").
Flag("-m").
FlagWithArg("-M ", "true").
Flag("-G").
FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
Inputs(android.PathsForModuleSrc(ctx, c.properties.Srcs)).
FlagWithOutput("-o ", bin).
FlagWithArg("-f ", os.DevNull)
if !ctx.Config().Debuggable() {
permissiveDomains := pathForModuleOut(ctx, c.stem()+"_permissive")
cmd := rule.Command().BuiltTool("sepolicy-analyze").
Input(bin).
Text("permissive")
allowedDomains := c.properties.Permissive_domains_on_user_builds
if len(allowedDomains) != 0 {
cmd.Text("| { grep -Fxv")
for _, d := range allowedDomains {
cmd.FlagWithArg("-e ", proptools.ShellEscape(d))
}
cmd.Text(" || true; }")
}
cmd.Text(" > ").Output(permissiveDomains)
msg := `==========\n` +
`ERROR: permissive domains not allowed in user builds\n` +
`List of invalid domains:`
rule.Command().Text("if test").
FlagWithInput("-s ", permissiveDomains).
Text("; then echo").
Flag("-e").
Text(`"` + msg + `"`).
Text("&& cat ").
Input(permissiveDomains).
Text("; exit 1; fi")
}这里区分了两个失败时机:语法、符号或 neverallow 问题会在 checkpolicy/secilc 阶段失败;user 构建中的 permissive domain 即使策略能够编译,也会在额外分析阶段阻断产物。构建成功只表示策略内部一致,不表示设备上的每个对象都已获得正确标签。
3.4 上下文产物
file_contexts、service_contexts、property_contexts 等走另一套 Soong 模块。它们同样经过 m4,但 file contexts 还可去注释并调用 fc_sort,测试模块会根据上下文类型选择 checkfc 或 property_info_checker。
源码文件:system/sepolicy/build/soong/selinux_contexts.go
相关函数:selinuxContextsModule.buildGeneralContexts
rule.Command().
Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Text("--fatal-warnings -s").
FlagForEachArg("-D", ctx.DeviceConfig().SepolicyM4Defs()).
Flags(flagsToM4Macros(flags)).
Inputs(inputsWithNewline).
FlagWithOutput("> ", builtContext)
if proptools.Bool(m.properties.Remove_comment) {
rule.Command().
Text("sed -e 's/#.*$//' -e '/^$/d'").
Input(builtContext).
FlagWithOutput("> ", remove_comment_output)
builtContext = remove_comment_output
}
if proptools.Bool(m.properties.Fc_sort) {
rule.Command().
Tool(ctx.Config().HostToolPath(ctx, "fc_sort")).
FlagWithInput("-i ", builtContext).
FlagWithOutput("-o ", sorted_output)
builtContext = sorted_output
}这解释了为什么修改上下文源码后不能只观察原文件:真正消费者读取的是经过分区合并、宏展开、去注释和排序后的安装产物。规则顺序、正则优先级或错误的分区归属都可能让“源码看起来正确”的标签没有生效。
4. 启动加载
4.1 三次init
从控制流看,/system/bin/init 会以不同参数执行三段职责:first stage 完成早期挂载;随后 exec 自己进入 selinux_setup;策略加载并恢复 init 可执行文件标签后,再 exec 自己进入 second_stage。
源码文件:system/core/init/first_stage_init.cpp
相关位置:FirstStageMain 尾部
setenv(kEnvFirstStageStartedAt,
std::to_string(start_time.time_since_epoch().count()).c_str(), 1);
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args));
// 本文注:execv 返回只可能是失败,init 会进入 fatal 路径。
PLOG(FATAL) << "execv(\"" << path << "\") failed";main() 根据参数分派到不同入口,而不是依赖静态全局变量记住“当前第几阶段”。
源码文件:system/core/init/main.cpp
相关函数:main
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();
return SubcontextMain(argc, argv, &function_map);
}
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}execv() 保留 pid,却替换进程映像。因而“init 重启了三次”并不准确:它是同一 PID 1 在不同可执行阶段之间切换,SELinux domain 转换发生在已加载策略和重新执行带标签文件的共同作用下。
4.2 策略选择
split policy 设备优先寻找 vendor 或 odm 上的预编译策略,但只有 system、system_ext、product 对应 hash 全部匹配时才接受。缺失或不匹配不会直接降级为无策略启动,而是转入运行时 secilc 编译。
源码文件:system/core/init/selinux.cpp
相关函数:FindPrecompiledSplitPolicy
static constexpr const char vendor_precompiled_sepolicy[] =
"/vendor/etc/selinux/precompiled_sepolicy";
static constexpr const char odm_precompiled_sepolicy[] =
"/odm/etc/selinux/precompiled_sepolicy";
if (access(odm_precompiled_sepolicy, R_OK) == 0) {
precompiled_sepolicy = odm_precompiled_sepolicy;
} else if (access(vendor_precompiled_sepolicy, R_OK) == 0) {
precompiled_sepolicy = vendor_precompiled_sepolicy;
} else {
return ErrnoError() << "No precompiled sepolicy at "
<< vendor_precompiled_sepolicy;
}
std::vector<std::pair<std::string, std::string>> sepolicy_hashes{
{"/system/etc/selinux/plat_sepolicy_and_mapping.sha256",
precompiled_sepolicy + ".plat_sepolicy_and_mapping.sha256"},
{"/system_ext/etc/selinux/system_ext_sepolicy_and_mapping.sha256",
precompiled_sepolicy + ".system_ext_sepolicy_and_mapping.sha256"},
{"/product/etc/selinux/product_sepolicy_and_mapping.sha256",
precompiled_sepolicy + ".product_sepolicy_and_mapping.sha256"},
};
for (const auto& [actual_id_path, precompiled_id_path] : sepolicy_hashes) {
// ... 读取两侧首行。
if (actual_id.empty() || actual_id != precompiled_id) {
return Error() << actual_id_path << " and "
<< precompiled_id_path << " differ";
}
}hash 是“这份预编译 binary 是否对应当前 platform policy”的身份校验,不是策略内容本身。userdebug 解锁设备若选择 debug policy,也会跳过 vendor 预编译策略,因为 debug CIL 必须参与重新编译。
预编译策略不可用时,init 在 /dev 创建临时输出,并组合 platform、mapping、vendor、可选 odm/system_ext/product CIL 运行 secilc。
源码文件:system/core/init/selinux.cpp
相关函数:OpenSplitPolicy
if (!use_userdebug_policy) {
if (auto res = FindPrecompiledSplitPolicy(); res.ok()) {
unique_fd fd(open(res->c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
if (fd != -1) {
policy_file->fd = std::move(fd);
policy_file->path = std::move(*res);
return true;
}
} else {
LOG(INFO) << res.error();
}
}
LOG(INFO) << "Compiling SELinux policy";
char compiled_sepolicy[] = "/dev/sepolicy.XXXXXX";
unique_fd compiled_sepolicy_fd(mkostemp(compiled_sepolicy, O_CLOEXEC));
if (compiled_sepolicy_fd < 0) {
PLOG(ERROR) << "Failed to create temporary file " << compiled_sepolicy;
return false;
}
std::vector<const char*> compile_args {
"/system/bin/secilc",
use_userdebug_policy ? *userdebug_plat_sepolicy : plat_policy_cil_file,
"-m", "-M", "true", "-G", "-N", "-v",
"-c", version_as_string.c_str(),
plat_mapping_file.c_str(),
"-o", compiled_sepolicy,
"-f", "/sys/fs/selinux/null",
};
// ... 追加 system_ext、product、vendor、odm 与 genfs CIL。
if (ForkExecveAndWaitForCompletion(
compile_args[0], (char**)compile_args.data()) != 0) {
unlink(compiled_sepolicy);
return false;
}注意失败策略:缺少预编译产物可以回退现场编译;但缺少必须的 vendor CIL、mapping 版本、临时文件或 secilc 编译失败会让 OpenSplitPolicy() 返回 false,随后 ReadPolicy() 触发 fatal。系统不会在没有有效策略的情况下继续 second stage。
4.3 加载与切换
ReadPolicy() 只负责获得字节串,LoadSelinuxPolicy() 才通过 security_load_policy() 把它交给内核。之后 init 同步 enforcing 状态、恢复 /system/bin/init 标签,再 exec 进入 second stage。
源码文件:system/core/init/selinux.cpp
相关函数:ReadPolicy、LoadSelinuxPolicy、SelinuxSetEnforcement
void ReadPolicy(std::string* policy) {
PolicyFile policy_file;
bool ok = IsSplitPolicyDevice() ? OpenSplitPolicy(&policy_file)
: OpenMonolithicPolicy(&policy_file);
if (!ok) {
LOG(FATAL) << "Unable to open SELinux policy";
}
if (!android::base::ReadFdToString(policy_file.fd, policy)) {
PLOG(FATAL) << "Failed to read policy file: " << policy_file.path;
}
}
static void LoadSelinuxPolicy(std::string& policy) {
LOG(INFO) << "Loading SELinux policy";
set_selinuxmnt("/sys/fs/selinux");
if (security_load_policy(policy.data(), policy.size()) < 0) {
PLOG(FATAL) << "SELinux: Could not load policy";
}
}
void SelinuxSetEnforcement() {
bool kernel_enforcing = (security_getenforce() == 1);
bool is_enforcing = IsEnforcing();
if (kernel_enforcing != is_enforcing) {
if (security_setenforce(is_enforcing)) {
PLOG(FATAL) << "security_setenforce("
<< (is_enforcing ? "true" : "false")
<< ") failed";
}
}
}security_load_policy() 成功表示内核已有策略数据库,但 init 还处于早期 kernel domain。SetupSelinux() 的剩余步骤决定标签和执行域真正开始生效。
源码文件:system/core/init/selinux.cpp
相关函数:SetupSelinux
SelinuxSetupKernelLogging();
bool use_overlays = EarlySetupOverlays();
if (IsMicrodroid()) {
LoadSelinuxPolicyMicrodroid();
} else {
LoadSelinuxPolicyAndroid();
}
SelinuxSetEnforcement();
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}
if (use_overlays) SetupOverlays();
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
PLOG(FATAL) << "execv(\"" << path << "\") failed";正常完成点不是 LoadSelinuxPolicy() 返回,而是 second-stage init 已通过带 init_exec 标签的文件重新执行。restorecon 失败、策略加载失败、enforcing 切换失败和第二次 execv 失败都是启动致命错误;它们没有“记录 warning 后继续”的降级路径。
图中的预编译与现场编译是选择分支,最终都必须汇合成同一 binary policy 加载路径。Microdroid 则使用独立的 microdroid_precompiled_sepolicy 路径,不经过完整 Android split policy 组合,不能把普通设备的分区列表外推到 Microdroid。
5. 标签来源
策略只认识安全上下文。Android 需要多个名字空间适配器,把文件路径、应用身份、Binder 服务名和属性名分别转换成上下文。
| 对象 | 标签数据库 | 写入或查询者 | 标签落点 |
|---|---|---|---|
| 文件与目录 | file_contexts | init、ueventd、restorecon | inode xattr 或内核对象标签 |
| 应用进程 | seapp_contexts + seinfo | Zygote/libselinux | 当前进程 context |
| system_server | 固定标签 + Zygote specialization | Zygote | u:r:system_server:s0 |
| Binder 服务名 | service_contexts | servicemanager | 名称查询得到的 target context |
| 系统属性 | property_contexts | property service | property info 映射结果 |
5.1 文件标签
下面几条真实规则说明路径只是匹配键,右侧上下文才进入策略判断。
源码文件:system/sepolicy/private/file_contexts
相关规则:init、Zygote、servicemanager 与 socket 路径
/dev/socket(/.*)? u:object_r:socket_device:s0
/dev/socket/property_service u:object_r:property_socket:s0
/dev/socket/zygote u:object_r:zygote_socket:s0
/dev/socket/zygote_secondary u:object_r:zygote_socket:s0
/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:s0init 缓存 file contexts handle,并把同一 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;
}这里存在两个状态:内存中的路径到 context 映射,以及对象当前实际标签。selabel_lookup() 只查询期望值,restorecon 才负责比较并修复对象标签。修改 file_contexts 而不重新生成产物、不执行合适的 restorecon,磁盘上已有对象不会自动重标。
5.2 进程标签
应用进程标签由 Zygote specialization 设置。普通应用使用 uid、seInfo 和进程名参与 selinux_android_setcontext();system_server 随后还显式切换到固定标签。
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
相关位置:进程 specialization 的 SELinux 设置
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) {
env->CallStaticVoidMethod(
gZygoteClass, gCallPostForkSystemServerHooks, runtime_flags);
if (env->ExceptionCheck()) {
fail_fn("Error calling post fork system server hooks.");
}
static const char* kSystemServerLabel = "u:r:system_server:s0";
if (selinux_android_setcon(kSystemServerLabel) != 0) {
fail_fn(CREATE_ERROR("selinux_android_setcon(%s)", kSystemServerLabel));
}
}设置失败会调用 fail_fn 终止子进程初始化,而不是让应用带着 Zygote domain 继续运行。应用映射的具体规则来自 seapp_contexts,并且会考虑平台签名产生的 seinfo、是否 privileged、target SDK、用户名和包名等条件。
源码文件:system/sepolicy/private/seapp_contexts
相关规则:平台应用、特权应用与普通应用
user=system seinfo=platform domain=system_app type=system_app_data_file
user=_app minTargetSdkVersion=37 seinfo=platform domain=platform_app type=app_data_file levelFrom=user
user=_app minTargetSdkVersion=37 isPrivApp=true domain=priv_app type=privapp_data_file levelFrom=user
user=_app minTargetSdkVersion=37 domain=untrusted_app type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=34 domain=untrusted_app_34 type=app_data_file levelFrom=all这组规则也说明“所有第三方应用都是同一个 untrusted_app domain”并不精确。target SDK 和其他输入可以选择不同兼容域;后续策略专题必须先确定最终 domain,再判断 allow 规则是否适用。
5.3 名称标签
Binder 服务和系统属性没有可直接读取的 inode 标签,它们各自用名称数据库得到 target context。
源码文件:system/sepolicy/private/service_contexts
相关规则:核心 Binder 服务
activity u:object_r:activity_service:s0
package u:object_r:package_service:s0
permission u:object_r:permission_service:s0
power u:object_r:power_service:s0
user u:object_r:user_service:s0
window u:object_r:window_service:s0源码文件:system/sepolicy/private/property_contexts
相关规则:电源控制、系统属性与 ctl 命令
sys.powerctl u:object_r:powerctl_prop:s0
persist.sys. u:object_r:system_prop:s0
persist.sys.safemode u:object_r:safemode_prop:s0
ro.build.fingerprint u:object_r:fingerprint_prop:s0 exact string
ctl.start$ u:object_r:ctl_start_prop:s0
ctl.stop$ u:object_r:ctl_stop_prop:s0
ctl.restart$ u:object_r:ctl_restart_prop:s0服务名和属性名前缀的匹配规则不同,消费者也不同。把某个名字添加到 service_contexts 只解决 servicemanager target context 查询,不会自动创建 Binder 服务;把属性加入 property_contexts 也不会赋予任何 domain set 权限,还需要策略规则允许对应 property_service 操作。
6. 访问裁决
6.1 服务注册
servicemanager 是理解 Android 用户空间 SELinux 检查的最佳入口。它先取得调用者 SID,再把服务名查成 target context,最后以 service_manager class 和 add、find 或 list permission 查询策略。
源码文件:frameworks/native/cmds/servicemanager/Access.cpp
相关函数:Access.getCallingContext
Access::CallingContext Access::getCallingContext() {
#ifdef __ANDROID__
IPCThreadState* ipc = IPCThreadState::self();
const char* callingSid = ipc->getCallingSid();
pid_t callingPid = ipc->getCallingPid();
return CallingContext {
.debugPid = callingPid,
.uid = ipc->getCallingUid(),
.sid = callingSid
? std::string(callingSid)
: getPidcon(callingPid),
};
#else
return CallingContext();
#endif
}Binder 驱动若提供 calling SID,servicemanager 直接消费;否则才按 pid 调用 getpidcon()。pid 和 uid 主要用于调试与其他访问控制,SELinux source context 使用 sid 字段。
服务名称标签 handle 会在策略状态更新后关闭并重建,避免长期缓存旧 mapping。
源码文件:frameworks/native/cmds/servicemanager/Access.cpp
相关函数:getSehandle
static struct selabel_handle* getSehandle() {
static struct selabel_handle* gSehandle = nullptr;
if (gSehandle != nullptr && selinux_status_updated()) {
selabel_close(gSehandle);
gSehandle = nullptr;
}
if (gSehandle == nullptr) {
gSehandle = kIsVendor
? selinux_android_vendor_service_context_handle()
: selinux_android_service_context_handle();
}
CHECK(gSehandle != nullptr);
return gSehandle;
}真正的名称查询和裁决位于下面两个函数。
源码文件:frameworks/native/cmds/servicemanager/Access.cpp
相关函数:actionAllowedFromLookup、actionAllowed
bool Access::actionAllowedFromLookup(
const CallingContext& sctx,
const std::string& name, const char* perm) {
#ifdef __ANDROID__
char* tctx = nullptr;
if (selabel_lookup(getSehandle(), &tctx, name.c_str(),
SELABEL_CTX_ANDROID_SERVICE) != 0) {
LOG(ERROR) << "SELinux: No match for " << name
<< " in service_contexts.\n";
return false;
}
bool allowed = actionAllowed(sctx, tctx, perm, name);
freecon(tctx);
return allowed;
#else
return true;
#endif
}
bool Access::actionAllowed(
const CallingContext& sctx, const char* tctx,
const char* perm, const std::string& tname) {
#ifdef __ANDROID__
const char* tclass = "service_manager";
AuditCallbackData data = {
.context = &sctx,
.tname = &tname,
};
return 0 == selinux_check_access(
sctx.sid.c_str(), tctx, tclass, perm,
reinterpret_cast<void*>(&data));
#else
return true;
#endif
}这条主线有两个独立拒绝点:服务名在 service_contexts 中没有匹配项时直接返回 false;找到标签但策略不允许 <source, target, service_manager, permission> 时,selinux_check_access() 返回非零。给 domain 增加 allow 无法修复“名称未标注”,给名称增加标签也无法修复“策略未授权”。
这也是 SELinux与Binder 与本文的边界:Binder 专题继续分析驱动事务和线程身份,本文只建立服务名称空间的 SELinux source、target、class、permission 组合。
6.2 属性设置
property service 使用相同四元组模型,但 target context 来自 property info 映射,class 是 property_service,permission 是 set。
源码文件:system/core/init/property_service.cpp
相关函数:CheckMacPerms
static bool CheckMacPerms(
const std::string& name, const char* target_context,
const char* source_context, const ucred& cr) {
if (!target_context || !source_context) {
return false;
}
PropertyAuditData audit_data;
audit_data.name = name.c_str();
audit_data.cr = &cr;
auto lock = std::lock_guard{selinux_check_access_lock};
return selinux_check_access(
source_context, target_context,
"property_service", "set", &audit_data) == 0;
}这里的 mutex 保护 selinux_check_access() 调用及其 audit callback 数据组合。即使两个属性映射到同一 target type,它们仍可通过 audit data 保留具体属性名与调用凭据,便于定位拒绝来源。
property service 在启动时按分区加载多份 contexts,构造统一的 property info trie。读取顺序本身不能被理解为简单“后者覆盖前者”,因为构建期还会执行命名空间和 owner 检查。
源码文件:system/core/init/property_service.cpp
相关位置:property contexts 文件加载
if (access("/system/etc/selinux/plat_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile(
"/system/etc/selinux/plat_property_contexts", &property_infos);
if (access("/system_ext/etc/selinux/system_ext_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile(
"/system_ext/etc/selinux/system_ext_property_contexts",
&property_infos);
}
if (access("/vendor/etc/selinux/vendor_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile(
"/vendor/etc/selinux/vendor_property_contexts",
&property_infos);
}
if (access("/product/etc/selinux/product_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile(
"/product/etc/selinux/product_property_contexts",
&property_infos);
}
if (access("/odm/etc/selinux/odm_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile(
"/odm/etc/selinux/odm_property_contexts", &property_infos);
}
}这段代码展示了 Android 分区边界如何进入运行期:platform、system_ext、vendor、product 和 odm 各自提供标签输入,最终由 property service 持有查询结构。Treble 设备上的策略兼容不只是 binary policy 能否编译,还包括这些名称数据库能否保持清晰的所有权。
7. 状态与失败
7.1 Enforcing状态
Android 17 的 user 构建不会因为普通属性值而进入 permissive。ALLOW_PERMISSIVE_SELINUX 关闭时,IsEnforcing() 固定返回 true;只有允许 permissive 的构建才读取启动属性决定状态。
源码文件:system/core/init/selinux.cpp
相关函数:IsEnforcing
bool IsEnforcing() {
if (ALLOW_PERMISSIVE_SELINUX) {
return StatusFromProperty() == SELINUX_ENFORCING;
}
return true;
}因此 setenforce 0 不是所有设备上的普遍调试入口。即使设备允许切换,permissive 也只改变拒绝是否阻断操作,策略查询与 AVC 日志仍然有意义;它不会修复错误标签,也不会替代最终 enforcing 环境验证。
7.2 审计路径
init 为 libselinux 安装日志 callback。AVC 类型日志会通过 audit netlink 发送,其余消息进入 Android kernel logger。
源码文件:system/core/init/selinux.cpp
相关函数:SelinuxKlogCallback、SelinuxSetupKernelLogging
int SelinuxKlogCallback(int type, const char* fmt, ...) {
android::base::LogSeverity severity = android::base::ERROR;
if (type == SELINUX_WARNING) {
severity = android::base::WARNING;
} else if (type == SELINUX_INFO) {
severity = android::base::INFO;
}
// ... 格式化并移除末尾换行。
if (type == SELINUX_AVC) {
SelinuxAvcLog(buf);
} else {
android::base::KernelLogger(
android::base::MAIN, severity,
"selinux", nullptr, 0, buf);
}
return 0;
}
void SelinuxSetupKernelLogging() {
selinux_callback cb;
cb.func_log = SelinuxKlogCallback;
selinux_set_callback(SELINUX_CB_LOG, cb);
}审计日志是失败消费者,不是策略 owner。日志中的 scontext、tcontext、tclass 和 { permission } 应反向映射到本文的四个裁决输入;只根据路径猜一条 allow,会遗漏名称未标注、domain 选错、class 选错或其他安全层先拒绝等情况。
7.3 失败矩阵
| 阶段 | 失败条件 | 当前 owner | 结果 | 恢复方向 |
|---|---|---|---|---|
| m4/conf | 宏、输入顺序或语法错误 | Soong 构建规则 | 构建失败 | 查看展开后的 policy.conf |
| CIL/binary | 符号、neverallow、版本或 permissive domain 违规 | checkpolicy、secilc、sepolicy-analyze | 构建失败 | 修正规则或分区接口,不能跳过检查 |
| context 构建 | 正则、类型、namespace 或 owner 错误 | checkfc、property_info_checker | 对应 contexts 模块失败 | 修复标签源或类型声明 |
| 预编译选择 | hash 不匹配 | init OpenSplitPolicy() | 回退现场编译 | 检查 system/vendor 产物是否来自同一组合 |
| 现场编译 | CIL 缺失、mapping 错误、secilc 失败 | init | 启动 fatal | 修复分区策略与版本映射 |
| 策略加载 | security_load_policy() 失败 | init 与内核 | 启动 fatal | 检查 binary policy 与内核能力 |
| 对象标注 | lookup 或 restorecon 失败 | init、Zygote、资源管理者 | 启动失败、子进程失败或对象保持错误标签 | 先修上下文映射,再重标对象 |
| 运行期裁决 | 名称无标签 | servicemanager/property service | 直接拒绝 | 补正确 contexts 条目并重新生成产物 |
| 运行期裁决 | 策略无权限 | 已加载策略 | enforcing 下拒绝并审计 | 从四元组定位最小授权或修正调用设计 |
这张表给出一个实用顺序:先问失败发生在“策略生成”“策略加载”“标签解析”还是“访问裁决”,再选择工具。audit2allow 只能处理最后一类中的部分情况,无法修复前面的构建、版本、加载和名称映射错误。
8. 测试约束
8.1 构建条件测试
Soong 单元测试构造多个 se_flags 模块,并把 product build flags 转成有序 m4 宏。缺失的 flag 不应被导出。
源码文件:system/sepolicy/build/soong/selinux_test.go
相关测试:TestFlagCollector
buildFlags := make(map[string]string)
buildFlags["RELEASE_FLAGS_BAR"] = "true"
buildFlags["RELEASE_FLAGS_FOO1"] = "false"
// "RELEASE_FLAGS_FOO2" is missing
buildFlags["RELEASE_AVF_ENABLE_DEVICE_ASSIGNMENT"] = "true"
variables.BuildFlags = buildFlags
// ... fixture 中声明三个 se_flags 来源并运行 Soong 测试上下文。
actual := flagsToM4Macros(collectorData.BuildFlags)
expected := []string{
"-D target_flag_RELEASE_AVF_ENABLE_DEVICE_ASSIGNMENT=true",
"-D target_flag_RELEASE_FLAGS_BAR=true",
"-D target_flag_RELEASE_FLAGS_FOO1=false",
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("M4 macros were not exported correctly"+
"\nactual: %v"+
"\nexpected: %v",
actual,
expected,
)
}测试输入包含 true、false 和缺失三种 flag,关键断言是实际 m4 参数与有序期望数组完全一致。它验证条件构建输入不会把不存在的 flag 当成默认值输出;但它没有编译完整策略,也没有证明某条条件 TE 规则最终允许或拒绝访问。
8.2 上下文测试
APEX sepolicy 测试加载真实 precompiled_sepolicy,再给检查器输入不同 file_contexts 行。合法四元组应无错误,缺少 level、使用未知 type 或把可执行文件标成普通 vendor file 必须报错。
源码文件:system/sepolicy/tests/apex_sepolicy_tests_test.py
相关测试:test_parse_lines、test_unknown_label、test_binaries
def assert_ok(self, line: str):
errors = apex.check_line(self.pol, line, apex.all_rules)
self.assertEqual(errors, [], "Should be no errors")
def assert_error(self, line: str, expected_error: str):
pattern = re.compile(expected_error)
errors = apex.check_line(self.pol, line, apex.all_rules)
for err in errors:
if re.search(pattern, err):
return
self.fail(f"Expected error '{expected_error}' is not found in {errors}")
def test_parse_lines(self):
self.assert_error('./path1 invalid_contexts',
r'Error: invalid file_contexts: .*')
self.assert_error('./path1 u:object_r:vendor_file',
r'Error: invalid file_contexts: .*')
self.assert_ok('./path1 u:object_r:vendor_file:s0')
def test_unknown_label(self):
self.assert_error('./bin/hw/foo u:object_r:foo_exec:s0',
r'Error: \./bin/hw/foo: tcontext\(foo_exec\) is unknown')
def test_binaries(self):
self.assert_ok('./bin/init u:object_r:init_exec:s0')
self.assert_error('./bin/hw/svc u:object_r:vendor_file:s0',
r"Error: .*svc: can't be labelled as 'vendor_file'")这组测试同时约束格式、类型是否存在和路径用途。它没有把文件真正安装到设备或执行 restorecon,因此不能证明运行中 inode 已获得预期标签;设备验证仍要读取 ls -Z 或 stat 结果。
8.3 分区归属测试
sepolicy_test 构建目标把五个分区的 file contexts 与 precompiled_sepolicy 一起交给 sepolicy_tests.py。其中 coredomain 检查根据可执行路径推导 domain 来源:从 system 启动的 domain 必须有 coredomain,从 vendor 启动的 domain 不得拥有它。
源码文件:system/sepolicy/tests/sepolicy_tests.py
相关测试:TestCoredomainViolations
violators = []
for d in test_policy.alldomains:
domain = test_policy.alldomains[d]
if domain.fromSystem and "coredomain" not in domain.attributes:
violators.append(d)
if len(violators) > 0:
ret += "The following domain(s) must be associated with the "
ret += '"coredomain" attribute because they are executed off of '
ret += "/system:\n"
ret += " ".join(str(x) for x in sorted(violators)) + "\n"
violators = []
for d in test_policy.alldomains:
domain = test_policy.alldomains[d]
if domain.fromVendor and "coredomain" in domain.attributes:
violators.append(d)
if len(violators) > 0:
ret += "The following domains must not be associated with the "
ret += '"coredomain" attribute because they are executed off of '
ret += "/vendor or /system/vendor:\n"测试输入不是孤立的一条规则,而是编译策略、类型属性和各分区 file contexts 的组合。它验证 domain 属性与可执行文件分区归属一致;它没有证明 domain 的每一项运行期权限都符合业务需求,也不覆盖厂商在设备外部追加但未进入构建输入的文件。
9. 源码导航
以下命令在 AOSP 根目录执行。先从产物 owner 定位,再进入具体规则,避免只在 .te 文件中搜索一个权限词。
# 查找策略从 conf、CIL 到 binary 的三个 Soong 模块及真实工具参数。
rg -n 'se_policy_conf|se_policy_cil|se_policy_binary|checkpolicy|secilc' \
system/sepolicy/build/soong/policy.go \
system/sepolicy/Android.bp如果问题属于启动阶段,继续查看预编译选择、现场编译和内核加载的汇合点:
# 跟踪 first stage 到 selinux_setup,再到 second stage 的控制流。
rg -n 'selinux_setup|SetupSelinux|OpenSplitPolicy|ReadPolicy|security_load_policy|second_stage' \
system/core/init/first_stage_init.cpp \
system/core/init/main.cpp \
system/core/init/selinux.cpp若现象是对象标签错误,应先确认由哪一种 contexts 数据库负责:
# 同时搜索文件、应用、Binder 服务和属性的标签来源。
rg -n 'init_exec|untrusted_app|activity_service|powerctl_prop' \
system/sepolicy/private/file_contexts \
system/sepolicy/private/seapp_contexts \
system/sepolicy/private/service_contexts \
system/sepolicy/private/property_contexts运行期拒绝要从资源管理者的实际 class 和 permission 反查:
# 对比 servicemanager 与 property service 怎样调用 selinux_check_access。
rg -n 'getCallingContext|actionAllowedFromLookup|selinux_check_access|CheckMacPerms' \
frameworks/native/cmds/servicemanager/Access.cpp \
system/core/init/property_service.cpp完成静态阅读后,可构建策略检查目标和运行 host 单元测试:
# sepolicy_test 消费编译策略和五个分区的 file_contexts。
m sepolicy_test
# 两个 host 测试分别检查 APEX 标签规则和路径前缀匹配。
atest apex_sepolicy_tests_test policy_test设备侧只读检查用于确认当前运行状态和实际标签,不应直接修改 enforcing 状态:
# 查看全局模式、当前 shell domain、关键进程和文件标签。
adb shell getenforce
adb shell id -Z
adb shell ps -AZ | rg 'init|servicemanager|zygote|system_server'
adb shell ls -lZ /system/bin/init /system/bin/servicemanager /dev/socket/zygote
# 提取拒绝日志中的四元组输入。
adb shell su 0 dmesg | rg 'avc:.*denied'getenforce 只能说明拒绝是否阻断操作,id -Z 和 ls -Z 只能说明当前标签;只有把标签、class、permission 与已加载策略结合起来,才能解释一次访问结果。
10. 闭环复述
- 从一个
.te或宏输入开始,复述它怎样经过 m4、checkpolicy、CIL、secilc变成 init 可读取的 binary policy,并指出 user 构建的 permissive domain 在哪一步被阻断。 - 从
FirstStageMain的execv(..., "selinux_setup")开始,追到预编译 hash 校验、现场编译回退、security_load_policy()、restorecon("/system/bin/init")和 second-stage exec,说明每个失败点为何不能继续启动。 - 选择 Binder 服务名
activity,从service_contexts找到 target context,再从 servicemanager 的 calling SID、service_managerclass 和add/findpermission 组成完整裁决输入。分别解释“查不到标签”和“策略拒绝”的修复方向。 - 给定一条包含
scontext、tcontext、tclass和 permission 的 denial,先确认主体 domain 的来源和客体标签数据库,再检查策略;不要在确认 owner 之前生成 allow 规则。
掌握这四条路径后,Android SELinux 不再是一堆分散的 .te 和 contexts 文件,而是一套有明确状态交接的系统:构建层生产策略与映射,init 把策略变成内核运行状态,标注组件把 Android 名字空间转换为安全上下文,资源管理者和内核再消费四元组完成裁决。后续阅读任何 domain、type 或 denial,都应先把它放回这四层中的具体位置。
