/* * Copyright (C) 2016 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.pm.dex; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageParser; import android.os.RemoteException; import android.os.storage.StorageManager; import android.os.UserHandle; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.pm.Installer; import com.android.server.pm.Installer.InstallerException; import com.android.server.pm.PackageDexOptimizer; import com.android.server.pm.PackageManagerServiceUtils; import com.android.server.pm.PackageManagerServiceCompilerMapping; import java.io.File; import java.io.IOException; import java.util.List; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; /** * This class keeps track of how dex files are used. * Every time it gets a notification about a dex file being loaded it tracks * its owning package and records it in PackageDexUsage (package-dex-usage.list). * * TODO(calin): Extract related dexopt functionality from PackageManagerService * into this class. */ public class DexManager { private static final String TAG = "DexManager"; private static final boolean DEBUG = false; // Maps package name to code locations. // It caches the code locations for the installed packages. This allows for // faster lookups (no locks) when finding what package owns the dex file. private final Map<String, PackageCodeLocations> mPackageCodeLocationsCache; // PackageDexUsage handles the actual I/O operations. It is responsible to // encode and save the dex usage data. private final PackageDexUsage mPackageDexUsage; private final IPackageManager mPackageManager; private final PackageDexOptimizer mPackageDexOptimizer; private final Object mInstallLock; @GuardedBy("mInstallLock") private final Installer mInstaller; // Possible outcomes of a dex search. private static int DEX_SEARCH_NOT_FOUND = 0; // dex file not found private static int DEX_SEARCH_FOUND_PRIMARY = 1; // dex file is the primary/base apk private static int DEX_SEARCH_FOUND_SPLIT = 2; // dex file is a split apk private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex public DexManager(IPackageManager pms, PackageDexOptimizer pdo, Installer installer, Object installLock) { mPackageCodeLocationsCache = new HashMap<>(); mPackageDexUsage = new PackageDexUsage(); mPackageManager = pms; mPackageDexOptimizer = pdo; mInstaller = installer; mInstallLock = installLock; } /** * Notify about dex files loads. * Note that this method is invoked when apps load dex files and it should * return as fast as possible. * * @param loadingPackage the package performing the load * @param dexPaths the list of dex files being loaded * @param loaderIsa the ISA of the app loading the dex files * @param loaderUserId the user id which runs the code loading the dex files */ public void notifyDexLoad(ApplicationInfo loadingAppInfo, List<String> dexPaths, String loaderIsa, int loaderUserId) { try { notifyDexLoadInternal(loadingAppInfo, dexPaths, loaderIsa, loaderUserId); } catch (Exception e) { Slog.w(TAG, "Exception while notifying dex load for package " + loadingAppInfo.packageName, e); } } private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo, List<String> dexPaths, String loaderIsa, int loaderUserId) { if (!PackageManagerServiceUtils.checkISA(loaderIsa)) { Slog.w(TAG, "Loading dex files " + dexPaths + " in unsupported ISA: " + loaderIsa + "?"); return; } for (String dexPath : dexPaths) { // Find the owning package name. DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId); if (DEBUG) { Slog.i(TAG, loadingAppInfo.packageName + " loads from " + searchResult + " : " + loaderUserId + " : " + dexPath); } if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) { // TODO(calin): extend isUsedByOtherApps check to detect the cases where // different apps share the same runtime. In that case we should not mark the dex // file as isUsedByOtherApps. Currently this is a safe approximation. boolean isUsedByOtherApps = !loadingAppInfo.packageName.equals( searchResult.mOwningPackageName); boolean primaryOrSplit = searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY || searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT; if (primaryOrSplit && !isUsedByOtherApps) { // If the dex file is the primary apk (or a split) and not isUsedByOtherApps // do not record it. This case does not bring any new usable information // and can be safely skipped. continue; } // Record dex file usage. If the current usage is a new pattern (e.g. new secondary, // or UsedBytOtherApps), record will return true and we trigger an async write // to disk to make sure we don't loose the data in case of a reboot. if (mPackageDexUsage.record(searchResult.mOwningPackageName, dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit)) { mPackageDexUsage.maybeWriteAsync(); } } else { // This can happen in a few situations: // - bogus dex loads // - recent installs/uninstalls that we didn't detect. // - new installed splits // If we can't find the owner of the dex we simply do not track it. The impact is // that the dex file will not be considered for offline optimizations. // TODO(calin): add hooks for move/uninstall notifications to // capture package moves or obsolete packages. if (DEBUG) { Slog.i(TAG, "Could not find owning package for dex file: " + dexPath); } } } } /** * Read the dex usage from disk and populate the code cache locations. * @param existingPackages a map containing information about what packages * are available to what users. Only packages in this list will be * recognized during notifyDexLoad(). */ public void load(Map<Integer, List<PackageInfo>> existingPackages) { try { loadInternal(existingPackages); } catch (Exception e) { mPackageDexUsage.clear(); Slog.w(TAG, "Exception while loading package dex usage. " + "Starting with a fresh state.", e); } } /** * Notifies that a new package was installed for {@code userId}. * {@code userId} must not be {@code UserHandle.USER_ALL}. * * @throws IllegalArgumentException if {@code userId} is {@code UserHandle.USER_ALL}. */ public void notifyPackageInstalled(PackageInfo pi, int userId) { if (userId == UserHandle.USER_ALL) { throw new IllegalArgumentException( "notifyPackageInstalled called with USER_ALL"); } cachePackageCodeLocation(pi.packageName, pi.applicationInfo.sourceDir, pi.applicationInfo.splitSourceDirs, pi.applicationInfo.dataDir, userId); } /** * Notifies that package {@code packageName} was updated. * This will clear the UsedByOtherApps mark if it exists. */ public void notifyPackageUpdated(String packageName, String baseCodePath, String[] splitCodePaths) { cachePackageCodeLocation(packageName, baseCodePath, splitCodePaths, null, /*userId*/ -1); // In case there was an update, write the package use info to disk async. // Note that we do the writing here and not in PackageDexUsage in order to be // consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs // multiple updates in PackaeDexUsage before writing it). if (mPackageDexUsage.clearUsedByOtherApps(packageName)) { mPackageDexUsage.maybeWriteAsync(); } } /** * Notifies that the user {@code userId} data for package {@code packageName} * was destroyed. This will remove all usage info associated with the package * for the given user. * {@code userId} is allowed to be {@code UserHandle.USER_ALL} in which case * all usage information for the package will be removed. */ public void notifyPackageDataDestroyed(String packageName, int userId) { boolean updated = userId == UserHandle.USER_ALL ? mPackageDexUsage.removePackage(packageName) : mPackageDexUsage.removeUserPackage(packageName, userId); // In case there was an update, write the package use info to disk async. // Note that we do the writing here and not in PackageDexUsage in order to be // consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs // multiple updates in PackaeDexUsage before writing it). if (updated) { mPackageDexUsage.maybeWriteAsync(); } } public void cachePackageCodeLocation(String packageName, String baseCodePath, String[] splitCodePaths, String dataDir, int userId) { PackageCodeLocations pcl = putIfAbsent(mPackageCodeLocationsCache, packageName, new PackageCodeLocations(packageName, baseCodePath, splitCodePaths)); pcl.updateCodeLocation(baseCodePath, splitCodePaths); if (dataDir != null) { pcl.mergeAppDataDirs(dataDir, userId); } } private void loadInternal(Map<Integer, List<PackageInfo>> existingPackages) { Map<String, Set<Integer>> packageToUsersMap = new HashMap<>(); // Cache the code locations for the installed packages. This allows for // faster lookups (no locks) when finding what package owns the dex file. for (Map.Entry<Integer, List<PackageInfo>> entry : existingPackages.entrySet()) { List<PackageInfo> packageInfoList = entry.getValue(); int userId = entry.getKey(); for (PackageInfo pi : packageInfoList) { // Cache the code locations. cachePackageCodeLocation(pi.packageName, pi.applicationInfo.sourceDir, pi.applicationInfo.splitSourceDirs, pi.applicationInfo.dataDir, userId); // Cache a map from package name to the set of user ids who installed the package. // We will use it to sync the data and remove obsolete entries from // mPackageDexUsage. Set<Integer> users = putIfAbsent( packageToUsersMap, pi.packageName, new HashSet<>()); users.add(userId); } } mPackageDexUsage.read(); mPackageDexUsage.syncData(packageToUsersMap); } /** * Get the package dex usage for the given package name. * @return the package data or null if there is no data available for this package. */ public PackageUseInfo getPackageUseInfo(String packageName) { return mPackageDexUsage.getPackageUseInfo(packageName); } /** * Perform dexopt on the package {@code packageName} secondary dex files. * @return true if all secondary dex files were processed successfully (compiled or skipped * because they don't need to be compiled).. */ public boolean dexoptSecondaryDex(String packageName, int compilerReason, boolean force) { return dexoptSecondaryDex(packageName, PackageManagerServiceCompilerMapping.getCompilerFilterForReason(compilerReason), force); } /** * Perform dexopt on the package {@code packageName} secondary dex files. * @return true if all secondary dex files were processed successfully (compiled or skipped * because they don't need to be compiled).. */ public boolean dexoptSecondaryDex(String packageName, String compilerFilter, boolean force) { // Select the dex optimizer based on the force parameter. // Forced compilation is done through ForcedUpdatePackageDexOptimizer which will adjust // the necessary dexopt flags to make sure that compilation is not skipped. This avoid // passing the force flag through the multitude of layers. // Note: The force option is rarely used (cmdline input for testing, mostly), so it's OK to // allocate an object here. PackageDexOptimizer pdo = force ? new PackageDexOptimizer.ForcedUpdatePackageDexOptimizer(mPackageDexOptimizer) : mPackageDexOptimizer; PackageUseInfo useInfo = getPackageUseInfo(packageName); if (useInfo == null || useInfo.getDexUseInfoMap().isEmpty()) { if (DEBUG) { Slog.d(TAG, "No secondary dex use for package:" + packageName); } // Nothing to compile, return true. return true; } boolean success = true; for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) { String dexPath = entry.getKey(); DexUseInfo dexUseInfo = entry.getValue(); PackageInfo pkg = null; try { pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0, dexUseInfo.getOwnerUserId()); } catch (RemoteException e) { throw new AssertionError(e); } // It may be that the package gets uninstalled while we try to compile its // secondary dex files. If that's the case, just ignore. // Note that we don't break the entire loop because the package might still be // installed for other users. if (pkg == null) { Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName + " for user " + dexUseInfo.getOwnerUserId()); mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId()); continue; } int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath, dexUseInfo.getLoaderIsas(), compilerFilter, dexUseInfo.isUsedByOtherApps()); success = success && (result != PackageDexOptimizer.DEX_OPT_FAILED); } return success; } /** * Reconcile the information we have about the secondary dex files belonging to * {@code packagName} and the actual dex files. For all dex files that were * deleted, update the internal records and delete any generated oat files. */ public void reconcileSecondaryDexFiles(String packageName) { PackageUseInfo useInfo = getPackageUseInfo(packageName); if (useInfo == null || useInfo.getDexUseInfoMap().isEmpty()) { if (DEBUG) { Slog.d(TAG, "No secondary dex use for package:" + packageName); } // Nothing to reconcile. return; } Set<String> dexFilesToRemove = new HashSet<>(); boolean updated = false; for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) { String dexPath = entry.getKey(); DexUseInfo dexUseInfo = entry.getValue(); PackageInfo pkg = null; try { // Note that we look for the package in the PackageManager just to be able // to get back the real app uid and its storage kind. These are only used // to perform extra validation in installd. // TODO(calin): maybe a bit overkill. pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0, dexUseInfo.getOwnerUserId()); } catch (RemoteException ignore) { // Can't happen, DexManager is local. } if (pkg == null) { // It may be that the package was uninstalled while we process the secondary // dex files. Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName + " for user " + dexUseInfo.getOwnerUserId()); // Update the usage and continue, another user might still have the package. updated = mPackageDexUsage.removeUserPackage( packageName, dexUseInfo.getOwnerUserId()) || updated; continue; } ApplicationInfo info = pkg.applicationInfo; int flags = 0; if (info.dataDir.equals(info.deviceProtectedDataDir)) { flags |= StorageManager.FLAG_STORAGE_DE; } else if (info.dataDir.equals(info.credentialProtectedDataDir)) { flags |= StorageManager.FLAG_STORAGE_CE; } else { Slog.e(TAG, "Could not infer CE/DE storage for package " + info.packageName); updated = mPackageDexUsage.removeUserPackage( packageName, dexUseInfo.getOwnerUserId()) || updated; continue; } boolean dexStillExists = true; synchronized(mInstallLock) { try { String[] isas = dexUseInfo.getLoaderIsas().toArray(new String[0]); dexStillExists = mInstaller.reconcileSecondaryDexFile(dexPath, packageName, pkg.applicationInfo.uid, isas, pkg.applicationInfo.volumeUuid, flags); } catch (InstallerException e) { Slog.e(TAG, "Got InstallerException when reconciling dex " + dexPath + " : " + e.getMessage()); } } if (!dexStillExists) { updated = mPackageDexUsage.removeDexFile( packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated; } } if (updated) { mPackageDexUsage.maybeWriteAsync(); } } /** * Return all packages that contain records of secondary dex files. */ public Set<String> getAllPackagesWithSecondaryDexFiles() { return mPackageDexUsage.getAllPackagesWithSecondaryDexFiles(); } /** * Return true if the profiling data collected for the given app indicate * that the apps's APK has been loaded by another app. * Note that this returns false for all apps without any collected profiling data. */ public boolean isUsedByOtherApps(String packageName) { PackageUseInfo useInfo = getPackageUseInfo(packageName); if (useInfo == null) { // No use info, means the package was not used or it was used but not by other apps. // Note that right now we might prune packages which are not used by other apps. // TODO(calin): maybe we should not (prune) so we can have an accurate view when we try // to access the package use. return false; } return useInfo.isUsedByOtherApps(); } /** * Retrieves the package which owns the given dexPath. */ private DexSearchResult getDexPackage( ApplicationInfo loadingAppInfo, String dexPath, int userId) { // Ignore framework code. // TODO(calin): is there a better way to detect it? if (dexPath.startsWith("/system/framework/")) { return new DexSearchResult("framework", DEX_SEARCH_NOT_FOUND); } // First, check if the package which loads the dex file actually owns it. // Most of the time this will be true and we can return early. PackageCodeLocations loadingPackageCodeLocations = new PackageCodeLocations(loadingAppInfo, userId); int outcome = loadingPackageCodeLocations.searchDex(dexPath, userId); if (outcome != DEX_SEARCH_NOT_FOUND) { // TODO(calin): evaluate if we bother to detect symlinks at the dexPath level. return new DexSearchResult(loadingPackageCodeLocations.mPackageName, outcome); } // The loadingPackage does not own the dex file. // Perform a reverse look-up in the cache to detect if any package has ownership. // Note that we can have false negatives if the cache falls out of date. for (PackageCodeLocations pcl : mPackageCodeLocationsCache.values()) { outcome = pcl.searchDex(dexPath, userId); if (outcome != DEX_SEARCH_NOT_FOUND) { return new DexSearchResult(pcl.mPackageName, outcome); } } // Cache miss. Return not found for the moment. // // TODO(calin): this may be because of a newly installed package, an update // or a new added user. We can either perform a full look up again or register // observers to be notified of package/user updates. return new DexSearchResult(null, DEX_SEARCH_NOT_FOUND); } private static <K,V> V putIfAbsent(Map<K,V> map, K key, V newValue) { V existingValue = map.putIfAbsent(key, newValue); return existingValue == null ? newValue : existingValue; } /** * Convenience class to store the different locations where a package might * own code. */ private static class PackageCodeLocations { private final String mPackageName; private String mBaseCodePath; private final Set<String> mSplitCodePaths; // Maps user id to the application private directory. private final Map<Integer, Set<String>> mAppDataDirs; public PackageCodeLocations(ApplicationInfo ai, int userId) { this(ai.packageName, ai.sourceDir, ai.splitSourceDirs); mergeAppDataDirs(ai.dataDir, userId); } public PackageCodeLocations(String packageName, String baseCodePath, String[] splitCodePaths) { mPackageName = packageName; mSplitCodePaths = new HashSet<>(); mAppDataDirs = new HashMap<>(); updateCodeLocation(baseCodePath, splitCodePaths); } public void updateCodeLocation(String baseCodePath, String[] splitCodePaths) { mBaseCodePath = baseCodePath; mSplitCodePaths.clear(); if (splitCodePaths != null) { for (String split : splitCodePaths) { mSplitCodePaths.add(split); } } } public void mergeAppDataDirs(String dataDir, int userId) { Set<String> dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>()); dataDirs.add(dataDir); } public int searchDex(String dexPath, int userId) { // First check that this package is installed or active for the given user. // If we don't have a data dir it means this user is trying to load something // unavailable for them. Set<String> userDataDirs = mAppDataDirs.get(userId); if (userDataDirs == null) { Slog.w(TAG, "Trying to load a dex path which does not exist for the current " + "user. dexPath=" + dexPath + ", userId=" + userId); return DEX_SEARCH_NOT_FOUND; } if (mBaseCodePath.equals(dexPath)) { return DEX_SEARCH_FOUND_PRIMARY; } if (mSplitCodePaths.contains(dexPath)) { return DEX_SEARCH_FOUND_SPLIT; } for (String dataDir : userDataDirs) { if (dexPath.startsWith(dataDir)) { return DEX_SEARCH_FOUND_SECONDARY; } } // TODO(calin): What if we get a symlink? e.g. data dir may be a symlink, // /data/data/ -> /data/user/0/. if (DEBUG) { try { String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath)); if (dexPathReal != dexPath) { Slog.d(TAG, "Dex loaded with symlink. dexPath=" + dexPath + " dexPathReal=" + dexPathReal); } } catch (IOException e) { // Ignore } } return DEX_SEARCH_NOT_FOUND; } } /** * Convenience class to store ownership search results. */ private class DexSearchResult { private String mOwningPackageName; private int mOutcome; public DexSearchResult(String owningPackageName, int outcome) { this.mOwningPackageName = owningPackageName; this.mOutcome = outcome; } @Override public String toString() { return mOwningPackageName + "-" + mOutcome; } } }