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
// 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,所以后者可以使用r_file_perms、rw_file_perms等权限集合;attributes|*.te位于宏定义之后,所以普通策略文件调用binder_call()、set_prop()时,m4 已经知道它们的定义。
顶层 Android.bp 也把两类宏作为独立输入登记,而不是把它们视为同一个文件。
源码文件:system/sepolicy/Android.bp
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
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_macros | class/permission 名称 | class 集合或 permission 集合 | r_file_perms |
te_macros | domain、type、service 等语义参数 | 完整 TE 规则组合 | binder_call、set_prop |
后续的权限集合专题会解释 global_macros;本篇只在宏展开需要时展示它的结果,避免把文件归属写错。
1.3 M4命令
Soong 为 m4 注入构建变量,并把排序后的所有策略文件作为输入。--fatal-warnings 只把 m4 warning 升级为失败,不会替宏检查参数类型或数量。
源码文件: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())).
// 与本篇无关的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
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
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
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
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
define(`binder_call', `
allow $1 $2:binder { call transfer };
allow $2 $1:binder transfer;
allow $1 $2:fd use;
')三个方向不能合并为一句“允许 Binder 通信”:
| 规则 | source → target | 消费操作 |
|---|---|---|
binder call | client → server | Binder transaction |
binder transfer | 双向 | Binder object/reference 传递 |
fd use | client → server owner | 使用跨进程传来的 fd |
4.2 内核消费者
Binder transaction hook 检查 call;Binder 引用传递检查 transfer;文件描述符传递先检查接收方是否可使用发送方持有的 fd,再检查接收方对底层 inode 的访问。
源码文件:kernel/common/security/selinux/hooks.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
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
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
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
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
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
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 hook | set_prop 包含所需规则 |
property_service set 被拒绝 | Property Service MAC | set_prop 包含所需规则 |
| 属性名没有 Context | property 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
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
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
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
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
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
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
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
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
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
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
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
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
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_eng | target_build_variant | eng 或 userdebug |
not_full_treble | target_full_treble | 不等于 true |
full_treble_only | target_full_treble | true,CTS 模式还保留标记 |
build_test_only | target_exclude_build_test | 不等于 true |
recovery_only | target_recovery | true |
源码文件:system/sepolicy/public/te_macros
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
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
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
io_uring_use(logd)内核的 io_uring_setup() 路径最终消费 io_uring allowed;创建 SQPOLL 线程时消费 sqpoll。
源码文件:kernel/common/security/selinux/hooks.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。
# 展开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'预期输出包含四条规则:
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 规则是否出现。
# 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。
# 将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
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。
# 故意漏掉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:
allow client :binder { call transfer };
allow client:binder transfer;
allow client :fd use;因此 m4 --fatal-warnings 通过不等于策略有效。参数数量、拼接命名与 type 是否存在,要靠展开结果、checkpolicy 和最终策略查询共同确认。
11.2 权限选错层
| 现象 | 不足的宏 | 缺少的层 |
|---|---|---|
| 能 find 服务但 transact denied | service_manager find | binder_call |
| 能 transact 但 find denied | binder_call | service manager find |
| 能连接 Property Service 但 set denied | unix_socket_connect | property_service set |
| 有 HAL client attribute 但 find denied | hal_client_domain | hal_attribute_service/hwservice 绑定 |
| 服务有 add allow 但名字无映射 | add_service | service_contexts |
宏的职责边界比宏名更重要。排错应从 denial 的 class/permission 找运行时检查,再反推哪个宏族负责生成规则。
11.3 条件消失
源码中能搜索到 userdebug_or_eng(...),不代表 user 策略含其参数。user 构建会把参数替换为 CTS suppression marker;同理,not_full_treble 在 full Treble 上展开为空。
遇到“源码有规则、设备仍 denied”时,应依次确认:
- 调用是否被条件宏包裹;
- Soong 向 m4 注入的实际变量值;
- 生成
policy.conf是否出现展开后的 allow; - binary policy 的
sesearch是否能找到规则; - 运行对象的 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.go | policyConfOrder、m4 命令 |
| 权限集合在哪定义 | system/sepolicy/public/global_macros | class/permission set |
| 高层 TE 宏在哪定义 | system/sepolicy/public/te_macros | 宏定义与嵌套调用 |
| 哪些策略调用宏 | system/sepolicy/private/*.te、vendor/*.te | 具体 domain 与 type |
| Unix connectto 谁检查 | kernel/common/security/selinux/hooks.c | selinux_socket_unix_stream_connect |
| Binder call/fd 谁检查 | kernel/common/security/selinux/hooks.c | Binder hooks |
| 属性 set 谁检查 | system/core/init/property_service.cpp | CheckMacPerms、CheckPermissions |
| 服务 add/find 谁检查 | frameworks/native/cmds/servicemanager/Access.cpp | actionAllowedFromLookup |
| 注册拒绝如何返回 | frameworks/native/cmds/servicemanager/ServiceManager.cpp | canAddService |
面对一个陌生宏,按下面的顺序阅读最稳妥:先记录每个位置参数的语义,再递归展开嵌套宏;把生成物分成 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,而不只是记住宏调用格式。
