1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm.dex; 18 19 import android.util.AtomicFile; 20 import android.util.Slog; 21 22 import com.android.internal.annotations.GuardedBy; 23 import com.android.internal.annotations.VisibleForTesting; 24 import com.android.internal.util.FastPrintWriter; 25 import com.android.server.pm.AbstractStatsBase; 26 27 import libcore.io.IoUtils; 28 29 import java.io.BufferedReader; 30 import java.io.FileInputStream; 31 import java.io.FileNotFoundException; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.InputStreamReader; 36 import java.io.OutputStream; 37 import java.io.PrintWriter; 38 import java.util.Arrays; 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.Iterator; 42 import java.util.Map; 43 import java.util.Map.Entry; 44 import java.util.Set; 45 import java.util.regex.Matcher; 46 import java.util.regex.Pattern; 47 48 /** 49 * Stats file which stores information about secondary code files that are dynamically loaded. 50 */ 51 class PackageDynamicCodeLoading extends AbstractStatsBase<Void> { 52 // Type code to indicate a secondary file containing DEX code. (The char value is how it 53 // is represented in the text file format.) 54 static final int FILE_TYPE_DEX = 'D'; 55 56 // Type code to indicate a secondary file containing native code. 57 static final int FILE_TYPE_NATIVE = 'N'; 58 59 private static final String TAG = "PackageDynamicCodeLoading"; 60 61 private static final String FILE_VERSION_HEADER = "DCL1"; 62 private static final String PACKAGE_PREFIX = "P:"; 63 64 private static final char FIELD_SEPARATOR = ':'; 65 private static final String PACKAGE_SEPARATOR = ","; 66 67 /** 68 * Limit on how many files we store for a single owner, to avoid one app causing 69 * unbounded memory consumption. 70 */ 71 @VisibleForTesting 72 static final int MAX_FILES_PER_OWNER = 100; 73 74 /** 75 * Regular expression to match the expected format of an input line describing one file. 76 * <p>Example: {@code D:10:package.name1,package.name2:/escaped/path} 77 * <p>The capturing groups are the file type, user ID, loading packages and escaped file path 78 * (in that order). 79 * <p>See {@link #write(OutputStream, Map)} below for more details of the format. 80 */ 81 private static final Pattern PACKAGE_LINE_PATTERN = 82 Pattern.compile("([A-Z]):([0-9]+):([^:]*):(.*)"); 83 84 private final Object mLock = new Object(); 85 86 // Map from package name to data about loading of dynamic code files owned by that package. 87 // (Apps may load code files owned by other packages, subject to various access 88 // constraints.) 89 // Any PackageDynamicCode in this map will be non-empty. 90 @GuardedBy("mLock") 91 private Map<String, PackageDynamicCode> mPackageMap = new HashMap<>(); 92 PackageDynamicCodeLoading()93 PackageDynamicCodeLoading() { 94 super("package-dcl.list", "PackageDynamicCodeLoading_DiskWriter", false); 95 } 96 97 /** 98 * Record dynamic code loading from a file. 99 * 100 * Note this is called when an app loads dex files and as such it should return 101 * as fast as possible. 102 * 103 * @param owningPackageName the package owning the file path 104 * @param filePath the path of the dex files being loaded 105 * @param fileType the type of code loading 106 * @param ownerUserId the user id which runs the code loading the file 107 * @param loadingPackageName the package performing the load 108 * @return whether new information has been recorded 109 * @throws IllegalArgumentException if clearly invalid information is detected 110 */ record(String owningPackageName, String filePath, int fileType, int ownerUserId, String loadingPackageName)111 boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId, 112 String loadingPackageName) { 113 if (!isValidFileType(fileType)) { 114 throw new IllegalArgumentException("Bad file type: " + fileType); 115 } 116 synchronized (mLock) { 117 PackageDynamicCode packageInfo = mPackageMap.get(owningPackageName); 118 if (packageInfo == null) { 119 packageInfo = new PackageDynamicCode(); 120 mPackageMap.put(owningPackageName, packageInfo); 121 } 122 return packageInfo.add(filePath, (char) fileType, ownerUserId, loadingPackageName); 123 } 124 } 125 isValidFileType(int fileType)126 private static boolean isValidFileType(int fileType) { 127 return fileType == FILE_TYPE_DEX || fileType == FILE_TYPE_NATIVE; 128 } 129 130 /** 131 * Return all packages that contain records of secondary dex files. (Note that data updates 132 * asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed 133 * one of these package names.) 134 */ getAllPackagesWithDynamicCodeLoading()135 Set<String> getAllPackagesWithDynamicCodeLoading() { 136 synchronized (mLock) { 137 return new HashSet<>(mPackageMap.keySet()); 138 } 139 } 140 141 /** 142 * Return information about the dynamic code file usage of the specified package, 143 * or null if there is currently no usage information. The object returned is a copy of the 144 * live information that is not updated. 145 */ getPackageDynamicCodeInfo(String packageName)146 PackageDynamicCode getPackageDynamicCodeInfo(String packageName) { 147 synchronized (mLock) { 148 PackageDynamicCode info = mPackageMap.get(packageName); 149 return info == null ? null : new PackageDynamicCode(info); 150 } 151 } 152 153 /** 154 * Remove all information about all packages. 155 */ clear()156 void clear() { 157 synchronized (mLock) { 158 mPackageMap.clear(); 159 } 160 } 161 162 /** 163 * Remove the data associated with package {@code packageName}. Affects all users. 164 * @return true if the package usage was found and removed successfully 165 */ removePackage(String packageName)166 boolean removePackage(String packageName) { 167 synchronized (mLock) { 168 return mPackageMap.remove(packageName) != null; 169 } 170 } 171 172 /** 173 * Remove all the records about package {@code packageName} belonging to user {@code userId}. 174 * @return whether any data was actually removed 175 */ removeUserPackage(String packageName, int userId)176 boolean removeUserPackage(String packageName, int userId) { 177 synchronized (mLock) { 178 PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName); 179 if (packageDynamicCode == null) { 180 return false; 181 } 182 if (packageDynamicCode.removeUser(userId)) { 183 if (packageDynamicCode.mFileUsageMap.isEmpty()) { 184 mPackageMap.remove(packageName); 185 } 186 return true; 187 } else { 188 return false; 189 } 190 } 191 } 192 193 /** 194 * Remove the specified dynamic code file record belonging to the package {@code packageName} 195 * and user {@code userId}. 196 * @return whether data was actually removed 197 */ removeFile(String packageName, String filePath, int userId)198 boolean removeFile(String packageName, String filePath, int userId) { 199 synchronized (mLock) { 200 PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName); 201 if (packageDynamicCode == null) { 202 return false; 203 } 204 if (packageDynamicCode.removeFile(filePath, userId)) { 205 if (packageDynamicCode.mFileUsageMap.isEmpty()) { 206 mPackageMap.remove(packageName); 207 } 208 return true; 209 } else { 210 return false; 211 } 212 } 213 } 214 215 /** 216 * Syncs data with the set of installed packages. Data about packages that are no longer 217 * installed is removed. 218 * @param packageToUsersMap a map from all existing package names to the users who have the 219 * package installed 220 */ syncData(Map<String, Set<Integer>> packageToUsersMap)221 void syncData(Map<String, Set<Integer>> packageToUsersMap) { 222 synchronized (mLock) { 223 Iterator<Entry<String, PackageDynamicCode>> it = mPackageMap.entrySet().iterator(); 224 while (it.hasNext()) { 225 Entry<String, PackageDynamicCode> entry = it.next(); 226 Set<Integer> packageUsers = packageToUsersMap.get(entry.getKey()); 227 if (packageUsers == null) { 228 it.remove(); 229 } else { 230 PackageDynamicCode packageDynamicCode = entry.getValue(); 231 packageDynamicCode.syncData(packageToUsersMap, packageUsers); 232 if (packageDynamicCode.mFileUsageMap.isEmpty()) { 233 it.remove(); 234 } 235 } 236 } 237 } 238 } 239 240 /** 241 * Request that data be written to persistent file at the next time allowed by write-limiting. 242 */ maybeWriteAsync()243 void maybeWriteAsync() { 244 super.maybeWriteAsync(null); 245 } 246 247 /** 248 * Writes data to persistent file immediately. 249 */ writeNow()250 void writeNow() { 251 super.writeNow(null); 252 } 253 254 @Override writeInternal(Void data)255 protected final void writeInternal(Void data) { 256 AtomicFile file = getFile(); 257 FileOutputStream output = null; 258 try { 259 output = file.startWrite(); 260 write(output); 261 file.finishWrite(output); 262 } catch (IOException e) { 263 file.failWrite(output); 264 Slog.e(TAG, "Failed to write dynamic usage for secondary code files.", e); 265 } 266 } 267 268 @VisibleForTesting write(OutputStream output)269 void write(OutputStream output) throws IOException { 270 // Make a deep copy to avoid holding the lock while writing to disk. 271 Map<String, PackageDynamicCode> copiedMap; 272 synchronized (mLock) { 273 copiedMap = new HashMap<>(mPackageMap.size()); 274 for (Entry<String, PackageDynamicCode> entry : mPackageMap.entrySet()) { 275 PackageDynamicCode copiedValue = new PackageDynamicCode(entry.getValue()); 276 copiedMap.put(entry.getKey(), copiedValue); 277 } 278 } 279 280 write(output, copiedMap); 281 } 282 283 /** 284 * Write the dynamic code loading data as a text file to {@code output}. The file format begins 285 * with a line indicating the file type and version - {@link #FILE_VERSION_HEADER}. 286 * <p>There is then one section for each owning package, introduced by a line beginning "P:". 287 * This is followed by a line for each file owned by the package this is dynamically loaded, 288 * containing the file type, user ID, loading package names and full path (with newlines and 289 * backslashes escaped - see {@link #escape}). 290 * <p>For example: 291 * <pre>{@code 292 * DCL1 293 * P:first.owning.package 294 * D:0:loading.package_1,loading.package_2:/path/to/file 295 * D:10:loading.package_1:/another/file 296 * P:second.owning.package 297 * D:0:loading.package:/third/file 298 * }</pre> 299 */ write(OutputStream output, Map<String, PackageDynamicCode> packageMap)300 private static void write(OutputStream output, Map<String, PackageDynamicCode> packageMap) 301 throws IOException { 302 PrintWriter writer = new FastPrintWriter(output); 303 304 writer.println(FILE_VERSION_HEADER); 305 for (Entry<String, PackageDynamicCode> packageEntry : packageMap.entrySet()) { 306 writer.print(PACKAGE_PREFIX); 307 writer.println(packageEntry.getKey()); 308 309 Map<String, DynamicCodeFile> mFileUsageMap = packageEntry.getValue().mFileUsageMap; 310 for (Entry<String, DynamicCodeFile> fileEntry : mFileUsageMap.entrySet()) { 311 String path = fileEntry.getKey(); 312 DynamicCodeFile dynamicCodeFile = fileEntry.getValue(); 313 314 writer.print(dynamicCodeFile.mFileType); 315 writer.print(FIELD_SEPARATOR); 316 writer.print(dynamicCodeFile.mUserId); 317 writer.print(FIELD_SEPARATOR); 318 319 String prefix = ""; 320 for (String packageName : dynamicCodeFile.mLoadingPackages) { 321 writer.print(prefix); 322 writer.print(packageName); 323 prefix = PACKAGE_SEPARATOR; 324 } 325 326 writer.print(FIELD_SEPARATOR); 327 writer.println(escape(path)); 328 } 329 } 330 331 writer.flush(); 332 if (writer.checkError()) { 333 throw new IOException("Writer failed"); 334 } 335 } 336 337 /** 338 * Read data from the persistent file. Replaces existing data completely if successful. 339 */ read()340 void read() { 341 super.read(null); 342 } 343 344 @Override readInternal(Void data)345 protected final void readInternal(Void data) { 346 AtomicFile file = getFile(); 347 348 FileInputStream stream = null; 349 try { 350 stream = file.openRead(); 351 read(stream); 352 } catch (FileNotFoundException expected) { 353 // The file may not be there. E.g. When we first take the OTA with this feature. 354 } catch (IOException e) { 355 Slog.w(TAG, "Failed to parse dynamic usage for secondary code files.", e); 356 } finally { 357 IoUtils.closeQuietly(stream); 358 } 359 } 360 361 @VisibleForTesting read(InputStream stream)362 void read(InputStream stream) throws IOException { 363 Map<String, PackageDynamicCode> newPackageMap = new HashMap<>(); 364 read(stream, newPackageMap); 365 synchronized (mLock) { 366 mPackageMap = newPackageMap; 367 } 368 } 369 read(InputStream stream, Map<String, PackageDynamicCode> packageMap)370 private static void read(InputStream stream, Map<String, PackageDynamicCode> packageMap) 371 throws IOException { 372 BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); 373 374 String versionLine = reader.readLine(); 375 if (!FILE_VERSION_HEADER.equals(versionLine)) { 376 throw new IOException("Incorrect version line: " + versionLine); 377 } 378 379 String line = reader.readLine(); 380 if (line != null && !line.startsWith(PACKAGE_PREFIX)) { 381 throw new IOException("Malformed line: " + line); 382 } 383 384 while (line != null) { 385 String packageName = line.substring(PACKAGE_PREFIX.length()); 386 387 PackageDynamicCode packageInfo = new PackageDynamicCode(); 388 while (true) { 389 line = reader.readLine(); 390 if (line == null || line.startsWith(PACKAGE_PREFIX)) { 391 break; 392 } 393 readFileInfo(line, packageInfo); 394 } 395 396 if (!packageInfo.mFileUsageMap.isEmpty()) { 397 packageMap.put(packageName, packageInfo); 398 } 399 } 400 } 401 readFileInfo(String line, PackageDynamicCode output)402 private static void readFileInfo(String line, PackageDynamicCode output) throws IOException { 403 try { 404 Matcher matcher = PACKAGE_LINE_PATTERN.matcher(line); 405 if (!matcher.matches()) { 406 throw new IOException("Malformed line: " + line); 407 } 408 409 char type = matcher.group(1).charAt(0); 410 int user = Integer.parseInt(matcher.group(2)); 411 String[] packages = matcher.group(3).split(PACKAGE_SEPARATOR); 412 String path = unescape(matcher.group(4)); 413 414 if (packages.length == 0) { 415 throw new IOException("Malformed line: " + line); 416 } 417 if (!isValidFileType(type)) { 418 throw new IOException("Unknown file type: " + line); 419 } 420 421 output.mFileUsageMap.put(path, new DynamicCodeFile(type, user, packages)); 422 } catch (RuntimeException e) { 423 // Just in case we get NumberFormatException, or various 424 // impossible out of bounds errors happen. 425 throw new IOException("Unable to parse line: " + line, e); 426 } 427 } 428 429 /** 430 * Escape any newline and backslash characters in path. A newline in a path is legal if unusual, 431 * and it would break our line-based file parsing. 432 */ 433 @VisibleForTesting escape(String path)434 static String escape(String path) { 435 if (path.indexOf('\\') == -1 && path.indexOf('\n') == -1 && path.indexOf('\r') == -1) { 436 return path; 437 } 438 439 StringBuilder result = new StringBuilder(path.length() + 10); 440 for (int i = 0; i < path.length(); i++) { 441 // Surrogates will never match the characters we care about, so it's ok to use chars 442 // not code points here. 443 char c = path.charAt(i); 444 switch (c) { 445 case '\\': 446 result.append("\\\\"); 447 break; 448 case '\n': 449 result.append("\\n"); 450 break; 451 case '\r': 452 result.append("\\r"); 453 break; 454 default: 455 result.append(c); 456 break; 457 } 458 } 459 return result.toString(); 460 } 461 462 /** 463 * Reverse the effect of {@link #escape}. 464 * @throws IOException if the input string is malformed 465 */ 466 @VisibleForTesting unescape(String escaped)467 static String unescape(String escaped) throws IOException { 468 // As we move through the input string, start is the position of the first character 469 // after the previous escape sequence and finish is the position of the following backslash. 470 int start = 0; 471 int finish = escaped.indexOf('\\'); 472 if (finish == -1) { 473 return escaped; 474 } 475 476 StringBuilder result = new StringBuilder(escaped.length()); 477 while (true) { 478 if (finish >= escaped.length() - 1) { 479 // Backslash mustn't be the last character 480 throw new IOException("Unexpected \\ in: " + escaped); 481 } 482 result.append(escaped, start, finish); 483 switch (escaped.charAt(finish + 1)) { 484 case '\\': 485 result.append('\\'); 486 break; 487 case 'r': 488 result.append('\r'); 489 break; 490 case 'n': 491 result.append('\n'); 492 break; 493 default: 494 throw new IOException("Bad escape in: " + escaped); 495 } 496 497 start = finish + 2; 498 finish = escaped.indexOf('\\', start); 499 if (finish == -1) { 500 result.append(escaped, start, escaped.length()); 501 break; 502 } 503 } 504 return result.toString(); 505 } 506 507 /** 508 * Represents the dynamic code usage of a single package. 509 */ 510 static class PackageDynamicCode { 511 /** 512 * Map from secondary code file path to information about which packages dynamically load 513 * that file. 514 */ 515 final Map<String, DynamicCodeFile> mFileUsageMap; 516 PackageDynamicCode()517 private PackageDynamicCode() { 518 mFileUsageMap = new HashMap<>(); 519 } 520 PackageDynamicCode(PackageDynamicCode original)521 private PackageDynamicCode(PackageDynamicCode original) { 522 mFileUsageMap = new HashMap<>(original.mFileUsageMap.size()); 523 for (Entry<String, DynamicCodeFile> entry : original.mFileUsageMap.entrySet()) { 524 DynamicCodeFile newValue = new DynamicCodeFile(entry.getValue()); 525 mFileUsageMap.put(entry.getKey(), newValue); 526 } 527 } 528 add(String path, char fileType, int userId, String loadingPackage)529 private boolean add(String path, char fileType, int userId, String loadingPackage) { 530 DynamicCodeFile fileInfo = mFileUsageMap.get(path); 531 if (fileInfo == null) { 532 if (mFileUsageMap.size() >= MAX_FILES_PER_OWNER) { 533 return false; 534 } 535 fileInfo = new DynamicCodeFile(fileType, userId, loadingPackage); 536 mFileUsageMap.put(path, fileInfo); 537 return true; 538 } else { 539 if (fileInfo.mUserId != userId) { 540 // This should be impossible: private app files are always user-specific and 541 // can't be accessed from different users. 542 throw new IllegalArgumentException("Cannot change userId for '" + path 543 + "' from " + fileInfo.mUserId + " to " + userId); 544 } 545 // Changing file type (i.e. loading the same file in different ways is possible if 546 // unlikely. We allow it but ignore it. 547 return fileInfo.mLoadingPackages.add(loadingPackage); 548 } 549 } 550 removeUser(int userId)551 private boolean removeUser(int userId) { 552 boolean updated = false; 553 Iterator<DynamicCodeFile> it = mFileUsageMap.values().iterator(); 554 while (it.hasNext()) { 555 DynamicCodeFile fileInfo = it.next(); 556 if (fileInfo.mUserId == userId) { 557 it.remove(); 558 updated = true; 559 } 560 } 561 return updated; 562 } 563 removeFile(String filePath, int userId)564 private boolean removeFile(String filePath, int userId) { 565 DynamicCodeFile fileInfo = mFileUsageMap.get(filePath); 566 if (fileInfo == null || fileInfo.mUserId != userId) { 567 return false; 568 } else { 569 mFileUsageMap.remove(filePath); 570 return true; 571 } 572 } 573 syncData(Map<String, Set<Integer>> packageToUsersMap, Set<Integer> owningPackageUsers)574 private void syncData(Map<String, Set<Integer>> packageToUsersMap, 575 Set<Integer> owningPackageUsers) { 576 Iterator<DynamicCodeFile> fileIt = mFileUsageMap.values().iterator(); 577 while (fileIt.hasNext()) { 578 DynamicCodeFile fileInfo = fileIt.next(); 579 int fileUserId = fileInfo.mUserId; 580 if (!owningPackageUsers.contains(fileUserId)) { 581 fileIt.remove(); 582 } else { 583 // Also remove information about any loading packages that are no longer 584 // installed for this user. 585 Iterator<String> loaderIt = fileInfo.mLoadingPackages.iterator(); 586 while (loaderIt.hasNext()) { 587 String loader = loaderIt.next(); 588 Set<Integer> loadingPackageUsers = packageToUsersMap.get(loader); 589 if (loadingPackageUsers == null 590 || !loadingPackageUsers.contains(fileUserId)) { 591 loaderIt.remove(); 592 } 593 } 594 if (fileInfo.mLoadingPackages.isEmpty()) { 595 fileIt.remove(); 596 } 597 } 598 } 599 } 600 } 601 602 /** 603 * Represents a single dynamic code file loaded by one or more packages. Note that it is 604 * possible for one app to dynamically load code from a different app's home dir, if the 605 * owning app: 606 * <ul> 607 * <li>Targets API 27 or lower and has shared its home dir. 608 * <li>Is a system app. 609 * <li>Has a shared UID with the loading app. 610 * </ul> 611 */ 612 static class DynamicCodeFile { 613 final char mFileType; 614 final int mUserId; 615 final Set<String> mLoadingPackages; 616 DynamicCodeFile(char type, int user, String... packages)617 private DynamicCodeFile(char type, int user, String... packages) { 618 mFileType = type; 619 mUserId = user; 620 mLoadingPackages = new HashSet<>(Arrays.asList(packages)); 621 } 622 DynamicCodeFile(DynamicCodeFile original)623 private DynamicCodeFile(DynamicCodeFile original) { 624 mFileType = original.mFileType; 625 mUserId = original.mUserId; 626 mLoadingPackages = new HashSet<>(original.mLoadingPackages); 627 } 628 } 629 } 630