用户空间隔离
本文承接 多用户包管理 和 多用户安装,聚焦“用户 A 为什么不能读取用户 B 的应用数据”这条源码链路。文章不把目录表当作安全模型,而是追踪 UserDataPreparer、AppDataHelper、installd、StorageManagerInternal 和跨用户权限检查各自拥有的状态。
Android 的隔离不是单一目录名:代码路径通常共享,应用数据按 userId 分目录,Linux UID 按 userId 重新映射,CE/DE key 决定数据何时可访问,serial xattr 防止 userId 回收后复用旧目录。任何一层缺失,隔离都可能退化为“路径看起来不同但状态不一致”。
1. 隔离层次
| 层 | owner | 保护对象 |
|---|---|---|
| 路径 | Environment / vold | /data/user/<id>、/data/user_de/<id> |
| 身份 | UserHandle + appId | 每用户不同 Linux UID |
| 密钥 | StorageManagerInternal | CE/DE 加密 key 与可用时机 |
| 目录 | UserDataPreparer / installd | 应用目录 owner、label、inode |
| API | Computer.enforceCrossUserPermission | 哪个调用者能操作哪个 user |
2. 路径生成
源码文件:frameworks/base/core/java/android/os/Environment.java,符号:getDataUserCeDirectory、getDataUserDeDirectory、package directory helpers
public static File getDataUserDeDirectory(
String volumeUuid, int userId) {
return newFilePrep(
getDataUserDeDirectory(volumeUuid),
String.valueOf(userId));
}
public static File getDataUserDePackageDirectory(
@Nullable String volumeUuid, int userId,
@NonNull String packageName) {
return newFilePrep(
getDataUserDeDirectory(volumeUuid, userId),
packageName);
}CE 路径对应 getDataUserCeDirectory,DE 路径对应 getDataUserDeDirectory;volumeUuid 为空代表内部存储,非空代表可写私有卷。代码不应硬编码 /data/user/0,因为路径 helper 还要处理 volume 和平台兼容目录。
源码文件:frameworks/base/core/java/android/os/Environment.java,符号:系统级用户目录
public static File getDataSystemDeDirectory(int userId) {
return buildPathPrep(getDataDirectory(),
"system_de", String.valueOf(userId));
}
public static File getDataSystemCeDirectory(int userId) {
return buildPathPrep(getDataDirectory(),
"system_ce", String.valueOf(userId));
}
public static File getDataMiscDeDirectory(int userId) {
return buildPathPrep(getDataDirectory(),
"misc_de", String.valueOf(userId));
}系统服务自己的 user data 也分 CE/DE;应用目录和 system/misc 目录由不同 owner 使用,但都要通过同一个 userId 和加密时机模型管理。
3. UID 隔离
源码文件:frameworks/base/core/java/android/os/UserHandle.java,符号:PER_USER_RANGE、getUid、getUserId
public static final int PER_USER_RANGE = 100000;
public static int getUid(int userId, int appId) {
if (MU_ENABLED && appId >= 0) {
return userId * PER_USER_RANGE
+ (appId % PER_USER_RANGE);
} else {
return appId;
}
}
public static int getUserId(int uid) {
if (MU_ENABLED) {
return uid / PER_USER_RANGE;
}
return UserHandle.USER_SYSTEM;
}同一包在 user 0 和 user 10 可能共享 appId,但最终 UID 不同。installd 创建目录时接收 appId/userId,结合 SELinux 和 Linux DAC 设置 owner;PMS 仅保存状态和参数,不能用 Java 目录访问替代 UID 隔离。
4. 用户存储准备
源码文件:frameworks/base/services/core/java/com/android/server/pm/UserDataPreparer.java,符号:prepareUserData
void prepareUserData(UserInfo userInfo, int flags) {
try (PackageManagerTracedLock lock =
mInstallLock.acquireLock()) {
StorageManager storage =
mContext.getSystemService(StorageManager.class);
if (storage == null) {
Log.e(TAG, "prepareUserData failed");
return;
}
prepareUserDataLI(null, userInfo, flags,
true /* allowRecover */);
for (VolumeInfo vol
: storage.getWritablePrivateVolumes()) {
String volumeUuid = vol.getFsUuid();
if (volumeUuid != null) {
prepareUserDataLI(volumeUuid,
userInfo, flags, true);
}
}
}
}内部存储先准备,再准备 adoptable/private volume,因为用户 volume key 存放在内部存储。整个过程持有 install lock,避免同一 user 的存储准备与包数据创建交错。
源码文件:frameworks/base/services/core/java/com/android/server/pm/UserDataPreparer.java,符号:prepareUserDataLI
final int userId = userInfo.id;
final int userSerial = userInfo.serialNumber;
getStorageManagerInternal().prepareUserStorage(
volumeUuid, userId, flags);
if ((flags & StorageManager.FLAG_STORAGE_DE) != 0) {
enforceSerialNumber(
getDataUserDeDirectory(volumeUuid, userId),
userSerial);
if (Objects.equals(volumeUuid,
StorageManager.UUID_PRIVATE_INTERNAL)) {
enforceSerialNumber(
getDataSystemDeDirectory(userId),
userSerial);
}
}
if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
enforceSerialNumber(
getDataUserCeDirectory(volumeUuid, userId),
userSerial);
}
mInstaller.createUserData(
volumeUuid, userId, userSerial, flags);顺序是:StorageManager 准备底层 key/根目录,PMS 检查目录 serial,再让 installd 创建应用数据结构。UserDataPreparer 不直接负责每个 APK 的 package directory;它负责用户级根存储和系统数据,包级目录由 AppDataHelper 调用 installd 完成。
5. serial 防止复用
源码文件:frameworks/base/services/core/java/com/android/server/pm/UserDataPreparer.java,符号:enforceSerialNumber、setSerialNumber
void enforceSerialNumber(File file,
int serialNumber) throws IOException {
final int foundSerial = getSerialNumber(file);
if (foundSerial == -1) {
try {
setSerialNumber(file, serialNumber);
} catch (IOException e) {
Slog.w(TAG,
"Failed to set serial number on " + file,
e);
}
} else if (foundSerial != serialNumber) {
throw new IOException(
"Found serial number " + foundSerial
+ " doesn't match expected "
+ serialNumber);
}
}
private static void setSerialNumber(File file,
int serialNumber) throws IOException {
byte[] buf = Integer.toString(serialNumber)
.getBytes(StandardCharsets.UTF_8);
Os.setxattr(file.getAbsolutePath(),
"user.serial", buf, OsConstants.XATTR_CREATE);
}userId 可能在未来被回收,但 serialNumber 不应复用旧用户的目录。xattr 位于目录 inode 上,若发现已有 serial 与当前用户不一致,准备流程失败,不会把旧目录当作新用户数据继续使用。
6. 准备失败与恢复
源码文件:frameworks/base/services/core/java/com/android/server/pm/UserDataPreparer.java,符号:prepareUserDataLI 异常分支
} catch (Exception e) {
final boolean isNewUser =
userInfo.lastLoggedInTime == 0;
if (isNewUser) {
logCriticalInfo(Log.ERROR,
"Destroying user " + userId
+ " because prepare failed");
destroyUserDataLI(volumeUuid, userId, flags);
} else {
logCriticalInfo(Log.ERROR,
"Failed to prepare existing user "
+ userId, e);
}
if (allowRecover) {
prepareUserDataLI(volumeUuid, userInfo,
flags | StorageManager.FLAG_STORAGE_DE,
false);
}
}新用户准备失败时可以先销毁再重试,避免 stale directory;已有用户不会自动 wipe,因为 OTA 或后续修复可能恢复现有数据。第一次启动的 system user 仍失败时,源码可能进入 recovery wipe 路径,这是用户数据安全优先于继续启动的边界。
7. 应用数据与 inode
源码文件:frameworks/base/services/core/java/com/android/server/pm/AppDataHelper.java,符号:prepareAppDataPostCommitLIF
public void prepareAppDataPostCommitLIF(
PackageSetting ps, int previousAppId,
int[] userIds) {
for (int userId : userIds) {
PackageUserStateInternal userState =
ps.readUserState(userId);
int flags;
if (userState.isInstalled()) {
flags = StorageManager.FLAG_STORAGE_DE
| StorageManager.FLAG_STORAGE_CE;
} else {
flags = StorageManager.FLAG_STORAGE_DE;
}
prepareAppDataLeafLIF(ps, userId, flags,
previousAppId);
}
}包级数据准备再次读取 per-user state:已安装用户通常准备 DE+CE,未安装但保留数据的用户可能只需要处理 DE 或清理路径。具体 CE 是否可用还受用户解锁状态和 key 状态影响。
源码文件:frameworks/base/services/core/java/com/android/server/pm/AppDataHelper.java,符号:创建结果写回
CreateAppDataResult result =
mInstaller.createAppData(args);
long ceDataInode = result.ceDataInode;
long deDataInode = result.deDataInode;
synchronized (mPm.mLock) {
if ((flags & StorageManager.FLAG_STORAGE_CE) != 0
&& ceDataInode != -1) {
ps.setCeDataInode(ceDataInode, userId);
}
if ((flags & StorageManager.FLAG_STORAGE_DE) != 0
&& deDataInode != -1) {
ps.setDeDataInode(deDataInode, userId);
}
}installd 返回的 inode 会写回 PackageUserStateImpl。后续清数据、销毁目录和恢复操作使用这些 inode,防止包名/目录竞态下误操作别的目录。inode 不是路径替代,而是目录身份的额外校验。
8. 用户销毁
源码文件:frameworks/base/services/core/java/com/android/server/pm/UserDataPreparer.java,符号:destroyUserDataLI
void destroyUserDataLI(String volumeUuid,
int userId, int flags) {
boolean wasProtected =
StrictMode.getAndDisableCredentialProtectedWhileLocked();
try {
mInstaller.destroyUserData(
volumeUuid, userId, flags);
if (Objects.equals(volumeUuid,
StorageManager.UUID_PRIVATE_INTERNAL)) {
if ((flags & StorageManager.FLAG_STORAGE_DE) != 0) {
FileUtils.deleteContentsAndDir(
getUserSystemDirectory(userId));
FileUtils.deleteContents(
getDataSystemDeDirectory(userId));
}
if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
FileUtils.deleteContents(
getDataSystemCeDirectory(userId));
}
}
getStorageManagerInternal().destroyUserStorage(
volumeUuid, userId, flags);
} finally {
if (wasProtected) {
StrictMode.enableCredentialProtectedWhileLocked();
}
}
}销毁先让 installd 清理应用数据/profile/media,再清 system user data,最后由 StorageManager 销毁用户存储。CE/DE key 和目录删除必须按顺序执行,不能只递归删除 /data/user/<id>。
9. 残留目录调和
源码文件:frameworks/base/services/core/java/com/android/server/pm/UserDataPreparer.java,符号:reconcileUsers
for (File file : files) {
if (!file.isDirectory()) {
continue;
}
int userId = Integer.parseInt(file.getName());
UserInfo info = users.get(userId);
boolean destroyUser = false;
if (info == null) {
destroyUser = true;
} else {
try {
enforceSerialNumber(file,
info.serialNumber);
} catch (IOException e) {
destroyUser = true;
}
}
if (destroyUser) {
destroyUserDataLI(volumeUuid, userId,
StorageManager.FLAG_STORAGE_DE
| StorageManager.FLAG_STORAGE_CE);
}
}挂载卷或启动恢复时,PMS 会扫描 user/、user_de/、system_ce/ 等目录,与 UMS 当前有效用户列表对照。没有匹配 UserInfo 或 serial 不一致的目录会被销毁,防止死用户数据和回收 userId 的旧数据继续存在。
10. 跨用户 API 检查
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:典型 Binder 入口
snapshot.enforceCrossUserPermission(
callingUid, userId,
true /* requireFullPermission */,
false /* checkShell */,
"clear application data");路径隔离不替代 API 授权。即使调用者知道 /data/user/10/com.example 的路径,也不能通过 PMS API 操作 user 10;Binder 入口先根据 callingUid、目标 user 和权限判断是否允许跨用户。系统/root/shell 的例外行为由具体入口参数决定。
11. 隔离失败边界
| 现象 | 应检查 |
|---|---|
| 新 user 读到旧目录 | serial xattr、reconcileUsers、userId 是否回收 |
| CE 数据开机不可见 | CE key 是否准备、用户是否解锁、flags 是否含 CE |
| 应用目录 owner 错误 | installd.createAppData、appId/userId、SELinux label |
| user 删除后残留数据 | destroyUserDataLI、StorageManager key、mounted volumes |
| 能看到路径但 API 拒绝 | enforceCrossUserPermission 与调用者权限 |
| 包状态与目录不一致 | PackageUserState inode、reconcileAppsData |
用户空间隔离不是“路径前缀加 userId”这么简单。路径、serial、UID、key、inode 和权限检查必须共同成立;任何日志只证明其中一层时,都不能推断整个隔离链已经正确。
12. 阅读检查
沿下面两条路径复述源码:
- 用户创建:
prepareUserData→prepareUserStorage→enforceSerialNumber→createUserData。 - 包数据:
prepareAppDataPostCommitLIF→ 读取 per-user state →createAppData→ inode 写回PackageUserState。
然后回答:为什么已有用户准备失败时不会自动 wipe?为什么 serial xattr 检查必须发生在 installd 创建应用目录之前?为什么删掉 /data/user/<id> 仍不足以完成用户删除?这些答案分别对应数据恢复边界、userId 回收防护和 CE/DE/system/kernel 多 owner 清理。
