M4预处理
本文面向已经读过 TE规则语法、TE宏库 和 Global Macros 的读者。前两篇宏专题回答“宏生成哪些规则”,本篇回答“这些宏何时、按什么输入顺序、在什么构建变量下生成规则”:Soong 如何把 global_macros、te_macros、attributes 与 .te 排序,GNU m4 如何处理引号、参数与递归重扫,-s 如何保留源码位置,policy.conf 又怎样进入 checkpolicy 与 secilc。
本文不会把 Android 构建命令简化成一条脱离源码的 m4 *.te,也不把 m4 成功误认为策略编译成功。读完后,你应能区分四类问题:宏定义不可见或引号未闭合、构建变量让规则被裁剪、展开文本不符合 SELinux 语法、规则合法但违反 neverallow;还能从一个产品的 Soong 模块找到实际输入、变量与中间产物。
1. 流水线边界
1.1 三个阶段
Android SELinux 的源码预处理至少包含三个清晰阶段:
se_policy_conf收集并排序输入,调用 m4 生成policy.conf;se_policy_cil用 checkpolicy 把 policy.conf 编译成 CIL,并可用 secilc 做合并校验;se_policy_binary把多个 CIL 输入交给 secilc,生成最终 binary policy。
源码文件: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)
}三个 module type 分开意味着 m4 只负责文本变换。它不认识 type 是否声明、permission 是否属于 class,也不执行 neverallow 的集合冲突判断。
1.2 多个Policy Conf
源码树不是只生成一个全局 policy.conf。平台 public、平台完整策略、system_ext、product、vendor、odm、recovery 与 CTS neverallow 都有各自的 se_policy_conf 输入集合。
源码文件:system/sepolicy/Android.bp
se_policy_conf {
name: "plat_sepolicy.conf",
defaults: ["se_policy_conf_flags_defaults"],
srcs: plat_public_policy +
plat_private_policy,
installable: false,
}
se_policy_cil {
name: "plat_sepolicy.cil",
src: ":plat_sepolicy.conf",
additional_cil_files: [":sepolicy_technical_debt{.plat_private}"],
}Vendor policy 的输入还包含平台 public policy 与 required mask,以便 checkpolicy 解析其可见符号,随后再从 CIL 过滤不应输出的部分。
源码文件:system/sepolicy/Android.bp
se_policy_conf {
name: "vendor_sepolicy.conf",
defaults: ["se_policy_conf_flags_defaults"],
srcs: plat_public_policy +
system_ext_public_policy +
product_public_policy +
reqd_mask_policy +
vendor_policy,
vendor: true,
installable: false,
}
se_policy_cil {
name: "vendor_sepolicy.unversioned.cil",
src: ":vendor_sepolicy.conf",
filter_out: [":reqd_policy_mask.cil"],
secilc_check: false,
vendor: true,
installable: false,
}因此“在 platform policy.conf 中搜不到 vendor 规则”不一定是构建错误,可能只是查看了错误的 conf module。
2. 输入排序
2.1 固定类别顺序
checkpolicy 对声明顺序有要求。Soong 用 policyConfOrder 把 security classes、access vectors、宏、attributes/TE、roles、users 与 contexts 放到固定阶段。
源码文件:system/sepolicy/build/soong/policy.go
// This order should be kept. checkpolicy syntax requires it.
var policyConfOrder = []string{
"flagging_macros",
"security_classes",
"initial_sids",
"access_vectors",
"global_macros",
"neverallow_macros",
"mls_macros",
"mls_decl",
"mls",
"policy_capabilities",
"te_macros",
"ioctl_defines",
"ioctl_macros",
"nlmsg_defines",
"nlmsg_macros",
"attributes|*.te",
"roles_decl",
"roles",
"users",
"initial_sid_contexts",
"fs_use",
"genfs_contexts",
"port_contexts",
}global_macros 位于 te_macros 之前,所以后者能引用 permission set;宏文件又位于 .te 之前,所以策略调用点可见定义。flagging_macros 最早加载,因为后续 attributes 与 TE 文件都可能调用 release flag 宏。
2.2 稳定排序
Soong 只按文件 basename 对应的类别排序,同属 attributes|*.te 的输入保持 srcs 原有顺序,因为使用的是 stable sort。
源码文件:system/sepolicy/build/soong/policy.go
func findPolicyConfOrder(name string) int {
for idx, pattern := range policyConfOrder {
if pattern == "attributes|*.te" &&
(name == "attributes" || strings.HasSuffix(name, ".te")) {
return idx
} else if pattern == name {
return idx
}
}
return len(policyConfOrder)
}
srcs := android.PathsForModuleSrc(ctx, c.properties.Srcs)
sort.SliceStable(srcs, func(x, y int) bool {
return findPolicyConfOrder(srcs[x].Base()) <
findPolicyConfOrder(srcs[y].Base())
})这不是按完整路径字典排序。一个设备 .te 文件即使来自其他目录,也会进入统一 TE 阶段;同阶段内部的 source list 所有者仍是具体 se_policy_conf 模块的 srcs 组合。
2.3 文件分隔
源文件不一定以换行结尾。直接拼接会把前一文件最后一行和后一文件第一行黏在一起,因此 Soong 创建临时 newline 文件,并插到每个输入后面。
源码文件:system/sepolicy/build/soong/policy.go
// Policy files may not all end with a new line. This will be an issue
// when concatenating them in the next step. Create a "newline" file
// and insert it between each source file.
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)
}newline 文件是构建临时资源,由 RuleBuilder 在结束时删除。它不进入发布策略,只保证 m4 输入 token 边界稳定。
3. M4命令
3.1 真实参数
Soong 使用 AOSP 预置的 GNU m4,并传入 --fatal-warnings、大量 -D 变量、-s 与所有排序后的输入文件。它没有使用 include() 加载 te_macros,也没有传 --prefix-builtins。
源码文件:system/sepolicy/build/soong/policy.go
flags := c.getBuildFlags(ctx)
rule.Command().Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Flag("--fatal-warnings").
FlagForEachArg("-D ", ctx.DeviceConfig().SepolicyM4Defs()).
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_with_asan=", c.withAsan(ctx)).
FlagWithArg("-D target_with_dexpreopt=", strconv.FormatBool(ctx.DeviceConfig().WithDexpreopt())).
FlagWithArg("-D target_with_native_coverage=", strconv.FormatBool(
ctx.DeviceConfig().ClangCoverageEnabled() ||
ctx.DeviceConfig().GcovCoverageEnabled())).
FlagWithArg("-D target_build_variant=", c.buildVariant(ctx)).
FlagWithArg("-D target_full_treble=", c.sepolicySplit(ctx)).
FlagWithArg("-D target_compatible_property=", c.compatibleProperty(ctx)).
FlagWithArg("-D target_exclude_build_test=", strconv.FormatBool(
proptools.Bool(c.properties.Exclude_build_test))).
FlagWithArg("-D target_recovery=", strconv.FormatBool(c.isTargetRecovery())).
Flag(boardApiLevelToM4Macro(ctx, c.properties.Board_api_level)).
Flags(flagsToM4Macros(flags)).
Flag("-s").
Inputs(srcsWithNewline).
Text("> ").Output(conf)这里摘录省略了若干 property/debugfs/ashmem 兼容变量,但保留了命令结构与本篇使用的输入。
3.2 构建变体
build_variant 可以由 module 显式覆盖,否则由产品配置决定:eng 优先,其次 debuggable 对应 userdebug,最后是 user。
源码文件:system/sepolicy/build/soong/policy.go
func (c *policyConf) buildVariant(ctx android.ModuleContext) string {
if variant := proptools.String(c.properties.Build_variant); variant != "" {
return variant
}
if ctx.Config().Eng() {
return "eng"
}
if ctx.Config().Debuggable() {
return "userdebug"
}
return "user"
}userdebug_plat_sepolicy.conf 与普通 platform conf 使用同一套源码,却显式把 build variant 固定为 userdebug。
源码文件:system/sepolicy/Android.bp
se_policy_conf {
name: "userdebug_plat_sepolicy.conf",
defaults: ["se_policy_conf_flags_defaults"],
srcs: plat_public_policy +
plat_private_policy,
build_variant: "userdebug",
installable: false,
}所以构建 user 产品时仍可能生成一份单独的 userdebug platform policy,供 debug ramdisk 或 GSI 使用。不要只根据 lunch variant 推断所有 conf module 的 target_build_variant。
3.3 Prebuilt版本
AOSP 的 host prebuilt m4 为 GNU m4 1.4.19。隔离实验应优先使用源码树中的 host 工具,避免本机旧版本在 diagnostics 或 debug flag 上产生差异。
# 按当前host选择AOSP预置m4。
case "$(uname -s)-$(uname -m)" in
Darwin-*) M4=prebuilts/build-tools/darwin-x86/bin/m4 ;;
Linux-x86_64) M4=prebuilts/build-tools/linux-x86/bin/m4 ;;
Linux-aarch64) M4=prebuilts/build-tools/linux-arm64/bin/m4 ;;
*) printf '%s\n' 'unsupported host'; exit 1 ;;
esac
"$M4" --version | head -2这个命令只选择调试工具;正式产品仍应通过 Soong 生成 conf,确保变量和输入集合与产品一致。
4. 定义与输出
4.1 Divert隐藏定义
Android 的宏库开头调用 divert(-1),结尾恢复 divert(0)。负 diversion 丢弃宏定义文件的普通文本输出,但 define() 对 m4 symbol table 的修改仍保留。
源码文件:system/sepolicy/public/global_macros
divert(-1)
define(`capability_class_set', `{ capability capability2 cap_userns cap2_userns }')
define(`global_capability_class_set', `{ capability cap_userns }')
define(`global_capability2_class_set', `{ capability2 cap2_userns }')
divert(0)上面只保留文件首尾与三条连续定义,中间其他集合定义未摘录。
te_macros、neverallow_macros 与 private macro 文件也采用同一结构。这样 policy.conf 不会包含宏库的定义正文,只包含之后调用宏产生的规则。
# 验证负diversion丢弃普通文本,但保留定义。
"$M4" - <<'EOF'
divert(-1)
define(`hidden', `VISIBLE:$1')
THIS_IS_DIVERTED
divert(0)
hidden(ok)
EOF关键输出只有 VISIBLE:ok,不会出现 THIS_IS_DIVERTED。这说明 diversion 控制输出流,不是宏作用域。
4.2 参数是文本
m4 不知道 $1 应该是 SELinux domain、type 还是 permission。它只按位置替换文本,展开结果随后重新进入扫描流。
源码文件:system/sepolicy/public/te_macros
define(`set_prop', `
unix_socket_connect($1, property, init)
allow $1 $2:property_service set;
get_prop($1, $2)
')set_prop(demo, demo_prop) 的第一轮结果仍包含两个宏调用;m4 重扫这段文本后,才继续展开 unix_socket_connect 与 get_prop。
4.3 引号只延迟扫描
Android 宏使用反引号与单引号作为 m4 quote。引号保护文本在当前扫描层不展开,外层处理后引号被移除,结果重扫时仍可能展开。
# 两次show最终都会得到expanded,第二次只延迟了一层扫描。
"$M4" - <<'EOF'
define(`name', `expanded')
define(`show', `<$1>')
show(name)
show(`name')
EOF输出为两行 <expanded>。因此给宏参数加 quote 不是永久转义。要分析复杂嵌套,必须明确“当前层是否展开”和“结果是否会再次扫描”。
4.4 定义期裁剪
部分 Android 条件宏故意不引用整个 ifelse 表达式。m4 读取 te_macros 时就根据 -D 变量执行 ifelse,最终把宏定义成 $1、空串或 marker 包装。
源码文件:system/sepolicy/public/te_macros
define(`full_treble_only', ifelse(target_full_treble, `true', $1,
ifelse(target_full_treble, `cts',
# BEGIN_TREBLE_ONLY -- this marker is used by CTS -- do not modify
$1
# END_TREBLE_ONLY -- this marker is used by CTS -- do not modify
, )))
define(`userdebug_or_eng', ifelse(target_build_variant, `eng', $1, ifelse(target_build_variant, `userdebug', $1,
#
# SUPPRESSED_BY_USERDEBUG_OR_ENG -- this marker is used by CTS -- do not modify
)))这不是每个调用点重新读取 build variant。相同 m4 进程加载宏文件时,userdebug_or_eng 已经根据本次命令定义成确定的 body;随后所有调用共享这一定义。
5. 条件输入
5.1 User与Userdebug
同一宏调用在 user 构建保留 suppression marker,在 userdebug/eng 构建保留参数规则。
# 对同一调用比较三种build variant。
for variant in user userdebug eng; do
printf 'variant=%s\n' "$variant"
"$M4" \
-D target_build_variant="$variant" \
-D target_full_treble=true \
-D target_exclude_build_test=false \
system/sepolicy/public/global_macros \
system/sepolicy/public/te_macros - <<'EOF' |
userdebug_or_eng(`allow demo debug_file:file read;')
EOF
sed '/^[[:space:]]*$/d'
done关键结果是 user 只有 SUPPRESSED_BY_USERDEBUG_OR_ENG marker,userdebug 与 eng 输出 allow。实验验证的是 m4 条件裁剪,不验证 allow 能通过 type/class 与 neverallow 检查。
5.2 Treble三态
Soong 的 sepolicySplit() 对普通 platform policy 返回 true,对 recovery 返回 false,CTS 模式返回特殊字符串 cts。
源码文件:system/sepolicy/build/soong/policy.go
func (c *policyConf) sepolicySplit(ctx android.ModuleContext) string {
if c.cts() {
return "cts"
}
if c.isTargetRecovery() {
return "false"
}
return strconv.FormatBool(true)
}full_treble_only 对三个值分别输出规则、空文本和带 CTS marker 的规则。marker 让后续过滤工具保留版本条件边界。
# 对比true、false和cts三种target_full_treble。
for treble in true false cts; do
printf 'treble=%s\n' "$treble"
"$M4" \
-D target_build_variant=user \
-D target_full_treble="$treble" \
-D target_exclude_build_test=false \
system/sepolicy/public/global_macros \
system/sepolicy/public/te_macros - <<'EOF' |
full_treble_only(`neverallow demo target:file write;')
EOF
sed '/^[[:space:]]*$/d'
done5.3 Build Test
build_test_only 由 target_exclude_build_test 控制。Neverallow 测试会构建两份 conf:一份包含 build-test assertions 交给 checkpolicy,另一份排除它们交给 sepolicy-analyze neverallow。
源码文件:system/sepolicy/build/soong/sepolicy_neverallow.go
checkpolicyConf := n.checkpolicyConfModuleName()
ctx.CreateModule(policyConfFactory, &nameProperties{
Name: proptools.StringPtr(checkpolicyConf),
}, &policyConfProperties{
Srcs: n.properties.Srcs,
Build_variant: proptools.StringPtr("user"),
Installable: proptools.BoolPtr(false),
}, &struct {
Defaults []string
}{
Defaults: n.properties.Defaults,
})
sepolicyAnalyzeConf := n.sepolicyAnalyzeConfModuleName()
ctx.CreateModule(policyConfFactory, &nameProperties{
Name: proptools.StringPtr(sepolicyAnalyzeConf),
}, &policyConfProperties{
Srcs: n.properties.Srcs,
Build_variant: proptools.StringPtr("user"),
Exclude_build_test: proptools.BoolPtr(true),
Installable: proptools.BoolPtr(false),
}, &struct {
Defaults []string
}{
Defaults: n.properties.Defaults,
})因此“某条 neverallow 只在 build_test_only 中”不等于它毫无作用;它仍参与第一份完整 checkpolicy 编译,只是不进入第二条 expanded-attribute 分析路径。
6. Release Flags
6.1 Flag收集
Android 17 的 sepolicy flagging defaults 把 flagging_macros 加入最前面的输入,并从 se_flags_collector 获取 build flags。
源码文件:system/sepolicy/flagging/Android.bp
se_policy_conf_defaults {
name: "se_policy_conf_flags_defaults",
srcs: [":sepolicy_flagging_macros"],
build_flags: ["all_selinux_flags"],
}
filegroup {
name: "sepolicy_flagging_macros",
srcs: ["flagging_macros"],
}Soong 将每个 flag 转成按名称排序的 -D target_flag_NAME=value 参数。
源码文件:system/sepolicy/build/soong/selinux.go
func flagsToM4Macros(flags map[string]string) []string {
flagMacros := []string{}
for _, flag := range android.SortedKeys(flags) {
flagMacros = append(flagMacros,
"-D target_flag_"+flag+"="+flags[flag])
}
return flagMacros
}排序保证命令行稳定,便于增量构建与比较。缺失 flag 不会被默认成 false,宏会主动失败。
6.2 可见性断言
is_flag_enabled 先构造 target_flag_$1,再调用 assert_define_visible。如果 Soong 没有导出对应定义,m4 通过 m4exit(1) 终止。
源码文件:system/sepolicy/flagging/flagging_macros
define(`fatal_error', `errprint(`m4: '__file__: __line__`: fatal error: $*')m4exit(1)')
define(`assert_define_visible', `ifelse($1()$1, `$1()$1', `fatal_error(`definition $1 is not visible in policy')', ``'')')
define(`is_flag_enabled', `
assert_define_visible(`target_flag_$1')
ifelse(target_flag_$1, `true', `$2')
')
define(`is_flag_disabled', `
assert_define_visible(`target_flag_$1')
ifelse(target_flag_$1, `true', , `$2')
')真实策略会在 type 集合内部使用 flag 宏,而不只包裹完整规则。
源码文件:system/sepolicy/private/app.te
allow { appdomain -isolated_app_all -pcc_component -mlstrustedsubject -sdk_sandbox_all } {
app_data_file
privapp_data_file
is_flag_enabled(RELEASE_UNLOCKED_STORAGE_API, `storage_area_content_file')
}:dir create_dir_perms;flag 为 true 时 target set 多一个 type,false 时对应位置变为空白。规则主体仍保留。
6.3 Board API
Board API 由 module property 或当前 vendor API level转成 target_board_api_level。宏要求 YYYY04 格式,再用 m4 eval 做数值比较。
源码文件:system/sepolicy/build/soong/selinux.go
func boardApiLevelToM4Macro(ctx android.ModuleContext, apiLevel *string) string {
level := proptools.StringDefault(apiLevel, "current")
if level == "current" {
level = ctx.Config().VendorApiLevel()
}
return "-D target_board_api_level=" + level
}源码文件:system/sepolicy/flagging/flagging_macros
define(`assert_api_level_format', `
ifelse(regexp(`$1', `^[0-9][0-9][0-9][0-9]04$'), 0, , `
fatal_error(`Invalid API level format: $1. Expected YYYY04, e.g., 202604.')
')
')
define(`starting_at_board_api', `
assert_define_visible(`target_board_api_level')
assert_api_level_format(`$1')
ifelse(eval(target_board_api_level >= $1), 1, `$2')
')
define(`until_board_api', `
assert_define_visible(`target_board_api_level')
assert_api_level_format(`$1')
ifelse(eval(target_board_api_level < $1), 1, `$2')
')格式检查防止把 Android API 35 与日期型 board API 202604 混用。错误发生在 m4 阶段,checkpolicy 不会收到输出。
7. 同步行标记
7.1 M4的-s
Soong 给 m4 传 -s,要求输出同步行标记。宏库自身位于 negative diversion,所以 policy.conf 主要看到恢复输出、进入 TE 文件和宏展开位置时生成的 #line。
# 观察最小set_prop展开中的同步标记。
"$M4" -s \
-D target_build_variant=user \
-D target_full_treble=true \
-D target_exclude_build_test=false \
system/sepolicy/public/global_macros \
system/sepolicy/public/te_macros - <<'EOF' |
set_prop(demo, demo_prop)
EOF
head -30输出中可见:
#line 56 "system/sepolicy/public/global_macros"
#line 1196 "system/sepolicy/public/te_macros"
#line 1 "stdin"宏展开出的多条规则会继续带调用点 line marker。诊断定位因此可以回到 .te 的宏调用,而不是只能落在展开后 policy.conf 的巨大行号。
7.2 Checkpolicy的-L
checkpolicy 阶段又传 -L,为 allow 规则向 CIL 写 line markers。
源码文件:system/sepolicy/build/soong/policy.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)m4 -s 负责 policy.conf 的源位置,checkpolicy -L 负责把允许规则的位置继续带入 CIL。两者解决的是连续流水线中的不同边界。
8. 下游编译
8.1 Checkpolicy输出
se_policy_cil 用 -C 输出 CIL,-M 启用 MLS,policy version 固定来自 PolicyVers。这一步才检查 SELinux policy language 的符号、class/permission 和 neverallow 等语义。
源码文件:system/sepolicy/build/soong/policy.go
const (
MlsSens = 1
MlsCats = 1024
PolicyVers = 30
)m4 可能成功输出如下文本,但它仍不是合法策略:
allow client :binder { call transfer };这种结果来自宏参数缺失。m4 没有 type system,只有 checkpolicy 才能报告空 target 或未知符号。
8.2 CIL过滤与追加
部分 CIL module 会过滤 required mask 或上游分区的 CIL,再追加 technical_debt.cil 等输入。
源码文件:system/sepolicy/build/soong/policy.go
if len(c.properties.Filter_out) > 0 {
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 {
rule.Command().Text("cat").
Inputs(android.PathsForModuleSrc(ctx, c.properties.Additional_cil_files)).
Text(">> ").Output(cil)
}因此最终 CIL 不一定与单个 policy.conf 一一对应。排查“规则为何出现在最终策略”时,还要检查 additional/filter inputs,而不能只搜索 m4 输出。
8.3 Secilc校验
默认情况下,生成 CIL 后会用 secilc 做一次可合并性检查。-G 展开并移除自动生成 attributes,-N 仅在允许忽略 neverallow 时加入。
源码文件:system/sepolicy/build/soong/policy.go
if proptools.BoolDefault(c.properties.Secilc_check, true) {
secilcCmd := rule.Command().BuiltTool("secilc").
Flag("-m").
FlagWithArg("-M ", "true").
Flag("-G").
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")
if proptools.BoolDefault(c.properties.Ignore_neverallow,
ctx.Config().SelinuxIgnoreNeverallows()) {
secilcCmd.Flag("-N")
}
}9. Neverallow输出
9.1 CTS专用Conf
general_sepolicy.conf 固定为 user、CTS mode、排除 build-test rules,并设置 only_neverallow_rules: true。
源码文件:system/sepolicy/Android.bp
se_policy_conf {
name: "general_sepolicy.conf",
defaults: ["se_policy_conf_flags_defaults"],
srcs: plat_public_policy +
plat_private_policy,
build_variant: "user",
cts: true,
exclude_build_test: true,
only_neverallow_rules: true,
dist: {
targets: ["sepolicy_finalize"],
},
}CTS mode 让 full_treble_only 等宏保留 BEGIN/END marker;随后过滤工具只保留 marker 与 neverallow statement。
9.2 过滤算法
源码文件:system/sepolicy/tools/sepolicy_filter_neverallow.py
# Add newlines after semicolons to separate statements with newlines.
blob = re.sub(r"^([^#]+;)", "\\1\n", blob, flags=re.MULTILINE)
# Trim whitespaces.
blob = re.sub(r"^\s+\n", "", blob, flags=re.MULTILINE)
blob = re.sub(r"\n+", "\n", blob)
in_neverallow_rule = False
outputs = []
for line in blob.strip().split('\n'):
if re.search(r"#.*-- this marker is used by CTS --", line):
if len(outputs) > 0 and markers_match(outputs[-1], line):
outputs.pop()
else:
outputs.append(line.rstrip())
continue
line = re.sub(r"#.*", "", line)
if line.strip() == "":
continue
if line.lstrip().startswith("neverallow"):
in_neverallow_rule = True
if in_neverallow_rule:
outputs.append(line.rstrip())
if line.endswith(';'):
in_neverallow_rule = False相邻且中间没有 statement 的匹配 BEGIN/END marker 会一起移除;有 neverallow 的 marker block 则保留边界。这个过滤发生在 m4 之后,不是 m4 builtin。
10. 失败分层
10.1 M4语法
未闭合 quote 或参数列表会在 m4 扫描阶段终止,并带输入文件与行号。
# 故意遗漏右括号,观察m4 parser错误。
"$M4" --fatal-warnings - <<'EOF'
define(`broken', `allow $1 target:file read;')
broken(`demo'
EOF预期错误为 end of file in argument list。此时没有完整 policy.conf,下游 checkpolicy 不会运行。
10.2 定义不可见
Release flag 宏把缺失定义升级为主动 fatal error,而不是默认为 false。
# 未传-D target_flag_RELEASE_MISSING_FLAG,预处理应失败。
"$M4" --fatal-warnings \
system/sepolicy/flagging/flagging_macros - <<'EOF'
is_flag_enabled(RELEASE_MISSING_FLAG, `allow demo target:file read;')
EOF关键输出包含:
fatal error: definition target_flag_RELEASE_MISSING_FLAG is not visible in policy10.3 展开后语法
m4 参数少传、拼接 type 名错误、条件展开留下空集合,都可能生成文本但无法被 checkpolicy 接受。最典型的漏参实验是:
# m4会成功,但输出的target为空。
"$M4" \
-D target_build_variant=user \
-D target_full_treble=true \
-D target_exclude_build_test=false \
system/sepolicy/public/global_macros \
system/sepolicy/public/te_macros - <<'EOF' |
binder_call(client)
EOF
sed '/^#/d;/^[[:space:]]*$/d'输出是:
allow client :binder { call transfer };
allow client:binder transfer;
allow client :fd use;所以 m4 exit 0 只说明文本处理完成,不说明 SELinux policy 合法。
10.4 Neverallow冲突
宏展开和语法解析都可能成功,但 allow 与 neverallow 的集合相交时,checkpolicy/secilc 或专用 neverallow 测试仍会失败。这个阶段应检查展开后的 source、target、class、permission 与 attribute 成员,而不是继续调整 m4 quote。
11. 调试方法
11.1 构建实际Module
最可靠的方法是构建目标 conf module,而不是手工猜测全部 -D 与输入文件。
# 初始化Android构建环境并选择产品后执行。
source build/envsetup.sh
lunch PRODUCT-userdebug
# 只构建平台m4输出和对应CIL。
m plat_sepolicy.conf plat_sepolicy.cil
# 在Soong中间目录定位输出。
find out/soong/.intermediates/system/sepolicy \
-path '*plat_sepolicy.conf*' -type fmodule 构建继承真实产品 flags、board API、Treble/recovery 状态与 source filegroups。单宏实验适合理解语义,不能替代这一步。
11.2 Trace指定宏
GNU m4 的 -t NAME 可以只追踪目标宏,避免 -dV 输出所有 macro definitions。
# stdout丢弃,只查看三个相关宏的trace。
"$M4" \
-t set_prop \
-t unix_socket_connect \
-t get_prop \
-D target_build_variant=user \
-D target_full_treble=true \
-D target_exclude_build_test=false \
system/sepolicy/public/global_macros \
system/sepolicy/public/te_macros - >/dev/null <<'EOF'
set_prop(demo, demo_prop)
EOF关键 trace 顺序为:
m4trace: -1- set_prop
m4trace: -1- unix_socket_connect
m4trace: -1- get_prop它证明重扫后的嵌套调用顺序,但不显示最终 policy permission 是否有效。
11.3 Dump定义
dumpdef 把当前 m4 symbol 的 body 写到 stderr,可用于确认条件宏在加载后究竟被定义成什么。
# user与userdebug分别加载te_macros,再查看最终定义。
for variant in user userdebug; do
printf 'variant=%s\n' "$variant"
"$M4" \
-D target_build_variant="$variant" \
-D target_full_treble=true \
-D target_exclude_build_test=false \
system/sepolicy/public/global_macros \
system/sepolicy/public/te_macros - 2>&1 >/dev/null <<'EOF'
dumpdef(`userdebug_or_eng')
EOF
doneuser 的定义是 suppression marker,userdebug 的定义是 $1。这比只观察一个调用点更直接地说明条件在定义加载期完成。
11.4 搜索产物
定位条件规则时,建议同时搜索三个层次:
# 源码调用点:规则是否被宏包裹。
rg -n 'userdebug_or_eng|is_flag_enabled|starting_at_board_api' \
system/sepolicy/private
# policy.conf:本次产品预处理后是否仍存在规则。
rg -n '目标type或permission' \
out/soong/.intermediates/system/sepolicy
# CIL:checkpolicy和过滤后是否进入分区输出。
rg -n '目标type或permission' \
out/soong/.intermediates/system/sepolicy -g '*.cil'第一层回答“源码写了什么”,第二层回答“m4 本次保留了什么”,第三层回答“策略编译和分区过滤后留下什么”。
12. 反向验证
12.1 Flag单元测试
Soong 测试注入三个可见 build flags 和一个缺失 flag,构建 collector 后断言 flagsToM4Macros() 只输出已存在的 flag,并按名称排序。
源码文件:system/sepolicy/build/soong/selinux_test.go
android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) {
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
})源码文件:system/sepolicy/build/soong/selinux_test.go
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,
)
}测试输入是产品 build flag map 与多个 se_flags module。关键断言是缺失 flag 不被伪造为 false,现有 flags 转换成稳定排序的 target_flag_* 参数。它验证 Soong 参数生成,不验证 flag 宏调用点的策略结果。
12.2 条件实验
Board API 实验可验证格式、比较方向与输出互斥性。
# 202504保留旧规则,202604保留新规则。
for level in 202504 202604; do
printf 'board_api=%s\n' "$level"
"$M4" -D target_board_api_level="$level" \
system/sepolicy/flagging/flagging_macros - <<'EOF' |
starting_at_board_api(202604, `allow demo new_api:file read;')
until_board_api(202604, `allow demo old_api:file read;')
EOF
sed '/^[[:space:]]*$/d'
done输入只有 target board API 与两条互补规则。断言是在边界值 202604 时 starting_at 生效、until 不生效。它验证 m4 比较逻辑,不证明 new_api/old_api type 已声明。
12.3 Neverallow双路径
Neverallow test 的第一步用含 build tests 的 conf 构建 binary policy;第二步用排除 build tests 的 conf 交给 sepolicy-analyze -w -f。
源码文件:system/sepolicy/build/soong/sepolicy_neverallow.go
rule.Command().BuiltTool("checkpolicy").
Flag("-M").
FlagWithArg("-c ", strconv.Itoa(PolicyVers)).
FlagWithOutput("-o ", binaryPolicy).
Input(checkpolicyConfPath)
rule.Command().BuiltTool("sepolicy-analyze").
Input(binaryPolicy).
Text("neverallow").
Flag("-w").
FlagWithInput("-f ", sepolicyAnalyzeConfPath)输入是同源但 target_exclude_build_test 不同的两份 conf。断言范围是普通与 expanded-attribute neverallow 都能被检查;它不验证设备运行时 denial,因为 neverallow 是构建期不变量。
13. 源码导航
| 问题 | 首选文件 | 关键符号 |
|---|---|---|
| 输入为什么按此顺序 | system/sepolicy/build/soong/policy.go | policyConfOrder、findPolicyConfOrder |
| m4 命令有哪些参数 | system/sepolicy/build/soong/policy.go | transformPolicyToConf |
| module 的源码集合是什么 | system/sepolicy/Android.bp | 各 se_policy_conf |
| Release flags 从哪里来 | system/sepolicy/flagging/Android.bp | se_policy_conf_flags_defaults |
| Flag与Board API如何判断 | system/sepolicy/flagging/flagging_macros | is_flag_enabled、starting_at_board_api |
| User/Treble条件如何裁剪 | system/sepolicy/public/te_macros | userdebug_or_eng、full_treble_only |
| policy.conf如何进CIL | system/sepolicy/build/soong/policy.go | compileConfToCil |
| CTS neverallow如何过滤 | system/sepolicy/tools/sepolicy_filter_neverallow.py | marker 与 statement 状态 |
| Neverallow为何构建两份conf | system/sepolicy/build/soong/sepolicy_neverallow.go | loadHook、双路径检查 |
从 is_flag_enabled(RELEASE_UNLOCKED_STORAGE_API, ...) 复述完整路径时,应包括:flag 在 flagging/Android.bp 中被 collector 导出,Soong 把它转成 -D target_flag_RELEASE_UNLOCKED_STORAGE_API=value,flagging_macros 先验证定义可见,再在 m4 扫描时保留或删除参数文本,-s 把调用点写入 policy.conf,checkpolicy 解析展开后的 type set 并生成 CIL,最终 secilc 合并策略。任何一层的输入不同,最后的规则集合都可能不同。
同样,m4 成功只说明文本扫描结束;policy.conf 中出现规则也不代表它通过 checkpolicy;生成 CIL 也不代表最终分区合并和 neverallow 测试通过。把这三个完成条件分开,才是调试 Android sepolicy 预处理的正确起点。
