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