写在前面
了解本节内容,你会收获:
- 系统是如何实现 App 进程之间的数据隔离,换句话说就是为什么 App 进程的工作目录(
/data/data/package-name/
) 只有本进程能访问,其他进程不能访问 - 为什么 普通进程没有权限访问 system 或者 vendor 目录下的文件
UGO (User、Group and Others) 背景
UGO 机制起源于 Linux 的多用户的环境需求,每个文件(客体)只能被特定的一个用户或者一组用户所访问。所以必须对文件的访问权限进行管理。 因此,Linux 对每个文件都打了一个标签,这个标签就是 UID (User ID) 和 GID (Group ID),然后每个用户也有 UID 和 GID,所以系统就可以基于文件的 UID/GID 标识和用户本身的 UID/GID 标识制定一个匹配的规则,来确保不同的用户对不同的文件具有不同的访问权限(权限包括:读、写、执行),而这个规则就是 UGO (User、Group and Others) 权限模型。基于以上的分析,我们可以抽象出基于 UGO 权限和 UID/GID 组成的安全控制策略的顶层模型

Linux UGO 权限模型
根据上图描述的安全控制策略的顶层模型,用一句话来描述就是:主体对于客体的访问规则 ,这个模型由 主体 、客体 、规则 三部分组成。
UGO 权限模型的主体(Subject)
从 UGO 的背景来看,UGO 权限模型的主体是用户,但是用户并不能直接访问文件,用户必须通过它的载体-进程才能访问文件,用户的一系列访问行为,最终都演变成这个用户环境下一系列进程的行为,所以用户的 UID/GID 最终标识的就是进程的 UID/GID。所以,在 UGO 权限模型中的主体是进程
当我们使用 chawan 这个用户登录 Linux 系统,然后 run 一个 top 进程,我们通过命令 *ps -eo pid, user, group, comm*
来查看这个 top 进程的 UID/GID 标识

我们可以看到对于 top 这个进程的 UID 是 chawan, GID 是 users
UGO 权限模型的客体(Object)
在 Linux 系统中,UGO 的客体是什么,是文件,文件也需要带有不同的 UID 和 GID 的标识来区分或者隔离不同的主体的访问限制

可以看到,.ssh 文件的 UID 是 chawan,GID 是 user
UGO 权限模型的规则(Access Rule)
主体和客体的 UID 和 GID 有了,接下来分析 UGO 权限模型基于 UID/GID 的访问规则,举个例子

同样以 .ssh 文件夹为例子:
- 文件的 UID 是 chawan
- 文件的 GID 是 users
- d 则标识这是一个文件夹
- 对于所有者的权限是: rwx, 可读可写可执行
- 对于组用户的权限是:---, 不可读不可写不可执行
- 对于其它用户的权限是: ---, 不可读不可写不可执行
所以,只有主体(进程)带有 chawan 这个 UID 这个标识的主体(进程)才能对这 .ssh 这个文件具有读写权限,其它用户 run 的进程就没有读写权限
Android UGO 权限模型
Android UGO 和 Linux GUO 的异同
了解了 Linux 基本的 UGO 机制之后,我们接下来思考一个问题 Android 系统同样也是基于 Linux kernel,所以同样可以把 UGO 机制移植到 Android 系统上。但是在 Linux 中,进程的 UID/GID 是通过登录的用户身份来赋予每个运行的进程相对应的 UID/GID 标识。 Android 虽然也支持多用户,但是与 Linux 的多用户还是有区别的,Android 系统并没有基于多用户给 App进程设置 UIG/GID, 所以把 UGO 机制移植到 Android 系统上最主要的问题就是要解决 App 进程 UID/GID 赋值的问题
那么既然 Linux 是通过当前登录的用户来给予主体(进程)相对应的 UID 和 GID,那么 Android 系统也可以采取相似的策略来给予主体(进程) 相对应的 UID 和 GID。也确实是如此,Android 系统中运行的 App 进程的 UID 和 GID 都是由 PackageManageService 在安装时分配的,相比于 Linux 系统,其实也是"换汤不换药",最终都还是变成了主体(进程)对客体(文件)的访问规则。
App 进程 UID 和 GID 的分配
上面我们提到 Android 的 UID/GID 的分配是 PackageManageService 在安装时,我们从代码角度来看一下,这里我们不谈整个 App 的安装过程,只关注在安装过程中 UID/GID 的分配(以 Android 15 为例子,不同的 Android 版本代码可能不一样),
java
//frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
void installPackagesTraced(List<InstallRequest> requests) {
mInstallPackageHelper.installPackagesTraced(requests);
}
//frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
void installPackagesTraced(List<InstallRequest> requests) {
...
if (prepareInstallPackages(requests)
&& scanInstallPackages(requests, createdAppId, versionInfos)) {}
...
}
// frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private boolean scanInstallPackages(List<InstallRequest> requests,
Map<String, Boolean> createdAppId, Map<String, Settings.VersionInfo> versionInfos) {
for (InstallRequest request : requests) {
final ParsedPackage packageToScan = request.getParsedPackage();
final String packageName = packageToScan.getPackageName();
...
if (isApex || (isSdkLibrary && disallowSdkLibsToBeApps())) {
request.getScannedPackageSetting().setAppId(Process.INVALID_UID);
} else {
createdAppId.put(packageName, optimisticallyRegisterAppId(request));
}
...
}
return true;
}
// frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private boolean optimisticallyRegisterAppId(@NonNull InstallRequest installRequest) {
...
PMS 持有 Settings 这个变量,里面存储所有的 App 的安装信息
mPm.mSettings.registerAppIdLPw(installRequest.getScannedPackageSetting(),
installRequest.needsNewAppId());
}
}
//frameworks/base/services/core/java/com/android/server/pm/Settings.java
boolean registerAppIdLPw(PackageSetting p, boolean forceNew) throws PackageManagerException {
if (p.getAppId() == 0 || forceNew) {
// Assign new user ID
p.setAppId(mAppIds.acquireAndRegisterNewAppId(p));
} else {
// Add new setting to list of user IDs
createdNew = mAppIds.registerExistingAppId(p.getAppId(), p, p.getPackageName());
}
}
// frameworks/base/services/core/java/com/android/server/pm/AppIdSettingMap.java
public int acquireAndRegisterNewAppId(SettingBase obj) {
final int size = mNonSystemSettings.size();
// 考虑到有 app 被 uninstall 的情况,复用之前的 UID
for (int i = mFirstAvailableAppId - Process.FIRST_APPLICATION_UID; i < size; i++) {
if (mNonSystemSettings.get(i) == null) {
mNonSystemSettings.set(i, obj);
return Process.FIRST_APPLICATION_UID + i;
}
}
mNonSystemSettings.add(obj);
// 如果不能复用,则UID 在已安装的 app 的UID 上递增
return Process.FIRST_APPLICATION_UID + size;
}
App 在安装过程中的分配,是在 Process.FIRST_APPLICATION_UID = 10000 的基础上递增,并且保存到 Settings 中,这也就解释了普通应用的 UID 都大于 10000 了
Runtime UID/GID 设置
PMS 在安装的时候为每个 App 进程分配了 UID 和 GID,真正设置 App 进程的 UID 和 GID 是发生 Zygote fork App 进程的阶段。 Android App 进程都是由 Zygote 进程 fork 出来,Zygote 是 App 进程的父进程,如果我们不对 App 进程重新设置 UIG/GID 的话,那么所以 App 都会继承 Zygote 的 UID/GID , 这样会导致进程隔离性失效。因此,Zygote 在 fork 出 App 进程之后,立马重新设置了 App 进程的 UID 和 GID
C++
static jint com_android_internal_os_Zygote_nativeForkSystemServer(
JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities,
jlong effective_capabilities) {
...
// 这里会通过 fork 出一个子进程
pid_t pid = zygote::ForkCommon(env, true,
fds_to_close,
fds_to_ignore,
true);
// pid == 0 代表代码进入了子进程
if (pid == 0) {
...
SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities,
effective_capabilities, 0, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true,
false, nullptr, nullptr, /* is_top_app= */ false,
/* pkg_data_info_list */ nullptr,
/* allowlisted_data_info_list */ nullptr, false, false, false);
} else if (pid > 0) {
...
}
return pid;
}
在 FrokCommon 这个方法中,会调用 fork 系统命令,创建一个新的子进程,接下来新的 子进程会执行 SpecializeCommon 内部的代码,在 SpecializeCommon 内部中会分别调用 setresgid
和 setresuid
来设置当前子进程的 UID 和 GID
C++
static void SpecializeCommon() {
...
if (setresgid(gid, gid, gid) == -1) {
fail_fn(CREATE_ERROR("setresgid(%d) failed: %s", gid, strerror(errno)));
}
...
if (setresuid(uid, uid, uid) == -1) {
fail_fn(CREATE_ERROR("setresuid(%d) failed: %s", uid, strerror(errno)));
}
}
这里注意到,无论是 setgid 还是 setuid 都传入了三个一样的参数。这里为什么要传入三个呢? 实际上这三个参数分别是 RUID(real UID), EUID(effective UID) 和 SUID (Saved UID)
RUID
- RUID 是进程的身份标识,它在进程创建时被设定为启动该进程的用户 UID
- 操作系统对于访问权限的判断和 RUID 无关,所以 RUID 没有真正的权利
- 子进程会继承父进程的 RUID
EUID
- 有效用户ID,对文件系统和其他资源的访问都以 EUID 为依据
- 通常情况下 EUID = RUID = 父进程的 RUID
- 在特殊情况下,EUID 会被临时修改,比如二进制文件设置了 set-UID 位 的时候,如果该进程执这个二进制文件的时候,那么这个进程的 EUID 会被设置为文件的 UID,这样说有点绕,举个例子:
在Linux 中我们可以查看 /usr/bin/passwd 这个可执行文件的 UGO 权限
diff
...
-rwxr-xr-x 1 root root 63952 Dec 9 10:54 ldappasswd
-rwsr-xr-x 1 root root 59976 Feb 6 2024 passwd
-rwxr-xr-x 1 root root 39224 Jan 6 2024 smbpasswd
...
可以看到, passwd 相比于其他二进制文件,对于 User 的权限是rws而不是 rwx ,结合上面提到的 EUID 的作用,当一个进程执行这个二进制文件的时候,会 fork 一个子进程,子进程的 RUID 会继承父进程 RUID不会改变,但是这个子进程的 EUID 会被修改为文件的 UID,而这个文件的 UID 是 root,那么意味着这个子进程的 EUID 会临时变成 root。前面提到,所有的权限相关的判断都只跟 EUID 有关系,所以这个子进程就拥有了 root 的权限。
- EUID 不会被子进程继承,所以即使父进程的 EUID 为 root,但是 RUID 为普通用户,那么 fork 出来的子进程的 EUID 依旧为普通用户的权限
SUID
- 保存用户用于在进程运行过程中暂存原有的 EUID
- 用于恢复权限修改之前的 EUID
APP工作目录创建和权限设置
给 App 创建了 UID/GID 之后,系统会给每一个 App 创建一个单独的工作目录,位于 /data/user/user-id/package-name
,并且给每个 App 的工作目录都设置了 UID/GID。 APP工作目录创建和权限设置也是发生在 APP 安装时候,由 PMS 发起,但是并不是 PMS 完成,而是借助了installd 这个 deamon 进程来完成。这里同样也是考虑到了权限的问题,因为 PMS 是运行在 system_server 进程的,system_server 这个进程的 UID 和 GID 是 system, 而 installd 的 UID 和 GID 是 root,所以 installd 有 "超能力" 为 APP 创建工作目录并且设置权限。
PMS 安装的代码很长,前面的代码我们不分析了,我们直接看 installd 是如何创建目录并且设置对应的权限
C++
//frameworks/native/cmds/installd/InstalldNativeService.cpp
Binder::Status InstalldNativeService::createAppDataLocked(...) {
...
// pkgname 就是 app 的包名,例如:com.android.example
const char* pkgname = packageName.c_str();
...
// 为工作目录创建读写权限,
// 0700: rwx------, 只有 Owner 才有读写权限
// 0751: rwxr-x--x, 只有 Owner 才有读写权限,但是 Group 也有读的权限
mode_t targetMode = targetSdkVersion >= MIN_RESTRICTED_HOME_SDK_VERSION ? 0700 : 0751;
// FLAG_STORAGE_CE 和 FLAG_STORAGE_DE 是加密相关,这里暂时不扩展,默认是使用 CE(Credential Encrypted)
if (flags & FLAG_STORAGE_CE) {
// path 就是 app 的工作目录:/data/user/userid/package_name/
auto path = create_data_user_ce_package_path(uuid_, userId, pkgname);
// 内部会调用 fs_prepare_dir_strict
1. 来创建目录
2. 根据 Apk 的 UID 和 GID,修改文件的 UID 和 GID 属性
3. 重新设置文件的访问权限为 0700 或 0751
auto status = createAppDataDirs(path, uid, uid, previousUid, cacheGid, seInfo, targetMode,
projectIdApp, projectIdCache);
...
}
if (flags & FLAG_STORAGE_DE) {
...
}
...
return ok();
}
createAppDataDirs
这个方法最终会调用 system/core/libcutils/fs.cpp 的 fs_prepare_path_impl
fs.cpp
static int fs_prepare_path_impl(const char* path, mode_t mode, uid_t uid, gid_t gid,
int allow_fixup, int prepare_as_dir) {
...
create_result = prepare_as_dir
? TEMP_FAILURE_RETRY(mkdir(path, mode))
: TEMP_FAILURE_RETRY(open(path, O_CREAT | O_CLOEXEC | O_NOFOLLOW | O_RDONLY, 0644));
if (TEMP_FAILURE_RETRY(chmod(path, mode)) == -1) {
ALOGE("Failed to chmod(%s, %d): %s", path, mode, strerror(errno));
return -1;
}
if (TEMP_FAILURE_RETRY(chown(path, uid, gid)) == -1) {
ALOGE("Failed to chown(%s, %d, %d): %s", path, uid, gid, strerror(errno));
return -1;
}
...
}
在 fs_prepare_path_impl
中,
- 通过
mkdir
创建了文件,也就是 /data/user/user-id/package-name/ 目录 - 通过
chown
修改文件的 UID 和 GID - 通过
chmod
修改文件的访问权限
文件的创建以及权限的设置就分析完了,通过 App 本身的 UID/GID 设置 App工作目录的 UID/GID,同时设置权限为 rwx------
或者 rwxr-x--x
这样就确保了只有 App 进程本身才有权限读写自己的工作目录从而达到了 App 进程之间的数据隔离。 Android 对于 App 进程之间的数据隔离,整个流程如下

可以看出,Android 对于 App 进程之间的数据保护/隔离,本质上还是依赖于 Linux Kernel 的 UGO 权限模型。
UGO 权限模型的缺点
- 颗粒度太大,导致权限过剩/溢出,比如某个进程需要访问某几个文件,不同的文件具有不同的 UID/GID 标识,我们有的时候不得不给 root 权限来满足我们的需求,这样会导致这个进程的权利太大
- root 权利太大,如果被"不法分子"拿到了 root 权限,会导致系统毫无安全性可言
为了解决这个问题,Android 引入了 SELinux (Security-Enhanced Linux),并且基于自身情况扩展成了 SEAndroid,即使在设备被 root 的情况下也能有效的保护系统资源。