blob: cc26c9b5f76ca89ce5c936bcdda35dd557a75ffb [file] [log] [blame]
/*
* Copyright 2018 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.util.AtomicFile;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastPrintWriter;
import com.android.server.pm.AbstractStatsBase;
import libcore.io.IoUtils;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Stats file which stores information about secondary code files that are dynamically loaded.
*/
class PackageDynamicCodeLoading extends AbstractStatsBase<Void> {
// Type code to indicate a secondary file containing DEX code. (The char value is how it
// is represented in the text file format.)
static final int FILE_TYPE_DEX = 'D';
// Type code to indicate a secondary file containing native code.
static final int FILE_TYPE_NATIVE = 'N';
private static final String TAG = "PackageDynamicCodeLoading";
private static final String FILE_VERSION_HEADER = "DCL1";
private static final String PACKAGE_PREFIX = "P:";
private static final char FIELD_SEPARATOR = ':';
private static final String PACKAGE_SEPARATOR = ",";
/**
* Limit on how many files we store for a single owner, to avoid one app causing
* unbounded memory consumption.
*/
@VisibleForTesting
static final int MAX_FILES_PER_OWNER = 100;
/**
* Regular expression to match the expected format of an input line describing one file.
* <p>Example: {@code D:10:package.name1,package.name2:/escaped/path}
* <p>The capturing groups are the file type, user ID, loading packages and escaped file path
* (in that order).
* <p>See {@link #write(OutputStream, Map)} below for more details of the format.
*/
private static final Pattern PACKAGE_LINE_PATTERN =
Pattern.compile("([A-Z]):([0-9]+):([^:]*):(.*)");
private final Object mLock = new Object();
// Map from package name to data about loading of dynamic code files owned by that package.
// (Apps may load code files owned by other packages, subject to various access
// constraints.)
// Any PackageDynamicCode in this map will be non-empty.
@GuardedBy("mLock")
private Map<String, PackageDynamicCode> mPackageMap = new HashMap<>();
PackageDynamicCodeLoading() {
super("package-dcl.list", "PackageDynamicCodeLoading_DiskWriter", false);
}
/**
* Record dynamic code loading from a file.
*
* Note this is called when an app loads dex files and as such it should return
* as fast as possible.
*
* @param owningPackageName the package owning the file path
* @param filePath the path of the dex files being loaded
* @param fileType the type of code loading
* @param ownerUserId the user id which runs the code loading the file
* @param loadingPackageName the package performing the load
* @return whether new information has been recorded
* @throws IllegalArgumentException if clearly invalid information is detected
*/
boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId,
String loadingPackageName) {
if (!isValidFileType(fileType)) {
throw new IllegalArgumentException("Bad file type: " + fileType);
}
synchronized (mLock) {
PackageDynamicCode packageInfo = mPackageMap.get(owningPackageName);
if (packageInfo == null) {
packageInfo = new PackageDynamicCode();
mPackageMap.put(owningPackageName, packageInfo);
}
return packageInfo.add(filePath, (char) fileType, ownerUserId, loadingPackageName);
}
}
private static boolean isValidFileType(int fileType) {
return fileType == FILE_TYPE_DEX || fileType == FILE_TYPE_NATIVE;
}
/**
* Return all packages that contain records of secondary dex files. (Note that data updates
* asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed
* one of these package names.)
*/
Set<String> getAllPackagesWithDynamicCodeLoading() {
synchronized (mLock) {
return new HashSet<>(mPackageMap.keySet());
}
}
/**
* Return information about the dynamic code file usage of the specified package,
* or null if there is currently no usage information. The object returned is a copy of the
* live information that is not updated.
*/
PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
synchronized (mLock) {
PackageDynamicCode info = mPackageMap.get(packageName);
return info == null ? null : new PackageDynamicCode(info);
}
}
/**
* Remove all information about all packages.
*/
void clear() {
synchronized (mLock) {
mPackageMap.clear();
}
}
/**
* Remove the data associated with package {@code packageName}. Affects all users.
* @return true if the package usage was found and removed successfully
*/
boolean removePackage(String packageName) {
synchronized (mLock) {
return mPackageMap.remove(packageName) != null;
}
}
/**
* Remove all the records about package {@code packageName} belonging to user {@code userId}.
* @return whether any data was actually removed
*/
boolean removeUserPackage(String packageName, int userId) {
synchronized (mLock) {
PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
if (packageDynamicCode == null) {
return false;
}
if (packageDynamicCode.removeUser(userId)) {
if (packageDynamicCode.mFileUsageMap.isEmpty()) {
mPackageMap.remove(packageName);
}
return true;
} else {
return false;
}
}
}
/**
* Remove the specified dynamic code file record belonging to the package {@code packageName}
* and user {@code userId}.
* @return whether data was actually removed
*/
boolean removeFile(String packageName, String filePath, int userId) {
synchronized (mLock) {
PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
if (packageDynamicCode == null) {
return false;
}
if (packageDynamicCode.removeFile(filePath, userId)) {
if (packageDynamicCode.mFileUsageMap.isEmpty()) {
mPackageMap.remove(packageName);
}
return true;
} else {
return false;
}
}
}
/**
* Syncs data with the set of installed packages. Data about packages that are no longer
* installed is removed.
* @param packageToUsersMap a map from all existing package names to the users who have the
* package installed
*/
void syncData(Map<String, Set<Integer>> packageToUsersMap) {
synchronized (mLock) {
Iterator<Entry<String, PackageDynamicCode>> it = mPackageMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, PackageDynamicCode> entry = it.next();
Set<Integer> packageUsers = packageToUsersMap.get(entry.getKey());
if (packageUsers == null) {
it.remove();
} else {
PackageDynamicCode packageDynamicCode = entry.getValue();
packageDynamicCode.syncData(packageToUsersMap, packageUsers);
if (packageDynamicCode.mFileUsageMap.isEmpty()) {
it.remove();
}
}
}
}
}
/**
* Request that data be written to persistent file at the next time allowed by write-limiting.
*/
void maybeWriteAsync() {
super.maybeWriteAsync(null);
}
/**
* Writes data to persistent file immediately.
*/
void writeNow() {
super.writeNow(null);
}
@Override
protected final void writeInternal(Void data) {
AtomicFile file = getFile();
FileOutputStream output = null;
try {
output = file.startWrite();
write(output);
file.finishWrite(output);
} catch (IOException e) {
file.failWrite(output);
Slog.e(TAG, "Failed to write dynamic usage for secondary code files.", e);
}
}
@VisibleForTesting
void write(OutputStream output) throws IOException {
// Make a deep copy to avoid holding the lock while writing to disk.
Map<String, PackageDynamicCode> copiedMap;
synchronized (mLock) {
copiedMap = new HashMap<>(mPackageMap.size());
for (Entry<String, PackageDynamicCode> entry : mPackageMap.entrySet()) {
PackageDynamicCode copiedValue = new PackageDynamicCode(entry.getValue());
copiedMap.put(entry.getKey(), copiedValue);
}
}
write(output, copiedMap);
}
/**
* Write the dynamic code loading data as a text file to {@code output}. The file format begins
* with a line indicating the file type and version - {@link #FILE_VERSION_HEADER}.
* <p>There is then one section for each owning package, introduced by a line beginning "P:".
* This is followed by a line for each file owned by the package this is dynamically loaded,
* containing the file type, user ID, loading package names and full path (with newlines and
* backslashes escaped - see {@link #escape}).
* <p>For example:
* <pre>{@code
* DCL1
* P:first.owning.package
* D:0:loading.package_1,loading.package_2:/path/to/file
* D:10:loading.package_1:/another/file
* P:second.owning.package
* D:0:loading.package:/third/file
* }</pre>
*/
private static void write(OutputStream output, Map<String, PackageDynamicCode> packageMap)
throws IOException {
PrintWriter writer = new FastPrintWriter(output);
writer.println(FILE_VERSION_HEADER);
for (Entry<String, PackageDynamicCode> packageEntry : packageMap.entrySet()) {
writer.print(PACKAGE_PREFIX);
writer.println(packageEntry.getKey());
Map<String, DynamicCodeFile> mFileUsageMap = packageEntry.getValue().mFileUsageMap;
for (Entry<String, DynamicCodeFile> fileEntry : mFileUsageMap.entrySet()) {
String path = fileEntry.getKey();
DynamicCodeFile dynamicCodeFile = fileEntry.getValue();
writer.print(dynamicCodeFile.mFileType);
writer.print(FIELD_SEPARATOR);
writer.print(dynamicCodeFile.mUserId);
writer.print(FIELD_SEPARATOR);
String prefix = "";
for (String packageName : dynamicCodeFile.mLoadingPackages) {
writer.print(prefix);
writer.print(packageName);
prefix = PACKAGE_SEPARATOR;
}
writer.print(FIELD_SEPARATOR);
writer.println(escape(path));
}
}
writer.flush();
if (writer.checkError()) {
throw new IOException("Writer failed");
}
}
/**
* Read data from the persistent file. Replaces existing data completely if successful.
*/
void read() {
super.read(null);
}
@Override
protected final void readInternal(Void data) {
AtomicFile file = getFile();
FileInputStream stream = null;
try {
stream = file.openRead();
read(stream);
} catch (FileNotFoundException expected) {
// The file may not be there. E.g. When we first take the OTA with this feature.
} catch (IOException e) {
Slog.w(TAG, "Failed to parse dynamic usage for secondary code files.", e);
} finally {
IoUtils.closeQuietly(stream);
}
}
@VisibleForTesting
void read(InputStream stream) throws IOException {
Map<String, PackageDynamicCode> newPackageMap = new HashMap<>();
read(stream, newPackageMap);
synchronized (mLock) {
mPackageMap = newPackageMap;
}
}
private static void read(InputStream stream, Map<String, PackageDynamicCode> packageMap)
throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String versionLine = reader.readLine();
if (!FILE_VERSION_HEADER.equals(versionLine)) {
throw new IOException("Incorrect version line: " + versionLine);
}
String line = reader.readLine();
if (line != null && !line.startsWith(PACKAGE_PREFIX)) {
throw new IOException("Malformed line: " + line);
}
while (line != null) {
String packageName = line.substring(PACKAGE_PREFIX.length());
PackageDynamicCode packageInfo = new PackageDynamicCode();
while (true) {
line = reader.readLine();
if (line == null || line.startsWith(PACKAGE_PREFIX)) {
break;
}
readFileInfo(line, packageInfo);
}
if (!packageInfo.mFileUsageMap.isEmpty()) {
packageMap.put(packageName, packageInfo);
}
}
}
private static void readFileInfo(String line, PackageDynamicCode output) throws IOException {
try {
Matcher matcher = PACKAGE_LINE_PATTERN.matcher(line);
if (!matcher.matches()) {
throw new IOException("Malformed line: " + line);
}
char type = matcher.group(1).charAt(0);
int user = Integer.parseInt(matcher.group(2));
String[] packages = matcher.group(3).split(PACKAGE_SEPARATOR);
String path = unescape(matcher.group(4));
if (packages.length == 0) {
throw new IOException("Malformed line: " + line);
}
if (!isValidFileType(type)) {
throw new IOException("Unknown file type: " + line);
}
output.mFileUsageMap.put(path, new DynamicCodeFile(type, user, packages));
} catch (RuntimeException e) {
// Just in case we get NumberFormatException, or various
// impossible out of bounds errors happen.
throw new IOException("Unable to parse line: " + line, e);
}
}
/**
* Escape any newline and backslash characters in path. A newline in a path is legal if unusual,
* and it would break our line-based file parsing.
*/
@VisibleForTesting
static String escape(String path) {
if (path.indexOf('\\') == -1 && path.indexOf('\n') == -1 && path.indexOf('\r') == -1) {
return path;
}
StringBuilder result = new StringBuilder(path.length() + 10);
for (int i = 0; i < path.length(); i++) {
// Surrogates will never match the characters we care about, so it's ok to use chars
// not code points here.
char c = path.charAt(i);
switch (c) {
case '\\':
result.append("\\\\");
break;
case '\n':
result.append("\\n");
break;
case '\r':
result.append("\\r");
break;
default:
result.append(c);
break;
}
}
return result.toString();
}
/**
* Reverse the effect of {@link #escape}.
* @throws IOException if the input string is malformed
*/
@VisibleForTesting
static String unescape(String escaped) throws IOException {
// As we move through the input string, start is the position of the first character
// after the previous escape sequence and finish is the position of the following backslash.
int start = 0;
int finish = escaped.indexOf('\\');
if (finish == -1) {
return escaped;
}
StringBuilder result = new StringBuilder(escaped.length());
while (true) {
if (finish >= escaped.length() - 1) {
// Backslash mustn't be the last character
throw new IOException("Unexpected \\ in: " + escaped);
}
result.append(escaped, start, finish);
switch (escaped.charAt(finish + 1)) {
case '\\':
result.append('\\');
break;
case 'r':
result.append('\r');
break;
case 'n':
result.append('\n');
break;
default:
throw new IOException("Bad escape in: " + escaped);
}
start = finish + 2;
finish = escaped.indexOf('\\', start);
if (finish == -1) {
result.append(escaped, start, escaped.length());
break;
}
}
return result.toString();
}
/**
* Represents the dynamic code usage of a single package.
*/
static class PackageDynamicCode {
/**
* Map from secondary code file path to information about which packages dynamically load
* that file.
*/
final Map<String, DynamicCodeFile> mFileUsageMap;
private PackageDynamicCode() {
mFileUsageMap = new HashMap<>();
}
private PackageDynamicCode(PackageDynamicCode original) {
mFileUsageMap = new HashMap<>(original.mFileUsageMap.size());
for (Entry<String, DynamicCodeFile> entry : original.mFileUsageMap.entrySet()) {
DynamicCodeFile newValue = new DynamicCodeFile(entry.getValue());
mFileUsageMap.put(entry.getKey(), newValue);
}
}
private boolean add(String path, char fileType, int userId, String loadingPackage) {
DynamicCodeFile fileInfo = mFileUsageMap.get(path);
if (fileInfo == null) {
if (mFileUsageMap.size() >= MAX_FILES_PER_OWNER) {
return false;
}
fileInfo = new DynamicCodeFile(fileType, userId, loadingPackage);
mFileUsageMap.put(path, fileInfo);
return true;
} else {
if (fileInfo.mUserId != userId) {
// This should be impossible: private app files are always user-specific and
// can't be accessed from different users.
throw new IllegalArgumentException("Cannot change userId for '" + path
+ "' from " + fileInfo.mUserId + " to " + userId);
}
// Changing file type (i.e. loading the same file in different ways is possible if
// unlikely. We allow it but ignore it.
return fileInfo.mLoadingPackages.add(loadingPackage);
}
}
private boolean removeUser(int userId) {
boolean updated = false;
Iterator<DynamicCodeFile> it = mFileUsageMap.values().iterator();
while (it.hasNext()) {
DynamicCodeFile fileInfo = it.next();
if (fileInfo.mUserId == userId) {
it.remove();
updated = true;
}
}
return updated;
}
private boolean removeFile(String filePath, int userId) {
DynamicCodeFile fileInfo = mFileUsageMap.get(filePath);
if (fileInfo == null || fileInfo.mUserId != userId) {
return false;
} else {
mFileUsageMap.remove(filePath);
return true;
}
}
private void syncData(Map<String, Set<Integer>> packageToUsersMap,
Set<Integer> owningPackageUsers) {
Iterator<DynamicCodeFile> fileIt = mFileUsageMap.values().iterator();
while (fileIt.hasNext()) {
DynamicCodeFile fileInfo = fileIt.next();
int fileUserId = fileInfo.mUserId;
if (!owningPackageUsers.contains(fileUserId)) {
fileIt.remove();
} else {
// Also remove information about any loading packages that are no longer
// installed for this user.
Iterator<String> loaderIt = fileInfo.mLoadingPackages.iterator();
while (loaderIt.hasNext()) {
String loader = loaderIt.next();
Set<Integer> loadingPackageUsers = packageToUsersMap.get(loader);
if (loadingPackageUsers == null
|| !loadingPackageUsers.contains(fileUserId)) {
loaderIt.remove();
}
}
if (fileInfo.mLoadingPackages.isEmpty()) {
fileIt.remove();
}
}
}
}
}
/**
* Represents a single dynamic code file loaded by one or more packages. Note that it is
* possible for one app to dynamically load code from a different app's home dir, if the
* owning app:
* <ul>
* <li>Targets API 27 or lower and has shared its home dir.
* <li>Is a system app.
* <li>Has a shared UID with the loading app.
* </ul>
*/
static class DynamicCodeFile {
final char mFileType;
final int mUserId;
final Set<String> mLoadingPackages;
private DynamicCodeFile(char type, int user, String... packages) {
mFileType = type;
mUserId = user;
mLoadingPackages = new HashSet<>(Arrays.asList(packages));
}
private DynamicCodeFile(DynamicCodeFile original) {
mFileType = original.mFileType;
mUserId = original.mUserId;
mLoadingPackages = new HashSet<>(original.mLoadingPackages);
}
}
}