客体类别与权限
本文面向已经读过 类型强制、LSM框架 和 安全上下文 的读者。TE 规则中的 target_type:class permission 只有 class 和 permission 对得上实际 object manager 才有意义:file read、dir search、binder call、service_manager find、property_service set 是五个不同决策面,不能用名字相似的权限互相替代。
本文不复制一张难以维护的权限全表,而是沿定义、编译映射和真实消费者阅读 Android 17 源码:security_classes 声明 class;access_vectors 定义 common 与 class-specific permissions;kernel classmap.h 把内核 hook 的名称映射到策略;servicemanager 和 property service 作为用户空间 object manager,直接以字符串 class/permission 查询策略。读完后,你应能从 denial 的 tclass 和 { permission } 找到准确调用点,并判断问题属于错误 class、缺失基础权限、缺失扩展权限还是名称标签错误。
1. 三层定义
| 层次 | 文件或对象 | owner | 作用 |
|---|---|---|---|
| 策略 class 顺序 | security_classes | Android sepolicy | 声明 policy 中所有 object class |
| 权限符号表 | access_vectors | Android sepolicy | 为 common/class 定义 permission 名与顺序 |
| 内核名称映射 | classmap.h | kernel SELinux | 将 kernel class/permission 映射到已加载 policy |
| 用户空间查询 | selinux_check_access() | object manager | 按字符串 class/permission 查询 policy |
| 运行期 bit | class_datum.permissions、AVC | security server | 把 permission 名转成 access vector bit |
class 的 permission bit 只在该 class 内有意义。read 在 file、ipc、key 等 class 中可以出现,但数值和调用语义不同;一条 allow a b:file read 不会授权 a b:key read。
图中的 Binder hook 只消费进程 SID 和 binder class;服务名授权已经在第 2 节的 servicemanager 用户空间检查中完成。一次完整服务调用可能先经过 service_manager find,再经过 binder call。
2. 内核映射
2.1 classmap
kernel classmap.h 按名称列出 kernel hook 可能使用的 class 与 permission。它必须与加载策略中的名字兼容,但 class 数值不要求在编译期写死相同。
源码文件:kernel/common/security/selinux/include/classmap.h
static const struct security_class_mapping secclass_map[] = {
{ "process",
{ "fork", "transition", "sigchld", "sigkill",
"sigstop", "signull", "signal", "ptrace",
"getsched", "setsched", "getsession", "getpgid",
"setpgid", "getcap", "setcap", "share",
"getattr", "setexec", "setfscreate", "noatsecure",
"siginh", "setrlimit", "rlimitinh", "dyntransition",
"setcurrent", "execmem", "execstack", "execheap",
"setkeycreate", "setsockcreate", "getrlimit", NULL } },
{ "file",
{ COMMON_FILE_PERMS,
"execute_no_trans", "entrypoint", NULL } },
{ "dir",
{ COMMON_FILE_PERMS,
"add_name", "remove_name", "reparent",
"search", "rmdir", NULL } },
{ "binder",
{ "impersonate", "call",
"set_context_mgr", "transfer", NULL } },
};userspace classes 不在这张表里,因为 kernel 不直接发起 service_manager find 或 property_service set。它们仍存在于 policydb,由 libselinux 字符串查询访问。
2.2 映射建立
策略加载后,selinux_set_mapping() 遍历 kernel class mapping,用 policy 符号表查找 class 和 permission 编号。未知 class/permission 会根据 reject/allow unknown 策略处理。
源码文件:kernel/common/security/selinux/ss/services.c
相关函数:selinux_set_mapping
p_out->value = string_to_security_class(
pol, p_in->name);
if (!p_out->value) {
pr_info("SELinux: Class %s not defined in policy.\n",
p_in->name);
if (pol->reject_unknown)
goto err;
p_out->num_perms = 0;
print_unknown_handle = true;
continue;
}
while (p_in->perms[k]) {
p_out->perms[k] = string_to_av_perm(
pol, p_out->value, p_in->perms[k]);
if (!p_out->perms[k]) {
pr_info("SELinux: Permission %s in class %s not defined in policy.\n",
p_in->perms[k], p_in->name);
if (pol->reject_unknown)
goto err;
}
k++;
}这层映射解释了为什么 kernel 与 policy 可以使用名称同步,而不依赖相同编译时枚举值;也解释了新增 kernel permission 却没有更新 Android policy 时为何可能在策略加载阶段失败。
2.3 权限bit
policydb 的 class_datum.permissions 为 class-specific permission 建立 symbol table,继承的 common permissions 位于 comdatum.permissions。二进制 policy 加载时会分别读取并构造表。
源码文件:kernel/common/security/selinux/ss/policydb.c
相关函数:common_read
comdatum = kzalloc(sizeof(*comdatum), GFP_KERNEL);
if (!comdatum)
return -ENOMEM;
rc = symtab_init(&comdatum->permissions, nel);
if (rc)
goto bad;
comdatum->permissions.nprim = le32_to_cpu(buf[2]);
for (i = 0; i < nel; i++) {
rc = perm_read(p, &comdatum->permissions, fp);
if (rc)
goto bad;
}permission 顺序会决定 access vector bit。宏 r_file_perms 只是一组 permission 名,编译后成为 file/dir 等 class 对应 bitmask;运行期 AVC 只比较 bit,不保留宏名。
3. 错误边界
| 现象 | 应检查的 class | 典型 permission | 常见误修 |
|---|---|---|---|
| 找不到 Binder 服务名 | service_manager | find | 只补 binder call |
| 服务注册失败 | service_manager | add | 只改 service_contexts 标签 |
| Binder transaction 被拒绝 | binder | call/transfer | 只补 service_manager find |
| 传入 fd 后无法使用 | fd + inode class | use + read/write | 只补 Binder transfer |
| 设置属性失败 | property_service | set | 补 file write |
| 读取属性数据库失败 | file | open/read/map | 补 property_service set |
| 目录能读但无法进入 | dir | search | 只补 file read |
| TCP connect 失败 | tcp_socket/port type | connect/name_connect | 只补 socket write |
| ioctl 仍拒绝 | file class + xperm | ioctl + command | 只补普通 ioctl bit |
同一个 syscall 可能触发多个 class。例如 Binder 发送文件描述符需要 binder transfer、fd use 和目标 file permission;目录创建文件需要 dir add_name 与 file create。修复时必须追踪完整调用链,而不是从最后一条日志推断只有一个检查。
图中有两类消费者:内核 LSM hook 使用 kernel mapping,用户空间 object manager 使用字符串 class/permission。先看用户空间消费者,再回到 class 定义和内核映射。
4. 用户空间类别
4.1 service_manager
service manager class 定义 add、find 和 list。servicemanager 自己获得调用方 SID,把服务名通过 service_contexts 映射为 target context,再用字符串 class/permission 查询策略。
源码文件:system/sepolicy/private/access_vectors
class service_manager
{
add
find
list
}源码文件:frameworks/native/cmds/servicemanager/Access.cpp
相关函数:canFind、canAdd、canList、actionAllowed
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::canList(const CallingContext& ctx) {
return actionAllowed(
ctx, mThisProcessContext,
"list", "service_manager");
}
bool Access::actionAllowed(
const CallingContext& sctx,
const char* tctx, const char* perm,
const std::string& tname) {
const char* tclass = "service_manager";
AuditCallbackData data = {
.context = &sctx,
.tname = &tname,
};
return 0 == selinux_check_access(
sctx.sid.c_str(), tctx,
tclass, perm, &data);
}find/add 的 target 是服务名对应的 service type,list 的 target 是 servicemanager 自身 context。它们不等于 Binder call;一个客户端可能获准找到服务名,却被 kernel binder class 拒绝调用服务进程,反之亦然。
4.2 名称失败
在调用 actionAllowed() 前,servicemanager 必须先完成服务名标签查询。没有 service_contexts 条目时直接拒绝,不会构造一个默认 target type。
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;因此 service lookup 失败至少有两类:名称没有 target context,以及 service_manager find 不允许。为第二类增加 allow 无法修复第一类。
4.3 property_service
property service class 只有 set,但读 property contexts 数据文件使用普通 file read class。相同组件在两个位置使用不同 class。
源码文件:system/sepolicy/private/access_vectors
class property_service
{
set
}源码文件:system/core/init/property_service.cpp
相关函数:property info read 与 CheckMacPerms
auto lock = std::lock_guard{selinux_check_access_lock};
return selinux_check_access(
source_context.c_str(), target_context,
"file", "read", &audit_data) == 0;文件读取检查之后,真正的属性修改使用另一组 class/permission:
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;
}第一段检查调用者是否能读取 property info file,第二段检查是否能设置目标 property type。看到 tclass=file 的 property denial 时,不能补 property_service set;要先确认具体调用路径。
5. Class声明
5.1 内核类别
security_classes 的顺序参与 policy class 编号。文件、网络、process、Binder、BPF 等内核对象 class 在 userspace classes 之前声明。
源码文件:system/sepolicy/private/security_classes
相关声明:代表性 kernel classes
class security
class process
class system
class capability
class filesystem
class file
class anon_inode
class dir
class memfd_file
class fd
class lnk_file
class chr_file
class blk_file
class sock_file
class fifo_file
class socket
class tcp_socket
class udp_socket
class unix_stream_socket
class unix_dgram_socket
class binder
class process2
class bpf
class perf_event
class io_uring这些名字要与 kernel classmap.h 保持兼容。新增 address family 或 class 时,内核源码中的编译期检查会要求更新映射;调整已有 class/permission 顺序可能破坏 policy/kernel ABI,不能把文件顺序当成纯排版。
5.2 用户空间类别
Android 还在同一个 policy 中定义 userspace object manager classes,它们由用户空间服务主动调用 libselinux 查询。
源码文件:system/sepolicy/private/security_classes
class property_service # userspace
class service_manager # userspace
class hwservice_manager # userspace
class keystore_key # userspace
class keystore2 # userspace
class keystore2_key # userspace
class diced # userspace
class drmservice # userspace
class tee_service # userspace这些 class 不需要对应 kernel hook 或 SECCLASS_* 常量。资源 owner 自己负责获得 source context、把名字映射为 target context,并调用 selinux_check_access()。如果 object manager 漏掉检查,kernel 不会自动替它执行 service_manager find。
5.3 Class数据
policydb 为每个 class 保存独立 permission symbol table、可选 common permission 引用、constraints 和默认 user/role/type/range 规则。
源码文件:kernel/common/security/selinux/ss/policydb.h
相关结构:class_datum
struct class_datum {
u32 value;
char *comkey;
struct common_datum *comdatum;
struct symtab permissions;
struct constraint_node *constraints;
struct constraint_node *validatetrans;
char default_user;
char default_role;
char default_type;
char default_range;
};comdatum 指向继承的 common permission 集;permissions 只保存 class 自有权限。constraint 也挂在 class 上,所以相同名称的 permission 在不同 class 中可以受到不同约束。
6. Common权限
6.1 file common
文件相关 class 共享 common file,它包含 I/O、属性、标签、打开、映射和 watch 等基础权限。
源码文件:system/sepolicy/private/access_vectors
common file
{
ioctl
read
write
create
getattr
setattr
lock
relabelfrom
relabelto
append
map
unlink
link
rename
execute
quotaon
mounton
audit_access
open
execmod
watch
watch_mount
watch_sb
watch_with_perm
watch_reads
}open 与 read 是独立 bit;允许 read 不一定允许打开新 fd,已有 fd 的后续 read 又可能走不同 file hook。map、execute、execmod 也分别描述普通映射、执行和修改后执行映射,不能合并成一个“可执行”权限。
6.2 继承扩展
file、dir、chr_file 等继承 common file,再增加 class-specific permission。
class dir
inherits file
{
add_name
remove_name
reparent
search
rmdir
}
class file
inherits file
{
execute_no_trans
entrypoint
}
class fd
{
use
}目录创建文件通常同时需要父目录 search/write/add_name 和新文件 type 的 create/open/write。Binder 传递 fd 后,接收方还可能需要 fd use 以及 inode/file 权限;只补 file read 不一定能使用跨进程收到的 fd。
6.3 socket common
socket common 在 file-like 基础权限之外增加 bind、connect、listen、accept、sendto、recvfrom 等网络动作;具体 socket class 再添加 node_bind、name_connect 或 connectto。
common socket
{
ioctl read write create getattr setattr lock
relabelfrom relabelto append map
bind connect listen accept getopt setopt shutdown
recvfrom sendto name_bind
}
class tcp_socket
inherits socket
{
node_bind
name_connect
}
class unix_stream_socket
inherits socket
{
connectto
}connect 是 socket 对地址执行连接,name_connect 是 TCP 连接到标记端口的权限,connectto 是 Unix stream socket 到目标进程/socket 的权限。它们名称相近但 target object 不同。
7. 进程与Binder
7.1 process
process class 描述 fork、domain transition、信号、ptrace、调度、context 设置和可执行内存等操作。
源码文件:system/sepolicy/private/access_vectors
class process
{
fork
transition
sigchld
sigkill
sigstop
signull
signal
ptrace
getsched
setsched
getsession
getpgid
setpgid
getcap
setcap
share
getattr
setexec
setfscreate
noatsecure
siginh
setrlimit
rlimitinh
dyntransition
setcurrent
execmem
execstack
execheap
setkeycreate
setsockcreate
getrlimit
}transition 是 exec 过程中按策略进入新 domain,dyntransition/setcurrent 涉及显式 context 改变;signull 是 kill(pid, 0) 存在性检查,不会发送实际信号。分析 denial 时应回到发起操作的 syscall/hook,不要把所有 process 权限归为“进程控制”。
7.2 binder
Binder class 只有四个权限,但每个权限都有明确调用点。
class binder
{
impersonate
call
set_context_mgr
transfer
}源码文件:kernel/common/security/selinux/hooks.c
相关函数:Binder SELinux hooks
static int selinux_binder_set_context_mgr(
const struct cred *mgr)
{
return avc_has_perm(
current_sid(), cred_sid(mgr),
SECCLASS_BINDER,
BINDER__SET_CONTEXT_MGR, NULL);
}
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);
if (mysid != fromsid) {
int 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);
}call 检查 client domain 到 server domain;impersonate 只在当前执行线程 SID 与 recorded sender SID 不一致时检查;Binder 引用转移使用独立 transfer hook。这里完全没有服务名,说明 Binder transaction 与 servicemanager name lookup 是两条安全链。
图中的 Binder hook 只消费进程 SID 和 binder class;服务名授权已经在第 4 节的 servicemanager 用户空间检查中完成。一次完整服务调用可能先经过 service_manager find,再经过 binder call。
8. 测试验证
8.1 Context类型测试
APEX sepolicy 测试加载真实 precompiled policy,并用不同 file contexts 验证 type 是否存在、路径是否能由消费者读取。它反向证明“context 语法正确”和“target type/class 使用合理”是两层检查。
源码文件:system/sepolicy/tests/apex_sepolicy_tests_test.py
相关测试:test_unknown_label、test_binaries
def test_unknown_label(self):
self.assert_error(
'./bin/hw/foo u:object_r:foo_exec:s0',
r'Error: \./bin/hw/foo: tcontext\(foo_exec\) is unknown')
def test_binaries(self):
self.assert_ok(
'./bin/init u:object_r:init_exec:s0')
self.assert_ok(
'./bin/hw/svc u:object_r:init_exec:s0')
self.assert_error(
'./bin/hw/svc u:object_r:vendor_file:s0',
r"Error: .*svc: can't be labelled as 'vendor_file'")第一个输入使用不存在的 type,断言要求报告 unknown target context;第二组输入用相同路径类别对比可执行 type 与普通 vendor file type,断言消费者权限决定某标签是否合理。测试没有执行实际文件访问,也没有证明所有 file class permissions 都存在。
8.2 Context模块测试
Soong 根据 contexts 类型选择不同校验工具与 flags:service contexts 使用 checkfc -s,hwservice 使用 -l,vendor service 使用 -v,property contexts 改用 property_info_checker。
源码文件:system/sepolicy/build/soong/selinux_contexts.go
相关函数:contextsTestModule.GenerateAndroidBuildActions
tool := "checkfc"
if m.context == PropertyContext {
tool = "property_info_checker"
}
flags := []string(nil)
switch m.context {
case FileContext:
if !validateWithPolicy {
flags = []string{"-t"}
}
case ServiceContext:
flags = []string{"-s"}
case HwServiceContext:
flags = []string{"-e", "-l"}
case VndServiceContext:
flags = []string{"-e", "-v"}
}
rule.Command().BuiltTool(tool).
Flags(flags).
Input(sepolicy).
Inputs(srcs)输入是 context 文件类型、编译策略和具体 contexts 源;关键断言由对应工具完成。它证明不同 object manager class 需要不同 backend 解释,不能用 file contexts 的检查方式验证 service_manager 标签。
8.3 Unknown边界
kernel mapping 对 policy 缺失 class/permission 的处理取决于 reject_unknown。该行为需要在策略加载阶段验证,不能等到运行期 denial 才发现。
# 在可用构建环境中构建完整policy和contexts检查目标。
m selinux_policy
m sepolicy_test构建成功说明 kernel class mapping、policy class/permission 和 contexts tests 能够组合;它不证明目标设备内核与当前 out 目录产物来自同一 build,也不证明每次业务调用都命中预期 target context。
9. 源码导航
先确认 class 名和权限定义的单一来源:
# 查看class声明、common权限和class-specific权限。
sed -n '1,240p' system/sepolicy/private/security_classes
sed -n '1,860p' system/sepolicy/private/access_vectors再检查 kernel object classes 的名称映射与加载边界:
# 比较kernel classmap和policy加载时的名称映射。
rg -n 'security_class_mapping|secclass_map|selinux_set_mapping|string_to_security_class|string_to_av_perm' \
kernel/common/security/selinux/include/classmap.h \
kernel/common/security/selinux/ss/services.c若 denial 来自 Android 用户空间 object manager,回到真实调用点:
# 查看service_manager和property_service如何选择class/permission。
rg -n 'canFind|canAdd|canList|service_manager|CheckMacPerms|property_service' \
frameworks/native/cmds/servicemanager/Access.cpp \
system/core/init/property_service.cpp若 denial 来自内核 Binder、file 或 process hook,搜索 class/permission 常量:
# 从SECCLASS和PERMISSION常量追到avc_has_perm调用。
rg -n 'SECCLASS_(BINDER|FILE|DIR|PROCESS)|BINDER__(CALL|TRANSFER)|FILE__(READ|WRITE|OPEN)' \
kernel/common/security/selinux/hooks.c设备侧先收集 class/permission,再决定搜索方向:
# 读取denial并观察主体/客体标签。
adb shell su 0 dmesg | rg 'avc:.*denied'
adb shell 'id -Z; ps -AZ | grep -E "servicemanager|system_server"'
adb shell 'ls -lZ /system/bin/servicemanager /dev/socket/zygote'10. 闭环复述
- 给定
allow a b:file read,解释 class 为什么决定readbit 的编号,并说明它为何不授权dir search、fd use或key read。 - 从
common file展开dir与file的差异,给出“进入目录并创建文件”至少需要的 parent dir 和 new file 两组 class/permission。 - 对比一次服务调用中的
service_manager find与binder call:指出它们的 target context、消费者进程和源码入口为什么不同。 - 对比 property service 读取属性数据库时的
file read与设置属性时的property_service set,解释只按组件名搜索策略为何会误修。 - 从
classmap.h和selinux_set_mapping()说明 kernel class/permission 如何按名称映射到 policy;再解释 unknown class 在什么阶段失败。 - 使用 contexts Soong 测试说明
checkfc -s、-l、-v和 property checker 分别服务哪些 object manager,哪些运行期行为仍未覆盖。
客体类别不是 type 的附属描述,而是 permission 位的解释器和调用点边界。排查 SELinux 问题时,只有 source type、target type、class、permission 四项都来自真实 owner,规则才有意义;缺少其中任何一项,都可能把 Binder 名称、Binder transaction、文件、属性或 socket 的权限修到错误位置。
