1 /*
2  * Copyright (C) 2017 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 static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_DEX;
20 import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_NATIVE;
21 
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.IPackageManager;
24 import android.content.pm.PackageInfo;
25 import android.os.FileUtils;
26 import android.os.RemoteException;
27 import android.os.UserHandle;
28 import android.os.storage.StorageManager;
29 import android.util.EventLog;
30 import android.util.PackageUtils;
31 import android.util.Slog;
32 import android.util.SparseArray;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.server.pm.Installer;
36 import com.android.server.pm.Installer.InstallerException;
37 import com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
38 import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
39 
40 import libcore.util.HexEncoding;
41 
42 import java.io.File;
43 import java.io.IOException;
44 import java.util.Map;
45 import java.util.Set;
46 
47 /**
48  * This class is responsible for logging data about secondary dex files and native code executed
49  * from an app's private directory. The data logged includes hashes of the name and content of each
50  * file.
51  */
52 public class DynamicCodeLogger {
53     private static final String TAG = "DynamicCodeLogger";
54 
55     // Event log tag & subtags used for SafetyNet logging of dynamic code loading (DCL) -
56     // see b/63927552.
57     private static final int SNET_TAG = 0x534e4554;
58     private static final String DCL_DEX_SUBTAG = "dcl";
59     private static final String DCL_NATIVE_SUBTAG = "dcln";
60 
61     private final IPackageManager mPackageManager;
62     private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
63     private final Installer mInstaller;
64 
DynamicCodeLogger(IPackageManager pms, Installer installer)65     DynamicCodeLogger(IPackageManager pms, Installer installer) {
66         this(pms, installer, new PackageDynamicCodeLoading());
67     }
68 
69     @VisibleForTesting
DynamicCodeLogger(IPackageManager pms, Installer installer, PackageDynamicCodeLoading packageDynamicCodeLoading)70     DynamicCodeLogger(IPackageManager pms, Installer installer,
71             PackageDynamicCodeLoading packageDynamicCodeLoading) {
72         mPackageManager = pms;
73         mPackageDynamicCodeLoading = packageDynamicCodeLoading;
74         mInstaller = installer;
75     }
76 
getAllPackagesWithDynamicCodeLoading()77     public Set<String> getAllPackagesWithDynamicCodeLoading() {
78         return mPackageDynamicCodeLoading.getAllPackagesWithDynamicCodeLoading();
79     }
80 
81     /**
82      * Write information about code dynamically loaded by {@code packageName} to the event log.
83      */
logDynamicCodeLoading(String packageName)84     public void logDynamicCodeLoading(String packageName) {
85         PackageDynamicCode info = getPackageDynamicCodeInfo(packageName);
86         if (info == null) {
87             return;
88         }
89 
90         SparseArray<ApplicationInfo> appInfoByUser = new SparseArray<>();
91         boolean needWrite = false;
92 
93         for (Map.Entry<String, DynamicCodeFile> fileEntry : info.mFileUsageMap.entrySet()) {
94             String filePath = fileEntry.getKey();
95             DynamicCodeFile fileInfo = fileEntry.getValue();
96             int userId = fileInfo.mUserId;
97 
98             int index = appInfoByUser.indexOfKey(userId);
99             ApplicationInfo appInfo;
100             if (index >= 0) {
101                 appInfo = appInfoByUser.get(userId);
102             } else {
103                 appInfo = null;
104 
105                 try {
106                     PackageInfo ownerInfo =
107                             mPackageManager.getPackageInfo(packageName, /*flags*/ 0, userId);
108                     appInfo = ownerInfo == null ? null : ownerInfo.applicationInfo;
109                 } catch (RemoteException ignored) {
110                     // Can't happen, we're local.
111                 }
112                 appInfoByUser.put(userId, appInfo);
113                 if (appInfo == null) {
114                     Slog.d(TAG, "Could not find package " + packageName + " for user " + userId);
115                     // Package has probably been uninstalled for user.
116                     needWrite |= mPackageDynamicCodeLoading.removeUserPackage(packageName, userId);
117                 }
118             }
119 
120             if (appInfo == null) {
121                 continue;
122             }
123 
124             int storageFlags;
125 
126             if (fileIsUnder(filePath, appInfo.credentialProtectedDataDir)) {
127                 storageFlags = StorageManager.FLAG_STORAGE_CE;
128             } else if (fileIsUnder(filePath, appInfo.deviceProtectedDataDir)) {
129                 storageFlags = StorageManager.FLAG_STORAGE_DE;
130             } else {
131                 Slog.e(TAG, "Could not infer CE/DE storage for path " + filePath);
132                 needWrite |= mPackageDynamicCodeLoading.removeFile(packageName, filePath, userId);
133                 continue;
134             }
135 
136             byte[] hash = null;
137             try {
138                 // Note that we do not take the install lock here. Hashing should never interfere
139                 // with app update/compilation/removal. We may get anomalous results if a file
140                 // changes while we hash it, but that can happen anyway and is harmless for our
141                 // purposes.
142                 hash = mInstaller.hashSecondaryDexFile(filePath, packageName, appInfo.uid,
143                         appInfo.volumeUuid, storageFlags);
144             } catch (InstallerException e) {
145                 Slog.e(TAG, "Got InstallerException when hashing file " + filePath
146                         + ": " + e.getMessage());
147             }
148 
149             String subtag = fileInfo.mFileType == FILE_TYPE_DEX
150                     ? DCL_DEX_SUBTAG
151                     : DCL_NATIVE_SUBTAG;
152             String fileName = new File(filePath).getName();
153             String message = PackageUtils.computeSha256Digest(fileName.getBytes());
154 
155             // Valid SHA256 will be 256 bits, 32 bytes.
156             if (hash != null && hash.length == 32) {
157                 message = message + ' ' + HexEncoding.encodeToString(hash);
158             } else {
159                 Slog.d(TAG, "Got no hash for " + filePath);
160                 // File has probably been deleted.
161                 needWrite |= mPackageDynamicCodeLoading.removeFile(packageName, filePath, userId);
162             }
163 
164             for (String loadingPackageName : fileInfo.mLoadingPackages) {
165                 int loadingUid = -1;
166                 if (loadingPackageName.equals(packageName)) {
167                     loadingUid = appInfo.uid;
168                 } else {
169                     try {
170                         loadingUid = mPackageManager.getPackageUid(loadingPackageName, /*flags*/ 0,
171                                 userId);
172                     } catch (RemoteException ignored) {
173                         // Can't happen, we're local.
174                     }
175                 }
176 
177                 if (loadingUid != -1) {
178                     writeDclEvent(subtag, loadingUid, message);
179                 }
180             }
181         }
182 
183         if (needWrite) {
184             mPackageDynamicCodeLoading.maybeWriteAsync();
185         }
186     }
187 
fileIsUnder(String filePath, String directoryPath)188     private boolean fileIsUnder(String filePath, String directoryPath) {
189         if (directoryPath == null) {
190             return false;
191         }
192 
193         try {
194             return FileUtils.contains(new File(directoryPath).getCanonicalPath(),
195                     new File(filePath).getCanonicalPath());
196         } catch (IOException e) {
197             return false;
198         }
199     }
200 
201     @VisibleForTesting
getPackageDynamicCodeInfo(String packageName)202     PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
203         return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
204     }
205 
206     @VisibleForTesting
writeDclEvent(String subtag, int uid, String message)207     void writeDclEvent(String subtag, int uid, String message) {
208         EventLog.writeEvent(SNET_TAG, subtag, uid, message);
209     }
210 
recordDex(int loaderUserId, String dexPath, String owningPackageName, String loadingPackageName)211     void recordDex(int loaderUserId, String dexPath, String owningPackageName,
212             String loadingPackageName) {
213         if (mPackageDynamicCodeLoading.record(owningPackageName, dexPath,
214                 FILE_TYPE_DEX, loaderUserId, loadingPackageName)) {
215             mPackageDynamicCodeLoading.maybeWriteAsync();
216         }
217     }
218 
219     /**
220      * Record that an app running in the specified uid has executed native code from the file at
221      * {@param path}.
222      */
recordNative(int loadingUid, String path)223     public void recordNative(int loadingUid, String path) {
224         String[] packages;
225         try {
226             packages = mPackageManager.getPackagesForUid(loadingUid);
227             if (packages == null || packages.length == 0) {
228                 return;
229             }
230         } catch (RemoteException e) {
231             // Can't happen, we're local.
232             return;
233         }
234 
235         String loadingPackageName = packages[0];
236         int loadingUserId = UserHandle.getUserId(loadingUid);
237 
238         if (mPackageDynamicCodeLoading.record(loadingPackageName, path,
239                 FILE_TYPE_NATIVE, loadingUserId, loadingPackageName)) {
240             mPackageDynamicCodeLoading.maybeWriteAsync();
241         }
242     }
243 
clear()244     void clear() {
245         mPackageDynamicCodeLoading.clear();
246     }
247 
removePackage(String packageName)248     void removePackage(String packageName) {
249         if (mPackageDynamicCodeLoading.removePackage(packageName)) {
250             mPackageDynamicCodeLoading.maybeWriteAsync();
251         }
252     }
253 
removeUserPackage(String packageName, int userId)254     void removeUserPackage(String packageName, int userId) {
255         if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) {
256             mPackageDynamicCodeLoading.maybeWriteAsync();
257         }
258     }
259 
readAndSync(Map<String, Set<Integer>> packageToUsersMap)260     void readAndSync(Map<String, Set<Integer>> packageToUsersMap) {
261         mPackageDynamicCodeLoading.read();
262         mPackageDynamicCodeLoading.syncData(packageToUsersMap);
263     }
264 
writeNow()265     void writeNow() {
266         mPackageDynamicCodeLoading.writeNow();
267     }
268 }
269