Skip to content

DAC与MAC

沿真实 VFS、LSM、SELinux 和 Zygote 源码解释 Android 中 DAC 与 MAC 如何共同决定访问结果。

基于android-17.0.0_r1
AndroidSELinuxDACMACCapabilityVFS源码阅读

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. 两套状态 ​

同一次文件访问至少涉及两套彼此独立的状态:

控制面主体状态客体状态规则来源典型绕行能力
DACfsuid、fsgid、supplementary groups、capabilityinode uid、gid、mode、POSIX ACL文件所有者、创建者、系统组件CAP_DAC_OVERRIDE、CAP_DAC_READ_SEARCH
SELinux MACsource SID/domaintarget 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

cpp
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 与多用户偏移

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

c
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

c
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

c
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

c
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

c
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

c
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

c
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 结果矩阵 ​

DACSELinux策略模式结果常见线索
拒绝未执行到对应 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

c
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

c
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

text
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

text
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

cpp
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,完全没有相关 AVCowner/group/mode、父目录 execute、POSIX ACL、capability请求可能在到达 LSM 前已失败
mode 已允许且出现 AVC denialscontext、tcontext、tclass、permissionDAC 已通过到足以触发对应 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 或策略 allowrestorecon 不修改 mode 与 owner
只有某 Android user 失败合成 UID、文件 uid/gid 和 MCS leveluser id 同时影响 Linux UID 与 SELinux level

调试时不要先运行 audit2allow。先确认请求是否到达 SELinux,再确认标签是否正确,最后才判断是否真的缺少最小 allow。错误 domain 或错误 file type 生成的 allow 只会固化标注问题。

7.2 DAC实验 ​

下面实验只改变临时文件 mode,不改变 SELinux 标签,用来观察 DAC 单独生效。它会创建并删除 /data/local/tmp/dac_mac_demo。

bash
# 创建 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 运行状态 ​

bash
# 同时查看 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

cpp
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

c
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. 源码导航 ​

bash
# 沿 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 分支:

bash
# 定位 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 策略同时展开:

bash
# 查看 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 中汇合:

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

bash
# 构建 Android UID 单元测试;kernel capability selftest 需对应 kernel 构建环境。
atest libcutils_test
make -C kernel/common/tools/testing/selftests/capabilities

10. 闭环复述 ​

  1. 给定一个 mode 为 0640 的文件,分别以 owner、supplementary group 成员和 others 身份复述 acl_permission_check() 如何选择权限位;若返回 -EACCES,再说明两个 DAC capability 的补救条件。
  2. 从 inode_permission() 开始,指出哪些错误会在 security_inode_permission() 前返回,并解释为什么此时没有 AVC denial 不能作为 SELinux allow 的依据。
  3. 从 current_sid()、inode SID/class 和 file_mask_to_av() 追到 task-local cache、全局 AVC、security server 与 audit,说明 enforcing 和 permissive 在 avc_denied() 中如何产生不同返回值。
  4. 从 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 处结束,再修改对应状态。