Skip to content

进程UID/GID设置

追踪 ProcessList 权限 GID 计算、用户 UID 组成、Zygote setgroups/setresgid/setresuid 和 isolated UID。

基于android-17.0.0_r1
AndroidProcessListZygoteUID/GID源码阅读

进程UID/GID设置 ​

同一个 APK 在主用户和工作资料中运行,为什么 Linux UID 不同?应用有 INTERNET 权限,为什么进程的身份还需要一组 GID?一个 isolatedProcess 服务明明来自同一 APK,为什么不能沿用宿主身份?这些问题都要从“身份由谁选定”和“凭据何时生效”两端一起回答。

本文面向能够阅读 Java、C++,了解进程与 fork() 的读者。先读子进程PID管理可以理解 ProcessRecord 与 Linux PID 的关联;Zygote参数传递协议介绍启动请求的传输形式。本篇沿普通应用的 ProcessList → Process.start → ZygoteProcess → Zygote → SpecializeCommon 路径,分析 UID、主 GID 和附加组的选择、校验及落地,再讨论隔离 UID 的分配与释放。

阅读时要始终区分“已经选好 uid 参数”“已 fork 出 PID”“内核凭据已切换”和“应用入口可执行”。它们是不同阶段。capability 的具体位集合、seccomp 过滤规则和存储挂载算法另有专题;这里只展开它们对凭据切换顺序的约束。

1. 身份的三层含义 ​

Android userId 表示系统中的用户空间,appId 表示该用户内的应用身份部分。Linux 并不认识 APK、包名或工作资料;它处理的是数值 UID/GID。因此,多用户信息必须先编码为数值,再通过系统调用交给内核。

对象来源或保存位置用途
ApplicationInfo.uid包与用户对应的应用信息普通应用身份的输入
ProcessRecord.uidAMS 中该进程的记录可与包 UID 不同,如隔离进程
启动参数 uid、gidProcessList.startProcess 的实参目标 Linux UID 与主 GID
启动参数 gids权限 GID、用户及存储相关附加项Linux supplementary groups
real/effective/saved UID、GIDLinux 进程凭据身份、访问检查和身份切换语义

主 GID 是单值,附加组是列表,二者共同参与传统文件访问控制。Android 权限本身还可以由 Binder 服务、AppOps 等实现;“授予 Android 权限”不等于必定增加一个 Linux GID,“加入组”也不等于能够越过 SELinux 或文件系统挂载边界。

1.1 用户编码 ​

源码文件:frameworks/base/core/java/android/os/UserHandle.java

相关符号:getUid / getAppId / getUserGid。以下为源码节选。

java
// getUid 对非负 appId 取用户范围内的余数;userGid 则把共享组放入指定用户。
@UnsupportedAppUsage
@TestApi
public static int getUid(@UserIdInt int userId, @AppIdInt int appId) {
    if (MU_ENABLED && appId >= 0) {
        return userId * PER_USER_RANGE + (appId % PER_USER_RANGE);
    } else {
        return appId;
    }
}
// ...
@SystemApi
public static @AppIdInt int getAppId(int uid) {
    return uid % PER_USER_RANGE;
}

/**
 * Returns the gid shared between all apps with this userId.
 * @hide
 */
public static int getUserGid(@UserIdInt int userId) {
    return getUid(userId, Process.SHARED_USER_GID);
}
// ...

PER_USER_RANGE 为 100000。对普通非负 appId,getUid(10, 10123) 得到 1010123;反向拆解得到 userId 10、appId 10123。这个数值例子是按函数计算的结果,不是某台设备上安装包的实测身份。

同一应用的不同进程通常可以共享 UID,因此“每个进程都有唯一 UID”是错误前提。PID 标识一次进程实例,UID 标识访问控制身份;两个进程的 PID 不同,并不意味着凭据必须不同。后文的 isolated UID 是明确分配出来的例外,而不是普通多进程组件默认行为。

1.2 主组与附加组 ​

先看最终调用方实际传了什么,防止在进入 GID 算法前混淆主组和附加组。

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:startProcess。以下为源码节选。

java
// 普通 Zygote 路径把 uid 连续传入 UID 和主 GID 两个参数;gids 是另一个参数。
regularZygote = true;
startResult = Process.start(entryPoint,
        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
        app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags,
        isTopApp, app.getDisabledCompatChanges(), app.getEnabledCompatChanges(),
        useDeliQueue, pkgDataInfoMap,
        allowlistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs,
        bindOverrideSysprops, app.getStartSeq(),
        new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
// By now the process group should have been created by zygote.
app.mProcessGroupCreated = true;
// ...

这里两次 uid 意味着普通应用的主 GID 与目标 UID 使用同一个数值。随后变化的是 gids 的内容,而不是从这个数组里选一个元素作为主 GID。mProcessGroupCreated 指向的是资源管理所用的进程组记录,也不是 supplementary groups;名字都带 group 不代表是同一层概念。

2. 附加组构造 ​

AMS 已有 ProcessRecord 后,在准备启动参数时取得包权限对应的 GID 与外部存储访问模式。关键条件是 !app.isolated,它包住整个查询和合成过程。

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:startProcessLocked。以下为源码节选。

java
// 隔离进程保留初始的 null gids;普通进程先移除进程级 deniedPermissions 对应组,再合成附加组。
int uid = app.uid;
int[] gids = null;
int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
boolean externalStorageAccess = false;
if (!app.isolated) {
    int[] permGids = null;
    try {
        checkSlow(startUptime, "startProcess: getting gids from package manager");
        final IPackageManager pm = AppGlobals.getPackageManager();
        permGids = pm.getPackageGids(app.info.packageName,
                MATCH_DIRECT_BOOT_AUTO, app.userId);
        StorageManagerInternal storageManagerInternal = LocalServices.getService(
                StorageManagerInternal.class);
        mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,
                app.info.packageName);
        externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid,
                app.info.packageName);
    } catch (RemoteException e) {
        throw e.rethrowAsRuntimeException();
    }

    // Remove any gids needed if the process has been denied permissions.
    // NOTE: eventually we should probably have the package manager pre-compute
    // this for us?
    if (app.processInfo != null && app.processInfo.deniedPermissions != null) {
        for (int i = app.processInfo.deniedPermissions.size() - 1; i >= 0; i--) {
            int[] denyGids = mService.mPackageManagerInt.getPermissionGids(
                    app.processInfo.deniedPermissions.valueAt(i), app.userId);
            if (denyGids != null) {
                for (int gid : denyGids) {
                    permGids = ArrayUtils.removeInt(permGids, gid);
                }
            }
        }
    }

    gids = computeGidsForProcess(mountExternal, uid, permGids, externalStorageAccess);
}
app.setMountMode(mountExternal);
checkSlow(startUptime, "startProcess: building args");
if (app.getWindowProcessController().isFactoryTestProcess()) {
    uid = 0;
}
int runtimeFlags = 0;
// ...

这段实现有四个需要沿实参理解的点。

第一,uid 从 app.uid 出发,权限 GID 查询却使用包名和 app.userId。一个是进程记录中的身份,一个是包权限的查询上下文,不能在所有特殊进程上当作同一个对象。

第二,deniedPermissions 是进程信息上的拒绝权限集合。这里做的是从 permGids 移除相应组,不能将这段代码泛化成“所有运行时权限撤销都会同步修改存活进程的内核组列表”。它发生在启动参数构造期间;已经运行的进程是否重启、如何重新应用权限,是另一条生命周期路径。

第三,外部存储能力不是仅由 permGids 决定。StorageManagerInternal 给出的 mount mode 和 external access 会继续影响附加组,而具体目录是否可见还受挂载空间约束。

第四,工厂测试进程在 GID 合成后可能把局部 uid 改为 0。这是特殊分支,普通应用身份规则不能不加条件地套到它身上;接收端也有对应的工厂测试 UID 下限规则。

2.1 基础组计算 ​

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:computeGidsForProcess。以下为源码节选。

java
// 注意 cacheAppGid 的实参已经去掉用户部分;必须按调用点计算,不能只看函数名字。
private int[] computeGidsForProcess(int mountExternal, int uid, int[] permGids,
        boolean externalStorageAccess) {
    ArrayList<Integer> gidList = new ArrayList<>(permGids.length + 5);

    final int sharedAppGid = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));
    final int cacheAppGid = UserHandle.getCacheAppGid(UserHandle.getAppId(uid));
    final int userGid = UserHandle.getUserGid(UserHandle.getUserId(uid));

    // Add shared application and profile GIDs so applications can share some
    // resources like shared libraries and access user-wide resources
    for (int permGid : permGids) {
        gidList.add(permGid);
    }
    if (sharedAppGid != UserHandle.ERR_GID) {
        gidList.add(sharedAppGid);
    }
    if (cacheAppGid != UserHandle.ERR_GID) {
        gidList.add(cacheAppGid);
    }
    if (userGid != UserHandle.ERR_GID) {
        gidList.add(userGid);
    }
// ...

permGids 按原顺序复制,然后加入 shared app、cache app 和 user GID;返回 ERR_GID 的附加项不会进入列表。读这段代码最值得停下来的地方,是前两个调用都显式传入了 UserHandle.getAppId(uid),第三个调用才传入 userId。

源码文件:frameworks/base/core/java/android/os/UserHandle.java

相关符号:getSharedAppGid / getCacheAppGid。以下为源码节选。

java
// 单参数重载把输入当作完整 UID 再拆解;传入 appId 会使其解析出的 userId 为 0。
public static int getSharedAppGid(int uid) {
    return getSharedAppGid(getUserId(uid), getAppId(uid));
}

/** @hide */
public static int getSharedAppGid(@UserIdInt int userId, @AppIdInt int appId) {
    if (appId >= AID_APP_START && appId <= AID_APP_END) {
        return (appId - AID_APP_START) + AID_SHARED_GID_START;
    } else if (appId >= AID_ROOT && appId <= AID_APP_START) {
        return appId;
    } else {
        return -1;
    }
}
// ...
public static int getCacheAppGid(int uid) {
    return getCacheAppGid(getUserId(uid), getAppId(uid));
}

/** @hide */
public static int getCacheAppGid(@UserIdInt int userId, @AppIdInt int appId) {
    if (appId >= AID_APP_START && appId <= AID_APP_END) {
        return getUid(userId, (appId - AID_APP_START) + AID_CACHE_GID_START);
    } else if (appId >= Process.FIRST_PCC_UID && appId <= Process.LAST_PCC_UID) {
        return getUid(userId, (appId - Process.FIRST_PCC_UID)
                + Process.FIRST_PCC_CACHE_GID);
    } else {
        return -1;
    }
}
// ...

getSharedAppGid(userId, appId) 对普通应用返回 appId - 10000 + 50000,有意不叠加用户部分;但 getCacheAppGid(userId, appId) 的实现会调用 getUid(userId, ...)。因此,cache GID 函数支持按用户编码,不等于 ProcessList 的这个调用保留了用户编码。

以 userId 10、appId 10123 为例,并假设没有权限 GID、没有额外存储模式:

表达式结果解释
UserHandle.getUid(10, 10123)1010123进程 UID,普通路径也用作主 GID
getSharedAppGid(getAppId(uid))50123共享应用组不含用户前缀
getCacheAppGid(getAppId(uid))20123当前调用先剥掉 userId
getCacheAppGid(uid)1020123对照表达式;不是此处实际调用
getUserGid(getUserId(uid))1009997用户共享组保留用户部分

这个差异不能靠“cache 应当按用户隔离”的印象修正源码,也不能仅凭算式认定某条缓存目录访问必然成功或失败。要判断目录访问,还需要对照目录实际 owner、mode、挂载位置和访问策略。这里能够直接推出的是传给 setgroups 的数值来源。

2.2 存储组分支 ​

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:computeGidsForProcess。以下为源码节选。

java
// 不同存储模式添加不同组;只有部分组携带 userId,不能统一批量加用户偏移。
if (mountExternal == Zygote.MOUNT_EXTERNAL_ANDROID_WRITABLE
        || mountExternal == Zygote.MOUNT_EXTERNAL_PASS_THROUGH) {
    // For DownloadProviders and MTP: To grant access to /sdcard/Android/
    // And a special case for the FUSE daemon since it runs an MTP server and should have
    // access to Android/
    // Note that we must add in the user id, because sdcardfs synthesizes this permission
    // based on the user
    gidList.add(UserHandle.getUid(UserHandle.getUserId(uid), Process.SDCARD_RW_GID));

    // For devices without sdcardfs, these GIDs are needed instead; note that we
    // consciously don't add the user_id in the GID, since these apps are anyway
    // isolated to only their own user
    gidList.add(Process.EXT_DATA_RW_GID);
    gidList.add(Process.EXT_OBB_RW_GID);
}
if (mountExternal == Zygote.MOUNT_EXTERNAL_INSTALLER) {
    // For devices without sdcardfs, this GID is needed to allow installers access to OBBs
    gidList.add(Process.EXT_OBB_RW_GID);
}
if (mountExternal == Zygote.MOUNT_EXTERNAL_PASS_THROUGH) {
    // For the FUSE daemon: To grant access to the lower filesystem.
    // EmulatedVolumes: /data/media and /mnt/expand/<volume>/data/media
    // PublicVolumes: /mnt/media_rw/<volume>
    gidList.add(Process.MEDIA_RW_GID);
}
if (externalStorageAccess) {
    // Apps with MANAGE_EXTERNAL_STORAGE PERMISSION need the external_storage gid to access
    // USB OTG (unreliable) volumes on /mnt/media_rw/<vol name>
    gidList.add(Process.EXTERNAL_STORAGE_GID);
}

int[] gidArray = new int[gidList.size()];
for (int i = 0; i < gidArray.length; i++) {
    gidArray[i] = gidList.get(i);
}
return gidArray;
// ...

这些分支没有把所有存储能力归并为“可写 SD 卡”。它们分别服务 Android 目录访问、OBB 安装和 FUSE 下层文件系统访问。MOUNT_EXTERNAL_PASS_THROUGH 会同时满足前面的 Android-writable 条件和后面的 pass-through 条件,所以会累加多项 GID。

条件追加内容边界
Android writable 或 pass-through带 userId 的 SDCARD_RW_GID,以及 EXT_DATA_RW_GID、EXT_OBB_RW_GID后两者不叠加 userId
installerEXT_OBB_RW_GID不能外推为所有存储目录都可写
pass-throughMEDIA_RW_GID对应下层文件系统访问需求
externalStorageAccessEXTERNAL_STORAGE_GID与前面的模式判断分别执行

最后把 ArrayList<Integer> 转成 int[]。这里只是形成一次启动快照,没有向已运行的 Linux 进程发送修改组列表的请求。调试组不符合预期时,先确认进程是否在权限或策略变更后重新启动。

3. 协议与调用者 ​

UID/GID 的选择留在 system_server,Zygote 接收参数并在子进程里应用。以下图用不同进程框标明何时只是数据、何时成为凭据。

父 Zygote 没有把自己的长期凭据改成应用 UID。相同 UID/GID 参数在子分支被消费,父分支继续作为进程创建者服务下一次请求。USAP 路径把 fork 提前,但仍需在特化阶段应用目标身份。

3.1 三个独立字段 ​

源码文件:frameworks/base/core/java/android/os/ZygoteProcess.java

相关符号:startViaZygote。以下为源码节选。

java
// null 和零长度 gids 都不生成 --setgroups;主 UID/GID 字段始终单独发送。
ArrayList<String> argsForZygote = new ArrayList<>();

// --runtime-args, --setuid=, --setgid=,
// and --setgroups= must go first
argsForZygote.add("--runtime-args");
argsForZygote.add("--setuid=" + uid);
argsForZygote.add("--setgid=" + gid);
argsForZygote.add("--runtime-flags=" + runtimeFlags);
// ...
// --setgroups is a comma-separated list
if (gids != null && gids.length > 0) {
    final StringBuilder sb = new StringBuilder();
    sb.append("--setgroups=");

    final int sz = gids.length;
    for (int i = 0; i < sz; i++) {
        if (i != 0) {
            sb.append(',');
        }
        sb.append(gids[i]);
    }

    argsForZygote.add(sb.toString());
}
// ...

字段名看起来接近系统调用,传输本身却不会改变 Linux 身份。字符串先经过 Zygote 解析,才会成为 JNI 参数。省略 --setgroups 与发送一个非空列表不同,后者最终触发一次替换附加组的系统调用。

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteArguments.java

相关符号:parseArgs。以下为源码节选。

java
// 重复身份参数会抛出异常,避免同一请求里出现两个竞争解释。
} else if (arg.startsWith("--setuid=")) {
    if (mUidSpecified) {
        throw new IllegalArgumentException(
            "Duplicate arg specified");
    }
    mUidSpecified = true;
    mUid = Integer.parseInt(getAssignmentValue(arg));
} else if (arg.startsWith("--setgid=")) {
    if (mGidSpecified) {
        throw new IllegalArgumentException(
            "Duplicate arg specified");
    }
    mGidSpecified = true;
    mGid = Integer.parseInt(getAssignmentValue(arg));
// ...
} else if (arg.startsWith("--setgroups=")) {
    if (mGids != null) {
        throw new IllegalArgumentException(
            "Duplicate arg specified");
    }

    String[] params = getAssignmentList(arg);

    mGids = new int[params.length];

    for (int i = params.length - 1; i >= 0; i--) {
        mGids[i] = Integer.parseInt(params[i]);
    }
// ...

mUidSpecified、mGidSpecified 区分“客户端显式传入”与“尚未填默认值”。mGids != null 则用于发现第二次 --setgroups。这些拒绝分支反向证明解析不是任意顺序的最后值覆盖;非法整数还会在 Integer.parseInt 处失败。

3.2 连接准入 ​

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java

相关符号:ZygoteConnection。以下为源码节选。

java
// 可信身份来自 socket peer credentials,不能由请求字符串自行声称。
ZygoteConnection(LocalSocket socket, String abiList) throws IOException {
    mSocket = socket;
    this.abiList = abiList;

    mSocketOutStream = new DataOutputStream(socket.getOutputStream());

    mSocket.setSoTimeout(CONNECTION_TIMEOUT_MILLIS);

    try {
        peer = mSocket.getPeerCredentials();
    } catch (IOException ex) {
        Log.e(TAG, "Cannot read peer credentials", ex);
        throw ex;
    }

    if (peer.getUid() != Process.SYSTEM_UID) {
        throw new ZygoteSecurityException("Only system UID is allowed to connect to Zygote.");
    }
    isEof = false;
}
// ...

当前普通 Zygote 连接入口只接受 Process.SYSTEM_UID。所以解释后续策略时,不能假设“任意普通应用都可以连上来,然后只允许设置自己的 UID”。连接准入已经先拒绝了这个前提。文件权限、SELinux 和这里的 peer 检查是不同层次,也不应只凭 --setuid 参数讨论安全性。

源码文件:frameworks/base/core/java/com/android/internal/os/Zygote.java

相关符号:minChildUid / applyUidSecurityPolicy。以下为源码节选。

java
// 正常模式下显式 UID 不能低于 system;缺省 UID/GID 使用对端凭据。
static int minChildUid(Credentials peer) {
    if (peer.getUid() == Process.SYSTEM_UID
            && FactoryTest.getMode() == FactoryTest.FACTORY_TEST_OFF) {
        /* In normal operation, SYSTEM_UID can only specify a restricted
         * set of UIDs. In factory test mode, SYSTEM_UID may specify any uid.
         */
        return Process.SYSTEM_UID;
    } else {
        return 0;
    }
}
// ...
static void applyUidSecurityPolicy(ZygoteArguments args, Credentials peer)
        throws ZygoteSecurityException {

    if (args.mUidSpecified && (args.mUid < minChildUid(peer))) {
        throw new ZygoteSecurityException(
                "System UID may not launch process with UID < "
                + Process.SYSTEM_UID);
    }

    // If not otherwise specified, uid and gid are inherited from peer
    if (!args.mUidSpecified) {
        args.mUid = peer.getUid();
        args.mUidSpecified = true;
    }
    if (!args.mGidSpecified) {
        args.mGid = peer.getGid();
        args.mGidSpecified = true;
    }
}
// ...

minChildUid 在 system UID 且非工厂测试时返回 1000。工厂测试允许更低 UID;缺省参数则填入 peer 的 UID/GID,而不是把字段初始的 0 当成授权后的 root 身份。

这一版没有单独的 applyGidSecurityPolicy():applyUidSecurityPolicy 同时完成 UID 检查和两个缺省值的填充。对于这个已经受连接准入限制的 system 调用者,源码注释允许它选择 GID 与附加组。不能凭函数名称补造一条“GID 必须与 peer 相等”的检查。

此外,ZygoteConnection.processCommand() 会拒绝客户端指定非零 permitted/effective capabilities。Linux UID、GID 和 capability 是不同参数,允许 system_server 指定应用 UID 并不意味着普通启动请求可自由附带特权位。

4. 凭据切换 ​

specialization 是把 fork 出来的通用进程转成具体应用进程的阶段。它不仅改 UID:挂载空间、资源限制、调度、安全域和运行时状态都需要在应用代码开始前准备好。因此,定位凭据错误时应看完整顺序,而不是只搜索一个 setuid。

4.1 附加组替换 ​

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

相关符号:SetGids。以下为源码节选。

cpp
// 有数组时替换附加组;null 的 child zygote 显式清空,普通 null 分支直接返回。
static void SetGids(JNIEnv* env, jintArray managed_gids, jboolean is_child_zygote,
                    fail_fn_t fail_fn) {
  if (managed_gids == nullptr) {
    if (is_child_zygote) {
      // For child zygotes like webview and app zygote, we want to clear out
      // any supplemental groups the parent zygote had.
      if (setgroups(0, NULL) == -1) {
        fail_fn(CREATE_ERROR("Failed to remove supplementary groups for child zygote"));
      }
    }
    return;
  }

  ScopedIntArrayRO gids(env, managed_gids);
  if (gids.get() == nullptr) {
    fail_fn(CREATE_ERROR("Getting gids int array failed"));
  }

  if (setgroups(gids.size(), reinterpret_cast<const gid_t*>(&gids[0])) == -1) {
    fail_fn(CREATE_ERROR("setgroups failed: %s, gids.size=%zu", strerror(errno), gids.size()));
  }
}
// ...

setgroups 安装的是提供的整个附加组集合,不是在父进程继承列表后追加几项。JNI 数组读取失败与系统调用失败都进入 fail_fn,不会携带未知组集合继续运行应用。

这里必须逐格理解 null 的语义:

managed_gidsis_child_zygote本函数行为
非 null任意读取数组并调用 setgroups
nulltruesetgroups(0, NULL) 清空继承组
nullfalse返回,本函数不改附加组

最后一行不能写成“附加组为空”,也不能写成“UID 是 root”。这段函数对它没有做变更,实际组集合取决于父进程状态和此前执行路径。child zygote 主动清空组,是为了不把父 Zygote 的附加组无意保留到下一层创建者中。

4.2 主身份切换 ​

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

相关符号:SpecializeCommon。以下为源码节选。

cpp
// 先准备仍需要特权的工作,再同时设置 real、effective、saved UID;省略段不改变这里的相对顺序。
// Keep capabilities across UID change, unless we're staying root.
if (uid != 0) {
    EnableKeepCapabilities(fail_fn);
}

SetInheritable(permitted_capabilities, fail_fn);

DropCapabilitiesBoundingSet(fail_fn, bounding_capabilities);
// ...
SetGids(env, gids, is_child_zygote, fail_fn);
SetRLimits(env, rlimits, fail_fn);
// ...
if (setresgid(gid, gid, gid) == -1) {
    fail_fn(CREATE_ERROR("setresgid(%d) failed: %s", gid, strerror(errno)));
}

// Must be called when the new process still has CAP_SYS_ADMIN, in this case,
// before changing uid from 0, which clears capabilities.  The other
// alternative is to call prctl(PR_SET_NO_NEW_PRIVS, 1) afterward, but that
// breaks SELinux domain transition (see b/71859146).  As the result,
// privileged syscalls used below still need to be accessible in app process.
SetUpSeccompFilter(uid, is_child_zygote);

// Must be called before losing the permission to set scheduler policy.
SetSchedulerPolicy(fail_fn, is_top_app);

if (setresuid(uid, uid, uid) == -1) {
    fail_fn(CREATE_ERROR("setresuid(%d) failed: %s", uid, strerror(errno)));
}
// ...

三个相同参数分别把 real、effective 和 saved GID/UID 都设置为目标值。saved UID 不再保留原来的 0,避免只更改 effective UID 却留下直接恢复旧身份的条件。不过,“三个值相同”仍不能独立证明以后永远不能切换 UID;是否拥有 CAP_SETUID、是否属于 child zygote、还有哪些内核策略,都需要继续检查。

顺序的理由比函数清单更重要:

  1. SetGids 和主 GID 设置在 UID 切换之前,确保执行时具备所需权限。
  2. SetUpSeccompFilter 放在仍有 CAP_SYS_ADMIN 的阶段。源码明确讨论了另一方案——之后设置 PR_SET_NO_NEW_PRIVS——但该方案会影响 SELinux domain transition,因此不能直接交换顺序。
  3. 调度策略同样先设置,避免丢失设置调度策略所需权限。
  4. EnableKeepCapabilities、inheritable 和 bounding set 的准备,不代表应用最终保留父进程全部 capabilities;后面还要写最终集合。

系统调用逐步生效,没有一个“全部凭据同时提交”的事务。执行到 setresgid 后、setresuid 前,进程已经处于中间状态。安全边界在于该阶段还没把控制权交给普通应用入口,而且任一步失败都会终止特化。

4.3 特化完成点 ​

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

相关符号:SpecializeCommon。以下为源码节选。

cpp
// UID 切换后仍要设置 capability 和 SELinux context;setresuid 成功并非最后一步。

SetCapabilities(permitted_capabilities, effective_capabilities, permitted_capabilities,
                fail_fn);
// ...
AStatsSocket_close();

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));
}
// ...

UID、附加组和 SELinux context 共同约束后续访问。给组列表加一个 GID,并不能保证 selinux_android_setcontext 成功,也不能跳过该调用。失败消息带上 uid、system_server 标记、se_info 和进程名,是定位“参数已传到 native,但应用未进入 Java”时的重要入口。

源码文件:frameworks/base/core/java/com/android/internal/os/Zygote.java

相关符号:forkAndSpecialize。以下为源码节选。

java
// 只有 native 返回 child 分支后才按 INET GID 更新 Java 网络允许状态;无组参数时跳过该更新。
    ZygoteHooks.preFork();

    boolean useFifoUi = SystemProperties.getInt("sys.use_fifo_ui", 0) == 1;
    int pid = nativeForkAndSpecialize(
            uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
            fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp, useFifoUi,
            pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs,
            bindMountAppStorageDirs, bindMountSyspropOverrides);
    if (pid == 0) {
        // Note that this event ends at the end of handleChildProc,
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork");

        // If no GIDs were specified, don't make any permissions changes based on groups.
        if (gids != null && gids.length > 0) {
            NetworkUtilsInternal.setAllowNetworkingForProcess(containsInetGid(gids));
        }
    }

    // Set the Java Language thread priority to the default value for new apps.
    Thread.currentThread().setPriority(Thread.NORM_PRIORITY);

    ZygoteHooks.postForkCommon();
    return pid;
}
// ...

containsInetGid(gids) 的消费者不是内核 setgroups,而是 NetworkUtilsInternal 保存的进程网络允许状态。因此出现网络问题时,需要区分权限 GID 构造、Linux 附加组落地和 Java 网络状态三个环节;这条 Java 分支也不能概括设备上的所有网络授权与防火墙策略。

主线程优先级恢复、ZygoteHooks 等继续执行后,后续子进程处理才会走向应用运行时入口。正常 fork 路径中的父进程返回 PID,不是“应用已经完成 onCreate”的确认信号。

5. 隔离身份生命周期 ​

普通进程可直接使用应用身份;isolated process 则需要额外分配身份。不要只记 99000~99999:这个范围是用户内的身份部分,App Zygote 还有 90000~98999 范围,多用户仍需经 UserHandle.getUid 组合。

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:newProcessRecordLocked。以下为源码节选。

java
// 隔离 UID 分配失败直接返回 null;特殊内部入口也可以显式提供 isolatedUid。
@GuardedBy("mService")
ProcessRecord newProcessRecordLocked(ApplicationInfo info, String customProcess,
        boolean isolated, int isolatedUid, boolean isSdkSandbox, int sdkSandboxUid,
        String sdkSandboxClientAppPackage, HostingRecord hostingRecord) {
    String proc = customProcess != null ? customProcess : info.processName;
    final int userId = UserHandle.getUserId(info.uid);
    int uid = hostingRecord.isPcc() ? info.pccUid : info.uid;
    if (isSdkSandbox) {
        uid = sdkSandboxUid;
    }
    if (Process.isSdkSandboxUid(uid) && (!isSdkSandbox || sdkSandboxClientAppPackage == null)) {
        Slog.e(TAG, "Abort creating new sandbox process as required parameters are missing.");
        return null;
    }
    if (isolated) {
        if (isolatedUid == 0) {
            IsolatedUidRange uidRange = getOrCreateIsolatedUidRangeLocked(info, hostingRecord);
            if (uidRange == null) {
                return null;
            }
            uid = uidRange.allocateIsolatedUidLocked(userId);
            if (uid == -1) {
                return null;
            }
        } else {
            // Special case for startIsolatedProcess (internal only), where
            // the uid of the isolated process is specified by the caller.
            uid = isolatedUid;
        }
        mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(uid, info.uid);
        mService.getPackageManagerInternal().addIsolatedUid(uid, info.uid);

        // Register the isolated UID with this application so BatteryStats knows to
        // attribute resource usage to the application.
        //
        // NOTE: This is done here before addProcessNameLocked, which will tell BatteryStats
        // about the process state of the isolated UID *before* it is registered with the
        // owning application.
        mService.mBatteryStatsService.addIsolatedUid(uid, info.uid);
    }
    final ProcessRecord r = new ProcessRecord(mService, info, proc, uid,
            sdkSandboxClientAppPackage,
            hostingRecord.getDefiningUid(), hostingRecord.getDefiningProcessName());
// ...

newProcessRecordLocked() 首先处理 SDK sandbox、PCC 等身份选择,再处理 isolated 分支。它没有把所有特殊进程都统一视为普通 isolated service。普通隔离创建需要取得可用 range;没有 range 或返回 -1 时,不构造一个“暂用宿主 UID”的替代进程。

隔离 UID 与宿主 UID 的关联登记给退出信息、包管理和电量统计,服务于归属和统计。这个关联不是把宿主 Linux 权限复制给隔离进程;前面 !app.isolated 的门槛仍然跳过权限 GID 合成。

5.1 分配与占用 ​

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:IsolatedUidRange。以下为源码节选。

java
// mUidUsed 记录完整 UID;分配和释放都要求持有 AMS 的相应锁。
final class IsolatedUidRange {
    @VisibleForTesting
    public final int mFirstUid;
    @VisibleForTesting
    public final int mLastUid;

    @GuardedBy("ProcessList.this.mService")
    private final SparseBooleanArray mUidUsed = new SparseBooleanArray();

    @GuardedBy("ProcessList.this.mService")
    private int mNextUid;

    IsolatedUidRange(int firstUid, int lastUid) {
        mFirstUid = firstUid;
        mLastUid = lastUid;
        mNextUid = firstUid;
    }

    @GuardedBy("ProcessList.this.mService")
    int allocateIsolatedUidLocked(int userId) {
        int uid;
        int stepsLeft = (mLastUid - mFirstUid + 1);
        for (int i = 0; i < stepsLeft; ++i) {
            if (mNextUid < mFirstUid || mNextUid > mLastUid) {
                mNextUid = mFirstUid;
            }
            uid = UserHandle.getUid(userId, mNextUid);
            mNextUid++;
            if (!mUidUsed.get(uid, false)) {
                mUidUsed.put(uid, true);
                return uid;
            }
        }
        return -1;
    }

    @GuardedBy("ProcessList.this.mService")
    void freeIsolatedUidLocked(int uid) {
        mUidUsed.delete(uid);
    }
};
// ...

算法维护一个环形游标 mNextUid。每次最多检查范围大小那么多候选值,遇到空闲完整 UID 就标记占用并返回;走完一圈仍无空位才返回 -1。因此它有明确的耗尽行为,不会无限循环。

mNextUid 是范围内值,mUidUsed 的 key 却是加上 userId 后的完整 UID。这解释了为什么同一个 appId 候选可在不同用户下形成不同身份。@GuardedBy 也不是装饰:检查空闲与标记占用必须在同一锁约束下,否则两个并发启动可能选中相同身份。

5.2 释放边界 ​

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:removeProcessNameLocked。以下为源码节选。

java
// 普通全局池释放和 App Zygote 专属清理分别处理;不能只删除一个 PID 映射。
mIsolatedProcesses.remove(uid);
mGlobalIsolatedUids.freeIsolatedUidLocked(uid);
// Remove the (expected) ProcessRecord from the app zygote
if (record != null && record.appZygote) {
    removeProcessFromAppZygoteLocked(record);
}
// ...

全局隔离池释放是删除占用记录;App Zygote 分支还委托 removeProcessFromAppZygoteLocked() 清理该创建者相关状态。UID 回收到候选池不意味着它永远属于原来那个进程,排查时必须把 UID、PID、进程启动序列和时间一起看。

Process.isIsolatedUid() 先调用 UserHandle.getAppId(uid),再检查两段 isolated 范围。直接拿多用户完整 UID 与 99000~99999 比较,会把 userId 非零的隔离进程错误判为普通应用。

App Zygote 的实际 UID transition 还有条件路径:ProcessList.startProcess 在 Flags.useSafesetidUidPolicy2()、UidTransitionPolicy.isEnabled() 且策略对象存在时,先授权创建者到目标 UID 的转换。这里的 UID 分配器不能替代该内核策略;SDK sandbox、PCC 和 child zygote 也不能只凭普通应用的 UID/GID 顺序断言所有安全行为一致。

6. 失败后的状态 ​

如果已经改完 GID,接下来 setresuid 失败,应当回滚还是继续启动?真实实现既不会让应用以中间身份继续,也没有在这里实现一套凭据回滚事务。

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

相关符号:SpecializeCommon / zygote::ZygoteFailure。以下为源码节选。

cpp
// fail_fn 绑定到不可返回的 ZygoteFailure,最终进入 JNI FatalError。
auto fail_fn = std::bind(ZygoteFailure, env, process_name, managed_nice_name, _1);
// ...
[[noreturn]]
void zygote::ZygoteFailure(JNIEnv* env,
                           const char* process_name,
                           jstring managed_process_name,
                           const std::string& msg) {
  std::unique_ptr<ScopedUtfChars> scoped_managed_process_name_ptr = nullptr;
  if (managed_process_name != nullptr) {
    scoped_managed_process_name_ptr.reset(new ScopedUtfChars(env, managed_process_name));
    if (scoped_managed_process_name_ptr->c_str() != nullptr) {
      process_name = scoped_managed_process_name_ptr->c_str();
    }
  }

  const std::string& error_msg =
      (process_name == nullptr || process_name[0] == '\0') ?
      msg : StringPrintf("(%s) %s", process_name, msg.c_str());

  env->FatalError(error_msg.c_str());
  __builtin_unreachable();
}
// ...

native 特化失败发生在当前正在特化的子进程中。FatalError 终止当前运行时路径,成功返回到应用入口不再可能。父 Zygote 与 system_server 的状态清理由它们的独立失败、死亡和启动超时路径负责;不能把 fail_fn 描述成直接删除 AMS 的 ProcessRecord。

更早的 Java 参数准备失败有另一条清理路径:

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关符号:startProcessLocked。以下为源码节选。

java
// 参数准备阶段的 RuntimeException 进入启动失败清理,并向调用方返回 false。
    } catch (RuntimeException e) {
        Slog.e(ActivityManagerService.TAG, "Failure starting process " + app.processName, e);

        // Something went very wrong while trying to start this process; one
        // common case is when the package is frozen due to an active
        // upgrade. To recover, clean up any active bookkeeping related to
        // starting this process. (We already invoked this method once when
        // the package was initially frozen through KILL_APPLICATION_MSG, so
        // it doesn't hurt to use it again.)
        mService.forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid),
                false, false, true, false, false, false, app.userId, "start failure");
        return false;
    }
}
// ...

这是早期启动异常的收尾,不应把它当成“所有 native 子进程崩溃都会同步抛回此 catch”。如果父进程已返回 PID、子进程随后特化失败,还需结合SIGCHLD进程死亡处理中的父进程回收与 AMS death 路径判断时间线。

失败阶段当前已发生的事应追踪的入口
包权限或存储参数查询可能尚未 forkstartProcessLocked 的异常收尾
重复 UID/GID 参数参数未形成可信请求ZygoteArguments.parseArgs
非 system peer连接未获准处理ZygoteConnection 构造函数
目标 UID 低于正常下限请求被拒绝applyUidSecurityPolicy
setgroups 或 setresgid 失败子进程可能已完成部分准备SetGids、SpecializeCommon、ZygoteFailure
setresuid 或安全域设置失败可能处于已降权的中间状态native fatal 日志及子进程退出
隔离范围耗尽没有可用目标身份allocateIsolatedUidLocked 返回 -1

7. 数值与凭据核对 ​

数值映射和实际进程凭据需要不同证据。UserHandleTest 可以验证函数算式,但不能证明某个启动分支确实传了你以为的参数;/proc 能展示最终凭据,却不会告诉你是哪个 Java 调用算出了它。

7.1 映射断言 ​

源码文件:frameworks/base/core/tests/coretests/src/android/os/UserHandleTest.java

相关符号:testMerge / testCache / testShared。以下为源码节选。

java
// 测试同时覆盖主用户、多用户、非法 cache/shared 输入;辅助断言最终调用 assertEquals。
@Test
public void testMerge() throws Exception {
    EXPECT_EQ(0, multiuser_get_uid(0, 0));
    EXPECT_EQ(1000, multiuser_get_uid(0, 1000));
    EXPECT_EQ(10000, multiuser_get_uid(0, 10000));
    EXPECT_EQ(50000, multiuser_get_uid(0, 50000));
    EXPECT_EQ(1000000, multiuser_get_uid(10, 0));
    EXPECT_EQ(1001000, multiuser_get_uid(10, 1000));
    EXPECT_EQ(1010000, multiuser_get_uid(10, 10000));
    EXPECT_EQ(1050000, multiuser_get_uid(10, 50000));
}
// ...
@Test
public void testCache() throws Exception {
    EXPECT_EQ(ERR_GID, multiuser_get_cache_gid(0, 0));
    EXPECT_EQ(ERR_GID, multiuser_get_cache_gid(0, 1000));
    EXPECT_EQ(20000, multiuser_get_cache_gid(0, 10000));
    EXPECT_EQ(ERR_GID, multiuser_get_cache_gid(0, 50000));
    EXPECT_EQ(ERR_GID, multiuser_get_cache_gid(10, 0));
    EXPECT_EQ(ERR_GID, multiuser_get_cache_gid(10, 1000));
    EXPECT_EQ(1020000, multiuser_get_cache_gid(10, 10000));
    EXPECT_EQ(ERR_GID, multiuser_get_cache_gid(10, 50000));

    // PCC UIDs
    EXPECT_EQ(60000, multiuser_get_cache_gid(0, 30000));
    EXPECT_EQ(1060000, multiuser_get_cache_gid(10, 30000));
    EXPECT_EQ(69999, multiuser_get_cache_gid(0, 39999));
    EXPECT_EQ(1069999, multiuser_get_cache_gid(10, 39999));
}
// ...
@Test
public void testShared() throws Exception {
    EXPECT_EQ(0, multiuser_get_shared_gid(0, 0));
    EXPECT_EQ(1000, multiuser_get_shared_gid(0, 1000));
    EXPECT_EQ(50000, multiuser_get_shared_gid(0, 10000));
    EXPECT_EQ(ERR_GID, multiuser_get_shared_gid(0, 50000));
    EXPECT_EQ(0, multiuser_get_shared_gid(10, 0));
    EXPECT_EQ(1000, multiuser_get_shared_gid(10, 1000));
    EXPECT_EQ(50000, multiuser_get_shared_gid(10, 10000));
    EXPECT_EQ(ERR_GID, multiuser_get_shared_gid(10, 50000));
}
// ...

testMerge 输入 userId 10 与 appId 10000,断言 1010000,验证多用户组合;testCache 输入 (10, 10000) 断言 1020000,并对系统 UID 或不适用范围断言 ERR_GID;testShared 在 userId 0 和 10 下均断言 appId 10000 的共享组为 50000,验证它不携带用户前缀。

测试文件里的 multiuser_get_cache_gid(userId, appId) 实际调用双参数 getCacheAppGid。因此,测试中的 1020000 不能直接拿来推翻前面 ProcessList 单参数调用先剥掉 userId 的事实。这正是阅读测试时必须同时核对测试入口与生产调用方的原因。

在完成 AOSP 构建环境与目标设备准备后,可以执行对应测试类:

bash
atest FrameworksCoreTests:android.os.UserHandleTest

这组测试覆盖数值映射,不验证 Zygote 连接身份、Linux setgroups 成功率、SELinux context 或设备存储目录可见性。解析器的重复参数拒绝与 native 的失败终止,应分别针对其分支验证,不能因为一个核心测试类通过便认为完整进程特化已通过测试。

7.2 进程凭据 ​

对允许访问目标进程信息的调试环境,可以先取得测试进程 PID,再读取 /proc/<pid>/status。PID 查询结果可能含多个进程,应逐个核对;以下命令中的 12345 只是需要替换的示例 PID。

bash
adb shell pidof com.example.identitydemo
adb shell cat /proc/12345/status

重点看 Uid:、Gid: 与 Groups:。前两项通常展示 real、effective、saved、filesystem 四个值,最后一项展示附加组。setresuid(uid, uid, uid) 中只有三个参数,不能把 /proc 的四列误读成源码传了四个 UID。目标设备的 proc 访问限制可能禁止 shell 读取其他进程,此时应使用有权限的测试环境或由测试应用读取自身状态。

核对步骤应从调用方往下收窄:先确认进程是否 isolated、所属 userId 和 ProcessRecord.uid,再算 computeGidsForProcess,随后核对传输参数和 /proc 的实际集合。只看数字大小猜“这是普通进程还是隔离进程”,会在多用户和 SDK sandbox 等情况下失效。

假设测试应用的普通进程附加组包含某个权限 GID,而 isolated service 不包含它,下一步应定位 !app.isolated 的参数构造分支;若普通进程仍不含预期组,应继续查包权限查询、deniedPermissions 和是否使用旧进程。若 /proc 凭据完全正确但文件访问失败,调查对象就应转向目录 mode/owner、挂载空间与 SELinux,而不是继续重复修改 setresuid。

bash
rg -n 'computeGidsForProcess|deniedPermissions|allocateIsolatedUidLocked'   frameworks/base/services/core/java/com/android/server/am/ProcessList.java
rg -n -- '--setuid|--setgid|--setgroups|applyUidSecurityPolicy'   frameworks/base/core/java/android/os/ZygoteProcess.java   frameworks/base/core/java/com/android/internal/os/{ZygoteArguments,Zygote}.java
rg -n 'SetGids|setresgid|setresuid|ZygoteFailure'   frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

完成这些定位后,读者应能把一个实际 UID/GID 问题归入参数计算、协议准入、内核切换或后续访问控制中的某一层。继续阅读进程Capability设置时,重点就是本篇尚未展开的“切换 UID 前准备哪些能力,切换后最终留下哪些能力”,而不是重复一遍身份设置函数列表。