Skip to content

SELinux架构

从策略构建、init 加载、对象标注和访问裁决四条主线理解 Android SELinux 的真实架构。

基于android-17.0.0_r1
AndroidSELinuxsepolicyinitservicemanager源码阅读

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

相关位置:模块注册与工厂说明

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

go
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

go
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。

go
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

go
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 尾部

cpp
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

cpp
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

cpp
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

cpp
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

cpp
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

cpp
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_contextsinit、ueventd、restoreconinode xattr 或内核对象标签
应用进程seapp_contexts + seinfoZygote/libselinux当前进程 context
system_server固定标签 + Zygote specializationZygoteu:r:system_server:s0
Binder 服务名service_contextsservicemanager名称查询得到的 target context
系统属性property_contextsproperty serviceproperty info 映射结果

5.1 文件标签 ​

下面几条真实规则说明路径只是匹配键,右侧上下文才进入策略判断。

源码文件:system/sepolicy/private/file_contexts

相关规则:init、Zygote、servicemanager 与 socket 路径

text
/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:s0

init 缓存 file contexts handle,并把同一 handle 提供给 restorecon,避免重复解析数据库。

源码文件:system/core/init/selabel.cpp

相关函数:SelabelInitialize、SelabelLookupFileContext

cpp
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 设置

cpp
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

相关规则:平台应用、特权应用与普通应用

text
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 服务

text
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 命令

text
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

cpp
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

cpp
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

cpp
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

cpp
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 文件加载

cpp
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

cpp
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

cpp
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

go
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

python
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

python
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 文件中搜索一个权限词。

bash
# 查找策略从 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

如果问题属于启动阶段,继续查看预编译选择、现场编译和内核加载的汇合点:

bash
# 跟踪 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 数据库负责:

bash
# 同时搜索文件、应用、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 反查:

bash
# 对比 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 单元测试:

bash
# sepolicy_test 消费编译策略和五个分区的 file_contexts。
m sepolicy_test

# 两个 host 测试分别检查 APEX 标签规则和路径前缀匹配。
atest apex_sepolicy_tests_test policy_test

设备侧只读检查用于确认当前运行状态和实际标签,不应直接修改 enforcing 状态:

bash
# 查看全局模式、当前 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. 闭环复述 ​

  1. 从一个 .te 或宏输入开始,复述它怎样经过 m4、checkpolicy、CIL、secilc 变成 init 可读取的 binary policy,并指出 user 构建的 permissive domain 在哪一步被阻断。
  2. 从 FirstStageMain 的 execv(..., "selinux_setup") 开始,追到预编译 hash 校验、现场编译回退、security_load_policy()、restorecon("/system/bin/init") 和 second-stage exec,说明每个失败点为何不能继续启动。
  3. 选择 Binder 服务名 activity,从 service_contexts 找到 target context,再从 servicemanager 的 calling SID、service_manager class 和 add/find permission 组成完整裁决输入。分别解释“查不到标签”和“策略拒绝”的修复方向。
  4. 给定一条包含 scontext、tcontext、tclass 和 permission 的 denial,先确认主体 domain 的来源和客体标签数据库,再检查策略;不要在确认 owner 之前生成 allow 规则。

掌握这四条路径后,Android SELinux 不再是一堆分散的 .te 和 contexts 文件,而是一套有明确状态交接的系统:构建层生产策略与映射,init 把策略变成内核运行状态,标注组件把 Android 名字空间转换为安全上下文,资源管理者和内核再消费四元组完成裁决。后续阅读任何 domain、type 或 denial,都应先把它放回这四层中的具体位置。