进程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.uid | AMS 中该进程的记录 | 可与包 UID 不同,如隔离进程 |
启动参数 uid、gid | ProcessList.startProcess 的实参 | 目标 Linux UID 与主 GID |
启动参数 gids | 权限 GID、用户及存储相关附加项 | Linux supplementary groups |
| real/effective/saved UID、GID | Linux 进程凭据 | 身份、访问检查和身份切换语义 |
主 GID 是单值,附加组是列表,二者共同参与传统文件访问控制。Android 权限本身还可以由 Binder 服务、AppOps 等实现;“授予 Android 权限”不等于必定增加一个 Linux GID,“加入组”也不等于能够越过 SELinux 或文件系统挂载边界。
1.1 用户编码
源码文件:frameworks/base/core/java/android/os/UserHandle.java
相关符号:getUid / getAppId / getUserGid。以下为源码节选。
// 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。以下为源码节选。
// 普通 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。以下为源码节选。
// 隔离进程保留初始的 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。以下为源码节选。
// 注意 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。以下为源码节选。
// 单参数重载把输入当作完整 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。以下为源码节选。
// 不同存储模式添加不同组;只有部分组携带 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 |
| installer | EXT_OBB_RW_GID | 不能外推为所有存储目录都可写 |
| pass-through | MEDIA_RW_GID | 对应下层文件系统访问需求 |
| externalStorageAccess | EXTERNAL_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。以下为源码节选。
// 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。以下为源码节选。
// 重复身份参数会抛出异常,避免同一请求里出现两个竞争解释。
} 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。以下为源码节选。
// 可信身份来自 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。以下为源码节选。
// 正常模式下显式 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。以下为源码节选。
// 有数组时替换附加组;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_gids | is_child_zygote | 本函数行为 |
|---|---|---|
| 非 null | 任意 | 读取数组并调用 setgroups |
| null | true | setgroups(0, NULL) 清空继承组 |
| null | false | 返回,本函数不改附加组 |
最后一行不能写成“附加组为空”,也不能写成“UID 是 root”。这段函数对它没有做变更,实际组集合取决于父进程状态和此前执行路径。child zygote 主动清空组,是为了不把父 Zygote 的附加组无意保留到下一层创建者中。
4.2 主身份切换
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
相关符号:SpecializeCommon。以下为源码节选。
// 先准备仍需要特权的工作,再同时设置 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、还有哪些内核策略,都需要继续检查。
顺序的理由比函数清单更重要:
SetGids和主 GID 设置在 UID 切换之前,确保执行时具备所需权限。SetUpSeccompFilter放在仍有CAP_SYS_ADMIN的阶段。源码明确讨论了另一方案——之后设置PR_SET_NO_NEW_PRIVS——但该方案会影响 SELinux domain transition,因此不能直接交换顺序。- 调度策略同样先设置,避免丢失设置调度策略所需权限。
EnableKeepCapabilities、inheritable 和 bounding set 的准备,不代表应用最终保留父进程全部 capabilities;后面还要写最终集合。
系统调用逐步生效,没有一个“全部凭据同时提交”的事务。执行到 setresgid 后、setresuid 前,进程已经处于中间状态。安全边界在于该阶段还没把控制权交给普通应用入口,而且任一步失败都会终止特化。
4.3 特化完成点
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
相关符号:SpecializeCommon。以下为源码节选。
// 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。以下为源码节选。
// 只有 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。以下为源码节选。
// 隔离 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。以下为源码节选。
// 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。以下为源码节选。
// 普通全局池释放和 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。以下为源码节选。
// 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。以下为源码节选。
// 参数准备阶段的 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 路径判断时间线。
| 失败阶段 | 当前已发生的事 | 应追踪的入口 |
|---|---|---|
| 包权限或存储参数查询 | 可能尚未 fork | startProcessLocked 的异常收尾 |
| 重复 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。以下为源码节选。
// 测试同时覆盖主用户、多用户、非法 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 构建环境与目标设备准备后,可以执行对应测试类:
atest FrameworksCoreTests:android.os.UserHandleTest这组测试覆盖数值映射,不验证 Zygote 连接身份、Linux setgroups 成功率、SELinux context 或设备存储目录可见性。解析器的重复参数拒绝与 native 的失败终止,应分别针对其分支验证,不能因为一个核心测试类通过便认为完整进程特化已通过测试。
7.2 进程凭据
对允许访问目标进程信息的调试环境,可以先取得测试进程 PID,再读取 /proc/<pid>/status。PID 查询结果可能含多个进程,应逐个核对;以下命令中的 12345 只是需要替换的示例 PID。
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。
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 前准备哪些能力,切换后最终留下哪些能力”,而不是重复一遍身份设置函数列表。
