Skip to content

CIL 中间语言

结合 Android 17 真实 CIL 产物与构建命令,解释声明、attribute、allow、兼容映射和 secilc 合并检查的关系。

基于android-17.0.0_r1
AndroidSELinuxCILcheckpolicysecilc源码阅读

CIL 中间语言 ​

本文承接 policy.conf 生成:上一阶段得到的是 M4 展开的传统策略文本,本文追踪它如何被 checkpolicy -C 转成 CIL,以及 CIL 如何被 secilc 作为多个输入合并。本文不会把 CIL 写成一份脱离 Android 的语法手册,也不展开 version_policy 的兼容算法;重点是读者能拿真实 .cil 文件辨认声明、集合、规则和映射,并能判断某个错误发生在生成、过滤还是合并阶段。

CIL(Common Intermediate Language)在这里是一个结构化的策略表示:同一个文件可以包含 (type ...)、(allow ...)、(typeattributeset ...) 等 S-expression;平台、system_ext、product、vendor 和 odm 可以各自产生 CIL,最后由 secilc 读取。CIL 文件本身不是内核格式,只有 secilc -o 生成的二进制才交给 init 和内核。这个边界决定了排查顺序:先确认 CIL 是否表达了预期语义,再确认所有分区 CIL 能否在同一 policy version 下合并。

1. 位置与角色 ​

1.1 三个消费者 ​

CIL 产物生成者直接消费者失败含义
raw CILcheckpolicy -Cfilter_out、version_policypolicy.conf 语法或声明无法转换
versioned CILversion_policy、build_sepolicyvendor/odm 的 secilc merge checkpublic API 映射或 attributize 不完整
merged CIL 输入集合Soong se_policy_binary 或 initsecilc跨文件声明、规则、版本或 neverallow 冲突

secilc 有两个不同的消费时机。se_policy_cil 可以把输出写到 /dev/null 做合并预检查;se_policy_binary 才把 CIL 集合写成二进制;启动时 init 又可能读取分区 CIL 重新合并。因此“某个 CIL module 成功”只证明它自己的 action 成功,不证明运行时所有分区输入都能合并。

1.2 数据流 ​

图中 filter_out 和 version_policy 不是 CIL 编译器的同义词:前者按文件内容删除不应导出的行,后者读取 CIL AST 并生成映射或目标策略。它们的输入都仍是文本;只有最后的 secilc 才创建二进制 policy。

2. 真实语句 ​

2.1 类型与集合 ​

源码文件:system/sepolicy/private/compat/34.0/34.0.compat.cil

text
;; A compatibility fragment may redeclare a type used by an old vendor image.
(type vendor_hidraw_device)
(typeattributeset dev_type (vendor_hidraw_device))

(allow system_server vendor_hidraw_device
    (dir (open getattr read search ioctl lock watch watch_reads)))
(allow system_server vendor_hidraw_device
    (chr_file (getattr open read ioctl lock map watch watch_reads append write)))

(type vendor_hidraw_device) 声明一个类型;(typeattributeset dev_type (vendor_hidraw_device)) 把它加入 dev_type 集合。后面的 allow 以三个位置表达主体、客体和对象类权限:system_server 是 source,vendor_hidraw_device 是 target,dir/chr_file 是 class,括号内是权限集合。它与 TE 中的 allow source target:class { perms }; 不是简单的字符替换,而是把 class 和权限组变成嵌套列表,便于工具按节点解析。

2.2 集合表达式 ​

源码文件:system/sepolicy/private/technical_debt.cil

text
;; These sets are generated because the policy language cannot express the workaround.
(typeattributeset hal_allocator_client ((and (appdomain) ((not (isolated_app_all))))))
(typeattributeset halclientdomain (hal_allocator_client))
(typeattributeset hal_drm_client ((and (appdomain)
    ((not (or (isolated_app_all) (sdk_sandbox_all))))))

typeattributeset 的右侧不只接受一个类型名,也可以是 and、not 等集合表达式。这里先计算 appdomain 去掉 isolated_app_all,再把结果赋给 attribute;下一行又把 hal_allocator_client 纳入 halclientdomain。这类表达式的消费者是 CIL 编译器的集合解析,而不是 M4:M4 只负责把源文本送入 checkpolicy,不会替你计算 attribute 集合。

2.3 映射节点 ​

源码文件:system/sepolicy/prebuilts/api/202604/202604_mapping.cil

text
(typeattributeset mediadrmserver_service_202604 (mediadrmserver_service))
(expandtypeattribute (mediadrmserver_service_202604) true)
(typeattribute mediadrmserver_service_202604)

(typeattributeset sysfs_gpu_202604 (sysfs_gpu))
(expandtypeattribute (sysfs_gpu_202604) true)
(typeattribute sysfs_gpu_202604)

版本映射文件为旧接口创建带版本后缀的 attribute,并显式声明是否展开。expandtypeattribute 和 typeattribute 的组合不是普通业务 domain 的写法,而是兼容层给 version_policy/secilc 使用的接口节点。看到 _202604 这样的名字时,先把它当作 API 映射对象,不要误认为设备上会运行一个同名 domain。

这张关系图回答“为什么先看声明和集合,再看 allow”:规则解析需要知道 source/target 是类型还是 attribute,attribute 是否展开又会影响最终规则集合。若 CIL 中缺少声明,secilc 可能在合并阶段报 unknown type;若声明存在但集合成员错误,构建可以通过语法解析却产生错误的权限集合。

3. 生成路径 ​

3.1 checkpolicy命令 ​

源码文件:system/sepolicy/build/soong/policy.go

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)

输入是 policy.conf,输出是当前 policyCil module 的 CIL 文件。-C 选择 CIL 输出,-M 打开 MLS,-L 保留 allow 规则的行标记,-c 30 对应 Android 17 代码中的 PolicyVers。这一步的 owner 是 Soong policyCil.compileConfToCil;它不读取 vendor 其他分区的 CIL,因此不能在这里判断跨分区兼容。

3.2 追加与过滤 ​

源码文件:system/sepolicy/build/soong/policy.go

go
if len(c.properties.Filter_out) > 0 {
    // Remove exported helper declarations from the generated CIL in place.
    rule.Command().BuiltTool("build_sepolicy").
        Text("filter_out").
        Flag("-f").
        Inputs(android.PathsForModuleSrc(ctx, c.properties.Filter_out)).
        FlagWithOutput("-t ", cil)
}
if len(c.properties.Additional_cil_files) > 0 {
    // Technical-debt CIL is appended after checkpolicy output.
    rule.Command().Text("cat").
        Inputs(android.PathsForModuleSrc(ctx, c.properties.Additional_cil_files)).
        Text(">> ").Output(cil)
}

过滤会改变 CIL 的可见内容,追加则扩大输出内容;二者都发生在 checkpolicy 之后。reqd_policy_mask.cil 就是一个典型过滤输入:它帮助 public/vendor policy 通过 checkpolicy,却不应作为导出的接口声明。additional_cil_files 用于策略语言无法表达的 workaround,不能把它误读成 M4 宏展开结果。

3.3 生成时序 ​

时序中的 O 是文件 owner 的抽象表示:实际 action 通过临时文件和 RuleBuilder 命令完成。过滤失败会让 module action 失败;追加成功也不能掩盖前面 raw CIL 缺少声明的问题。

4. secilc消费 ​

4.1 Soong检查 ​

源码文件:system/sepolicy/build/soong/policy.go

go
secilcCmd := rule.Command().BuiltTool("secilc").
    Flag("-m").                 // Multiple declarations are allowed.
    FlagWithArg("-M ", "true"). // Enable MLS.
    Flag("-G").                 // Expand and remove auto attributes.
    FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
    Inputs(android.PathsForModuleSrc(ctx, c.properties.Filter_out)).
    Text(cil.String()).
    FlagWithArg("-o ", os.DevNull).
    FlagWithArg("-f ", os.DevNull).
    Flag("-v")

这个 action 把过滤输入和当前 CIL 一起传给 secilc,但输出到 /dev/null,所以它是 merge check 而不是最终 policy 生成。-m 允许多个声明,-M true 启用 MLS,-G 展开并移除自动生成 attribute,-c 30 固定 policy version;-N 只有 Ignore_neverallow 生效时才追加。检查的消费者是构建失败状态,而不是一个可安装文件。

4.2 二进制输出 ​

源码文件:system/sepolicy/build/soong/policy.go

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)
rule.Temporary(bin)

se_policy_binary 与前面的检查 action 相比,输入是完整 CIL 集合,-o 指向临时二进制;命令完成后再复制到最终输出。随后 user build 还会运行 sepolicy-analyze permissive,这意味着“CIL 能合并”与“二进制满足产品构建约束”是两个独立条件。

4.3 合并时序 ​

mapping CIL 只是输入之一;它不会替代 platform CIL,也不会单独产生权限。secilc 必须在同一 policy version 下解析所有名称和集合,任何一个分区的 CIL 失败都会阻止二进制输出。启动时 init 可能再次执行类似合并,详见 Policy Compilation 的运行时边界。

5. 兼容片段 ​

5.1 版本输入 ​

源码文件:system/sepolicy/tools/version_policy.c

c
static int read_cil_file(struct cil_db **db, char *path) {
    int rc = SEPOL_ERR;
    FILE *file;
    struct stat filedata;
    uint32_t file_size;
    char *buff = NULL;

    // Initialize a CIL database and parse one input file into its tree.
    cil_db_init(db);
    file = fopen(path, "re");
    if (!file) {
        fprintf(stderr, "Could not open file: %s\n", path);
        goto file_err;
    }
    ...
    rc = cil_add_file(*db, path, buff, file_size);
    if (rc != SEPOL_OK) {
        fprintf(stderr, "Failure adding %s to parse tree\n", path);
        goto parse_err;
    }
    free(buff);
    return SEPOL_OK;
}

version_policy 不是简单的文本替换器:它为 base 和 target 各初始化一个 cil_db,调用 cil_add_file 解析 CIL,再交给 libsepol 的 Android 扩展。上面的 ... 是为聚焦资源释放而省略的文件读取和错误清理分支;真实源码在该分支中会关闭文件、释放 buffer 并销毁 CIL database。

5.2 两种模式 ​

源码文件:system/sepolicy/tools/version_policy.c

c
if (mapping) {
    rc = cil_android_attrib_mapping(&out_db, base_db, num);
    if (rc != SEPOL_OK)
        goto exit;
} else {
    // Read the target policy and associate it with the base API.
    rc = read_cil_file(&out_db, tgt_policy);
    if (rc != SEPOL_OK) {
        goto exit;
    }
    rc = cil_android_attributize(out_db, base_db, num);
    if (rc != SEPOL_OK) {
        goto exit;
    }
}

--mapping 模式从 base public CIL 生成版本 attribute;target 模式读取目标 CIL 并依据 base 做 attributize。两种模式互斥,失败会跳到统一清理路径;输出由 cil_write_build_ast 写回 CIL AST。本文只解释输入/输出关系,具体 API 兼容策略和版本文件布局属于 Policy Compilation 的上层构建主题。

5.3 兼容边界 ​

兼容 CIL 可以重复声明某些类型或补充旧 vendor 所需 allow,但它仍受普通 CIL 名称解析和 neverallow 约束。比如 34.0.compat.cil 中的 vendor_hidraw_device 是为旧标签保留行为,不代表 platform private 类型被导出。判断一段兼容 CIL 是否安全,要问三个问题:它依赖的类型在哪个 public base 中声明?它由哪个 mapping 文件引入?它最终与哪些 dependent CIL 一起交给 secilc?

6. 错误与诊断 ​

6.1 阶段表 ​

错误位置典型症状应检查的对象不应外推的结论
checkpolicypolicy.conf 行号、语法错误M4 输出、声明排序、宏展开不能说明其他分区可合并
filter_out过滤工具非零或声明消失filter 输入与目标 CIL不能说明原始策略没有该声明
version_policybase/target 解析或 attributize 失败public base、target CIL、版本号不能说明运行时 binary 已加载
secilc merge checkunknown type、duplicate/conflict、neverallow全部 CIL 输入、-c 版本、-N不能把 /dev/null 输出当作可加载策略
se_policy_binarypermissive domain 检查失败binary 内容与 allowlist不能只改 .te 文本绕过产品约束

6.2 命令 ​

sh
# Read-only: find the CIL-producing and binary-producing modules.
rg -n 'se_policy_cil|se_policy_binary|checkpolicy|secilc' \
  system/sepolicy/Android.bp system/sepolicy/build/soong/policy.go

# Read-only: inspect concrete CIL declarations and versioned attributes.
rg -n '^\(type |^\(allow |^\(typeattribute|^\(typeattributeset|^\(expandtypeattribute' \
  system/sepolicy/private system/sepolicy/prebuilts/api --glob '*.cil'

# Read-only: inspect generated CIL files for one product.
find out/target/product/<device>/obj/ETC \
  -path '*sepolicy*intermediates/*.cil' -print

# Read-only on a device: inspect the loaded policy's rule view.
adb shell 'dmesg | grep -E "SELinux|avc: denied|Could not load policy"'

第一条命令定位 owner,第二条确认 CIL 节点是否存在,第三条把源 module 与实际产物对齐,第四条观察最终加载或运行时拒绝。sesearch 查询应在已经导出的 policy 上进行;如果设备只留下 AVC,不能反推某条 CIL 在哪个分区生成。

6.3 两步验证 ​

第一步验证生成语义,而不是运行时权限。构建 platform 和 vendor CIL 后,选择一个已知的类型或 allow,检查它在对应 CIL 中是否以预期节点出现:

sh
# Build only the CIL producers; this changes host build outputs, not a device.
m plat_sepolicy.cil vendor_sepolicy.unversioned.cil

# Arrange/assert: declarations and allow nodes must be present in their owner CIL.
rg -n '^\(type |^\(typeattributeset |^\(allow ' \
  out/soong/.intermediates/system/sepolicy --glob '*.cil'

这能证明 policy.conf -> checkpolicy 的输出包含目标节点,但不能证明 vendor CIL 已经完成版本化或与 platform 合并。

第二步验证合并边界。使用 Soong action 中同样的选项,把相关 CIL 传给 host secilc,并将输出丢弃:

sh
# Read-only merge check; replace <host-tools> and <cil-files> from the build log.
<host-tools>/secilc -m -M true -G -c 30 \
  <cil-files> -o /dev/null -f /dev/null

断言是命令返回 0;非零时按错误中的文件和 symbol 回到对应 CIL。这个实验覆盖名称解析、集合展开、class/permission 和 neverallow 的合并检查,但不覆盖 se_policy_binary 的 permissive-domain 产品约束,也不覆盖 init 重新加载时的分区文件选择。

7. 读码练习 ​

给定一个新增的 my_daemon domain,可以按以下顺序复述:

  1. 它的 .te 文件由哪个 se_build_files tag 收集,进入哪些 policy.conf?
  2. checkpolicy -C 后,类型声明、attribute 关联和 allow 在 CIL 中分别对应什么节点?
  3. vendor policy 是否需要 reqd_mask.cil、public base 或 mapping CIL?哪些行会被 filter_out 删除?
  4. secilc 合并失败时,错误属于名称解析、集合展开、neverallow 还是 policy version?应该先看哪一份 CIL?

另一个练习是反向阅读 34.0.compat.cil:找到 vendor_hidraw_device 的声明、attribute 关联和 system_server allow,判断它为什么必须与 platform 和 mapping CIL 一起编译,以及为什么单独运行 secilc 不能证明旧 vendor 镜像兼容。

8. 源码导航 ​

  1. system/sepolicy/build/soong/policy.go:checkpolicy、secilc 和 binary module 的命令构造。
  2. system/sepolicy/Android.bp:各分区 CIL module 的输入集合和 secilc_check 开关。
  3. system/sepolicy/tools/version_policy.c:CIL database 读取、mapping/attributize 分派和资源清理。
  4. system/sepolicy/private/technical_debt.cil:集合表达式和额外 CIL 的真实例子。
  5. system/sepolicy/private/compat/34.0/34.0.compat.cil:兼容片段中的声明、allow 与旧 vendor 场景。
  6. system/sepolicy/prebuilts/api/202604/202604_mapping.cil:带版本后缀的 mapping attribute。