Skip to content

用户空间隔离

追踪 Android 17 用户级 DE/CE 存储、应用数据目录、UID/serial 隔离和跨用户访问检查。

基于android-17.0.0_r1
AndroidPMSMultiUserStorage

用户空间隔离 ​

本文承接 多用户包管理 和 多用户安装,聚焦“用户 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
密钥StorageManagerInternalCE/DE 加密 key 与可用时机
目录UserDataPreparer / installd应用目录 owner、label、inode
APIComputer.enforceCrossUserPermission哪个调用者能操作哪个 user

2. 路径生成 ​

源码文件:frameworks/base/core/java/android/os/Environment.java,符号:getDataUserCeDirectory、getDataUserDeDirectory、package directory helpers

java
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,符号:系统级用户目录

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

java
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

java
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

java
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

java
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 异常分支

java
} 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

java
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,符号:创建结果写回

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

java
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

java
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 入口

java
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. 阅读检查 ​

沿下面两条路径复述源码:

  1. 用户创建:prepareUserData → prepareUserStorage → enforceSerialNumber → createUserData。
  2. 包数据:prepareAppDataPostCommitLIF → 读取 per-user state → createAppData → inode 写回 PackageUserState。

然后回答:为什么已有用户准备失败时不会自动 wipe?为什么 serial xattr 检查必须发生在 installd 创建应用目录之前?为什么删掉 /data/user/<id> 仍不足以完成用户删除?这些答案分别对应数据恢复边界、userId 回收防护和 CE/DE/system/kernel 多 owner 清理。