androidam机制,从amstart的--user参数说到Android多用户

androidam机制,从amstart的--user参数说到Android多用户

2023年6月23日发(作者:)

androidam机制,从amstart的--user参数说到Android多⽤户本⽂的讨论围绕⼀个 tyException 展开,异常的关键词是权限CT_ACROSS_USERS_FULL。tyException: Permission Denial: startActivity asks to run as user -2 but is calling from user 0; this CT_ACROSS_USERS_FULLat ception(:1425)at ception(:1379)at ctivityAsUser(:1921)at rt(:494)at (:109)at (:82)at FinishInit(Native Method)at (:235)先就我所了解的知识说⼀下此异常发⽣的背景。背景Android 系统⾥的多⽤户Android 系统是基于 Linux 内核,⽽ Linux 内核中⽤于⽀持多⽤户机制的 uid 在 Android 中被⽤于标识 app-specific sandbox。s 类的 myUid() ⽅法 的描述⾥的原话是:Returns the identifier of this process’s uid. This is the kernel uid that the process is running under, which is the identity ofits app-specific sandbox. It is different from myUserHandle() in that a uid identifies a specific app sandbox in a specificuser.所以注定 Android 如果要实现多⽤户不能直接使⽤ Linux 的 uid 机制了,需要另做⼀套机制。在 Android API level 17 的 Features 列表⾥有⼀项是Multiple user accounts (tablets only)所以在 API level 17 以上的 Android 系统⾥其实已经内置了多⽤户的⽀持,只不过暂时只对平板启⽤(据说是因为多⽤户⼿机专利早已被Symbian 雇员注册,不知真假。)。在实现上是新引⼊了 UserHandle 的概念,封装了 user id,在 s 类的myUserHandle() ⽅法 的描述⾥的原话是:Returns this process’s user handle. This is the user the process is running under. It is distinct from myUid() in that aparticular user will have multiple distinct apps running under it each with their own id 与 uid结论user id = uid / 100000⽬前 Android ⼿机上所有 APP 的 user id 都为 0root 权限与 uid 是否为 0 有关,与 user id ⽆关分析/*** Representation of a user on the device.*/public final class UserHandle implements Parcelable {/*** @hide Range of uids allocated for a user.*/public static final int PER_USER_RANGE = 100000;....../*** @hide Enable multi-user related side effects. Set this to false if* there are problems with single user use-cases.*/public static final boolean MU_ENABLED = true;....../*** Returns the user id for a given uid.* @hide*/public static final int getUserId(int uid) {if (MU_ENABLED) {return uid / PER_USER_RANGE;} else {return 0;}}......}这个类定义在 frameworks/base/core/java/android/os/ ⾥。上⾯这段代码能得出结论 1 ⾥的公式。/*** Tools for managing OS processes.*/public class Process {....../*** Defines the root UID.* @hide*/public static final int ROOT_UID = 0;....../*** Defines the start of a range of UIDs (and GIDs), going from this* number to {@link #LAST_APPLICATION_UID} that are reserved for assigning* to applications.*/public static final int FIRST_APPLICATION_UID = 10000;/*** Last of application-specific UIDs starting at* {@link #FIRST_APPLICATION_UID}.*/public static final int LAST_APPLICATION_UID = 19999;......}这个类定义在 frameworks/base/core/java/android/os/ ⾥。从 FIRST_APPLICATION_UID 与 LAST_APPLICATION_UID 的值,结合结论 1 ⾥的公式来看,所有 APP 运⾏时获取⾃⾝的 user id都为 0;⽽运⾏时 uid 为 ROOT_UID (即 0)的 APP 获取⾃⾝的 user id 也为 0,所以 user id 是否为 0 与是否获取 root 权限并⽆关联。异常发⽣的场景该异常发⽣在 API level 17 以上的机型⾥,在 APP 或者 APP 调⽤的 Native 进程⾥使⽤ am start 来启动 Activity 时。在 Java 代码中直接 startActivity 并不会触发此异常。好了,背景交待完毕,下⾯按惯例先上结论及解决⽅案,以便急于解决⽂章开始提到的异常⽽不想探究原理的同学可以省时地带着结论⼼满意⾜地离去。结论及解决⽅案在 API level 17 以上的 Android 设备⾥,通过 am start 命令来启动 Activity 时会校验调⽤ am 命令的进程的 user id 与 am 进程从 –user 参数获取到的 user id(默认值为 _CURRENT,即 -2)是否相等, 如果想在 APP 或者 APP 调⽤的 Native 进程⾥使⽤ am start 来启动 Activity,那么需要给其传递能通过校验的 –user 参数,参数值可以直接硬编码为 0,也可以使⽤Handle().hashCode() 的值。如果不给 am start 传递正确的 –user 参数,那调⽤进程对应 uid 需要拥有 INTERACT_ACROSS_USERS_FULL 权限,但是该权限的protectionLevel 为 signature|installer,⼀般场景下是⽆法获取到的。我做了⼀个 Demo APP,通过 time().exec("am start xxxxxxx"); 来启动拔号程序界⾯,有两个按钮分别模拟了传递与不传递 –user 参数的情况,有兴趣的同学可以看看现象,完整源码在 AuthorityDemo。运⾏截图如下:分析接下来我们将借助于源码对 am start 的执⾏过程进⾏分析,⼀点⼀点吹散迷雾。am start 的执⾏过程am 命令的源码在 frameworks/base/cmds/am ⾥,⾥⾯的 am ⽂件即为 am 命令主体:#!/system/bin/sh## Script to start "am" on the device, which has a very rudimentary# shell.#base=/systemexportCLASSPATH=$base/framework/capp_process $base/bin "$@"这段代码在 framworks/base/cmds/am/am ⾥。am 命令是通过 app_process 最终调⽤到 类的 main ⽅法,并将所有参数传递给 main 来执⾏后续流程的。app_process 相关知识与 am start 执⾏逻辑⽆关,此处略去不表,放在本⽂最后⼀节附录中讲解。am start 的关键⽅法调⽤如下:⽂章开始处的异常就是在 handleIncomingUser ⽅法⾥校验 user id 和权限失败之后抛出的。下⾯按⽅法调⽤层级详细分析⼀下,如下源码所在源⽂件可以在上图中找到:blic static void main(String[] args) {(new Am()).run(args);}就是⼀个简单的 new 和 run ⽅法调⽤,⽽ Am 类中并⽆ run(String[]) 原型的⽅法,所以其实调⽤的是 Am 类的基类 BaseCommand的 run ⽅法。lic void run(String[] args) {......mArgs = args;......try {onRun();......}......}BaseCommand 类的 onRun ⽅法是⼀个抽象⽅法,所以其实 run ⽅法只是保存了参数,然后调⽤了 Am 的 onRun ⽅法。@Overridepublic void onRun() throws Exception {mAm = ault();......String op = nextArgRequired();if (("start")) {runStart();}......}onRun ⽅法主要是对 am 命令后第⼀个参数进⾏判断并进⾏相应的⽅法调⽤,我们可以看到 am start 是调⽤了 runStart ⽅法。这⾥可以顺便留意到的是 mAm 对象,追踪 ault() 可以知道它最终通过 binder 机制对应ActivityManagerService 对象。rtpublic class Am extends BaseCommand {......private boolean mWaitOption = false;......private int mUserId;......private Intent makeIntent(int defUser) throws URISyntaxException {......mWaitOption = false;......mUserId = defUser;......while ((opt=nextOption()) != null) {if (("-a")) {......} else if (("-W")) {mWaitOption = true;......} else if (("--user")) {mUserId = parseUserArg(nextArgRequired());}......}private void runStart() throws Exception {Intent intent = makeIntent(_CURRENT);......if (mWaitOption) {result = ctivityAndWait(null, null, intent, mimeType,null, null, 0, mStartFlags, profilerInfo, null, mUserId);res = ;} else {res = ctivityAsUser(null, null, intent, mimeType,null, null, 0, mStartFlags, profilerInfo, null, mUserId);}......switch (res) {// 启动 Activity 的结果提⽰}if (mWaitOption && launched) {// 输出启动 Activity 的结果状态,起⽌时间等}......}......}由如上代码可知,⼀般通过 am start 启动 Activity 时若未传 -W 参数(我⼀般的做法),会调⽤ctivityAsUser 来启动 Activity。(ctivityAndWait 其实与ctivityAsUser 类似,只是在启动 Activity 后多了⼀个等待过程,下⾯不再重复分析。)⽽ mUserId 的值,若命令⾏中有 –user 参数,则被赋为该参数的值;若命令⾏中⽆ –user 参数,则默认为_CURRENT 的值,在 frameworks/base/core/java/android/os/ 中可知此默认值为 -2。-2 这个数字是不是有点似曾相似?没错,⽂⾸的异常信息⾥就有这个值。ctivityAsUser@Overridepublic final int startActivityAsUser(IApplicationThread caller, String callingPackage,Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) {enforceNotIsolatedCaller("startActivity");userId = handleIncomingUser(lingPid(), lingUid(), userId,false, ALLOW_FULL_ONLY, "startActivity", null);// TODO: Switch to user app stacks ctivityMayWait(caller, -1, callingPackage, intent,resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,profilerInfo, null, null, options, false, userId, null, null);}lingUid() 是⼀个 native ⽅法,在 frameworks/base/core/java/android/os/ 中能找到,它其实就是返回了当前进程的 uid,⽽该 uid 是从⽗进程继承的。/*** Return the Linux uid assigned to the process that sent you the* current transaction that is being processed. This uid can be used with* higher-level system services to determine its identity and check* permissions. If the current thread is not currently executing an* incoming transaction, then its own uid is returned.*/public static final native int getCallingUid();IncomingUserint handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll,int allowMode, String name, String callerPackage) {final int callingUserId = rId(callingUid);if (callingUserId == userId) {return userId;}......if (callingUid != 0 && callingUid != _UID) {final boolean allow;if (checkComponentPermission(INTERACT_ACROSS_USERS_FULL, callingPid,callingUid, -1, true) == SION_GRANTED) {// If the caller has this permission, they always pass go. And collect $ = true;} else if (allowMode == ALLOW_FULL_ONLY) {// We require full access, sucks to be = false;} else if (......) {......}if (!allow) {if (userId == _CURRENT_OR_SELF) {// In this case, they would like to just execute as their// owner user instead of UserId = callingUserId;} else {StringBuilder builder = new StringBuilder(128);("Permission Denial: ");(name);if (callerPackage != null) {(" from ");(callerPackage);}(" asks to run as user ");(userId);(" but is calling from user ");(rId(callingUid));("; this requires ");(INTERACT_ACROSS_USERS_FULL);if (allowMode != ALLOW_FULL_ONLY) {(" or ");(INTERACT_ACROSS_USERS);}String msg = ng();Slog.w(TAG, msg);throw new SecurityException(msg);}}}......}它先校验了当前进程的 user id 与参数⾥的 userId(即 –user 的值或默认的 -2)是否相等,如果相等则正常返回,执⾏后续的启动Activity 动作;如果不相等,普通应⽤程序的 callingUid 必为 0,则先进⾏权限校验,看当前 pid 和 uid 是否被赋予了INTERACT_ACROSS_USERS_FULL 权限,我们在前⾯的「结论及解决⽅案」⼀节中已经交待过,该权限的 protectionLevel 为signature|installer,⼀般场景下是⽆法获取到的;如果没有 INTERACT_ACROSS_USERS_FULL 权限,allowMode 参数值⼜为 ALLOW_FULL_ONLY 则将抛出 SecurityException,从上⼀⼩节「ctivityAsUser」中调⽤ handleIncomingUser 的参数可知 allowMode 参数就是ALLOW_FULL_ONLY。上⽅代码段⾥的 Permission Denial:、asks to run as user 和 but is calling from user 等字符串是不是很熟悉?这就是从⽂⾸开始困惑我们的异常抛出的地⽅。⾄此,am start 的⼤概执⾏过程和异常发⽣的情景分析完成。实现功能,避免异常从上⽅的分析可知,要避免异常最直接有效的⽅法就是让 handleIncomingUser ⽅法正常返回,既然声明INTERACT_ACROSS_USERS_FULL 权限的路不通,就只有传递 –user 参数了。那么给这个参数传递什么值呢?从上⽂的分析可知,该参数应该与 am 进程的 user id 相等,所以传递⽗进程的 user id 即可。(user id 由 uid 运算得来,⽽ uid 与⽗进程相同。)由「背景」⼀节可知,所有 APP 进程的 user id 都为 0,所以该参数直接写 0 是可以的;如果不想硬编码,那么可以先⽤ Process 类的myUserHandle() ⽅法获取进程的 user handle:/*** Returns this process's user handle. This is the* user the process is running under. It is distinct from* {@link #myUid()} in that a particular user will have multiple* distinct apps running under it each with their own uid.*/public static final UserHandle myUserHandle() {return new UserHandle(rId(myUid()));}这段代码定义在 frameworks/base/core/java/android/os/ 中。继续分析 UserHandle 类⾥的相关⽅法:public final class UserHandle implements Parcelable {....../** @hide */public UserHandle(int h) {mHandle = h;}/*** Returns the userId stored in this UserHandle.* @hide*/@SystemApipublic int getIdentifier() {return mHandle;}......@Overridepublic int hashCode() {return mHandle;}......这段代码定义在 frameworks/base/core/java/android/os/ 中。构造⽅法 UserHandle(int h) ⾥将 user id 保存在 mHandle 成员⾥,本来 UserHandle 类有⼀个 getIdentifier ⽅法可以返回 mHandle的,但该⽅法被标为了 SystemApi 和 hide,⽆法正常调⽤,所以找了⼀个取巧的办法,使⽤也返回 mHandle 值的 hashCode ⽅法来达成⽬标。所以 am start 的 –user 的参数可以直接写为 0,也可以使⽤ Handle().hashCode() 的值。引申思考启动 Activity 的⽅法并⾮只有在 APP 进程⾥使⽤ am start ⼀种,还有通过 adb 命令 adb shell am start 或在 APP 进程⾥使⽤startActivity 等,它们为什么没有抛出此异常呢?继续探索。为何 adb shell am start 不抛此异常原因: shell 是拥有 INTERACT_ACROSS_USERS_FULL 权限的,所以 am start 作为其⼦进程继承了 shell 的 uid 和对应权限,在如上流程中 IncomingUser ⾥通过了权限检查,不会抛出⽂⾸的异常。在 frameworks/base/packages/Shell/ ⾥有如下声明:package=""coreApp="true"android:sharedUserId="">............Java 代码⾥ startActivity 的执⾏过程其实与 am start ⼀样,都是执⾏到了 ctivityAsUser,区别在于参数。@Overridepublic final int startActivity(IApplicationThread caller, String callingPackage,Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,int startFlags, ProfilerInfo profilerInfo, Bundle options) {return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,resultWho, requestCode, startFlags, profilerInfo, options,lingUserId());}在 am start 流程中的,传给 startActivityAsUser 的最后⼀个参数是 –user 传⼊的或者默认的 -2,⽽ Java 代码⾥的 startActivity 流程中传给 startActivityAsUser 的是 lingUserId(),相当于到 handleIncomingUser 中是当前进程的 user id 与当前进程的 user id ⽐较(必相等),如果相等则通过校验,所以必能通过校验,不会抛出⽂⾸说的异常。附录app_process 简要执⾏过程在第 2 步,AndroidRuntime::start 中调⽤了 startVm 启动虚拟机,最终在第 5 步 AppRuntime::onStarted 中调⽤通过参数传进来的类的 main ⽅法,并将类名后的参数传给它。

发布者:admin,转转请注明出处:http://www.yc00.com/xiaochengxu/1687516845a16294.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信