DAC与MAC
本文面向已经读过 SELinux架构、进程UID/GID设置 和 进程Capability设置 的读者。本文不满足于“DAC 根据 UID,MAC 根据标签”的对照表,而是沿一次真实 inode 权限检查回答:哪个对象拥有身份,哪段代码选择 owner/group/other 权限,capability 在何处参与,LSM hook 何时被调用,SELinux 怎样使用 AVC 决策,以及为什么没有 avc: denied 不能说明 MAC 已经放行。
本文以 Android 17 平台源码和对应 Android 17 common kernel 为基线。读完后,你应能根据 errno、文件 mode、进程 UID/GID、安全上下文和 AVC 日志判断拒绝发生在哪一层;解释 root 与 CAP_DAC_OVERRIDE 的真实边界;并从 Zygote specialization 找到 Linux credentials 与 SELinux domain 是怎样分别写入的。
1. 两套状态
同一次文件访问至少涉及两套彼此独立的状态:
| 控制面 | 主体状态 | 客体状态 | 规则来源 | 典型绕行能力 |
|---|---|---|---|---|
| DAC | fsuid、fsgid、supplementary groups、capability | inode uid、gid、mode、POSIX ACL | 文件所有者、创建者、系统组件 | CAP_DAC_OVERRIDE、CAP_DAC_READ_SEARCH |
| SELinux MAC | source SID/domain | target SID/type、object class | 已加载 sepolicy | 只能由策略显式允许;capability 本身也受 SELinux 检查 |
两套状态的交集只有“它们正在检查同一个操作”。文件 mode 改成 0777 不会改变 u:object_r:...:s0 标签;restorecon 改标签也不会改变 inode owner 和 mode。最终允许通常要求相关检查全部返回 0,但具体 hook 的先后顺序由内核调用路径决定,不能把所有系统调用都机械描述成同一个两步模板。
下面先固定本文的具体路径:进程对 inode 请求 MAY_READ、MAY_WRITE 或 MAY_EXEC,VFS 进入 inode_permission()。在这条路径中,文件系统/DAC 检查先返回,device cgroup 检查随后执行,最后才调用 security_inode_permission() 分发 LSM hook。
图中的 capability 不是第三套与 SELinux 平行的最终授权。它先参与 DAC fallback;当内核检查“进程是否可以使用该 capability”时,SELinux 的 capable hook 还能再次限制 capability 本身。
2. DAC状态
2.1 Android UID
Android 以 Linux UID 作为应用沙箱的一条基础边界。多用户 UID 由 user_id * AID_USER_OFFSET + app_id 合成,因而同一 app id 在不同 Android user 下得到不同 Linux UID。
源码文件:system/core/libcutils/multiuser.cpp
相关函数:multiuser_get_user_id、multiuser_get_app_id、multiuser_get_uid
userid_t multiuser_get_user_id(uid_t uid) {
return uid / AID_USER_OFFSET;
}
appid_t multiuser_get_app_id(uid_t uid) {
return uid % AID_USER_OFFSET;
}
uid_t multiuser_get_uid(userid_t user_id, appid_t app_id) {
return (user_id * AID_USER_OFFSET) + (app_id % AID_USER_OFFSET);
}源码文件:system/core/libcutils/include/private/android_filesystem_config.h
相关常量:系统 UID、应用 UID 与多用户偏移
#define AID_ROOT 0 /* traditional unix root user */
#define AID_SYSTEM 1000 /* system server */
#define AID_SHELL 2000 /* adb and debug shell user */
#define AID_APP 10000
#define AID_APP_START 10000 /* first app user */
#define AID_ISOLATED_START 90000
#define AID_USER 100000
#define AID_USER_OFFSET 100000 /* offset for uid ranges for each user */这些常量本身不授予文件访问。它们只是生成主体 credentials 和文件 owner/group 的命名空间。真正的 DAC 消费者是 VFS:它读取当前进程的 fsuid/groups 和 inode 的 uid/gid/mode/ACL。
2.2 Mode与ACL
acl_permission_check() 先处理一个快速路径:如果 owner、group、other 三组 mode 都包含请求权限且 inode 没有 ACL,就直接允许。否则它按 owner、POSIX ACL、group、other 的顺序选择权限位。
源码文件:kernel/common/fs/namei.c
相关函数:acl_permission_check
static int acl_permission_check(struct mnt_idmap *idmap,
struct inode *inode, int mask)
{
unsigned int mode = inode->i_mode;
vfsuid_t vfsuid;
if (!((mask & 7) * 0111 & ~mode)) {
if (no_acl_inode(inode))
return 0;
if (!IS_POSIXACL(inode))
return 0;
}
vfsuid = i_uid_into_vfsuid(idmap, inode);
if (likely(vfsuid_eq_kuid(vfsuid, current_fsuid()))) {
mask &= 7;
mode >>= 6;
return (mask & ~mode) ? -EACCES : 0;
}
if (IS_POSIXACL(inode) && (mode & S_IRWXG)) {
int error = check_acl(idmap, inode, mask);
if (error != -EAGAIN)
return error;
}
mask &= 7;
if (mask & (mode ^ (mode >> 3))) {
vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode);
if (vfsgid_in_group_p(vfsgid))
mode >>= 3;
}
return (mask & ~mode) ? -EACCES : 0;
}三个细节决定如何读这段代码。第一,文件访问使用 current_fsuid(),不一定等于调用者最初登录 UID。第二,idmapped mount 会先映射 inode uid/gid,再与当前 credentials 比较。第三,POSIX ACL 不是简单叠加在 mode 之后;非 owner 路径可能由 check_acl() 直接给出结果。
2.3 Capability补救
基础 mode/ACL 返回 -EACCES 后,generic_permission() 才检查 CAP_DAC_READ_SEARCH 与 CAP_DAC_OVERRIDE。这比“UID 0 自动放行”更准确:内核询问的是当前 credentials 在相关 user namespace 和 inode uid/gid 映射下是否拥有能力。
源码文件:kernel/common/fs/namei.c
相关函数:generic_permission
ret = acl_permission_check(idmap, inode, mask);
if (ret != -EACCES)
return ret;
if (S_ISDIR(inode->i_mode)) {
if (!(mask & MAY_WRITE))
if (capable_wrt_inode_uidgid(idmap, inode,
CAP_DAC_READ_SEARCH))
return 0;
if (capable_wrt_inode_uidgid(idmap, inode,
CAP_DAC_OVERRIDE))
return 0;
return -EACCES;
}
mask &= MAY_READ | MAY_WRITE | MAY_EXEC;
if (mask == MAY_READ)
if (capable_wrt_inode_uidgid(idmap, inode,
CAP_DAC_READ_SEARCH))
return 0;
if (!(mask & MAY_EXEC) || (inode->i_mode & S_IXUGO))
if (capable_wrt_inode_uidgid(idmap, inode,
CAP_DAC_OVERRIDE))
return 0;
return -EACCES;CAP_DAC_OVERRIDE 也不是无限制:对普通文件的 execute 请求,至少要有一个 execute mode bit;只读目录遍历和普通文件 read 还可能先由 CAP_DAC_READ_SEARCH 满足。若 acl_permission_check() 返回的不是 -EACCES,例如 RCU walk 需要重试的 -ECHILD,capability fallback 不会吞掉该错误。
3. MAC状态
3.1 LSM交接
inode_permission() 展示了本文路径中的真实顺序。它先检查只读文件系统、immutable 和 unmapped id,再调用文件系统 permission 实现或 generic_permission();任何非零结果都会提前返回。只有这些检查通过,才进入 device cgroup 和 LSM。
源码文件:kernel/common/fs/namei.c
相关函数:inode_permission
int inode_permission(struct mnt_idmap *idmap,
struct inode *inode, int mask)
{
int retval;
retval = sb_permission(inode->i_sb, inode, mask);
if (unlikely(retval))
return retval;
if (unlikely(mask & MAY_WRITE)) {
if (unlikely(IS_IMMUTABLE(inode)))
return -EPERM;
if (unlikely(HAS_UNMAPPED_ID(idmap, inode)))
return -EACCES;
}
retval = do_inode_permission(idmap, inode, mask);
if (unlikely(retval))
return retval;
retval = devcgroup_inode_permission(inode, mask);
if (unlikely(retval))
return retval;
return security_inode_permission(inode, mask);
}因此,当 mode/ACL/capability 已经拒绝时,security_inode_permission() 根本没有执行。这正是“没有 AVC 日志不代表 SELinux 允许”的源码依据:请求可能在到达 SELinux 之前已经结束。
security_inode_permission() 是 LSM 分发层,不等同于 SELinux 实现。它会调用已注册的 inode_permission hooks;内核可同时启用多个 LSM。
源码文件:kernel/common/security/security.c
相关函数:security_inode_permission
int security_inode_permission(struct inode *inode, int mask)
{
if (unlikely(IS_PRIVATE(inode)))
return 0;
return call_int_hook(inode_permission, inode, mask);
}private inode 在通用 LSM 层直接返回 0,是条件边界之一。对普通 inode,SELinux 通过 LSM_HOOK_INIT(inode_permission, selinux_inode_permission) 注册自己的消费者。
3.2 SELinux Hook
SELinux hook 把 VFS MAY_* mask 转换为 SELinux file permission,读取当前 task SID 和 inode SID/class,再查询 task-local AVC cache 或全局 AVC。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:selinux_inode_permission
static int selinux_inode_permission(struct inode *inode, int requested)
{
int mask;
u32 perms;
u32 sid = current_sid();
struct task_security_struct *tsec;
struct inode_security_struct *isec;
struct avdc_entry *avdc;
int rc, rc2;
u32 audited, denied;
mask = requested & (MAY_READ|MAY_WRITE|MAY_EXEC|MAY_APPEND);
if (!mask)
return 0;
tsec = selinux_task(current);
if (task_avdcache_permnoaudit(tsec, sid))
return 0;
isec = inode_security_rcu(inode, requested & MAY_NOT_BLOCK);
if (IS_ERR(isec))
return PTR_ERR(isec);
perms = file_mask_to_av(inode->i_mode, mask);
rc = task_avdcache_search(tsec, isec, &avdc);
if (likely(!rc)) {
audited = perms & avdc->audited;
denied = perms & ~avdc->allowed;
if (unlikely(denied && enforcing_enabled() &&
!avdc->permissive))
rc = -EACCES;
} else {
struct av_decision avd;
rc = avc_has_perm_noaudit(
sid, isec->sid, isec->sclass, perms, 0, &avd);
audited = avc_audit_required(perms, &avd, rc,
(requested & MAY_ACCESS) ? FILE__AUDIT_ACCESS : 0,
&denied);
task_avdcache_update(tsec, isec, &avd, audited);
}
if (likely(!audited))
return rc;
rc2 = audit_inode_permission(inode, perms, audited, denied, rc);
if (rc2)
return rc2;
return rc;
}source SID、target SID、class 和 permission 在这里聚合。缓存命中仍会检查当前 enforcing 与 per-domain permissive 状态;缓存未命中才调用通用 AVC 查询并更新 task-local cache。审计是否发生由 audited 决定,不是每次允许和拒绝都必然产生日志。
3.3 AVC决策
全局 AVC 以 <ssid, tsid, tclass> 查缓存,permission 作为 requested bitmask 与 allowed vector 比较。缓存未命中时才调用 security server 计算并插入节点。
源码文件:kernel/common/security/selinux/avc.c
相关函数:avc_has_perm_noaudit、avc_has_perm
inline int avc_has_perm_noaudit(u32 ssid, u32 tsid,
u16 tclass, u32 requested,
unsigned int flags,
struct av_decision *avd)
{
u32 denied;
struct avc_node *node;
if (WARN_ON(!requested))
return -EACCES;
rcu_read_lock();
node = avc_lookup(ssid, tsid, tclass);
if (unlikely(!node)) {
rcu_read_unlock();
return avc_perm_nonode(
ssid, tsid, tclass, requested, flags, avd);
}
denied = requested & ~node->ae.avd.allowed;
memcpy(avd, &node->ae.avd, sizeof(*avd));
rcu_read_unlock();
if (unlikely(denied))
return avc_denied(
ssid, tsid, tclass, requested,
0, 0, 0, flags, avd);
return 0;
}
int avc_has_perm(u32 ssid, u32 tsid, u16 tclass,
u32 requested, struct common_audit_data *auditdata)
{
struct av_decision avd;
int rc, rc2;
rc = avc_has_perm_noaudit(
ssid, tsid, tclass, requested, 0, &avd);
rc2 = avc_audit(
ssid, tsid, tclass, requested, &avd, rc, auditdata);
if (rc2)
return rc2;
return rc;
}策略重载会改变 policy sequence,相关缓存需要失效或重验证。后面的 selinux_file_open() 还会保存 inode SID 和 policy seqno,防止 inode permission 检查与 file object 建立之间发生标签或策略变化。
4. 顺序边界
4.1 Inode时序
下面的时序只描述 inode_permission() 路径,不声称所有 syscall、所有 LSM hook 都遵循完全相同的先后顺序。
这张图给出一个调试推论:DAC 层提前拒绝时,SELinux hook 没有机会为该操作生成 denial。反过来,出现 AVC denial 说明请求至少已经到达对应 SELinux hook,但不保证系统中其他后续检查一定放行。
4.2 Open重验证
inode permission 不是文件生命周期中的唯一 SELinux 检查。创建 struct file 时,selinux_file_open() 保存 inode SID 与 policy sequence,并重新检查打开权限。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:selinux_file_open
static int selinux_file_open(struct file *file)
{
struct file_security_struct *fsec;
struct inode_security_struct *isec;
fsec = selinux_file(file);
isec = inode_security(file_inode(file));
fsec->isid = isec->sid;
fsec->pseqno = avc_policy_seqno();
/*
* Since the inode label or policy seqno may have changed
* between the selinux_inode_permission check and the saving
* of state above, recheck that access is still permitted.
*/
return file_path_has_perm(
file->f_cred, file, open_file_to_av(file));
}这不是冗余检查。标签或策略可能在两次检查之间变化;后续 file_permission 还可根据保存的 SID/seqno 判断是否需要重验证。把一次 open() 简化成“DAC 一次、MAC 一次”会漏掉这种 TOCTOU 防护。
4.3 结果矩阵
| DAC | SELinux策略 | 模式 | 结果 | 常见线索 |
|---|---|---|---|---|
| 拒绝 | 未执行到对应 hook | 任意 | 拒绝 | mode/ACL/owner/capability;通常没有该操作的 AVC |
| 允许 | 允许 | enforcing | 允许 | 无日志或 auditallow 日志 |
| 允许 | 拒绝 | enforcing | 拒绝 | avc: denied,返回通常为 EACCES/EPERM |
| 允许 | 拒绝 | permissive | 操作继续 | 有 denial,日志含 permissive=1 |
permissive 分支的源码位于 avc_denied():严格查询或全局 enforcing 且 domain 非 permissive 时返回 -EACCES;否则把 requested 权限临时更新进 AVC 节点并返回 0。
源码文件:kernel/common/security/selinux/avc.c
相关函数:avc_denied
static noinline int avc_denied(
u32 ssid, u32 tsid, u16 tclass, u32 requested,
u8 driver, u8 base_perm, u8 xperm,
unsigned int flags, struct av_decision *avd)
{
if (flags & AVC_STRICT)
return -EACCES;
if (enforcing_enabled() &&
!(avd->flags & AVD_FLAGS_PERMISSIVE))
return -EACCES;
avc_update_node(
AVC_CALLBACK_GRANT, requested, driver, base_perm,
xperm, ssid, tsid, tclass, avd->seqno, NULL, flags);
return 0;
}permissive 改变的是拒绝结果,不改变 source/target 标签,也不让 DAC 失败变成成功。若 mode 是 000 且进程没有 DAC capability,即使 SELinux 全局 permissive,请求仍会在 VFS 层失败。
5. Root与能力
5.1 Root与UID
acl_permission_check() 和 generic_permission() 没有 if (uid == 0) return 0。传统 root 的强大来自它通常在初始 user namespace 拥有广泛 effective/permitted capabilities,而不是 VFS 对数字 0 的特殊无条件放行。
进程可以在 UID 仍为 0 时主动清空 capability;非 root 进程也可能在受控条件下保留特定 capability。user namespace、securebits、file capability、setuid exec 和 ambient set 都会影响最终能力集合。因此排查“root 为什么仍然失败”时,至少要同时查看 Uid/Gid、CapInh、CapPrm、CapEff、CapBnd、CapAmb 和 SELinux context。
5.2 能力也受MAC
当 DAC fallback 调用 capable_wrt_inode_uidgid() 时,capability framework 会进入 LSM capable hook。SELinux 把 capability 转成 capability、capability2 或 user-namespace 对应 class,再查询当前 domain 对自身是否拥有该能力权限。
源码文件:kernel/common/security/selinux/hooks.c
相关函数:cred_has_capability、selinux_capable
static int cred_has_capability(
const struct cred *cred,
int cap, unsigned int opts, bool initns)
{
struct common_audit_data ad;
struct av_decision avd;
u16 sclass;
u32 sid = cred_sid(cred);
u32 av = CAP_TO_MASK(cap);
int rc;
ad.type = LSM_AUDIT_DATA_CAP;
ad.u.cap = cap;
switch (CAP_TO_INDEX(cap)) {
case 0:
sclass = initns
? SECCLASS_CAPABILITY : SECCLASS_CAP_USERNS;
break;
case 1:
sclass = initns
? SECCLASS_CAPABILITY2 : SECCLASS_CAP2_USERNS;
break;
default:
pr_err("SELinux: out of range capability %d\n", cap);
BUG();
return -EINVAL;
}
rc = avc_has_perm_noaudit(sid, sid, sclass, av, 0, &avd);
if (!(opts & CAP_OPT_NOAUDIT)) {
int rc2 = avc_audit(sid, sid, sclass, av, &avd, rc, &ad);
if (rc2)
return rc2;
}
return rc;
}
static int selinux_capable(
const struct cred *cred, struct user_namespace *ns,
int cap, unsigned int opts)
{
return cred_has_capability(
cred, cap, opts, ns == &init_user_ns);
}这形成了两层不同判断:Linux capability sets 回答“credentials 是否持有 CAP_DAC_OVERRIDE”,SELinux capability class 回答“当前 domain 是否允许使用 dac_override”。任一层失败,capability fallback 都不能让 DAC 检查通过。
SELinux 不直接阻止 setuid() 改数字 UID;源码注释明确指出,SELinux 控制的是使用 CAP_SETUID/CAP_SETGID 的能力。domain 转换和 Linux identity 修改是两个独立机制。
5.3 Android约束
Android 策略用 neverallow 把 dac_override 限制在很小的 domain 集合中,并明确建议优先修正 Unix group 或文件 mode,而不是授予通用绕行能力。
源码文件:system/sepolicy/private/domain.te
相关规则:dac_override_allowed
define(`dac_override_allowed', `{
apexd
artd
casefolding_remover
dnsmasq
dumpstate
init
installd
lmkd
netd
recovery
ueventd
vendor_init
vold
zygote
zygote_next
userdebug_or_eng(`overlay_remounter')
}')
neverallow ~dac_override_allowed self:global_capability_class_set dac_override;
neverallow ~{
dac_override_allowed
traced_perf
traced_probes
heapprofd
} self:global_capability_class_set dac_read_search;列表在正文中省略了少量特殊 domain,但保留了规则结构;应以源文件为准。neverallow 是构建期约束:某个新 domain 即使写了 allow ... dac_override,只要不在允许集合中,策略编译也应失败。
traced_perf 展示了 capability 与对象 MAC 权限如何组合。它获得 dac_read_search 用于符号解析,但还要分别获得可读文件类型;同时策略禁止它读取任何 app data file。
源码文件:system/sepolicy/private/traced_perf.te
相关规则:DAC capability、文件类型访问和 neverallow
allow traced_perf self:capability { kill dac_read_search };
r_dir_file(traced_perf, nativetest_data_file)
r_dir_file(traced_perf, system_file_type)
r_dir_file(traced_perf, apk_data_file)
r_dir_file(traced_perf, dalvikcache_data_file)
r_dir_file(traced_perf, vendor_file_type)
neverallow traced_perf app_data_file_type:file *;dac_read_search 能绕过某些 mode/ACL read/search 拒绝,却不会生成对所有 SELinux file types 的通配许可。要完成一次读取,traced_perf 仍需同时拥有 capability 使用权限和目标 type 的 dir/file 权限。
6. 进程身份
Zygote child specialization 是 Android 同时写入 DAC 与 MAC 状态的真实交汇点。它先设置 supplementary groups 和 GID,仍有特权时安装 seccomp/调度状态,然后切换 UID;之后重建 capability 集合,最后调用 libselinux 选择进程 context。
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
相关位置:SpecializeCommon
SetGids(env, gids, is_child_zygote, fail_fn);
// ... 设置 rlimit、mount、scheduler 等进程状态。
if (setresgid(gid, gid, gid) == -1) {
fail_fn(CREATE_ERROR(
"setresgid(%d) failed: %s", gid, strerror(errno)));
}
SetUpSeccompFilter(uid, is_child_zygote);
SetSchedulerPolicy(fail_fn, is_top_app);
if (setresuid(uid, uid, uid) == -1) {
fail_fn(CREATE_ERROR(
"setresuid(%d) failed: %s", uid, strerror(errno)));
}
// ... 配置 dumpable、内存安全和运行时标志。
SetCapabilities(
permitted_capabilities,
effective_capabilities,
permitted_capabilities,
fail_fn);
const char* se_info_ptr = se_info.has_value()
? se_info.value().c_str() : nullptr;
if (selinux_android_setcontext(
uid, is_system_server, se_info_ptr, nice_name_ptr) == -1) {
fail_fn(CREATE_ERROR(
"selinux_android_setcontext(%d, %d, \"%s\", \"%s\") failed",
uid, is_system_server, se_info_ptr, nice_name_ptr));
}四个 owner 必须分开:kernel credentials 持有 UID/GID/groups,capability sets 持有能力,SELinux task security blob 持有 SID/domain,seccomp 持有 syscall filter。它们在一个函数中配置,不表示其中一项可以替代其他项。任意关键调用失败都会走 fail_fn 终止子进程,不能让应用带着 Zygote 的高权限状态进入 Java 代码。
7. 定位方法
7.1 先看哪一层
| 现象 | 优先检查 | 原因 |
|---|---|---|
cat 返回 Permission denied,完全没有相关 AVC | owner/group/mode、父目录 execute、POSIX ACL、capability | 请求可能在到达 LSM 前已失败 |
| mode 已允许且出现 AVC denial | scontext、tcontext、tclass、permission | DAC 已通过到足以触发对应 hook,MAC 拒绝 |
| root 仍被拒绝 | capability sets 与 SELinux capability/file 权限 | UID 0 不代表能力存在,也不代表 domain 获得 allow |
| permissive 下仍失败 | DAC、只读文件系统、immutable、device cgroup 或其他检查 | permissive 只改变 SELinux denial 结果 |
chmod 777 后仍失败 | 实际 SELinux 标签和策略 | chmod 不修改 target context |
restorecon 后仍失败 | DAC mode/owner/ACL 或策略 allow | restorecon 不修改 mode 与 owner |
| 只有某 Android user 失败 | 合成 UID、文件 uid/gid 和 MCS level | user id 同时影响 Linux UID 与 SELinux level |
调试时不要先运行 audit2allow。先确认请求是否到达 SELinux,再确认标签是否正确,最后才判断是否真的缺少最小 allow。错误 domain 或错误 file type 生成的 allow 只会固化标注问题。
7.2 DAC实验
下面实验只改变临时文件 mode,不改变 SELinux 标签,用来观察 DAC 单独生效。它会创建并删除 /data/local/tmp/dac_mac_demo。
# 创建 shell 自己拥有的临时文件,并记录初始 DAC 与 SELinux 状态。
adb shell 'echo demo > /data/local/tmp/dac_mac_demo'
adb shell 'ls -lnZ /data/local/tmp/dac_mac_demo'
# mode 000 让 owner 也没有 read 位;普通 shell 通常应读取失败。
adb shell 'chmod 000 /data/local/tmp/dac_mac_demo'
adb shell 'cat /data/local/tmp/dac_mac_demo; echo exit=$?'
adb shell 'ls -lnZ /data/local/tmp/dac_mac_demo'
# 恢复 owner read/write 后再次读取;前后 SELinux type 应保持不变。
adb shell 'chmod 600 /data/local/tmp/dac_mac_demo'
adb shell 'cat /data/local/tmp/dac_mac_demo; echo exit=$?'
adb shell 'ls -lnZ /data/local/tmp/dac_mac_demo'
# 清理实验文件。
adb shell 'rm -f /data/local/tmp/dac_mac_demo'输入是同一 owner、同一 path、同一 SELinux label,仅 mode 从创建默认值切换为 000 和 600。若第一次读取失败、第二次成功且 ls -Z 标签不变,说明差异来自 DAC。这个实验没有制造 MAC denial,也不能证明该 label 对其他 domain 一定允许。
7.3 运行状态
# 同时查看 Linux credentials、capability sets 和 SELinux context。
adb shell 'id; id -Z; grep -E "^(Uid|Gid|Groups|CapInh|CapPrm|CapEff|CapBnd|CapAmb):" /proc/self/status'
# 文件需要同时查看数值 owner/mode、ACL 与 SELinux label。
adb shell 'ls -ldnZ /path/to/object'
adb shell 'getfacl -n /path/to/object 2>/dev/null || true'
# AVC 日志用于确认 MAC 输入;没有命中时仍要检查 DAC。
adb shell su 0 dmesg | rg 'avc:.*denied'su 0、getfacl 和读取 kernel log 的能力依赖设备构建与调试权限。命令不可用不等于对应机制不存在,应回到 /proc/<pid>/status、stat、ls -Z 和可获得的日志源交叉判断。
8. 测试边界
8.1 UID合成测试
Android libcutils 单元测试用 user 0 和 user 10 验证同一 app id 如何映射为不同 Linux UID。
源码文件:system/core/libcutils/multiuser_test.cpp
相关测试:MultiuserTest.TestMerge
TEST(MultiuserTest, TestMerge) {
EXPECT_EQ(0U, multiuser_get_uid(0, 0));
EXPECT_EQ(1000U, multiuser_get_uid(0, 1000));
EXPECT_EQ(10000U, multiuser_get_uid(0, 10000));
EXPECT_EQ(50000U, multiuser_get_uid(0, 50000));
EXPECT_EQ(1000000U, multiuser_get_uid(10, 0));
EXPECT_EQ(1001000U, multiuser_get_uid(10, 1000));
EXPECT_EQ(1010000U, multiuser_get_uid(10, 10000));
EXPECT_EQ(1050000U, multiuser_get_uid(10, 50000));
}输入覆盖系统 app id、普通应用起始 id 和更高 id;断言精确验证 user_id * 100000 + app_id。它证明 Android user 会改变 DAC 主体 UID,但没有验证文件 mode、SELinux MCS category 或 PackageManager 的 UID 分配持久化。
8.2 Capability测试
common kernel capability selftest 创建 mount/user namespace,准备 setuid/setgid 测试文件,并在 exec 后检查 capability sets。下面两条断言区分 root 与非 root 的初始 exec 结果,并验证不满足 inheritable/permitted 前提时不能提升 ambient capability。
源码文件:kernel/common/tools/testing/selftests/capabilities/test_execve.c
相关函数:do_tests
if (uid == 0) {
ksft_print_msg("[RUN]\tRoot => ep\n");
if (fork_wait())
exec_validate_cap(true, true, false, false);
} else {
ksft_print_msg("[RUN]\tNon-root => no caps\n");
if (fork_wait())
exec_validate_cap(false, false, false, false);
}
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE,
CAP_NET_BIND_SERVICE, 0, 0, 0) != -1 || errno != EPERM) {
ksft_test_result_fail(
"PR_CAP_AMBIENT_RAISE should have failed on a non-inheritable cap\n");
return 1;
}
capng_update(CAPNG_ADD, CAPNG_INHERITABLE, CAP_NET_RAW);
capng_update(CAPNG_DROP, CAPNG_PERMITTED, CAP_NET_RAW);
capng_update(CAPNG_DROP, CAPNG_EFFECTIVE, CAP_NET_RAW);
if (capng_apply(CAPNG_SELECT_CAPS) != 0)
ksft_exit_fail_msg("capng_apply - %s\n", strerror(errno));
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE,
CAP_NET_RAW, 0, 0, 0) != -1 || errno != EPERM) {
ksft_test_result_fail(
"PR_CAP_AMBIENT_RAISE should have failed on a non-permitted cap\n");
return 1;
}测试输入覆盖 root/non-root exec、非 inheritable capability 和非 permitted capability;关键断言是 capability 传播受集合不变量限制,而不是只看 UID。它在 kernel selftest 环境验证 Linux capability 语义,不包含 Android sepolicy,因此不能证明某个 Android domain 允许使用同名 capability。
9. 源码导航
# 沿 inode_permission 跟踪 DAC、device cgroup 与 LSM 的真实顺序。
rg -n 'acl_permission_check|generic_permission|do_inode_permission|inode_permission' \
kernel/common/fs/namei.c进入 LSM 后,分别查看 SELinux hook、AVC 缓存和 permissive 分支:
# 定位 LSM 分发、SELinux inode/file hook 与 AVC 决策。
rg -n 'security_inode_permission|selinux_inode_permission|selinux_file_open|avc_has_perm|avc_denied' \
kernel/common/security/security.c \
kernel/common/security/selinux/hooks.c \
kernel/common/security/selinux/avc.c要判断 root 或 capability 问题,需要把 kernel 检查和 Android 策略同时展开:
# 查看 capability fallback、SELinux capable hook 与 Android neverallow。
rg -n 'CAP_DAC_OVERRIDE|CAP_DAC_READ_SEARCH|cred_has_capability|dac_override_allowed' \
kernel/common/fs/namei.c \
kernel/common/security/selinux/hooks.c \
system/sepolicy/private/domain.te应用进程的两套身份在 Zygote specialization 中汇合:
# 对比 UID/GID、capability 与 SELinux context 的写入位置。
rg -n 'SetGids|setresgid|setresuid|SetCapabilities|selinux_android_setcontext' \
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp最后再运行与这两条身份主线对应的单元测试和 kernel selftest:
# 构建 Android UID 单元测试;kernel capability selftest 需对应 kernel 构建环境。
atest libcutils_test
make -C kernel/common/tools/testing/selftests/capabilities10. 闭环复述
- 给定一个 mode 为
0640的文件,分别以 owner、supplementary group 成员和 others 身份复述acl_permission_check()如何选择权限位;若返回-EACCES,再说明两个 DAC capability 的补救条件。 - 从
inode_permission()开始,指出哪些错误会在security_inode_permission()前返回,并解释为什么此时没有 AVC denial 不能作为 SELinux allow 的依据。 - 从
current_sid()、inode SID/class 和file_mask_to_av()追到 task-local cache、全局 AVC、security server 与 audit,说明 enforcing 和 permissive 在avc_denied()中如何产生不同返回值。 - 从 Zygote 的
setresuid()、SetCapabilities()和selinux_android_setcontext()解释一个应用进程为何同时拥有 Linux UID 和 SELinux domain;再说明 root、capability 和 domain 三者为什么不能互相替代。
能完整复述这四条路径后,DAC 与 MAC 就不再是两个抽象缩写:DAC 是 credentials 与 inode ownership/mode/ACL 的计算,capability 是受集合与 namespace 约束的补救机制,SELinux MAC 是 SID、class、permission 与已加载策略的裁决。排查权限问题时,必须先确定请求在哪个 owner 处结束,再修改对应状态。
