Skip to content

M4预处理

从 Android Soong 输入排序、GNU m4 扫描与条件宏、同步行标记、policy.conf、checkpolicy/CIL 和 neverallow 分支解释 sepolicy 预处理。

基于android-17.0.0_r1
AndroidSELinuxM4Soongpolicy.confcheckpolicy源码阅读

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 的源码预处理至少包含三个清晰阶段:

  1. se_policy_conf 收集并排序输入,调用 m4 生成 policy.conf;
  2. se_policy_cil 用 checkpolicy 把 policy.conf 编译成 CIL,并可用 secilc 做合并校验;
  3. se_policy_binary 把多个 CIL 输入交给 secilc,生成最终 binary policy。

源码文件: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)
}

三个 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

make
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

make
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

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

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

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

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

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

make
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 上产生差异。

bash
# 按当前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

text
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 不会包含宏库的定义正文,只包含之后调用宏产生的规则。

bash
# 验证负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

text
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。引号保护文本在当前扫描层不展开,外层处理后引号被移除,结果重扫时仍可能展开。

bash
# 两次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

bash
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 构建保留参数规则。

bash
# 对同一调用比较三种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

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 让后续过滤工具保留版本条件边界。

bash
# 对比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'
done

5.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

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

make
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

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

text
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

text
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

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

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

bash
# 观察最小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

输出中可见:

text
#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

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

go
const (
	MlsSens    = 1
	MlsCats    = 1024
	PolicyVers = 30
)

m4 可能成功输出如下文本,但它仍不是合法策略:

text
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

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

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

make
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

python
# 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 扫描阶段终止,并带输入文件与行号。

bash
# 故意遗漏右括号,观察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。

bash
# 未传-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

关键输出包含:

text
fatal error: definition target_flag_RELEASE_MISSING_FLAG is not visible in policy

10.3 展开后语法 ​

m4 参数少传、拼接 type 名错误、条件展开留下空集合,都可能生成文本但无法被 checkpolicy 接受。最典型的漏参实验是:

bash
# 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'

输出是:

text
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 与输入文件。

bash
# 初始化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 f

module 构建继承真实产品 flags、board API、Treble/recovery 状态与 source filegroups。单宏实验适合理解语义,不能替代这一步。

11.2 Trace指定宏 ​

GNU m4 的 -t NAME 可以只追踪目标宏,避免 -dV 输出所有 macro definitions。

bash
# 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 顺序为:

text
m4trace: -1- set_prop
m4trace: -1- unix_socket_connect
m4trace: -1- get_prop

它证明重扫后的嵌套调用顺序,但不显示最终 policy permission 是否有效。

11.3 Dump定义 ​

dumpdef 把当前 m4 symbol 的 body 写到 stderr,可用于确认条件宏在加载后究竟被定义成什么。

bash
# 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
done

user 的定义是 suppression marker,userdebug 的定义是 $1。这比只观察一个调用点更直接地说明条件在定义加载期完成。

11.4 搜索产物 ​

定位条件规则时,建议同时搜索三个层次:

bash
# 源码调用点:规则是否被宏包裹。
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

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

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 实验可验证格式、比较方向与输出互斥性。

bash
# 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

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.gopolicyConfOrder、findPolicyConfOrder
m4 命令有哪些参数system/sepolicy/build/soong/policy.gotransformPolicyToConf
module 的源码集合是什么system/sepolicy/Android.bp各 se_policy_conf
Release flags 从哪里来system/sepolicy/flagging/Android.bpse_policy_conf_flags_defaults
Flag与Board API如何判断system/sepolicy/flagging/flagging_macrosis_flag_enabled、starting_at_board_api
User/Treble条件如何裁剪system/sepolicy/public/te_macrosuserdebug_or_eng、full_treble_only
policy.conf如何进CILsystem/sepolicy/build/soong/policy.gocompileConfToCil
CTS neverallow如何过滤system/sepolicy/tools/sepolicy_filter_neverallow.pymarker 与 statement 状态
Neverallow为何构建两份confsystem/sepolicy/build/soong/sepolicy_neverallow.goloadHook、双路径检查

从 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 预处理的正确起点。