前言

Android 11 起开始存在 data_mirror 机制,通过 mount tmpfs 的方式将常见的应用目录(如 /data/data/data/user_de)使得对应用而言仅可访问和发现自己的存储目录,进而避免 stat /data/data/<package_name> 的方式探测用户安装的其他应用的存在。

分析

本文将基于 Android 13 源码,分析应用目录隔离机制。

初始化

Android 最初由这个提交引入应用目录隔离机制,在根下添加了一个目录 data_mirror 作为数据镜像。

Android 13 中,系统启动时会进行如下操作:

system/core/rootdir/init.rc

# A tmpfs directory, which will contain all apps CE DE data directory that
# bind mount from the original source.
mount tmpfs tmpfs /data_mirror nodev noexec nosuid mode=0700,uid=0,gid=1000
restorecon /data_mirror
mkdir /data_mirror/data_ce 0700 root root
mkdir /data_mirror/data_de 0700 root root

# Create CE and DE data directory for default volume
mkdir /data_mirror/data_ce/null 0700 root root
mkdir /data_mirror/data_de/null 0700 root root

# Bind mount CE and DE data directory to mirror's default volume directory.
# The 'slave' option (MS_SLAVE) is needed to cause the later bind mount of
# /data/data onto /data/user/0 to propagate to /data_mirror/data_ce/null/0.
mount none /data/user /data_mirror/data_ce/null bind rec slave
mount none /data/user_de /data_mirror/data_de/null bind rec

# Create mirror directory for jit profiles
mkdir /data_mirror/cur_profiles 0700 root root
mount none /data/misc/profiles/cur /data_mirror/cur_profiles bind rec
mkdir /data_mirror/ref_profiles 0700 root root
mount none /data/misc/profiles/ref /data_mirror/ref_profiles bind rec

mount tmpfs/data_mirror,并创建 data_cedata_de 目录,以及内部存储卷目录 null

除此之外,还会创建两个目录 cur_profilesref_profiles,用于隔离 JIT 配置文件目录。

Zygote 启动参数

startProcess

AMS 通过 com.android.server.am.ProcessList.startProcess 方法进行启动应用进程的操作。

private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint,
        ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags,
        int mountExternal, String seInfo, String requiredAbi, String instructionSet,
        String invokeWith, long startTime) {
    <--implementation-->
}

与目录隔离有关的部分如下:首先,创建两个 map pkgDataInfoMapallowlistedAppDataInfoMap,分别表示目标应用的应用目录信息和允许访问的应用目录信息,其中 key 为包名,value 为存储卷的 uuid 和真实目录的 inode(之后会介绍为什么需要 inode)。接着是两个控制变量 bindMountAppStorageDirsbindMountAppsData,分别表示是否隔离 Android/obbAndroid/data 目录和是否隔离 CE、DE 及 JIT profile 目录。

满足以下条件会启用 AppStorageDirs 隔离:

  • 隔离功能已启用:由系统属性 persist.sys.vold_app_data_isolation_enabled 决定
  • 要启动的应用是用户应用,且不是 ExternalStorageService
  • 要启动的应用存储挂载模式不是以下几种之一

    • Zygote.MOUNT_EXTERNAL_ANDROID_WRITABLE:仍然使用分区存储,但 Android/ 可写
    • Zygote.MOUNT_EXTERNAL_PASS_THROUGH:旧的文件系统,需要直接 bind mount 到外部存储
    • Zygote.MOUNT_EXTERNAL_INSTALLER:安装器 App,可以访问自身目录和 Android/obb 文件夹
    • Zygote.MOUNT_EXTERNAL_NONE:不挂载外部存储

满足以下条件会启用 AppsData 隔离:

  • 隔离功能已启用:由系统属性 persist.zygote.app_data_isolation 决定
  • 要启动的应用是用户应用,或是 isolated process
  • 要启动的应用 target API >= 29
  • 要启动的应用存在数据目录
Map<String, Pair<String, Long>> pkgDataInfoMap;
Map<String, Pair<String, Long>> allowlistedAppDataInfoMap;
boolean bindMountAppStorageDirs = false;
boolean bindMountAppsData = mAppDataIsolationEnabled
        && (UserHandle.isApp(app.uid) || UserHandle.isIsolated(app.uid))
        && mPlatformCompat.isChangeEnabled(APP_DATA_DIRECTORY_ISOLATION, app.info);

接下来,获取所有属于同一 shared uid 的包名以及目标包名列表。如果应用存在 shared uid,则 targetPackagesList 即是 sharedPackages,否则 targetPackagesList 为应用的包名,sharedPackages 为空。

然后,通过 getPackageAppDataInfoMap 方法获取 pkgDataInfoMapallowlistedAppDataInfoMap。其中处于隔离白名单的包名列表由 SystemConfigapp-data-isolation-whitelisted-app 决定。如果两个 map 有一个为空,则停用目录隔离(但实际上这还是个 todo,截止 Android 13 getPackageAppDataInfoMap 永远不为空)。

// Get all packages belongs to the same shared uid. sharedPackages is empty array
// if it doesn't have shared uid.
final PackageManagerInternal pmInt = mService.getPackageManagerInternal();
final String[] sharedPackages = pmInt.getSharedUserPackagesForPackage(
        app.info.packageName, app.userId);
final String[] targetPackagesList = sharedPackages.length == 0
        ? new String[]{app.info.packageName} : sharedPackages;

final boolean hasAppStorage = hasAppStorage(pmInt, app.info.packageName);

pkgDataInfoMap = getPackageAppDataInfoMap(pmInt, targetPackagesList, uid);
if (pkgDataInfoMap == null) {
    // TODO(b/152760674): Handle inode == 0 case properly, now we just give it a
    // tmp free pass.
    bindMountAppsData = false;
}

// Remove all packages in pkgDataInfoMap from mAppDataIsolationAllowlistedApps, so
// it won't be mounted twice.
final Set<String> allowlistedApps = new ArraySet<>(mAppDataIsolationAllowlistedApps);
for (String pkg : targetPackagesList) {
    allowlistedApps.remove(pkg);
}

allowlistedAppDataInfoMap = getPackageAppDataInfoMap(pmInt,
        allowlistedApps.toArray(new String[0]), uid);
if (allowlistedAppDataInfoMap == null) {
    // TODO(b/152760674): Handle inode == 0 case properly, now we just give it a
    // tmp free pass.
    bindMountAppsData = false;
}

下面深入getPackageAppDataInfoMap 的实现:通过 android.content.pm.PackageManagerInternal.getPackageStateInternal 方法获取目标包名的 packageState,进而由这个 packageState 获取存储卷 uuid 和应用目录 inode。

private Map<String, Pair<String, Long>> getPackageAppDataInfoMap(PackageManagerInternal pmInt,
        String[] packages, int uid) {
    Map<String, Pair<String, Long>> result = new ArrayMap<>(packages.length);
    int userId = UserHandle.getUserId(uid);
    for (String packageName : packages) {
        final PackageStateInternal packageState = pmInt.getPackageStateInternal(packageName);
        if (packageState == null) {
            Slog.w(TAG, "Unknown package:" + packageName);
            continue;
        }
        String volumeUuid = packageState.getVolumeUuid();
        long inode = packageState.getUserStateOrDefault(userId).getCeDataInode();
        if (inode == 0) {
            Slog.w(TAG, packageName + " inode == 0 (b/152760674)");
            return null;
        }
        result.put(packageName, Pair.create(volumeUuid, inode));
    }

    return result;
}

PackageManagerInternal.getPackageStateInternal 方法由 com.android.server.pm.PackageManagerInternalBase 覆盖,实际上由 Computer 代理实现。

com.android.server.pm.Computer 是 Android 13 PMS 新增的一个接口,用于 live data 和 snapshot data,这里不再细究其具体用途(也许用来替换 IPackageManager?不少 IPackageManager 方法被标上了 @Deprecated)。

com.android.server.pm.Computer.getPackageStateInternal 方法又是由 com.android.server.pm.ComputerEngine 覆盖,然后接下来又是套各种代理套很多层,由于偏移了本文讨论内容,就不再继续深入。

public abstract class PackageManagerInternal {
    ...
    @NonNull
    private final PackageManagerService mService;

    @Override
    public final Computer snapshot() {
        return mService.snapshotComputer();
    }

    @Nullable
    @Override
    @Deprecated
    public final PackageStateInternal getPackageStateInternal(String packageName) {
        return snapshot().getPackageStateInternal(packageName);
    }
    ...
}

回到 startProcess,取得两个 map 后,接下来判断如果要启动的应用不存在数据目录,则将 bindMountAppsData 置为 false,并置空两个 map(说实话我不明白它为啥要先获取了再置空)。除此之外,如果要启动的应用是 isolated process,则它连自己的目录也不能访问,因此将两个 map 置空(所以我也不明白为啥这儿也是先获取了再置空)。

接着,通过 needsStorageDataIsolation 方法判断是否需要启用 AppStorageDirs 隔离。

if (!hasAppStorage) {
    bindMountAppsData = false;
    pkgDataInfoMap = null;
    allowlistedAppDataInfoMap = null;
}

int userId = UserHandle.getUserId(uid);
StorageManagerInternal storageManagerInternal = LocalServices.getService(
        StorageManagerInternal.class);
if (needsStorageDataIsolation(storageManagerInternal, app)) {
    // We will run prepareStorageDirs() after we trigger zygote fork, so it won't
    // slow down app starting speed as those dirs might not be cached.
    if (pkgDataInfoMap != null && storageManagerInternal.isFuseMounted(userId)) {
        bindMountAppStorageDirs = true;
    } else {
        // Fuse is not mounted or inode == 0,
        // so we won't mount it in zygote, but resume the mount after unlocking device.
        app.setBindMountPending(true);
            bindMountAppStorageDirs = false;
    }
}

// If it's an isolated process, it should not even mount its own app data directories,
// since it has no access to them anyway.
if (app.isolated) {
    pkgDataInfoMap = null;
    allowlistedAppDataInfoMap = null;
}

needsStorageDataIsolation 实现如下,具体要求上面已经提到,就不再细说。

private boolean needsStorageDataIsolation(StorageManagerInternal storageManagerInternal,
        ProcessRecord app) {
    final int mountMode = app.getMountMode();
    return mVoldAppDataIsolationEnabled && UserHandle.isApp(app.uid)
            && !storageManagerInternal.isExternalStorageService(app.uid)
            // Special mounting mode doesn't need to have data isolation as they won't
            // access /mnt/user anyway.
            && mountMode != Zygote.MOUNT_EXTERNAL_ANDROID_WRITABLE
            && mountMode != Zygote.MOUNT_EXTERNAL_PASS_THROUGH
            && mountMode != Zygote.MOUNT_EXTERNAL_INSTALLER
            && mountMode != Zygote.MOUNT_EXTERNAL_NONE;
}

至此,目录隔离的参数构造已经完成,接下来便是通知 zygote 创建应用进程。

最后,如果启用 AppStorageDirs 隔离,如果存储目录尚不存在,则创建一个。

// This runs after Process.start() as this method may block app process starting time
// if dir is not cached. Running this method after Process.start() can make it
// cache the dir asynchronously, so zygote can use it without waiting for it.
if (bindMountAppStorageDirs) {
    storageManagerInternal.prepareStorageDirs(userId, pkgDataInfoMap.keySet(),
            app.processName);
}

startViaZygote

android.os.Process.start 方法调用 android.os.ZygoteProcess.start,进而调用android.os.ZygoteProcess.startViaZygote,最终在这里将参数序列化,发送给 zygote 创建应用进程。

目录隔离

处理参数

native 函数 com_android_internal_os_Zygote_nativeForkAndSpecialize 完成 fork 后,子进程通过 SpecializeCommon 函数处理处理 zygote 参数并继续启动应用。

com_android_internal_os_Zygote.cpp

static jint com_android_internal_os_Zygote_nativeForkAndSpecialize(
        JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags,
        jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name,
        jintArray managed_fds_to_close, jintArray managed_fds_to_ignore, jboolean is_child_zygote,
        jstring instruction_set, jstring app_data_dir, jboolean is_top_app,
        jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list,
        jboolean mount_data_dirs, jboolean mount_storage_dirs) {
    ...
    pid_t pid = zygote::ForkCommon(env, /* is_system_server= */ false, fds_to_close, fds_to_ignore, true);

    if (pid == 0) {
        SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
                         mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,
                         instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,
                         allowlisted_data_info_list, mount_data_dirs == JNI_TRUE,
                         mount_storage_dirs == JNI_TRUE);
    }
    return pid;
}

SpecializeCommon 函数中,通过 mount_data_dirs 参数判断是否进行目录隔离。如果需要隔离,则调用 isolateAppDataisolateJitProfile 函数执行隔离。

// Utility routine to specialize a zygote child process.
static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags,
                             jobjectArray rlimits, jlong permitted_capabilities,
                             jlong effective_capabilities, jint mount_external,
                             jstring managed_se_info, jstring managed_nice_name,
                             bool is_system_server, bool is_child_zygote,
                             jstring managed_instruction_set, jstring managed_app_data_dir,
                             bool is_top_app, jobjectArray pkg_data_info_list,
                             jobjectArray allowlisted_data_info_list, bool mount_data_dirs,
                             bool mount_storage_dirs) {
    ...
    if (mount_data_dirs) {
        isolateAppData(env, pkg_data_info_list, allowlisted_data_info_list, uid, process_name,
                       managed_nice_name, fail_fn);
        isolateJitProfile(env, pkg_data_info_list, uid, process_name, managed_nice_name, fail_fn);
    }
    ...
}

执行隔离

isolateAppData

isolateAppData 函数负责隔离应用数据目录。

static void isolateAppData(JNIEnv* env, jobjectArray pkg_data_info_list,
                           jobjectArray allowlisted_data_info_list, uid_t uid,
                           const char* process_name, jstring managed_nice_name, fail_fn_t fail_fn) {
    std::vector<std::string> merged_data_info_list;
    insertPackagesToMergedList(env, merged_data_info_list, pkg_data_info_list, process_name,
                               managed_nice_name, fail_fn);
    insertPackagesToMergedList(env, merged_data_info_list, allowlisted_data_info_list, process_name,
                               managed_nice_name, fail_fn);

    isolateAppData(env, merged_data_info_list, uid, process_name, managed_nice_name, fail_fn);
}

首先,创建一个 vector,其中按包名、卷 uuid、包名目录 inode 组成三元组依次存储。

其次,通过 insertPackagesToMergedList 函数,将 pkg_data_info_listallowlisted_data_info_list 中的数据转换为 std::string 并将数据存入 vector 中。

static void insertPackagesToMergedList(JNIEnv* env,
  std::vector<std::string>& merged_data_info_list,
  jobjectArray data_info_list, const char* process_name,
  jstring managed_nice_name, fail_fn_t fail_fn) {

  auto extract_fn = std::bind(ExtractJString, env, process_name, managed_nice_name, _1);

  int size = (data_info_list != nullptr) ? env->GetArrayLength(data_info_list) : 0;
  // Size should be a multiple of 3, as it contains list of <package_name, volume_uuid, inode>
  if ((size % 3) != 0) {
    fail_fn(CREATE_ERROR("Wrong data_info_list size %d", size));
  }

  for (int i = 0; i < size; i += 3) {
    jstring package_str = (jstring) (env->GetObjectArrayElement(data_info_list, i));
    std::string packageName = extract_fn(package_str).value();
    merged_data_info_list.push_back(packageName);

    jstring vol_str = (jstring) (env->GetObjectArrayElement(data_info_list, i + 1));
    std::string volUuid = extract_fn(vol_str).value();
    merged_data_info_list.push_back(volUuid);

    jstring inode_str = (jstring) (env->GetObjectArrayElement(data_info_list, i + 2));
    std::string inode = extract_fn(inode_str).value();
    merged_data_info_list.push_back(inode);
  }
}

接下来便是隔离的真正实现部分。

/**
 * Hide the CE and DE data directories of non-related apps.
 *
 * Without this, apps can detect if any app is installed by trying to "touch" the app's CE
 * or DE data directory, e.g. /data/data/com.whatsapp.  This fails with EACCES if the app
 * is installed, or ENOENT if it's not.  Traditional file permissions or SELinux can only
 * block accessing those directories but can't fix fingerprinting like this.
 *
 * Instead, we hide non-related apps' data directories from the filesystem entirely by
 * mounting tmpfs instances over their parent directories and bind-mounting in just the
 * needed app data directories.  This is done in a private mount namespace.
 *
 * Steps:
 * (1) Collect a list of all related apps (apps with same uid and allowlisted apps) data info
 *     (package name, data stored volume uuid, and inode number of its CE data directory)
 * (2) Mount tmpfs on /data/data and /data/user{,_de}, and on /mnt/expand/$volume/user{,_de}
 *     for all adoptable storage volumes.  This hides all app data directories.
 * (3) For each related app, create stubs for its data directories in the relevant tmpfs
 *     instances, then bind mount in the actual directories from /data_mirror.  This works
 *     for both the CE and DE directories.  DE storage is always unlocked, whereas the
 *     app's CE directory can be found via inode number if CE storage is locked.
 *
 * Example assuming user 0, app "com.android.foo", no shared uid, and no adoptable storage:
 * (1) Info = ["com.android.foo", "null" (volume uuid "null"=default), "123456" (inode number)]
 * (2) Mount tmpfs on /data/data, /data/user, and /data/user_de.
 * (3) For DE storage, create a directory /data/user_de/0/com.android.foo and bind mount
 *     /data_mirror/data_de/0/com.android.foo onto it.
 * (4) Do similar for CE storage.  But if the device is in direct boot mode, then CE
 *     storage will be locked, so the app's CE data directory won't exist at the usual
 *     path /data_mirror/data_ce/0/com.android.foo.  It will still exist in
 *     /data_mirror/data_ce/0, but its filename will be an unpredictable no-key name.  In
 *     this case, we use the inode number to find the right directory instead.  Note that
 *     the bind-mounted app CE data directory will remain locked.  It will be unlocked
 *     automatically if/when the user's CE storage is unlocked, since adding an encryption
 *     key takes effect on a whole filesystem instance including all its mounts.
 */
static void isolateAppData(JNIEnv* env, const std::vector<std::string>& merged_data_info_list,
    uid_t uid, const char* process_name,
    jstring managed_nice_name, fail_fn_t fail_fn) {
    <-- implementation -->
}

获取当前用户 userId,保存下 vector 大小。

const userid_t userId = multiuser_get_user_id(uid);

int size = merged_data_info_list.size();

数据原始目录,包括内置存储和 SD 卡。

// Mount tmpfs on all possible data directories, so app no longer see the original apps data.
char internalCePath[PATH_MAX];
char internalLegacyCePath[PATH_MAX];
char internalDePath[PATH_MAX];
char externalPrivateMountPath[PATH_MAX];

snprintf(internalCePath, PATH_MAX, "/data/user");
snprintf(internalLegacyCePath, PATH_MAX, "/data/data");
snprintf(internalDePath, PATH_MAX, "/data/user_de");
snprintf(externalPrivateMountPath, PATH_MAX, "/mnt/expand");

保存 SELinux context。

// Get the "u:object_r:system_userdir_file:s0" security context.  This can be
// gotten from several different places; we use /data/user.
char* dataUserdirContext = nullptr;
if (getfilecon(internalCePath, &dataUserdirContext) < 0) {
  fail_fn(CREATE_ERROR("Unable to getfilecon on %s %s", internalCePath,
      strerror(errno)));
}
// Get the "u:object_r:system_data_file:s0" security context.  This can be
// gotten from several different places; we use /data/misc.
char* dataFileContext = nullptr;
if (getfilecon("/data/misc", &dataFileContext) < 0) {
  fail_fn(CREATE_ERROR("Unable to getfilecon on /data/misc %s", strerror(errno)));
}

挂载 tmpfs 到原始目录。

MountAppDataTmpFs(internalLegacyCePath, fail_fn);
MountAppDataTmpFs(internalCePath, fail_fn);
MountAppDataTmpFs(internalDePath, fail_fn);

// Mount tmpfs on all external vols DE and CE storage
DIR* dir = opendir(externalPrivateMountPath);
if (dir == nullptr) {
  fail_fn(CREATE_ERROR("Failed to opendir %s", externalPrivateMountPath));
}
struct dirent* ent;
while ((ent = readdir(dir))) {
  if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue;
  if (ent->d_type != DT_DIR) {
    fail_fn(CREATE_ERROR("Unexpected type: %d %s", ent->d_type, ent->d_name));
  }
  auto volPath = StringPrintf("%s/%s", externalPrivateMountPath, ent->d_name);
  auto cePath = StringPrintf("%s/user", volPath.c_str());
  auto dePath = StringPrintf("%s/user_de", volPath.c_str());
  // Wait until dir user is created.
  WaitUntilDirReady(cePath.c_str(), fail_fn);
  MountAppDataTmpFs(cePath.c_str(), fail_fn);
  // Wait until dir user_de is created.
  WaitUntilDirReady(dePath.c_str(), fail_fn);
  MountAppDataTmpFs(dePath.c_str(), fail_fn);
}
closedir(dir);

用户 0 的目录对应用而言始终存在,因此将 /data/data 软链接到 /data/user/0,并创建 /data/user_de/0 目录(当然,这里这么做实际上是有一个历史遗留原因)。

// Prepare default dirs for user 0 as user 0 always exists.
int result = symlink("/data/data", "/data/user/0");
if (result != 0) {
  fail_fn(CREATE_ERROR("Failed to create symlink /data/user/0 %s", strerror(errno)));
}
PrepareDirIfNotPresent("/data/user_de/0", DEFAULT_DATA_DIR_PERMISSION,
    AID_ROOT, AID_ROOT, fail_fn);

接下来,读取之前保存的 vector 中的包名目录信息,对其中的每个包名,为 tmpfs 覆盖后的原始目录创建对应的目录。当然由于历史遗留问题,可以看到 user_ce 对用户 0 和其他用户的处理方式略有区别:用户 0 的原始目录在 /data/data,而其他用户在 /data/user/<userId>

之后,调用函数 isolateAppDataPerPackage 处理每一个包名目录。

for (int i = 0; i < size; i += 3) {
  std::string const & packageName = merged_data_info_list[i];
  std::string const & volUuid  = merged_data_info_list[i + 1];
  std::string const & inode = merged_data_info_list[i + 2];

  std::string::size_type sz;
  long long ceDataInode = std::stoll(inode, &sz);

  std::string actualCePath, actualDePath;
  if (volUuid.compare("null") != 0) {
    // Volume that is stored in /mnt/expand
    char volPath[PATH_MAX];
    char volCePath[PATH_MAX];
    char volDePath[PATH_MAX];
    char volCeUserPath[PATH_MAX];
    char volDeUserPath[PATH_MAX];

    snprintf(volPath, PATH_MAX, "/mnt/expand/%s", volUuid.c_str());
    snprintf(volCePath, PATH_MAX, "%s/user", volPath);
    snprintf(volDePath, PATH_MAX, "%s/user_de", volPath);
    snprintf(volCeUserPath, PATH_MAX, "%s/%d", volCePath, userId);
    snprintf(volDeUserPath, PATH_MAX, "%s/%d", volDePath, userId);

    PrepareDirIfNotPresent(volPath, DEFAULT_DATA_DIR_PERMISSION, AID_ROOT, AID_ROOT, fail_fn);
    PrepareDirIfNotPresent(volCePath, DEFAULT_DATA_DIR_PERMISSION, AID_ROOT, AID_ROOT, fail_fn);
    PrepareDirIfNotPresent(volDePath, DEFAULT_DATA_DIR_PERMISSION, AID_ROOT, AID_ROOT, fail_fn);
    PrepareDirIfNotPresent(volCeUserPath, DEFAULT_DATA_DIR_PERMISSION, AID_ROOT, AID_ROOT,
        fail_fn);
    PrepareDirIfNotPresent(volDeUserPath, DEFAULT_DATA_DIR_PERMISSION, AID_ROOT, AID_ROOT,
        fail_fn);

    actualCePath = volCeUserPath;
    actualDePath = volDeUserPath;
  } else {
    // Internal volume that stored in /data
    char internalCeUserPath[PATH_MAX];
    char internalDeUserPath[PATH_MAX];
    snprintf(internalCeUserPath, PATH_MAX, "/data/user/%d", userId);
    snprintf(internalDeUserPath, PATH_MAX, "/data/user_de/%d", userId);
    // If it's not user 0, create /data/user/$USER.
    if (userId == 0) {
      actualCePath = internalLegacyCePath;
    } else {
      PrepareDirIfNotPresent(internalCeUserPath, DEFAULT_DATA_DIR_PERMISSION,
          AID_ROOT, AID_ROOT, fail_fn);
      actualCePath = internalCeUserPath;
    }
    PrepareDirIfNotPresent(internalDeUserPath, DEFAULT_DATA_DIR_PERMISSION,
        AID_ROOT, AID_ROOT, fail_fn);
    actualDePath = internalDeUserPath;
  }
  isolateAppDataPerPackage(userId, packageName, volUuid, ceDataInode,
      actualCePath, actualDePath, fail_fn);
}

通过 volume_uuidpackage_namece_data_inode 字段,组合出在 /data_mirror 中的真实目录,将其 bind mount 到原始目录。

// Isolate app's data directory, by mounting a tmpfs on CE DE storage,
// and create and bind mount app data in related_packages.
static void isolateAppDataPerPackage(int userId, std::string_view package_name,
    std::string_view volume_uuid, long long ce_data_inode, std::string_view actualCePath,
    std::string_view actualDePath, fail_fn_t fail_fn) {

  char mirrorCePath[PATH_MAX];
  char mirrorDePath[PATH_MAX];
  char mirrorCeParent[PATH_MAX];
  snprintf(mirrorCeParent, PATH_MAX, "/data_mirror/data_ce/%s", volume_uuid.data());
  snprintf(mirrorCePath, PATH_MAX, "%s/%d", mirrorCeParent, userId);
  snprintf(mirrorDePath, PATH_MAX, "/data_mirror/data_de/%s/%d", volume_uuid.data(), userId);

  createAndMountAppData(package_name, package_name, mirrorDePath, actualDePath, fail_fn,
                        true /*call_fail_fn*/);

  std::string ce_data_path = getAppDataDirName(mirrorCePath, package_name, ce_data_inode, fail_fn);
  if (!createAndMountAppData(package_name, ce_data_path, mirrorCePath, actualCePath, fail_fn,
                             false /*call_fail_fn*/)) {
    // CE might unlocks and the name is decrypted
    // get the name and mount again
    ce_data_path=getAppDataDirName(mirrorCePath, package_name, ce_data_inode, fail_fn);
    mountAppData(package_name, ce_data_path, mirrorCePath, actualCePath, fail_fn);
  }
}

需要注意的是,由于在系统启动阶段,data_ce 尚未解密,包名对应的真实目录名并非包名本身,但是目录的 inode 是确定的。因此 Android 系统采用的方式是遍历加密目录,根据提供的 inode 找到真实目录。

// Get the directory name stored in /data/data. If device is unlocked it should be the same as
// package name, otherwise it will be an encrypted name but with same inode number.
static std::string getAppDataDirName(std::string_view parent_path, std::string_view package_name,
      long long ce_data_inode, fail_fn_t fail_fn) {
  // Check if directory exists
  char tmpPath[PATH_MAX];
  snprintf(tmpPath, PATH_MAX, "%s/%s", parent_path.data(), package_name.data());
  struct stat s;
  int err = stat(tmpPath, &s);
  if (err == 0) {
    // Directory exists, so return the directory name
    return package_name.data();
  } else {
    if (errno != ENOENT) {
      fail_fn(CREATE_ERROR("Unexpected error in getAppDataDirName: %s", strerror(errno)));
      return nullptr;
    }
    {
      // Directory doesn't exist, try to search the name from inode
      std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(parent_path.data()), closedir);
      if (dir == nullptr) {
        fail_fn(CREATE_ERROR("Failed to opendir %s", parent_path.data()));
      }
      struct dirent* ent;
      while ((ent = readdir(dir.get()))) {
        if (ent->d_ino == ce_data_inode) {
          return ent->d_name;
        }
      }
    }

    // Fallback due to b/145989852, ce_data_inode stored in package manager may be corrupted
    // if ino_t is 32 bits.
    ino_t fixed_ce_data_inode = 0;
    if ((ce_data_inode & UPPER_HALF_WORD_MASK) == UPPER_HALF_WORD_MASK) {
      fixed_ce_data_inode = ce_data_inode & LOWER_HALF_WORD_MASK;
    } else if ((ce_data_inode & LOWER_HALF_WORD_MASK) == LOWER_HALF_WORD_MASK) {
      fixed_ce_data_inode = ((ce_data_inode >> 32) & LOWER_HALF_WORD_MASK);
    }
    if (fixed_ce_data_inode != 0) {
      std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(parent_path.data()), closedir);
      if (dir == nullptr) {
        fail_fn(CREATE_ERROR("Failed to opendir %s", parent_path.data()));
      }
      struct dirent* ent;
      while ((ent = readdir(dir.get()))) {
        if (ent->d_ino == fixed_ce_data_inode) {
          long long d_ino = ent->d_ino;
          ALOGW("Fallback success inode %lld -> %lld", ce_data_inode, d_ino);
          return ent->d_name;
        }
      }
    }
    // Fallback done

    fail_fn(CREATE_ERROR("Unable to find %s:%lld in %s", package_name.data(),
        ce_data_inode, parent_path.data()));
    return nullptr;
  }
}

所有目录处理完后,重设 SELinux context,完成隔离。

// We set the label AFTER everything is done, as we are applying
// the file operations on tmpfs. If we set the label when we mount
// tmpfs, SELinux will not happy as we are changing system_data_files.
// Relabel dir under /data/user, including /data/user/0
relabelSubdirs(internalCePath, dataFileContext, fail_fn);

// Relabel /data/user
relabelDir(internalCePath, dataUserdirContext, fail_fn);

// Relabel /data/data
relabelDir(internalLegacyCePath, dataFileContext, fail_fn);

// Relabel subdirectories of /data/user_de
relabelSubdirs(internalDePath, dataFileContext, fail_fn);

// Relabel /data/user_de
relabelDir(internalDePath, dataUserdirContext, fail_fn);

// Relabel CE and DE dirs under /mnt/expand
dir = opendir(externalPrivateMountPath);
if (dir == nullptr) {
  fail_fn(CREATE_ERROR("Failed to opendir %s", externalPrivateMountPath));
}
while ((ent = readdir(dir))) {
  if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue;
  auto volPath = StringPrintf("%s/%s", externalPrivateMountPath, ent->d_name);
  auto cePath = StringPrintf("%s/user", volPath.c_str());
  auto dePath = StringPrintf("%s/user_de", volPath.c_str());

  relabelSubdirs(cePath.c_str(), dataFileContext, fail_fn);
  relabelDir(cePath.c_str(), dataUserdirContext, fail_fn);
  relabelSubdirs(dePath.c_str(), dataFileContext, fail_fn);
  relabelDir(dePath.c_str(), dataUserdirContext, fail_fn);
}
closedir(dir);

freecon(dataUserdirContext);
freecon(dataFileContext);

isolateJitProfile

isolateJitProfile 函数负责隔离 JIT 配置目录,其大致方法与 isolateAppData 差不多。

/**
 * Like isolateAppData(), isolate jit profile directories, so apps don't see what
 * other apps are installed by reading content inside /data/misc/profiles/cur.
 *
 * The implementation is similar to isolateAppData(), it creates a tmpfs
 * on /data/misc/profiles/cur, and bind mounts related package profiles to it.
 */
static void isolateJitProfile(JNIEnv* env, jobjectArray pkg_data_info_list,
    uid_t uid, const char* process_name, jstring managed_nice_name,
    fail_fn_t fail_fn) {
   <-- implementation -->
}

获取当前用户 userId,保存下 pkg_data_info_list 大小。

auto extract_fn = std::bind(ExtractJString, env, process_name, managed_nice_name, _1);
const userid_t user_id = multiuser_get_user_id(uid);

int size = (pkg_data_info_list != nullptr) ? env->GetArrayLength(pkg_data_info_list) : 0;
// Size should be a multiple of 3, as it contains list of <package_name, volume_uuid, inode>
if ((size % 3) != 0) {
  fail_fn(CREATE_ERROR("Wrong pkg_inode_list size %d", size));
}

将 tmpfs 挂载到原始目录(/data/misc/profiles/cur/data/misc/profiles/ref)。

// Mount (namespace) tmpfs on profile directory, so apps no longer access
// the original profile directory anymore.
MountAppDataTmpFs(kCurProfileDirPath, fail_fn);
MountAppDataTmpFs(kRefProfileDirPath, fail_fn);

之后,处理每个该应用的包名目录,由于其原理和实现与 isolateAppData 相似,就不再具体展开。

// Create profile directory for this user.
std::string actualCurUserProfile = StringPrintf("%s/%d", kCurProfileDirPath, user_id);
PrepareDir(actualCurUserProfile, DEFAULT_DATA_DIR_PERMISSION, AID_ROOT, AID_ROOT, fail_fn);

for (int i = 0; i < size; i += 3) {
  jstring package_str = (jstring) (env->GetObjectArrayElement(pkg_data_info_list, i));
  std::string packageName = extract_fn(package_str).value();

  std::string actualCurPackageProfile = StringPrintf("%s/%s", actualCurUserProfile.c_str(),
      packageName.c_str());
  std::string mirrorCurPackageProfile = StringPrintf("/data_mirror/cur_profiles/%d/%s",
      user_id, packageName.c_str());
  std::string actualRefPackageProfile = StringPrintf("%s/%s", kRefProfileDirPath,
      packageName.c_str());
  std::string mirrorRefPackageProfile = StringPrintf("/data_mirror/ref_profiles/%s",
      packageName.c_str());

  if (access(mirrorCurPackageProfile.c_str(), F_OK) != 0) {
    ALOGW("Can't access app profile directory: %s", mirrorCurPackageProfile.c_str());
    continue;
  }
  if (access(mirrorRefPackageProfile.c_str(), F_OK) != 0) {
    ALOGW("Can't access app profile directory: %s", mirrorRefPackageProfile.c_str());
    continue;
  }

  PrepareDir(actualCurPackageProfile, DEFAULT_DATA_DIR_PERMISSION, uid, uid, fail_fn);
  BindMount(mirrorCurPackageProfile, actualCurPackageProfile, fail_fn);
  PrepareDir(actualRefPackageProfile, DEFAULT_DATA_DIR_PERMISSION, uid, uid, fail_fn);
  BindMount(mirrorRefPackageProfile, actualRefPackageProfile, fail_fn);
}
最后修改:2023 年 02 月 07 日
如果觉得我的文章对你有用,请随意赞赏