Skip to content

TE宏库

从 Soong 的 m4 输入顺序、嵌套宏展开、Binder/属性/服务/HAL 调用方与运行时检查解释 Android te_macros 的真实作用。

基于android-17.0.0_r1
AndroidSELinuxte_macrosm4BinderHAL源码阅读

TE宏库 ​

本文面向已经读过 TE规则语法、Type与Attribute 和 Type Transition 的读者。前三篇分别建立了规则、集合与 transition 的运行语义,本篇只回答宏这一层的问题:te_macros 在构建链中何时展开,一个宏调用最终生成哪些 allow、typeattribute、type_transition 与 neverallow,生成结果由哪个运行时入口消费,以及为什么宏名相同而 user、userdebug、Treble 与非 Treble 产品的最终策略可能不同。

本文不逐行罗列 1196 行宏库,也不重复上一篇已经展开的 domain/file transition。重点选择 Unix socket、Binder、属性、服务注册、HAL、条件策略和约束型宏,建立一套可迁移的阅读方法。读完后,你应能从 .te 中的一个宏调用追到宏定义、嵌套依赖、最终规则、运行时权限检查和失败日志,而不是把宏当成不可拆解的“权限套餐”。

1. 构建位置 ​

1.1 输入顺序 ​

te_macros 不是 checkpolicy 直接理解的语法。Soong 先把平台、设备与产品策略按固定顺序交给 GNU m4,宏调用在生成 policy.conf 时消失,随后 checkpolicy 才解析展开后的 SELinux 规则。

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

这个顺序解释了两个重要事实:

  1. global_macros 先于 te_macros,所以后者可以使用 r_file_perms、rw_file_perms 等权限集合;
  2. attributes|*.te 位于宏定义之后,所以普通策略文件调用 binder_call()、set_prop() 时,m4 已经知道它们的定义。

顶层 Android.bp 也把两类宏作为独立输入登记,而不是把它们视为同一个文件。

源码文件:system/sepolicy/Android.bp

make
se_build_files {
    name: "se_build_files",
    srcs: [
        "security_classes",
        "initial_sids",
        "access_vectors",
        "global_macros",
        "neverallow_macros",
        "mls_macros",
        "mls_decl",
        "mls",
        "policy_capabilities",
        "te_macros",
        "attributes",
        "ioctl_defines",
        "ioctl_macros",
        "nlmsg_defines",
        "nlmsg_macros",
        "*.te",
        // roles、users、contexts等后续输入省略。
    ],
}

1.2 宏库边界 ​

Android 17 的 r_file_perms 不在 te_macros,而在 global_macros。te_macros 消费这些权限集合来构造更高层能力。

源码文件:system/sepolicy/public/global_macros

text
define(`r_file_perms', `{ getattr open read ioctl lock map watch watch_reads }')
define(`w_file_perms', `{ open append write lock map }')
define(`rw_file_perms', `{ r_file_perms w_file_perms }')
define(`create_file_perms', `{ create rename setattr unlink rw_file_perms }')

define(`r_dir_perms', `{ open getattr read search ioctl lock watch watch_reads }')
define(`w_dir_perms', `{ open search write add_name remove_name lock }')
define(`rw_dir_perms', `{ r_dir_perms w_dir_perms }')
define(`create_dir_perms', `{ create reparent rename rmdir setattr rw_dir_perms }')

两层的职责可以这样区分:

层次输入输出例子
global_macrosclass/permission 名称class 集合或 permission 集合r_file_perms
te_macrosdomain、type、service 等语义参数完整 TE 规则组合binder_call、set_prop

后续的权限集合专题会解释 global_macros;本篇只在宏展开需要时展示它的结果,避免把文件归属写错。

1.3 M4命令 ​

Soong 为 m4 注入构建变量,并把排序后的所有策略文件作为输入。--fatal-warnings 只把 m4 warning 升级为失败,不会替宏检查参数类型或数量。

源码文件: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())).
	// 与本篇无关的coverage等构建变量省略。
	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("-s").
	Inputs(srcsWithNewline).
	Text("> ").Output(conf)

policy.conf 才是 checkpolicy 的直接输入。运行期内核不会保存“这是由 binder_call 生成的”这类来源信息,只保存编译后的 type、class、permission 与条件节点。

2. 展开语义 ​

2.1 参数替换 ​

m4 用 $1、$2、$3 表示位置参数。SELinux type 并不是 m4 类型系统中的对象;对 m4 来说,它们只是文本。因此宏作者必须在注释与命名中约定每个位置应传 domain、file type、service type 还是 attribute。

r_dir_file 接收 domain 与 target type,展开两条 allow,并继续调用 r_dir_perms、r_file_perms 权限集合。

源码文件:system/sepolicy/public/te_macros

text
define(`r_dir_file', `
allow $1 $2:dir r_dir_perms;
allow $1 $2:{ file lnk_file } r_file_perms;
')

调用 r_dir_file(demo, config_data) 时,m4 先替换两个位置参数,再递归展开 permission 宏。最终结果不含 r_dir_file、r_dir_perms 或 r_file_perms 名字。

2.2 嵌套展开 ​

高层宏经常调用其他高层宏。set_prop 是最清晰的例子:它先调用 unix_socket_connect 获得连接 Property Service 的传输权限,再生成 property_service set,最后调用 get_prop 获得读取属性区的文件权限。

2.3 宏不在运行期 ​

看到 AVC 中的 connectto、call、find 或 set 时,不能期待日志告诉你应该补哪个宏。运行时只知道实际被拒绝的 class 与 permission。宏是策略作者的构建期抽象,同一 permission 既可能来自宏,也可能来自手写 allow。

这也意味着“换成宏就会生效”是错误判断。宏只能生成规则;source/target type 是否正确、对象是否标成预期 type、条件宏是否保留、neverallow 是否允许编译,仍需分别验证。

3. Unix Socket ​

3.1 传输规则 ​

unix_socket_connect(client, socket, server) 生成两条规则:客户端可写 socket 文件节点,并可对服务端 Unix stream socket 执行 connectto。

源码文件:system/sepolicy/public/te_macros

text
define(`unix_socket_connect', `
allow $1 $2_socket:sock_file write;
allow $1 $3:unix_stream_socket connectto;
')

第二个参数不是完整 type。宏会自动拼接 _socket,因此 unix_socket_connect(system_server, lmkd, lmkd) 的文件 target 是 lmkd_socket,进程/套接字 target 是 lmkd。

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

text
unix_socket_connect(system_server, lmkd, lmkd)
unix_socket_connect(system_server, zygote, zygote)
unix_socket_connect(system_server, zygote_next, zygote_next)
unix_socket_connect(system_server, uncrypt, uncrypt)

3.2 内核检查 ​

文件节点的 write 在路径访问阶段检查;连接建立时,Unix stream LSM hook 消费 connectto。hook 使用连接方 socket SID 作为 source,监听 socket SID 作为 target。

源码文件:kernel/common/security/selinux/hooks.c

c
static int selinux_socket_unix_stream_connect(struct sock *sock,
					      struct sock *other,
					      struct sock *newsk)
{
	struct sk_security_struct *sksec_sock = selinux_sock(sock);
	struct sk_security_struct *sksec_other = selinux_sock(other);
	struct sk_security_struct *sksec_new = selinux_sock(newsk);
	struct common_audit_data ad;
	struct lsm_network_audit net;
	int err;

	ad_net_init_from_sk(&ad, &net, other);

	err = avc_has_perm(sksec_sock->sid, sksec_other->sid,
			   sksec_other->sclass,
			   UNIX_STREAM_SOCKET__CONNECTTO, &ad);
	if (err)
		return err;

	/* server child socket */
	sksec_new->peer_sid = sksec_sock->sid;
	err = security_sid_mls_copy(sksec_other->sid,
				    sksec_sock->sid, &sksec_new->sid);
	if (err)
		return err;

	/* connecting socket */
	sksec_sock->peer_sid = sksec_new->sid;

	return 0;
}

连接成功后,内核还把双方 peer SID 写入 socket security state。宏本身不负责 peer 状态,它只为这次 hook 检查提供 allow。

3.3 两段失败 ​

denial失败阶段常见原因
sock_file write打开或写 socket 路径socket 文件标签错误、缺第一条 allow
unix_stream_socket connectto建立连接服务端进程/sock SID 不符合第三个参数

只补 connectto 不能修复路径节点 denial;只补 sock_file write 也不能让内核接受跨域连接。unix_socket_connect 把两个阶段放在一起,但排错时仍应按对象层级拆开。

4. Binder宏 ​

4.1 三条规则 ​

binder_call(client, server) 不只生成 call。它允许客户端向服务端调用并传递 Binder 引用,允许服务端在 reply 中向客户端传递引用,还允许客户端使用服务端传来的文件描述符。

源码文件:system/sepolicy/public/te_macros

text
define(`binder_call', `
allow $1 $2:binder { call transfer };
allow $2 $1:binder transfer;
allow $1 $2:fd use;
')

三个方向不能合并为一句“允许 Binder 通信”:

规则source → target消费操作
binder callclient → serverBinder transaction
binder transfer双向Binder object/reference 传递
fd useclient → server owner使用跨进程传来的 fd

4.2 内核消费者 ​

Binder transaction hook 检查 call;Binder 引用传递检查 transfer;文件描述符传递先检查接收方是否可使用发送方持有的 fd,再检查接收方对底层 inode 的访问。

源码文件:kernel/common/security/selinux/hooks.c

c
static int selinux_binder_transaction(const struct cred *from,
				      const struct cred *to)
{
	u32 mysid = current_sid();
	u32 fromsid = cred_sid(from);
	u32 tosid = cred_sid(to);
	int rc;

	if (mysid != fromsid) {
		rc = avc_has_perm(mysid, fromsid, SECCLASS_BINDER,
				  BINDER__IMPERSONATE, NULL);
		if (rc)
			return rc;
	}

	return avc_has_perm(fromsid, tosid,
			    SECCLASS_BINDER, BINDER__CALL, NULL);
}

static int selinux_binder_transfer_binder(const struct cred *from,
					  const struct cred *to)
{
	return avc_has_perm(cred_sid(from), cred_sid(to),
			    SECCLASS_BINDER, BINDER__TRANSFER,
			    NULL);
}

源码文件:kernel/common/security/selinux/hooks.c

c
static int selinux_binder_transfer_file(const struct cred *from,
					const struct cred *to,
					const struct file *file)
{
	u32 sid = cred_sid(to);
	struct file_security_struct *fsec = selinux_file(file);
	struct dentry *dentry = file->f_path.dentry;
	struct inode_security_struct *isec;
	struct common_audit_data ad;
	int rc;

	ad.type = LSM_AUDIT_DATA_PATH;
	ad.u.path = file->f_path;

	if (sid != fsec->sid) {
		rc = avc_has_perm(sid, fsec->sid,
				  SECCLASS_FD,
				  FD__USE,
				  &ad);
		if (rc)
			return rc;
	}

#ifdef CONFIG_BPF_SYSCALL
	rc = bpf_fd_pass(file, sid);
	if (rc)
		return rc;
#endif

	if (unlikely(IS_PRIVATE(d_backing_inode(dentry))))
		return 0;

	isec = backing_inode_security(dentry);
	return avc_has_perm(sid, isec->sid, isec->sclass, file_to_av(file),
			    &ad);
}

fd use 通过后不代表接收方可以任意操作文件。最后一行仍按 inode type 与实际打开模式检查 read/write/ioctl 等权限。因此 binder_call 解决的是 fd 所有权跨域使用,不替代文件对象权限。

4.3 不含服务发现 ​

binder_call 不生成 service_manager find,也不生成 service_manager add。客户端可能有调用服务进程的 Binder 权限,却在获取 handle 时先被 ServiceManager 拒绝;服务端也可能能处理 Binder transaction,却无法注册名字。

binder_use(domain) 只允许 domain 与 servicemanager 互调和传递引用,也不代表它能 find 任意服务。

源码文件:system/sepolicy/public/te_macros

text
define(`binder_use', `
allow $1 servicemanager:binder { call transfer };
allow servicemanager $1:binder { call transfer };
')

5. Property宏 ​

5.1 组合能力 ​

set_prop(source, property_type) 组合 Unix socket 传输、Property Service 操作权限与属性读取权限。

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

define(`get_prop', `
allow $1 $2:file { getattr open read map };
')

以 fingerprint HAL 为例,调用点只写一行,展开后至少涉及 property socket、init Unix socket、property type 的 property_service set 和 property file read/map。

源码文件:system/sepolicy/vendor/hal_fingerprint_default.te

text
set_prop(hal_fingerprint_default, virtual_fingerprint_prop)

5.2 用户空间检查 ​

请求到达 init 后,Property Service 根据属性名从 property contexts 查出 target Context,再用调用方 Context、property_service class 和 set permission 调用 libselinux。

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

cpp
static bool CheckMacPerms(const std::string& name, const char* target_context,
                          const char* source_context, const ucred& cr) {
    if (!target_context || !source_context) {
        return false;
    }

    PropertyAuditData audit_data;

    audit_data.name = name.c_str();
    audit_data.cr = &cr;

    auto lock = std::lock_guard{selinux_check_access_lock};
    return selinux_check_access(source_context, target_context, "property_service", "set",
                                &audit_data) == 0;
}

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

cpp
const char* target_context = nullptr;
const char* type = nullptr;
property_info_area->GetPropertyInfo(name.c_str(), &target_context, &type);

if (!CheckMacPerms(name, target_context, source_context.c_str(), cr)) {
    *error = StringPrintf(
            "SELinux permission check failed "
            "(source_context=%s, target_context=%s)",
            source_context.c_str(), target_context ?: "(null)");
    return PROP_ERROR_PERMISSION_DENIED;
}

if (!CheckType(type, value)) {
    *error = StringPrintf("Property type check failed, value doesn't match expected type '%s'",
                          (type ?: "(null)"));
    return PROP_ERROR_INVALID_VALUE;
}

Property Service 还会在 MAC 通过后验证值类型。因此 set_prop 只能解决 SELinux 权限,不能让非法 enum、bool、int 或格式错误的值通过 CheckType()。

5.3 失败分层 ​

现象所在层宏能否直接解决
无法写 property_socket传输路径set_prop 包含所需规则
connectto init 被拒绝Unix socket hookset_prop 包含所需规则
property_service set 被拒绝Property Service MACset_prop 包含所需规则
属性名没有 Contextproperty contexts不能,只改宏无效
属性值类型错误Property Service schema不能,只改宏无效

排查属性设置不能只看最后一条 denial。请求甚至可能在 socket 路径阶段就到不了 CheckMacPerms()。

6. Service注册 ​

6.1 Add与Find ​

add_service(domain, service_type) 允许指定 domain 对服务执行 add 与 find,并用 neverallow 阻止其他 domain 注册同一 service type。

源码文件:system/sepolicy/public/te_macros

text
define(`add_service', `
  allow $1 $2:service_manager { add find };
  neverallow { domain -$1 } $2:service_manager add;

  userdebug_or_eng(`
    allow $1 su:tcp_socket { accept getopt read write };
  ')
')

这个宏同时包含能力和所有权约束。若另一个 domain 手写 allow ... add,不是“覆盖” neverallow,而是在策略编译阶段发生冲突。

真实调用点 mediatuner 还需要 binder_use、面向客户端/服务端的 binder_call 与其他服务的 find。这说明注册、发现和 transaction 是三组独立权限。

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

text
binder_use(mediatuner)
binder_call(mediatuner, appdomain)
binder_service(mediatuner)

add_service(mediatuner, mediatuner_service)
allow mediatuner system_server:fd use;
allow mediatuner tv_tuner_resource_mgr_service:service_manager find;
allow mediatuner package_native_service:service_manager find;
binder_call(mediatuner, system_server)

6.2 名字到Context ​

ServiceManager 的 Access 层先用服务名字查询 service_contexts,得到 target Context,再检查 service_manager add/find。

源码文件:frameworks/native/cmds/servicemanager/Access.cpp

cpp
bool Access::canFind(const CallingContext& ctx,const std::string& name) {
    return actionAllowedFromLookup(ctx, name, "find");
}

bool Access::canAdd(const CallingContext& ctx, const std::string& name) {
    return actionAllowedFromLookup(ctx, name, "add");
}

bool Access::actionAllowed(const CallingContext& sctx, const char* tctx, const char* perm,
        const std::string& tname) {
#ifdef __ANDROID__
    const char* tclass = "service_manager";

    AuditCallbackData data = {
        .context = &sctx,
        .tname = &tname,
    };

    return 0 == selinux_check_access(sctx.sid.c_str(), tctx, tclass, perm,
        reinterpret_cast<void*>(&data));
#else
    (void)sctx;
    (void)tctx;
    (void)perm;
    (void)tname;

    return true;
#endif
}

源码文件:frameworks/native/cmds/servicemanager/Access.cpp

cpp
bool Access::actionAllowedFromLookup(const CallingContext& sctx, const std::string& name,
                                     const char *perm) {
#ifdef __ANDROID__
    char *tctx = nullptr;
    if (selabel_lookup(getSehandle(), &tctx, name.c_str(),
                       SELABEL_CTX_ANDROID_SERVICE) != 0) {
        LOG(ERROR) << "SELinux: No match for " << name << " in service_contexts.\n";
        return false;
    }

    bool allowed = actionAllowed(sctx, tctx, perm, name);
    freecon(tctx);
    return allowed;
#else
    (void)sctx;
    (void)name;
    (void)perm;
    (void)kIsVendor;

    return true;
#endif
}

服务名字没有 service_contexts 映射时,Access 在执行 SELinux permission query 之前就返回 false。此时新增 add_service 宏调用仍不完整,因为 $2 对应的 service type 没有被名字解析路径使用。

6.3 拒绝返回 ​

ServiceManager 把 canAdd() 的 false 转成安全异常,服务不会写入注册表。

源码文件:frameworks/native/cmds/servicemanager/ServiceManager.cpp

cpp
Status ServiceManager::canAddService(const Access::CallingContext& ctx, const std::string& name,
                                     std::optional<std::string>* accessor) {
    if (!mAccess->canAdd(ctx, name)) {
        return Status::fromExceptionCode(Status::EX_SECURITY, "SELinux denied for service.");
    }
#ifndef VENDORSERVICEMANAGER
    *accessor = getVintfAccessorName(name);
#endif
    if (accessor->has_value()) {
        if (!mAccess->canAdd(ctx, accessor->value())) {
            return Status::fromExceptionCode(Status::EX_SECURITY,
                                             "SELinux denied for the accessor of the service.");
        }
    }
    return Status::ok();
}

7. HAL模板 ​

HAL 宏不是单个“允许访问 HAL”的开关,而是一组 attribute 建模、服务发现、注册所有权、Binder/HwBinder 与非 Treble passthrough 兼容规则。

7.1 三个Attribute ​

hal_attribute(name) 为每个 HAL 建立基础、client、server 三个 attribute,并限制 server/client 标记必须与全局 HAL attribute 一致。

源码文件:system/sepolicy/public/te_macros

text
define(`hal_attribute', `
attribute hal_$1;
expandattribute hal_$1 true;
attribute hal_$1_client;
expandattribute hal_$1_client true;
attribute hal_$1_server;
expandattribute hal_$1_server false;

neverallow { hal_$1_server -halserverdomain } domain:process fork;
build_test_only(`
neverallow { hal_$1_server -hal_$1 } domain:process fork;
neverallow { hal_$1_client -halclientdomain } domain:process fork;
')
')

平台在 attributes 输入中批量调用它。hal_attribute(audio) 最终生成 hal_audio、hal_audio_client 和 hal_audio_server。

源码文件:system/sepolicy/public/attributes

text
hal_attribute(allocator);
hal_attribute(atrace);
hal_attribute(audio);
hal_attribute(audiocontrol);
hal_attribute(authgraph);
hal_attribute(authsecret);
hal_attribute(bluetooth);

7.2 Server标记 ​

hal_server_domain(concrete_domain, hal_type) 只建立 attribute 成员关系,不生成服务注册或 Binder transaction 权限。

源码文件:system/sepolicy/public/te_macros

text
define(`hal_server_domain', `
typeattribute $1 halserverdomain;
typeattribute $1 $2_server;
typeattribute $1 $2;
')

Audio HAL 默认实现把具体 hal_audio_default domain 加入三个集合;init transition 则由另一宏单独建立。

源码文件:system/sepolicy/vendor/hal_audio_default.te

text
type hal_audio_default, domain;
hal_server_domain(hal_audio_default, hal_audio)

type hal_audio_default_exec, exec_type, vendor_file_type, file_type;
init_daemon_domain(hal_audio_default)

7.3 Client标记 ​

hal_client_domain 总是添加 halclientdomain 与具体 client attribute,并允许双方访问彼此的 memfd。只有非 full Treble 构建才把客户端加入基础 HAL attribute,并授予加载 passthrough vendor 实现的目录与文件权限。

源码文件:system/sepolicy/public/te_macros

text
define(`hal_client_domain', `
typeattribute $1 halclientdomain;
typeattribute $1 $2_client;
allow $1 $2:memfd_file { read write getattr map };
allow $2 $1:memfd_file { read write getattr map };

not_full_treble(`
typeattribute $1 $2;
allow $2 system_file:dir r_dir_perms;
allow $2 vendor_file:dir r_dir_perms;
allow $2 vendor_file:file { read open getattr execute map };
')
')

system_server 对多个 HAL 调用该宏,但这组调用本身不等于可以 find 每个 AIDL/HIDL service;service type 绑定由 HAL 专属策略继续完成。

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

text
hal_client_domain(system_server, hal_allocator)
hal_client_domain(system_server, hal_audio)
hal_client_domain(system_server, hal_authgraph)
hal_client_domain(system_server, hal_authsecret)
hal_client_domain(system_server, hal_bluetooth)
hal_client_domain(system_server, hal_broadcastradio)

7.4 Service绑定 ​

hal_attribute_service 允许 client attribute 查找 service type,并让 server attribute 通过 add_service 获得注册权和独占 neverallow。

源码文件:system/sepolicy/public/te_macros

text
define(`hal_attribute_service', `
  allow $1_client $2:service_manager find;
  add_service($1_server, $2)

  build_test_only(`
    neverallow {
        domain
        -$1_client
        -$1_server
        -atrace
        -shell
        -system_app
        -traceur_app
    } $2:service_manager find;
  ')
')

Audio HAL 策略同时描述 Binder 方向、HwBinder/AIDL 服务类型和 servicemanager 通信,展示了模板的组合方式。

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

text
binder_call(hal_audio_client, hal_audio_server)
binder_call(hal_audio_server, hal_audio_client)

hal_attribute_hwservice(hal_audio, hal_audio_hwservice)
hal_attribute_service(hal_audio, hal_audio_service)

binder_call(hal_audio_server, servicemanager)

8. 条件宏 ​

8.1 构建变量 ​

条件宏读取的是 Soong 通过 -D 注入的字符串,而不是设备运行时属性。生成 binary policy 后,target_build_variant 与 target_full_treble 这些 m4 变量已经不存在。

常见条件如下:

宏输入变量保留规则的条件
userdebug_or_engtarget_build_varianteng 或 userdebug
not_full_trebletarget_full_treble不等于 true
full_treble_onlytarget_full_trebletrue,CTS 模式还保留标记
build_test_onlytarget_exclude_build_test不等于 true
recovery_onlytarget_recoverytrue

源码文件:system/sepolicy/public/te_macros

text
define(`not_full_treble', ifelse(target_full_treble, `true', , $1))
define(`build_test_only', ifelse(target_exclude_build_test, `true', , $1))
define(`recovery_only', ifelse(target_recovery, `true', $1, ))
define(`not_recovery', ifelse(target_recovery, `true', , $1))

8.2 Treble分叉 ​

前面的 hal_client_domain 在 full Treble 下只保留 client 标记和 memfd 权限;非 full Treble 下额外允许 passthrough 实现。两种产品对同一宏调用生成不同规则,不是运行时 feature flag 分支。

8.3 Test-only约束 ​

build_test_only 常包裹辅助 neverallow。正常平台构建会编译这些断言,用来尽早发现 attribute 使用错误;某些只为兼容测试生成的策略可通过 target_exclude_build_test=true 排除它们。

这类宏不能理解成“测试环境才有的运行权限”。它包裹的通常是编译期约束,移除后改变的是错误发现能力,不是给进程增加一条运行 allow。

9. 约束型宏 ​

宏不一定扩大权限。app_domain 与 io_uring_use 同时创建类型、生成 transition、授予最小权限并添加 neverallow,用一个调用建立完整的不变量。

9.1 App边界 ​

app_domain 先把具体 domain 加入 appdomain,为 tmpfs 与 userfaultfd 建立专用类型,再限制跨 app 文件、ptrace、匿名 inode 和 io_uring。

源码文件:system/sepolicy/public/te_macros

text
define(`app_domain', `
typeattribute $1 appdomain;
type_transition $1 tmpfs:file appdomain_tmpfs;
userfaultfd_use($1)
allow $1 appdomain_tmpfs:file { execute getattr map read write };
allow $1 appdomain:memfd_file { execute getattr map read write };
neverallow { $1 -runas_app -shell -simpleperf } { domain -$1 }:file no_rw_file_perms;
neverallow { appdomain -runas_app -shell -simpleperf -$1 } $1:file no_rw_file_perms;
neverallow { domain -$1 -crash_dump userdebug_or_eng(`-llkd') -runas_app -simpleperf } $1:process ptrace;
neverallow $1 ~{ $1_userfaultfd system_server_wrapfd }:anon_inode *;
type $1_block_io_uring;
type_transition $1 $1:anon_inode $1_block_io_uring "[io_uring]";
type $1_wrapfd;
type_transition $1 $1:anon_inode $1_wrapfd "[wrapfd]";
')

$1_block_io_uring 与 $1_wrapfd 是按参数生成的新 type。宏调用不只是复用规则,还能生成与 domain 同名派生符号。若同一 domain 重复调用该宏,类型重复声明会在编译期失败。

9.2 IoUring能力 ​

明确需要 io_uring 的 domain 使用 io_uring_use,得到专用匿名 inode type、create/map/read/write、io_uring allowed/sqpoll 与禁止其他 domain 使用该匿名 inode 的 neverallow。

源码文件:system/sepolicy/public/te_macros

text
define(`io_uring_use', `
type $1_iouring;
type_transition $1 $1:anon_inode $1_iouring "[io_uring]";
allow $1 $1_iouring:anon_inode { create map read write };
allow $1 self:io_uring { sqpoll allowed };
neverallow { domain -$1 } $1_iouring:anon_inode *;
dontaudit $1 self:global_capability_class_set ipc_lock;
')

Android 17 中 logd、statsd、fastbootd 与 snapuserd 等 domain 显式调用该宏,而普通 app 由 app_domain 的命名 transition 与 neverallow 阻断。

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

text
io_uring_use(logd)

内核的 io_uring_setup() 路径最终消费 io_uring allowed;创建 SQPOLL 线程时消费 sqpoll。

源码文件:kernel/common/security/selinux/hooks.c

c
static int selinux_uring_sqpoll(void)
{
	u32 sid = current_sid();

	return avc_has_perm(sid, sid,
			    SECCLASS_IO_URING, IO_URING__SQPOLL, NULL);
}

static int selinux_uring_allowed(void)
{
	u32 sid = current_sid();

	return avc_has_perm(sid, sid, SECCLASS_IO_URING, IO_URING__ALLOWED,
			    NULL);
}

9.3 所有权约束 ​

add_service 的 neverallow、io_uring_use 的匿名 inode neverallow、hal_attribute_service 的 client allowlist 都表达“谁可以拥有或发现资源”。删除宏并改成几条 allow,往往会丢掉这些负向不变量。

因此评审宏替换时不能只比较新增 allow。至少还要比较:

  • 是否创建了 type 或 attribute;
  • 是否生成 type transition;
  • 是否包含 neverallow 或 neverallowxperm;
  • 是否调用其他宏;
  • 是否受构建变量控制。

10. 验证方法 ​

10.1 单宏展开 ​

可以直接用本地 m4 对一个宏做最小展开。输入顺序必须先 global_macros、后 te_macros,并为本次用到的条件提供 -D。

bash
# 展开set_prop,并过滤宏文件带来的空行和注释。
printf '%s\n' 'set_prop(demo, demo_prop)' | \
  m4 --fatal-warnings \
    -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 - | \
  sed '/^#/d;/^[[:space:]]*$/d'

预期输出包含四条规则:

text
allow demo property_socket:sock_file write;
allow demo init:unix_stream_socket connectto;
allow demo demo_prop:property_service set;
allow demo demo_prop:file { getattr open read map };

实验输入是宏定义、三个构建变量和一次调用。断言是嵌套宏全部消失,输出只剩合法 TE 规则。它能验证文本展开,却不能证明 demo_prop 已声明、属性名已映射到该 type,或最终 neverallow 可以通过。

10.2 条件差异 ​

对同一 hal_client_domain 分别设置 full Treble true/false,可以直接观察 passthrough 规则是否出现。

bash
# full Treble:只保留client标记与memfd规则。
printf '%s\n' 'hal_client_domain(demo, hal_audio)' | \
  m4 --fatal-warnings \
    -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 - | \
  sed '/^#/d;/^[[:space:]]*$/d'

# 非full Treble:额外出现hal_audio成员关系和vendor文件执行权限。
printf '%s\n' 'hal_client_domain(demo, hal_audio)' | \
  m4 --fatal-warnings \
    -D target_build_variant=user \
    -D target_full_treble=false \
    -D target_exclude_build_test=false \
    system/sepolicy/public/global_macros \
    system/sepolicy/public/te_macros - | \
  sed '/^#/d;/^[[:space:]]*$/d'

关键断言是第二次输出比第一次多出 typeattribute demo hal_audio 和三条查找/执行 passthrough HAL 文件的规则。这个实验验证构建条件的展开差异,不说明某台设备在运行时采用哪种架构;产品值应回到实际 Soong 配置确认。

10.3 最终策略 ​

单宏实验之后,还要查询产品编译出的最终策略。宏可能被条件抑制,也可能因 attribute 展开影响远多于调用点表面显示的 type。

bash
# 将PRODUCT替换为实际产品名。
ANDROID_SEPOLICY='out/target/product/PRODUCT/root/sepolicy'

# 验证Binder三类权限是否进入最终策略。
sesearch -A -s mediatuner -t system_server \
  -c binder -p call "$ANDROID_SEPOLICY"
sesearch -A -s mediatuner -t system_server \
  -c fd -p use "$ANDROID_SEPOLICY"

# 验证服务注册与发现权限。
sesearch -A -s mediatuner -t mediatuner_service \
  -c service_manager "$ANDROID_SEPOLICY"

输入是最终 binary policy。断言分别对应 binder_call 的 transaction/fd 规则,以及 add_service 的 add/find。查询成功仍不能证明服务名字已正确写入 service_contexts,运行时名字映射要单独验证。

10.4 服务测试 ​

ServiceManager 单元测试用可控的 MockAccess 让 canAdd() 返回 false,并断言 addService() 返回失败。

源码文件:frameworks/native/cmds/servicemanager/test_sm.cpp

cpp
TEST(AddService, NoPermissions) {
    std::unique_ptr<MockAccess> access = std::make_unique<NiceMock<MockAccess>>();

    EXPECT_CALL(*access, getCallingContext()).WillOnce(Return(Access::CallingContext{}));
    EXPECT_CALL(*access, canAdd(_, _)).WillOnce(Return(false));

    sp<ServiceManager> sm = sp<NiceMock<MockServiceManager>>::make(std::move(access));

    EXPECT_FALSE(sm->addService("foo", getBinder(), false /*allowIsolated*/,
        IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT).isOk());
}

测试输入是服务名 foo、一个 Binder 对象和拒绝注册的 Access mock。关键断言是 addService() 不成功,说明注册表不会绕过 Access 层继续提交。它验证 ServiceManager 对授权结果的消费,不验证真实 service_contexts、binary policy 或 add_service 宏展开。

11. 故障定位 ​

11.1 参数缺失 ​

m4 不理解 binder_call 的第二个参数必须是 server domain。漏传参数时,m4 仍可能成功,并生成缺 target 的规则;真正的语法错误留给后续 checkpolicy。

bash
# 故意漏掉server参数,观察m4不会做语义校验。
printf '%s\n' 'binder_call(client)' | \
  m4 --fatal-warnings \
    -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 - | \
  sed '/^#/d;/^[[:space:]]*$/d'

输出会出现空 target:

text
allow client :binder { call transfer };
allow  client:binder transfer;
allow client :fd use;

因此 m4 --fatal-warnings 通过不等于策略有效。参数数量、拼接命名与 type 是否存在,要靠展开结果、checkpolicy 和最终策略查询共同确认。

11.2 权限选错层 ​

现象不足的宏缺少的层
能 find 服务但 transact deniedservice_manager findbinder_call
能 transact 但 find deniedbinder_callservice manager find
能连接 Property Service 但 set deniedunix_socket_connectproperty_service set
有 HAL client attribute 但 find deniedhal_client_domainhal_attribute_service/hwservice 绑定
服务有 add allow 但名字无映射add_serviceservice_contexts

宏的职责边界比宏名更重要。排错应从 denial 的 class/permission 找运行时检查,再反推哪个宏族负责生成规则。

11.3 条件消失 ​

源码中能搜索到 userdebug_or_eng(...),不代表 user 策略含其参数。user 构建会把参数替换为 CTS suppression marker;同理,not_full_treble 在 full Treble 上展开为空。

遇到“源码有规则、设备仍 denied”时,应依次确认:

  1. 调用是否被条件宏包裹;
  2. Soong 向 m4 注入的实际变量值;
  3. 生成 policy.conf 是否出现展开后的 allow;
  4. binary policy 的 sesearch 是否能找到规则;
  5. 运行对象的 source/target Context 是否与规则一致。

11.4 Neverallow冲突 ​

约束型宏可能在远离调用点的位置生成 neverallow。典型场景包括:

  • 第二个 domain 尝试注册已由 add_service 独占的 service type;
  • HAL server 加入 $hal_server 却没有 halserverdomain;
  • 其他 domain 访问 io_uring_use 生成的专用匿名 inode;
  • app 策略试图为 [io_uring] 声明与 app_domain 冲突的 named transition。

此时不应删除 neverallow 来让编译通过。先展开宏,确定它保护的所有权或隔离不变量,再修正 domain/attribute/service 建模。

12. 源码导航 ​

要回答的问题源码入口继续追踪
宏何时展开system/sepolicy/build/soong/policy.gopolicyConfOrder、m4 命令
权限集合在哪定义system/sepolicy/public/global_macrosclass/permission set
高层 TE 宏在哪定义system/sepolicy/public/te_macros宏定义与嵌套调用
哪些策略调用宏system/sepolicy/private/*.te、vendor/*.te具体 domain 与 type
Unix connectto 谁检查kernel/common/security/selinux/hooks.cselinux_socket_unix_stream_connect
Binder call/fd 谁检查kernel/common/security/selinux/hooks.cBinder hooks
属性 set 谁检查system/core/init/property_service.cppCheckMacPerms、CheckPermissions
服务 add/find 谁检查frameworks/native/cmds/servicemanager/Access.cppactionAllowedFromLookup
注册拒绝如何返回frameworks/native/cmds/servicemanager/ServiceManager.cppcanAddService

面对一个陌生宏,按下面的顺序阅读最稳妥:先记录每个位置参数的语义,再递归展开嵌套宏;把生成物分成 allow、attribute、transition、type 声明、neverallow 和条件分支;然后按 class/permission 找运行时消费者;最后用单宏 m4 实验与最终 binary policy 查询互相校验。

以 hal_attribute_service(hal_audio, hal_audio_service) 为例,完整复述应包括:client attribute 获得 find,server attribute 通过 add_service 获得 add/find,其他 domain 被 neverallow 禁止 add,build-test 约束限制额外 finder;服务名字还必须映射到 hal_audio_service,ServiceManager 才会以该 target Context 执行检查。能够讲清这些阶段,才算真正理解 te_macros,而不只是记住宏调用格式。