blob: 6c69f186448cd554d3cb4db168ff5c3549680200 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.sdksandbox;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.SharedLibraryInfo;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Base64;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.PackageManagerLocal;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
/**
* Helper class to handle all logics related to sdk data
*/
class SdkSandboxStorageManager {
private static final String TAG = "SdkSandboxManager";
private final Context mContext;
private final Object mLock = new Object();
// Prefix to prepend with all sdk storage paths.
private final String mRootDir;
private final SdkSandboxManagerLocal mSdkSandboxManagerLocal;
private final PackageManagerLocal mPackageManagerLocal;
SdkSandboxStorageManager(
Context context,
SdkSandboxManagerLocal sdkSandboxManagerLocal,
PackageManagerLocal packageManagerLocal) {
this(context, sdkSandboxManagerLocal, packageManagerLocal, /*rootDir=*/ "");
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
SdkSandboxStorageManager(
Context context,
SdkSandboxManagerLocal sdkSandboxManagerLocal,
PackageManagerLocal packageManagerLocal,
String rootDir) {
mContext = context;
mSdkSandboxManagerLocal = sdkSandboxManagerLocal;
mPackageManagerLocal = packageManagerLocal;
mRootDir = rootDir;
}
public void notifyInstrumentationStarted(CallingInfo callingInfo) {
synchronized (mLock) {
reconcileSdkDataSubDirs(callingInfo, /*forInstrumentation=*/true);
}
}
/**
* Handle package added or updated event.
*
* On package added or updated, we need to reconcile sdk subdirectories for the new/updated
* package.
*/
void onPackageAddedOrUpdated(CallingInfo callingInfo) {
synchronized (mLock) {
reconcileSdkDataSubDirs(callingInfo, /*forInstrumentation=*/false);
}
}
/**
* Handle user unlock event.
*
* When user unlocks their device, the credential encrypted storage becomes available for
* reconcilation.
*/
public void onUserUnlocking(int userId) {
synchronized (mLock) {
reconcileSdkDataPackageDirs(userId);
}
}
void prepareSdkDataOnLoad(CallingInfo callingInfo) {
synchronized (mLock) {
reconcileSdkDataSubDirs(callingInfo, /*forInstrumentation=*/false);
}
}
SdkDataDirInfo getSdkDataDirInfo(CallingInfo callingInfo, String sdkName) {
final int uid = callingInfo.getUid();
final String packageName = callingInfo.getPackageName();
String volumeUuid = null;
try {
volumeUuid = getVolumeUuidForPackage(getUserId(uid), packageName);
} catch (Exception e) {
Log.w(TAG, "Failed to find package " + packageName + " error: " + e.getMessage());
// TODO(b/238164644): SdkSandboxManagerService should fail loadSdk
return new SdkDataDirInfo(null, null);
}
final String cePackagePath =
getSdkDataPackageDirectory(
volumeUuid, getUserId(uid), packageName, /*isCeData=*/ true);
final String dePackagePath =
getSdkDataPackageDirectory(
volumeUuid, getUserId(uid), packageName, /*isCeData=*/ false);
// TODO(b/232924025): We should have these information cached, instead of rescanning dirs.
synchronized (mLock) {
final SubDirectories ceSubDirs = new SubDirectories(cePackagePath);
final String sdkCeSubDirPath = ceSubDirs.getSdkSubDir(sdkName, /*fullPath=*/ true);
final SubDirectories deSubDirs = new SubDirectories(dePackagePath);
final String sdkDeSubDirPath = deSubDirs.getSdkSubDir(sdkName, /*fullPath=*/ true);
return new SdkDataDirInfo(sdkCeSubDirPath, sdkDeSubDirPath);
}
}
private int getUserId(int uid) {
final UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
return userHandle.getIdentifier();
}
@GuardedBy("mLock")
private void reconcileSdkDataSubDirs(CallingInfo callingInfo, boolean forInstrumentation) {
final int uid = callingInfo.getUid();
final int userId = getUserId(uid);
final String packageName = callingInfo.getPackageName();
final List<String> sdksUsed = getSdksUsed(userId, packageName);
if (sdksUsed.isEmpty()) {
if (forInstrumentation) {
Log.w(TAG,
"Running instrumentation for the sdk-sandbox process belonging to client "
+ "app "
+ packageName + " (uid = " + uid
+ "). However client app doesn't depend on any SDKs. Only "
+ "creating \"shared\" sdk sandbox data sub directory");
} else {
return;
}
}
String volumeUuid = null;
try {
volumeUuid = getVolumeUuidForPackage(userId, packageName);
} catch (Exception e) {
Log.w(TAG, "Failed to find package " + packageName + " error: " + e.getMessage());
return;
}
final String deSdkDataPackagePath =
getSdkDataPackageDirectory(volumeUuid, userId, packageName, /*isCeData=*/ false);
final SubDirectories existingDeSubDirs = new SubDirectories(deSdkDataPackagePath);
final List<String> subDirNames = new ArrayList<>();
subDirNames.addAll(SubDirectories.NON_SDK_SUBDIRS);
for (int i = 0; i < sdksUsed.size(); i++) {
final String sdk = sdksUsed.get(i);
final String sdkSubDir = existingDeSubDirs.getSdkSubDir(sdk);
if (sdkSubDir == null) {
subDirNames.add(sdk + "@" + getRandomString());
} else {
subDirNames.add(sdkSubDir);
}
}
final int appId = UserHandle.getAppId(uid);
final UserManager um = mContext.getSystemService(UserManager.class);
int flags = 0;
boolean doesCeNeedReconcile = false;
boolean doesDeNeedReconcile = false;
final Set<String> expectedSdkNames = new ArraySet<>(sdksUsed);
final UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
if (um.isUserUnlockingOrUnlocked(userHandle)) {
final String ceSdkDataPackagePath =
getSdkDataPackageDirectory(volumeUuid, userId, packageName, /*isCeData=*/ true);
final SubDirectories ceSubDirsBeforeReconcilePrefix =
new SubDirectories(ceSdkDataPackagePath);
flags = PackageManagerLocal.FLAG_STORAGE_CE | PackageManagerLocal.FLAG_STORAGE_DE;
doesCeNeedReconcile = !ceSubDirsBeforeReconcilePrefix.isValid(expectedSdkNames);
doesDeNeedReconcile = !existingDeSubDirs.isValid(expectedSdkNames);
} else {
flags = PackageManagerLocal.FLAG_STORAGE_DE;
doesDeNeedReconcile = !existingDeSubDirs.isValid(expectedSdkNames);
}
if (doesCeNeedReconcile || doesDeNeedReconcile) {
try {
// TODO(b/224719352): Pass actual seinfo from here
mPackageManagerLocal.reconcileSdkData(
volumeUuid,
packageName,
subDirNames,
userId,
appId,
/*previousAppId=*/ -1,
/*seInfo=*/ "default",
flags);
} catch (Exception e) {
// We will retry when sdk gets loaded
Log.w(TAG, "Failed to reconcileSdkData for " + packageName + " subDirNames: "
+ String.join(", ", subDirNames) + " error: " + e.getMessage());
}
}
}
// Returns a random string.
private static String getRandomString() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
}
/**
* Returns list of sdks {@code packageName} uses
*/
@SuppressWarnings("MixedMutabilityReturnType")
private List<String> getSdksUsed(int userId, String packageName) {
PackageManager pm = getPackageManager(userId);
try {
ApplicationInfo info = pm.getApplicationInfo(
packageName, PackageManager.GET_SHARED_LIBRARY_FILES);
return getSdksUsed(info);
} catch (PackageManager.NameNotFoundException ignored) {
return Collections.emptyList();
}
}
private static List<String> getSdksUsed(ApplicationInfo info) {
List<String> result = new ArrayList<>();
List<SharedLibraryInfo> sharedLibraries = info.getSharedLibraryInfos();
for (int i = 0; i < sharedLibraries.size(); i++) {
final SharedLibraryInfo sharedLib = sharedLibraries.get(i);
if (sharedLib.getType() != SharedLibraryInfo.TYPE_SDK_PACKAGE) {
continue;
}
result.add(sharedLib.getName());
}
return result;
}
/**
* For the given {@code userId}, ensure that sdk data package directories are still valid.
*
* <p>The primary concern of this method is to remove invalid data directories. Missing valid
* directories will get created when the app loads sdk for the first time.
*/
@GuardedBy("mLock")
private void reconcileSdkDataPackageDirs(int userId) {
Log.i(TAG, "Reconciling sdk data package directories for " + userId);
PackageInfoHolder pmInfoHolder = new PackageInfoHolder(mContext, userId);
reconcileSdkDataPackageDirs(userId, /*isCeData=*/ true, pmInfoHolder);
reconcileSdkDataPackageDirs(userId, /*isCeData=*/ false, pmInfoHolder);
}
@GuardedBy("mLock")
private void reconcileSdkDataPackageDirs(
int userId, boolean isCeData, PackageInfoHolder pmInfoHolder) {
final List<String> volumeUuids = getMountedVolumes();
for (int i = 0; i < volumeUuids.size(); i++) {
final String volumeUuid = volumeUuids.get(i);
final String rootDir = getSdkDataRootDirectory(volumeUuid, userId, isCeData);
final String[] sdkPackages = new File(rootDir).list();
if (sdkPackages == null) {
continue;
}
// Now loop over package directories and remove the ones that are invalid
for (int j = 0; j < sdkPackages.length; j++) {
final String packageName = sdkPackages[j];
// Only consider installed packages which are not instrumented and either
// not using sdk or on incorrect volume for destroying
final int uid = pmInfoHolder.getUid(packageName);
final boolean isInstrumented =
mSdkSandboxManagerLocal.isInstrumentationRunning(packageName, uid);
final boolean hasCorrectVolume =
TextUtils.equals(volumeUuid, pmInfoHolder.getVolumeUuid(packageName));
final boolean isInstalled = !pmInfoHolder.isUninstalled(packageName);
final boolean usesSdk = pmInfoHolder.usesSdk(packageName);
if (!isInstrumented && isInstalled && (!hasCorrectVolume || !usesSdk)) {
destroySdkDataPackageDirectory(volumeUuid, userId, packageName, isCeData);
}
}
}
// Now loop over all installed packages and ensure all packages have sdk data directories
final Iterator<String> it = pmInfoHolder.getInstalledPackagesUsingSdks().iterator();
while (it.hasNext()) {
final String packageName = it.next();
final String volumeUuid = pmInfoHolder.getVolumeUuid(packageName);
// Verify if package dir contains a subdir for each sdk and a shared directory
final String packageDir = getSdkDataPackageDirectory(volumeUuid, userId, packageName,
isCeData);
final SubDirectories subDirs = new SubDirectories(packageDir);
final Set<String> expectedSdkNames = pmInfoHolder.getSdksUsed(packageName);
if (subDirs.isValid(expectedSdkNames)) {
continue;
}
Log.i(TAG, "Reconciling missing package directory for: " + packageDir);
final int uid = pmInfoHolder.getUid(packageName);
if (uid == -1) {
Log.w(TAG, "Failed to get uid for reconcilation of " + packageDir);
// Safe to continue since we will retry during loading sdk
continue;
}
final CallingInfo callingInfo = new CallingInfo(uid, packageName);
reconcileSdkDataSubDirs(callingInfo, /*forInstrumentation=*/false);
}
}
private PackageManager getPackageManager(int userId) {
return mContext.createContextAsUser(UserHandle.of(userId), 0).getPackageManager();
}
@GuardedBy("mLock")
private void destroySdkDataPackageDirectory(
@Nullable String volumeUuid, int userId, String packageName, boolean isCeData) {
final Path packageDir =
Paths.get(getSdkDataPackageDirectory(volumeUuid, userId, packageName, isCeData));
if (!Files.exists(packageDir)) {
return;
}
Log.i(TAG, "Destroying sdk data package directory " + packageDir);
// Even though system owns the package directory, the sub-directories are owned by sandbox.
// We first need to get rid of sub-directories.
try {
final int flag = isCeData
? PackageManagerLocal.FLAG_STORAGE_CE
: PackageManagerLocal.FLAG_STORAGE_DE;
mPackageManagerLocal.reconcileSdkData(volumeUuid, packageName,
Collections.emptyList(), userId, /*appId=*/-1, /*previousAppId=*/-1,
/*seInfo=*/"default", flag);
} catch (Exception e) {
Log.e(TAG, "Failed to destroy sdk data on user unlock for userId: " + userId
+ " packageName: " + packageName + " error: " + e.getMessage());
}
// Now that the package directory is empty, we can delete it
try {
Files.delete(packageDir);
} catch (Exception e) {
Log.e(
TAG,
"Failed to destroy sdk data on user unlock for userId: "
+ userId
+ " packageName: "
+ packageName
+ " error: "
+ e.getMessage());
}
}
private String getDataDirectory(@Nullable String volumeUuid) {
if (TextUtils.isEmpty(volumeUuid)) {
return mRootDir + "/data";
} else {
return mRootDir + "/mnt/expand/" + volumeUuid;
}
}
private String getSdkDataRootDirectory(
@Nullable String volumeUuid, int userId, boolean isCeData) {
return getDataDirectory(volumeUuid) + (isCeData ? "/misc_ce/" : "/misc_de/") + userId
+ "/sdksandbox";
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
String getSdkDataPackageDirectory(
@Nullable String volumeUuid, int userId, String packageName, boolean isCeData) {
return getSdkDataRootDirectory(volumeUuid, userId, isCeData) + "/" + packageName;
}
/**
* Class representing collection of sub-directories used for sdk sandox storage
*
* <p>There are two kinds of sub-directories:
*
* <ul>
* <li>Sdk sub-directory: belongs exclusively to individual sdk and has name <sdk>@random
* <li>Non-sdk sub-directory: not specific to a particular sdk. Can belong to other entities.
* Typically has structure <name>#random. The only exception being shared storage which is
* just named "shared".
* </ul>
*
* <p>This class helps in organizing the sdk-subdirectories in groups so that they are easier to
* process.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static class SubDirectories {
private static final String SHARED_DIR = "shared";
private static final ArraySet<String> NON_SDK_SUBDIRS =
new ArraySet(Arrays.asList(SHARED_DIR));
private final String mBaseDir;
private final ArrayMap<String, String> mSdkSubDirs;
private final ArrayMap<String, String> mNonSdkSubDirs;
private boolean mHasUnknownSubDirs = false;
/**
* Lists all the children of provided path and organizes them into sdk and non-sdk group.
*/
SubDirectories(String path) {
mBaseDir = path;
mSdkSubDirs = new ArrayMap<>();
mNonSdkSubDirs = new ArrayMap<>();
final File parent = new File(path);
final String[] children = parent.list();
if (children == null) {
return;
}
for (int i = 0; i < children.length; i++) {
final String child = children[i];
if (child.indexOf("@") != -1) {
final String[] tokens = child.split("@");
mSdkSubDirs.put(tokens[0], child);
} else if (child.equals(SHARED_DIR)) {
mNonSdkSubDirs.put(SHARED_DIR, SHARED_DIR);
} else {
mHasUnknownSubDirs = true;
}
}
}
/** Gets the sub-directory name of provided sdk with random suffix */
@Nullable
public String getSdkSubDir(String sdkName) {
return getSdkSubDir(sdkName, /*fullPath=*/ false);
}
/** Gets the full path of per-sdk storage with random suffix */
@Nullable
public String getSdkSubDir(String sdkName, boolean fullPath) {
final String subDir = mSdkSubDirs.getOrDefault(sdkName, null);
if (subDir == null || !fullPath) return subDir;
return Paths.get(mBaseDir, subDir).toString();
}
/**
* Provided a list of sdk names, verifies if the current collection of directories satisfies
* per-sdk and non-sdk sub-directory requirements.
*/
public boolean isValid(Set<String> expectedSdkNames) {
final boolean hasCorrectSdkSubDirs = mSdkSubDirs.keySet().equals(expectedSdkNames);
final boolean hasCorrectNonSdkSubDirs = mNonSdkSubDirs.keySet().equals(NON_SDK_SUBDIRS);
return hasCorrectSdkSubDirs && hasCorrectNonSdkSubDirs && !mHasUnknownSubDirs;
}
}
private static class PackageInfoHolder {
private final Context mContext;
final ArrayMap<String, Set<String>> mPackagesWithSdks = new ArrayMap<>();
final ArrayMap<String, Integer> mPackageNameToUid = new ArrayMap<>();
final ArrayMap<String, String> mPackageNameToVolumeUuid = new ArrayMap<>();
final Set<String> mUninstalledPackages = new ArraySet<>();
PackageInfoHolder(Context context, int userId) {
mContext = context.createContextAsUser(UserHandle.of(userId), 0);
PackageManager pm = mContext.getPackageManager();
final List<PackageInfo> packageInfoList = pm.getInstalledPackages(
PackageManager.GET_SHARED_LIBRARY_FILES);
final ArraySet<String> installedPackages = new ArraySet<>();
for (int i = 0; i < packageInfoList.size(); i++) {
final PackageInfo info = packageInfoList.get(i);
installedPackages.add(info.packageName);
final String volumeUuid =
StorageUuuidConverter.convertToVolumeUuid(info.applicationInfo.storageUuid);
mPackageNameToVolumeUuid.put(info.packageName, volumeUuid);
mPackageNameToUid.put(info.packageName, info.applicationInfo.uid);
final List<String> sdksUsedNames =
SdkSandboxStorageManager.getSdksUsed(info.applicationInfo);
if (sdksUsedNames.isEmpty()) {
continue;
}
mPackagesWithSdks.put(info.packageName, new ArraySet<>(sdksUsedNames));
}
// If an app is uninstalled with DELETE_KEEP_DATA flag, we need to preserve its sdk
// data. For that, we need names of uninstalled packages.
final List<PackageInfo> allPackages =
pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES);
for (int i = 0; i < allPackages.size(); i++) {
final String packageName = allPackages.get(i).packageName;
if (!installedPackages.contains(packageName)) {
mUninstalledPackages.add(packageName);
}
}
}
public boolean isUninstalled(String packageName) {
return mUninstalledPackages.contains(packageName);
}
public int getUid(String packageName) {
return mPackageNameToUid.getOrDefault(packageName, -1);
}
public Set<String> getInstalledPackagesUsingSdks() {
return mPackagesWithSdks.keySet();
}
public Set<String> getSdksUsed(String packageName) {
return mPackagesWithSdks.get(packageName);
}
public boolean usesSdk(String packageName) {
return mPackagesWithSdks.containsKey(packageName);
}
public String getVolumeUuid(String packageName) {
return mPackageNameToVolumeUuid.get(packageName);
}
}
// TODO(b/234023859): We will remove this class once the required APIs get unhidden
// The class below has been copied from StorageManager's convert logic
private static class StorageUuuidConverter {
private static final String FAT_UUID_PREFIX = "fafafafa-fafa-5afa-8afa-fafa";
private static final UUID UUID_DEFAULT =
UUID.fromString("41217664-9172-527a-b3d5-edabb50a7d69");
private static final String UUID_SYSTEM = "system";
private static final UUID UUID_SYSTEM_ =
UUID.fromString("5d258386-e60d-59e3-826d-0089cdd42cc0");
private static final String UUID_PRIVATE_INTERNAL = null;
private static final String UUID_PRIMARY_PHYSICAL = "primary_physical";
private static final UUID UUID_PRIMARY_PHYSICAL_ =
UUID.fromString("0f95a519-dae7-5abf-9519-fbd6209e05fd");
private static @Nullable String convertToVolumeUuid(@NonNull UUID storageUuid) {
if (UUID_DEFAULT.equals(storageUuid)) {
return UUID_PRIVATE_INTERNAL;
} else if (UUID_PRIMARY_PHYSICAL_.equals(storageUuid)) {
return UUID_PRIMARY_PHYSICAL;
} else if (UUID_SYSTEM_.equals(storageUuid)) {
return UUID_SYSTEM;
} else {
String uuidString = storageUuid.toString();
// This prefix match will exclude fsUuids from private volumes because
// (a) linux fsUuids are generally Version 4 (random) UUIDs so the prefix
// will contain 4xxx instead of 5xxx and (b) we've already matched against
// known namespace (Version 5) UUIDs above.
if (uuidString.startsWith(FAT_UUID_PREFIX)) {
String fatStr =
uuidString.substring(FAT_UUID_PREFIX.length()).toUpperCase(Locale.US);
return fatStr.substring(0, 4) + "-" + fatStr.substring(4);
}
return storageUuid.toString();
}
}
}
// We loop over "/mnt/expand" directory's children and find the volumeUuids
// TODO(b/234023859): We want to use storage manager api in future for this task
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
List<String> getMountedVolumes() {
// Collect package names from root directory
final List<String> volumeUuids = new ArrayList<>();
volumeUuids.add(null);
final String[] mountedVolumes = new File(mRootDir + "/mnt/expand").list();
if (mountedVolumes == null) {
return volumeUuids;
}
for (int i = 0; i < mountedVolumes.length; i++) {
final String volumeUuid = mountedVolumes[i];
volumeUuids.add(volumeUuid);
}
return volumeUuids;
}
private @Nullable String getVolumeUuidForPackage(int userId, String packageName)
throws PackageManager.NameNotFoundException {
PackageManager pm = getPackageManager(userId);
ApplicationInfo info = pm.getApplicationInfo(packageName, /*flags=*/ 0);
return StorageUuuidConverter.convertToVolumeUuid(info.storageUuid);
}
/**
* Sdk data directories for a particular sdk.
*
* Every sdk has two data directories. One is credentially encrypted storage and another is
* device encrypted.
*/
static class SdkDataDirInfo {
@Nullable final String mCeData;
@Nullable final String mDeData;
SdkDataDirInfo(@Nullable String ceDataPath, @Nullable String deDataPath) {
mCeData = ceDataPath;
mDeData = deDataPath;
}
@Nullable String getCeDataDir() {
return mCeData;
}
@Nullable String getDeDataDir() {
return mDeData;
}
}
}