Skip to content

APEX 与 SELinux

解析 APEX payload 标签、apexd 权限、压缩容器和策略测试,并说明 Android 不从 APEX 动态追加 TE/CIL 规则。

基于android-17.0.0_r1
AndroidSELinuxAPEXapexdfile_contexts源码阅读

APEX 与 SELinux ​

本文面向已经理解 file_contexts、内核加载流程 和 Treble 与 sepolicy 的读者。计划题目中的“动态策略”容易产生误解,因此先给出 Android 17 的源码结论:APEX 更新可以动态替换模块 payload,但不能在启动后向内核追加新的 TE/CIL 规则。APEX 中的可执行 type、domain、allow 和 neverallow 必须已经进入设备加载的 platform/vendor policy;APEX 自带的 SELinux 数据主要是 payload 内文件的 file_contexts。

这一区分决定了模块更新边界。如果新 APEX 只替换已有 type 标记的二进制,更新可沿既有 policy 工作;如果新版本需要一个从未存在的 exec type、domain 或 service type,仅更新 APEX 文件不够,必须配套 system sepolicy 更新。压缩 .capex 也只改变容器存储与解压,不会携带一个独立 policydb。

1. 两种动态性 ​

1.1 payload动态更新 ​

APEX 可以更新 /apex/<name> 下的文件内容和版本,apexd 负责验证、loop/device-mapper、挂载、回滚数据等资源。payload 文件标签来自 APEX image 构建时写入的 file_contexts;消费者在挂载后看到已经带标签的文件系统对象。

1.2 加载边界 ​

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

cpp
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",
};

if (!system_ext_policy_cil_file.empty())
    compile_args.push_back(system_ext_policy_cil_file.c_str());
if (!product_policy_cil_file.empty())
    compile_args.push_back(product_policy_cil_file.c_str());
if (!vendor_policy_cil_file.empty())
    compile_args.push_back(vendor_policy_cil_file.c_str());
if (!odm_policy_cil_file.empty())
    compile_args.push_back(odm_policy_cil_file.c_str());

init 的 split-policy 输入来自 system、system_ext、product、vendor、odm、mapping/compat 和 genfs version CIL;没有扫描 /apex 并追加 CIL 的路径。最终只调用一次 security_load_policy 安装 monolithic binary。因而 APEX 更新不能靠在包内放 .te 或 .cil 来引入新权限。

2. Payload标签 ​

2.1 APEX声明 ​

源码文件:art/build/apex/Android.bp

make
apex_test {
    name: "test_broken_com.android.art",
    defaults: ["com.android.art-base-defaults"],
    manifest: "test_apex_manifest.json",
    file_contexts: ":com.android.art-file_contexts",
    installable: false,
    compressible: false,
    native_shared_libs: ["libart-broken"],
}

APEX module 的 file_contexts property 指向 Soong filegroup;它描述 payload image 内相对路径的 label。installable 和 compressible 是容器/安装属性,不改变 file_contexts 中的 type,也不自动把这些 type 声明到 platform policy。

2.2 真实标签文件 ​

源码文件:system/sepolicy/apex/com.android.art-file_contexts

text
(/.*)?                         u:object_r:system_file:s0
/bin/art_boot                  u:object_r:art_boot_exec:s0
/bin/art_exec                  u:object_r:art_exec_exec:s0
/bin/artd                      u:object_r:artd_exec:s0
/bin/dex2oat(32|64)?           u:object_r:dex2oat_exec:s0
/bin/dexopt_chroot_setup       u:object_r:dexopt_chroot_setup_exec:s0
/bin/odrefresh                 u:object_r:odrefresh_exec:s0
/lib(64)?(/.*)?                u:object_r:system_lib_file:s0

规则以 APEX 根目录为 / 匹配,不写 /apex/com.android.art/... 前缀。默认内容是 system_file,关键 binary 细分为 exec type,动态链接库使用 system_lib_file。这些 type 必须已经在 system sepolicy 中声明并拥有相应 domain transition/allow。

2.3 Soong补全 ​

源码文件:build/soong/apex/builder.go

go
func (a *apexBundle) buildFileContexts(ctx android.ModuleContext) android.Path {
    var fileContexts android.Path
    if a.properties.File_contexts == nil {
        fileContexts = android.PathForSource(
            ctx, "system/sepolicy/apex", ctx.ModuleName()+"-file_contexts")
    } else {
        fileContexts = android.PathForModuleSrc(ctx, *a.properties.File_contexts)
    }

    if a.Platform() && !strings.HasPrefix(fileContextsDir, "system/sepolicy/") {
        ctx.PropertyErrorf("file_contexts",
            "should be under system/sepolicy, but found in %q", fileContextsDir)
    }

    labelForRoot := "u:object_r:system_file:s0"
    labelForManifest := "u:object_r:system_file:s0"
    if a.SocSpecific() && !a.vndkApex {
        labelForRoot = "u:object_r:vendor_file:s0"
        labelForManifest = "u:object_r:vendor_apex_metadata_file:s0"
    }

platform APEX 默认从 system/sepolicy/apex/<module>-file_contexts 查找;也可通过 property 指向 module。platform file_contexts 必须位于 system/sepolicy,避免模块随意绕过集中策略审查。vendor APEX 的 root 和 manifest 使用 vendor 类型。

2.4 manifest强制项 ​

源码文件:build/soong/apex/builder.go

go
output := android.PathForModuleOut(ctx, "file_contexts")
rule := android.NewRuleBuilder(pctx, ctx)

rule.Command().BuiltTool("rm").FlagWithOutput("-f ", output)
rule.Command().BuiltTool("cat").Input(fileContexts).Text(">>").Output(output)
rule.Command().BuiltTool("echo").Text(">>").Output(output)
rule.Command().BuiltTool("echo").
    Text("/apex_manifest\\.pb").Text(labelForManifest).
    Text(">>").Output(output)
rule.Command().BuiltTool("echo").
    Text("/").Text(labelForRoot).
    Text(">>").Output(output)

Soong 复制模块提供的规则后,强制追加 apex_manifest.pb 和根目录标签。owner 是 apex builder,消费者是 apexer payload image 构建。模块作者不能通过遗漏 manifest 规则让 metadata 使用未知 type。

3. APEXd权限 ​

3.1 挂载资源 ​

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

yaml
allow apexd dm_device:chr_file rw_file_perms;
allow apexd dm_device:blk_file rw_file_perms;
allow apexd apex_dm_device:blk_file { setattr rw_file_perms };

allow apexd self:global_capability_class_set {
    sys_admin chown dac_override dac_read_search fowner
};

allow apexd apex_mnt_dir:dir create_dir_perms;
allow apexd apex_mnt_dir:filesystem { mount unmount };
allow apexd apex_mnt_dir:dir mounton;
allow apexd apex_mnt_dir:lnk_file create_file_perms;
allow apexd apex_mnt_dir:file { create_file_perms relabelfrom mounton };
allow apexd apex_info_file:file relabelto;

apexd 的 owner 状态包括 loop/device-mapper、mount point、APEX data/metadata 和 apex-info-list。sys_admin 等 capability 不是 APEX 内 binary 继承的通用权限,而是 apexd domain 的专门能力;APEX 模块进程仍在各自 domain 中运行。

3.2 唯一管理者 ​

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

yaml
neverallow { domain -apexd -init } apex_data_file:dir no_w_dir_perms;
neverallow { domain -apexd -init -kernel } apex_data_file:file no_w_file_perms;
neverallow { domain -apexd } apex_mnt_dir:lnk_file no_w_file_perms;
neverallow { domain -apexd } apex_info_file:file no_w_file_perms;

neverallow { domain -apexd -init }
    apex_mnt_dir:filesystem { mount unmount };
neverallow { domain -apexd -dexopt_chroot_setup -init }
    apex_mnt_dir:dir mounton;

这些 neverallow 把写 APEX 数据、更新 apex-info 和管理 /apex mount 的责任限制在 apexd/init 等少数 domain。若某个模块进程触发 mount/write denial,正确方向通常是通过 apexd API 完成操作,而不是给模块 domain 增加 mount capability。

3.3 init标签恢复 ​

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

cpp
void SelinuxRestoreContext() {
    // Earlier calls restore labels for /dev and device-mapper paths.
    selinux_android_restorecon("/apex", 0);
    selinux_android_restorecon("/bootstrap-apex", 0);
    selinux_android_restorecon("/linkerconfig", 0);
    // Later calls handle optional snapshot and GSI paths.
}

这里恢复的是 mount root 和早期创建路径的标签,不是递归重写每个 APEX payload 文件。payload inode 标签由 image 构建时的 file_contexts 提供;把这次 restorecon 描述成“运行时加载 APEX sepolicy”是不准确的。

4. 策略测试 ​

4.1 测试输入 ​

源码文件:build/soong/apex/builder.go、system/sepolicy/tests/apex_sepolicy_tests.py

go
// Runs:
//   apex-ls -Z {apex_file} > {file_contexts}
//   apex_sepolicy_tests -f {file_contexts}
func runApexSepolicyTests(ctx android.ModuleContext,
                          a *apexBundle, apexFile android.Path) android.Path {
    timestamp := android.PathForModuleOut(ctx,
        "apex_sepolicy_tests.timestamp")
    ctx.Build(pctx, android.BuildParams{
        Rule: apexSepolicyTestsRule,
        Input: apexFile,
        Output: timestamp,
        Args: map[string]string{"partition_tag": a.partition()},
    })
    return timestamp
}

测试消费已经构建的 APEX,而不是源 file_contexts:apex-ls -Z 列出 payload 实际标签,再用 host 测试与 precompiled_sepolicy 对照。这能发现 builder、apexer 或标签文件组合后的真实错误。

4.2 规则模型 ​

源码文件:system/sepolicy/tests/apex_sepolicy_tests.py

python
generic_rules = [
    (BinaryFile(), NotAnyOf({'vendor_file'})),
    (Is('./etc/permissions/'), AllowRead('dir', {'system_server'})),
    (Glob('./etc/permissions/*.xml'), AllowRead('file', {'system_server'})),
    (Regex(r'\./etc/.*\.\d*rc'), AllowRead('file', {'init'})),
    (Is('./etc/vintf/'), AllowRead('dir', {'servicemanager', 'apexd'})),
    (Glob('./etc/vintf/*.xml'), AllowRead('file', {'servicemanager', 'apexd'})),
    (Is('./apex_manifest.pb'), AllowRead('file', {'linkerconfig', 'apexd'})),
    (Is('./'), AllowPerm('dir', {'linkerconfig', 'apexd'}, 'search')),
]

测试不仅验证 type 存在,还验证消费者是否拥有必要 read/search。binary 不能使用 generic vendor_file;permissions XML 必须让 system_server 读取;init rc、VINTF fragment、manifest/root 都有明确消费者。

4.3 policy查询 ​

源码文件:system/sepolicy/tests/apex_sepolicy_tests.py

python
def check_rule(pol, path: str, tcontext: str, rule: Rule) -> List[str]:
    errors = []
    match rule:
        case AllowPerm(tclass, scontext, perm):
            for s in scontext:
                te_rules = list(pol.QueryTERule(
                    scontext={s}, tcontext={tcontext},
                    tclass={tclass}, perms={perm}))
                if len(te_rules) == 0:
                    errors.append(
                        f"Error: {path}: {s} can't {perm}. "
                        f"(tcontext={tcontext})")
        case ResolveType():
            if tcontext not in pol.GetAllTypes(False):
                errors.append(
                    f"Error: {path}: tcontext({tcontext}) is unknown")
        case HasAttr(attr):
            if tcontext not in pol.QueryTypeAttribute(attr, True):
                errors.append(
                    f"Error: {path}: tcontext({tcontext}) must be associated "
                    f"with {attr}")
    return errors

输入是 path、实际 tcontext 和规则;断言包括 type 可解析、消费者 allow 存在、partition base attribute 正确。它不测试 APEX signature、dm-verity、版本选择或实际 mount 成功。

4.4 单元反例 ​

源码文件:system/sepolicy/tests/apex_sepolicy_tests_test.py

python
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'")

def test_system_vendor(self):
    line = './bin/foo u:object_r:vendor_file:s0'
    rules = [apex.system_vendor_rule('system')]
    errors = apex.check_line(self.pol, line, rules)
    self.assertRegex(errors[0], r'Error: .* must be associated')

第一个测试证明 binary generic label 会失败;第二个证明 system APEX payload type 必须具有 system_file_type,vendor APEX 则应具有 vendor_file_type。这些断言直接约束分区边界。

5. 压缩APEX ​

5.1 Soong约束 ​

源码文件:build/soong/apex/apex.go

go
func (a *apexBundle) setCompression(ctx android.ModuleContext) {
    if a.isCompressable() {
        if !a.Updatable() {
            ctx.PropertyErrorf("compressible",
                "do not compress non-updatable APEX")
        }
        if a.PartitionTag(ctx.DeviceConfig()) != "system" {
            ctx.PropertyErrorf("compressible",
                "do not compress non-system APEX")
        }
    }

    a.isCompressed = ctx.Config().ApexCompressionEnabled() &&
                     a.isCompressable()
}

只有 updatable、system APEX 才允许压缩;是否真正压缩还受 product 配置控制。压缩资格是容器构建约束,不是 SELinux 权限属性。

5.2 容器生成 ​

源码文件:build/soong/apex/builder.go

go
if a.isCompressed {
    unsignedCompressedOutputFile := android.PathForModuleOut(
        ctx, a.Name()+imageCapexSuffix+".unsigned")
    compressRule.Command().BuiltTool("apex_compression_tool").
        Flag("compress").
        FlagWithInput("--input ", signedOutputFile).
        FlagWithOutput("--output ", unsignedCompressedOutputFile)
    compressRule.Build("compressRule",
        "Generate unsigned compressed APEX file")

    signedCompressedOutputFile := android.PathForModuleOut(
        ctx, a.Name()+imageCapexSuffix)
    // The compressed container is signed after compression.
    a.outputFile = signedCompressedOutputFile
}

file_contexts 已用于构建内层 signed APEX payload;外层压缩再产生 .capex 并签名。解压后仍得到同一个带标签 payload image,因此 compressed/preinstalled 差异不会增加新 SELinux type 或规则。

6. 更新边界 ​

6.1 可以独立更新 ​

在已有 policy 能力范围内,APEX 可以更新 binary/library/config 内容,复用已有 exec/data type 和服务权限。新 payload 必须继续通过 file_contexts 测试,且消费者仍能读取 manifest、VINTF、permissions 和 init rc。

6.2 需要系统策略更新 ​

以下变化不能只靠 APEX 包完成:

  • 新增一个 policy 中不存在的 exec/data/service type;
  • 新增 daemon domain 或新的 domain transition;
  • 给已有 domain 增加新的 Binder、property、device 或 capability 权限;
  • 修改全局 neverallow/MLS/Treble public API;
  • 让 platform APEX 使用 vendor-only type,或反向混用分区 attribute。

更新包若引用未知 type,apex_sepolicy_tests --all 会报 unknown context;即使跳过 host test,设备 policydb 也无法识别该标签。

6.3 安装钩子 ​

APEX 安装钩子若存在,执行文件也必须映射到预定义 exec type,并在 system sepolicy 中提前定义 domain/transition/权限。它们不是从 APEX 动态生成 domain。gki_apex_prepostinstall、apex_test_prepostinstall 等策略文件说明了这种预声明模式。

7. 排查方法 ​

7.1 构建侧 ​

sh
# Read-only: inspect the module's selected file_contexts and payload labels.
rg -n 'file_contexts:|compressible:|updatable:' <apex-source>/Android.bp
rg -n '<exec-or-data-type>' system/sepolicy --glob '*.te' --glob '*file_contexts'

# Build validation: build the APEX and its host sepolicy checks.
m <apex-module-name>

# Read-only: inspect actual labels in the built payload.
apex-ls -Z out/target/product/<device>/system/apex/<module>.apex

7.2 设备侧 ​

sh
# Read-only: inspect mounted APEX versions, labels, and process domains.
adb shell 'mount | grep -E " /apex/| /bootstrap-apex/"'
adb shell 'ls -lZd /apex/<name> /apex/<name>/bin/*'
adb shell 'ps -AZ | grep <daemon-name>'

# Read-only: inspect APEX-related AVC and mount failures.
adb shell 'dmesg | grep -E "avc: denied|apexd|dm-|loop|/apex"'

若挂载失败但没有 AVC,优先检查签名、verity、container 或版本选择;若文件标签 unknown/generic,检查 payload file_contexts;若 exec type 正确但 domain transition 失败,检查 platform policy 中的 type/transition,而不是修改 APEX 容器格式。

8. 读码练习 ​

选择 com.android.art:

  1. 从 art/build/apex/Android.bp 找到 file_contexts module;
  2. 在 com.android.art-file_contexts 中定位 artd、dex2oat、library 标签;
  3. 回到 system sepolicy 确认 exec type、domain transition 和消费者 allow 已预声明;
  4. 用 apex-ls -Z 与 apex_sepolicy_tests 验证实际 payload;
  5. 假设新增 /bin/newdaemon,判断仅更新 file_contexts、仅更新 APEX binary、配套 system sepolicy 三种方案分别在哪一步失败;
  6. 将 APEX 改为 compressed,解释为什么 payload label 和 policydb 不应变化。

9. 源码导航 ​

  1. build/soong/apex/apex.go:APEX 属性、分区和压缩约束。
  2. build/soong/apex/builder.go:file_contexts 生成、apexer 输入、压缩容器和 sepolicy tests。
  3. system/sepolicy/apex/:platform APEX payload 标签文件。
  4. system/sepolicy/private/apexd.te:apexd 的 dm/loop/mount/data 权限和 neverallow。
  5. system/sepolicy/tests/apex_sepolicy_tests.py:payload type、allow 和 partition attribute 检查。
  6. system/sepolicy/tests/apex_sepolicy_tests_test.py:unknown/generic/跨分区标签反例。
  7. system/core/init/selinux.cpp:策略加载输入中没有 APEX 动态 CIL,以及 /apex 根标签恢复。
  8. art/build/apex/Android.bp、system/sepolicy/apex/com.android.art-file_contexts:真实 APEX 与标签关联示例。