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
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
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
(/.*)? 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
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
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
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
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
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
// 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
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
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
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
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
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 构建侧
# 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>.apex7.2 设备侧
# 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:
- 从
art/build/apex/Android.bp找到 file_contexts module; - 在
com.android.art-file_contexts中定位artd、dex2oat、library 标签; - 回到 system sepolicy 确认 exec type、domain transition 和消费者 allow 已预声明;
- 用
apex-ls -Z与apex_sepolicy_tests验证实际 payload; - 假设新增
/bin/newdaemon,判断仅更新 file_contexts、仅更新 APEX binary、配套 system sepolicy 三种方案分别在哪一步失败; - 将 APEX 改为 compressed,解释为什么 payload label 和 policydb 不应变化。
9. 源码导航
build/soong/apex/apex.go:APEX 属性、分区和压缩约束。build/soong/apex/builder.go:file_contexts 生成、apexer 输入、压缩容器和 sepolicy tests。system/sepolicy/apex/:platform APEX payload 标签文件。system/sepolicy/private/apexd.te:apexd 的 dm/loop/mount/data 权限和 neverallow。system/sepolicy/tests/apex_sepolicy_tests.py:payload type、allow 和 partition attribute 检查。system/sepolicy/tests/apex_sepolicy_tests_test.py:unknown/generic/跨分区标签反例。system/core/init/selinux.cpp:策略加载输入中没有 APEX 动态 CIL,以及/apex根标签恢复。art/build/apex/Android.bp、system/sepolicy/apex/com.android.art-file_contexts:真实 APEX 与标签关联示例。
