Android 7 多用户源码解析(七)系统服务的多用户感知处理

系列目录 :第一篇:功能演进与系统架构总览 | 第二篇:核心数据结构深度剖析 | 第三篇:用户创建与删除流程全链路分析 | 第四篇:用户切换机制源码剖析 | 第五篇:应用安装与管理隔离机制 | 第六篇:数据隔离与存储机制详解 | 第七篇:系统服务的多用户感知处理 | [第八篇:广播分发与 Intent 路由的用户隔离](#第八篇:广播分发与 Intent 路由的用户隔离) | 第九篇:权限系统与多用户的交互机制 | [第十篇:Work Profile 企业模式与开发实战指南](#第十篇:Work Profile 企业模式与开发实战指南)


一、核心问题

  • 一个 Binder API 的 userId 参数由调用方传入------服务端怎么防止它"冒充"别的用户?
  • 通知、设置、AppOps 这类服务,状态到底是"按用户分桶"还是"全局一份"?
  • 为什么 AOSP 7 没有统一的"多用户基类"?

这些问题的答案决定了系统服务是多用户"天然隔离"还是"漏洞百出"。本篇按"调用方识别 → 跨用户权限 → 典型服务实践 → 状态分桶"展开,修正原稿中整段虚构的代码(AMS 的 handleUserSwitch、NMS 的 mNotificationRecords HashMap、WallpaperManager 的 setWallpaper(name, which, outStream) 均不存在)。


二、识别调用方:UID → 用户

Binder 调用天然携带 pid/uid:

java 复制代码
// Binder.java
public static int getCallingUid() {
    return nativeGetCallingUid();
}
// UserHandle.java
public static @UserIdInt int getUserId(int uid) {
    if (MU_ENABLED) {
        return uid / PER_USER_RANGE;
    }
    return USER_SYSTEM;
}

关键设计 :uid 由内核在 Binder 事务时填充,调用方无法伪造 ------这是多用户识别的信任根。服务端的防御重点不是"识别",而是"限制":调用方声明的 userId 参数必须经过校验。


三、跨用户权限:两套校验路径

3.1 handleIncomingUser:用户归属校验

大多数服务用 ActivityManager.handleIncomingUser 校验"调用方是否有权代表这个 userId":

源码路径 :frameworks/base/core/java/android/app/ActivityManager.java

java 复制代码
public class ActivityManager {
    // ...
    public static int handleIncomingUser(int callingPid, int callingUid, int userId,
            boolean allowAll, boolean requireFull, String name, String callerPackage) {
        if (UserHandle.getUserId(callingUid) == userId) {
            return userId;          // 自己用户 → 直接放行
        }
        try {
            return ActivityManagerNative.getDefault().handleIncomingUser(callingPid,
                    callingUid, userId, allowAll, requireFull, name, callerPackage);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}

AMS 端委托给 UserController:

java 复制代码
// ActivityManagerService.java
public int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll,
        boolean requireFull, String name, String callerPackage) {
    return mUserController.handleIncomingUser(callingPid, callingUid, userId, allowAll,
            requireFull ? ALLOW_FULL_ONLY : ALLOW_NON_FULL, name, callerPackage);
}

UserController.handleIncomingUser 的核心规则:

复制代码
userId == 调用方用户                → 放行
userId == USER_ALL / USER_CURRENT  → 需 INTERACT_ACROSS_USERS(_FULL)
调用方是 system/shell              → 放行(shell 受 DISALLOW_DEBUGGING_FEATURES 限制)
requireFull 且只有 INTERACT_ACROSS_USERS → 拒绝
否则                                → 需 INTERACT_ACROSS_USERS(部分)或 _FULL(全部)

关键设计 :这是声明式 校验------API 带 userId 参数时,服务端第一行就该是 handleIncomingUser。它同时处理了"特殊值语义"(USER_ALL/USER_CURRENT)和权限分层,是 AOSP 7 最普遍的用户校验入口。

3.2 enforceCrossUserPermission:PMS 专用校验

源码路径 :frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java 复制代码
void enforceCrossUserPermission(int callingUid, int userId, boolean requireFullPermission,
        boolean checkShell, String message) {
    if (userId < 0) {
        throw new IllegalArgumentException("Invalid userId " + userId);
    }
    if (checkShell) {
        enforceShellRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES, callingUid, userId);
    }
    if (userId == UserHandle.getUserId(callingUid)) return;
    if (callingUid != Process.SYSTEM_UID && callingUid != 0) {
        if (requireFullPermission) {
            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
        } else {
            try {
                mContext.enforceCallingOrSelfPermission(
                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
            } catch (SecurityException se) {
                mContext.enforceCallingOrSelfPermission(
                        android.Manifest.permission.INTERACT_ACROSS_USERS, message);
            }
        }
    }
}

关键设计 :PMS 没有复用 handleIncomingUser,而是自己实现了一套"先 FULL 后降级"的校验(requireFullPermission=false 时 FULL 不满足则尝试普通 INTERACT_ACROSS_USERS)。两套并存的代价是语义不完全一致------读 PMS 代码时别被"应该用 handleIncomingUser"的直觉带偏。

3.3 跨用户权限矩阵

权限 级别 能力
INTERACT_ACROSS_USERS signature|privileged 读他用户状态、部分操作
INTERACT_ACROSS_USERS_FULL signature 全量跨用户操作
MANAGE_USERS signature 用户生命周期(建/删/广播接收)

关键设计 :三方 App 拿不到任何跨用户权限------多用户隔离对三方是"硬边界",只有系统签名组件能越界。


四、典型服务的用户感知实践

4.1 NotificationManagerService:全局列表 + 按 userId 过滤

原稿描述 NMS 用 HashMap<Integer, ArrayList<NotificationRecord>> 按用户分桶------实际不是 。AOSP 7 的 NMS 是单列表:

源码路径 :frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

java 复制代码
public class NotificationManagerService extends SystemService
        implements IStatusBarService.Stub {
    // ...
    final Object mNotificationLock = new Object();
    final ArrayList<NotificationRecord> mNotificationList =
            new ArrayList<NotificationRecord>();
    final ArrayMap<String, NotificationRecord> mNotificationsByKey =
            new ArrayMap<String, NotificationRecord>();
    final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();

    // NotificationManagerInternal 实现
    public void enqueueNotification(String pkg, String opPkg, int callingUid, int callingPid,
            String tag, int id, Notification notification, int[] idReceived, int userId) {
        enqueueNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, notification,
                idReceived, userId);
    }
}

入队时的用户校验(enqueueNotificationInternal):

java 复制代码
// NotificationManagerService.java
private void enqueueNotificationInternal(final String pkg, final String opPkg,
        final int callingPid, final int callingUid, final String tag, final int id,
        final Notification notification, int[] idOut, int incomingUserId) {
    checkCallerIsSystemOrSameApp(pkg);
    // ...
    final int userId = ActivityManager.handleIncomingUser(callingPid,
            callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
    final UserHandle user = new UserHandle(userId);
    // ...
    final StatusBarNotification n = new StatusBarNotification(
            pkg, opPkg, id, tag, callingUid, callingPid, 0, notification, user);
    // ...
}

NotificationRecord 持有 StatusBarNotification(内含 UserHandle),过滤靠 mAutobundledSummaries 的 key(userId)和查询时的 userId 匹配:

java 复制代码
final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();
// ...
final ArrayMap<String, String> summaries = mAutobundledSummaries.get(r.sbn.getUserId());

关键设计 :通知是"全局存储 + 查询时过滤"------每个 NotificationRecord 自带 userId,StatusBar 显示前按当前用户过滤。这比"按用户分桶"更简单(不用同步多桶),代价是查询要遍历。enqueueNotification 走 handleIncomingUser 保证"只能给自己用户发"(除非 FULL 权限)。

4.2 SettingsProvider:per-user 数据库

SettingsProvider 的隔离是存储级 的:每个用户一个 SQLite 文件 /data/system/users/<id>/settings_user.xml(或独立 db),API 带 userId 参数时按用户路由到对应存储。这是"按用户分桶"在持久化层的落地------与 NMS 的"全局列表"形成两种模式对照。

4.3 AppOpsService:per-user 操作状态

AppOpsService 的操作(如"后台定位")状态按 uid(含用户维度)存储,用户删除时 mAppOpsService.removeUser(userId) 清理(第三篇删除链路已见)。它的特殊值处理:USER_ALL 聚合所有用户状态。

关键设计:三种服务、三种隔离模式------

  • 全局列表 + userId 字段(NMS):写多读少、查询过滤;
  • per-user 存储(SettingsProvider):读写都按用户路由;
  • per-uid 状态 + 删除清理 (AppOps):UID 天然含用户维度,删除时批量清。
    选型取决于"按用户查询频率"和"删除/重置语义"。

4.4 ContentProvider:进程即用户

ContentProvider 的隔离靠进程模型 :provider 进程以自己的 UID(含用户)运行,ActivityManagerService.getContentProvider 查找时按调用方 userId 解析(resolveContentProvider(authority, ..., userId))。不同用户的同一 provider 是不同进程实例 (除非 FLAG_SINGLE_USER,后者需 INTERACT_ACROSS_USERS 权限):

java 复制代码
// ActivityManagerService.java
boolean isSingleton(String componentProcessName, ApplicationInfo aInfo,
        String className, int flags) {
    if (UserHandle.getAppId(aInfo.uid) >= Process.FIRST_APPLICATION_UID) {
        if ((flags & ServiceInfo.FLAG_SINGLE_USER) != 0) {
            if (ActivityManager.checkUidPermission(
                    INTERACT_ACROSS_USERS, aInfo.uid) != PackageManager.PERMISSION_GRANTED) {
                throw new SecurityException(msg);   // 三方不能声明 SINGLE_USER
            }
            return true;
        }
    }
    // ...
}

关键设计 :FLAG_SINGLE_USER(所有用户共享一个 provider 进程)是敏感声明------三方应用禁止,只有持有 INTERACT_ACROSS_USERS 的系统组件能声明。默认行为是"每用户一个进程",隔离最强。


五、没有统一基类:为什么

AOSP 7 没有 MultiUserService 之类的抽象基类,每个服务自己处理 userId 参数。原因是:

  1. API 签名各异 :有的按 userId 分桶(SettingsProvider),有的全局过滤(NMS),有的按 uid(AppOps)------统一抽象反而僵化;
  2. 校验入口统一 :虽然处理模式不同,但校验入口收敛在 handleIncomingUser / enforceCrossUserPermission 两处,保证了权限语义一致;
  3. 服务发现靠 LocalServices :服务间调用通过 LocalServices.getService(XxxInternal.class),接口方法自带 userId 参数,绕过 Binder 但同样做校验。

关键设计 :AOSP 7 的多用户是"校验集中、处理分散"架构------权限语义靠两个公共方法统一,状态管理交给各服务按场景选择。这比"强制基类"更灵活,但要求每个新服务自觉调用校验入口。


六、调试

bash 复制代码
# 查看某服务的用户维度状态
adb shell dumpsys notification | grep -i user
adb shell dumpsys settings | grep -i user

# 查看 ContentProvider 进程的用户归属
adb shell ps -A | grep <package>

七、本篇总结

  1. 识别 :Binder uid 内核填充不可伪造,UserHandle.getUserId 纯数学换算;
  2. 校验 :handleIncomingUser(普遍)+ enforceCrossUserPermission(PMS 专用)双入口,权限分 INTERACT_ACROSS_USERS / _FULL 两档;
  3. 隔离模式:全局列表+过滤(NMS)、per-user 存储(Settings)、per-uid 状态(AppOps)三种并存;
  4. Provider :进程即隔离单元,FLAG_SINGLE_USER 需 FULL 权限;
  5. 架构:无统一基类,"校验集中、处理分散"。

下一篇进入广播:sendBroadcastAsUser 的 userId 参数如何决定"谁收得到",有序广播与注册用户广播在多用户下的路由差异。

相关推荐
怪奇云呼军1 小时前
从 ElevenLabs 看工具调用:闪电智能 Voice Agent 的企业集成验收设计
android·大数据·运维·服务器·网络·人工智能·kotlin
Runwise创新社区1 小时前
开源大模型从本地跑通到生产上线:以千问为例的五级部署阶梯与六项选型检查
人工智能·系统架构·开源
157092511342 小时前
Android 架构模式详解:从 MVC、MVP 到 MVVM
android·架构·mvc
艺杯羹3 小时前
240GB流量击穿与频繁OOM:从云端瘫痪到客户端自治的本地优先架构重构实录
性能优化·系统架构·fastapi·桌面客户端·本地优先
aaajj3 小时前
Android手机连接鼠标情况
android
其实防守也摸鱼4 小时前
SQL注入实验笔记
android·运维·数据库·安全·oracle·自动化
找茬小王子4 小时前
Android原生Monkey工具的使用
android·monkey
AFinalStone4 小时前
Android7 多用户源码解析(十)Work Profile 企业模式与开发实战指南
系统架构·aosp·多用户
Code Man4 小时前
实现 Android 字符设备驱动
android·驱动开发·lineageos