1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.incrementalinstall;
6 
7 import android.annotation.SuppressLint;
8 import android.content.Context;
9 import android.os.Build;
10 import android.os.Process;
11 import android.util.Log;
12 
13 import dalvik.system.DexFile;
14 
15 import java.io.File;
16 import java.io.FileInputStream;
17 import java.io.FileNotFoundException;
18 import java.io.FileOutputStream;
19 import java.io.IOException;
20 import java.util.List;
21 import java.util.Locale;
22 
23 /**
24  * Provides the ability to add native libraries and .dex files to an existing class loader.
25  * Tested with Jellybean MR2 - Marshmellow.
26  */
27 final class ClassLoaderPatcher {
28     private static final String TAG = "incrementalinstall";
29     private final File mAppFilesSubDir;
30     private final ClassLoader mClassLoader;
31     private final Object mLibcoreOs;
32     private final int mProcessUid;
33     final boolean mIsPrimaryProcess;
34 
ClassLoaderPatcher(Context context)35     ClassLoaderPatcher(Context context) throws ReflectiveOperationException {
36         mAppFilesSubDir =
37                 new File(context.getApplicationInfo().dataDir, "incremental-install-files");
38         mClassLoader = context.getClassLoader();
39         mLibcoreOs = Reflect.getField(Class.forName("libcore.io.Libcore"), "os");
40         mProcessUid = Process.myUid();
41         mIsPrimaryProcess = context.getApplicationInfo().uid == mProcessUid;
42         Log.i(TAG, "uid=" + mProcessUid + " (isPrimary=" + mIsPrimaryProcess + ")");
43     }
44 
45     /**
46      * Loads all dex files within |dexDir| into the app's ClassLoader.
47      */
48     @SuppressLint({
49             "SetWorldReadable",
50             "SetWorldWritable",
51     })
loadDexFiles(File dexDir, String packageName)52     DexFile[] loadDexFiles(File dexDir, String packageName)
53             throws ReflectiveOperationException, IOException {
54         Log.i(TAG, "Installing dex files from: " + dexDir);
55 
56         File optimizedDir = null;
57         boolean isAtLeastOreo = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
58 
59         if (isAtLeastOreo) {
60             // In O, optimizedDirectory is ignored, and the files are always put in an "oat"
61             // directory that is a sibling to the dex files themselves. SELinux policies
62             // prevent using odex files from /data/local/tmp, so we must first copy them
63             // into the app's data directory in order to get the odex files to live there.
64             // Use a package-name subdirectory to prevent name collisions when apk-under-test is
65             // used.
66             File newDexDir = new File(mAppFilesSubDir, packageName + "-dexes");
67             if (mIsPrimaryProcess) {
68                 safeCopyAllFiles(dexDir, newDexDir);
69             }
70             dexDir = newDexDir;
71         } else {
72             // The optimized dex files will be owned by this process' user.
73             // Store them within the app's data dir rather than on /data/local/tmp
74             // so that they are still deleted (by the OS) when we uninstall
75             // (even on a non-rooted device).
76             File incrementalDexesDir = new File(mAppFilesSubDir, "optimized-dexes");
77             File isolatedDexesDir = new File(mAppFilesSubDir, "isolated-dexes");
78 
79             if (mIsPrimaryProcess) {
80                 ensureAppFilesSubDirExists();
81                 // Allows isolated processes to access the same files.
82                 incrementalDexesDir.mkdir();
83                 incrementalDexesDir.setReadable(true, false);
84                 incrementalDexesDir.setExecutable(true, false);
85                 // Create a directory for isolated processes to create directories in.
86                 isolatedDexesDir.mkdir();
87                 isolatedDexesDir.setWritable(true, false);
88                 isolatedDexesDir.setExecutable(true, false);
89 
90                 optimizedDir = incrementalDexesDir;
91             } else {
92                 // There is a UID check of the directory in dalvik.system.DexFile():
93                 // https://android.googlesource.com/platform/libcore/+/45e0260/dalvik/src/main/java/dalvik/system/DexFile.java#101
94                 // Rather than have each isolated process run DexOpt though, we use
95                 // symlinks within the directory to point at the browser process'
96                 // optimized dex files.
97                 optimizedDir = new File(isolatedDexesDir, "isolated-" + mProcessUid);
98                 optimizedDir.mkdir();
99                 // Always wipe it out and re-create for simplicity.
100                 Log.i(TAG, "Creating dex file symlinks for isolated process");
101                 for (File f : optimizedDir.listFiles()) {
102                     f.delete();
103                 }
104                 for (File f : incrementalDexesDir.listFiles()) {
105                     String to = "../../" + incrementalDexesDir.getName() + "/" + f.getName();
106                     File from = new File(optimizedDir, f.getName());
107                     createSymlink(to, from);
108                 }
109             }
110             Log.i(TAG, "Code cache dir: " + optimizedDir);
111         }
112 
113         // Ignore "oat" directory.
114         // Also ignore files that sometimes show up (e.g. .jar.arm.flock).
115         File[] dexFilesArr = dexDir.listFiles(f -> f.getName().endsWith(".jar"));
116         if (dexFilesArr == null) {
117             throw new FileNotFoundException("Dex dir does not exist: " + dexDir);
118         }
119 
120         Log.i(TAG, "Loading " + dexFilesArr.length + " dex files");
121 
122         Object dexPathList = Reflect.getField(mClassLoader, "pathList");
123         Object[] dexElements = (Object[]) Reflect.getField(dexPathList, "dexElements");
124         dexElements = addDexElements(dexFilesArr, optimizedDir, dexElements);
125         Reflect.setField(dexPathList, "dexElements", dexElements);
126 
127         // Return the list of new DexFile instances for the .jars in dexPathList.
128         DexFile[] ret = new DexFile[dexFilesArr.length];
129         int startIndex = dexElements.length - dexFilesArr.length;
130         for (int i = 0; i < ret.length; ++i) {
131             ret[i] = (DexFile) Reflect.getField(dexElements[startIndex + i], "dexFile");
132         }
133         return ret;
134     }
135 
136     /**
137      * Sets up all libraries within |libDir| to be loadable by System.loadLibrary().
138      */
139     @SuppressLint("SetWorldReadable")
importNativeLibs(File libDir)140     void importNativeLibs(File libDir) throws ReflectiveOperationException, IOException {
141         Log.i(TAG, "Importing native libraries from: " + libDir);
142         if (!libDir.exists()) {
143             Log.i(TAG, "No native libs exist.");
144             return;
145         }
146         // The library copying is not necessary on older devices, but we do it anyways to
147         // simplify things (it's fast compared to dexing).
148         // https://code.google.com/p/android/issues/detail?id=79480
149         File localLibsDir = new File(mAppFilesSubDir, "lib");
150         safeCopyAllFiles(libDir, localLibsDir);
151         addNativeLibrarySearchPath(localLibsDir);
152     }
153 
154     @SuppressLint("SetWorldReadable")
safeCopyAllFiles(File srcDir, File dstDir)155     private void safeCopyAllFiles(File srcDir, File dstDir) throws IOException {
156         // The library copying is not necessary on older devices, but we do it anyways to
157         // simplify things (it's fast compared to dexing).
158         // https://code.google.com/p/android/issues/detail?id=79480
159         File lockFile = new File(mAppFilesSubDir, dstDir.getName() + ".lock");
160         if (mIsPrimaryProcess) {
161             ensureAppFilesSubDirExists();
162             LockFile lock = LockFile.acquireRuntimeLock(lockFile);
163             if (lock == null) {
164                 LockFile.waitForRuntimeLock(lockFile, 10 * 1000);
165             } else {
166                 try {
167                     dstDir.mkdir();
168                     dstDir.setReadable(true, false);
169                     dstDir.setExecutable(true, false);
170                     copyChangedFiles(srcDir, dstDir);
171                 } finally {
172                     lock.release();
173                 }
174             }
175         } else {
176             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
177                 // TODO: Work around this issue by using APK splits to install each dex / lib.
178                 throw new RuntimeException("Incremental install does not work on Android M+ "
179                         + "with isolated processes. Build system should have removed this. "
180                         + "Please file a bug.");
181             }
182             // Other processes: Waits for primary process to finish copying.
183             LockFile.waitForRuntimeLock(lockFile, 10 * 1000);
184         }
185     }
186 
187     @SuppressWarnings("unchecked")
addNativeLibrarySearchPath(File nativeLibDir)188     private void addNativeLibrarySearchPath(File nativeLibDir) throws ReflectiveOperationException {
189         Object dexPathList = Reflect.getField(mClassLoader, "pathList");
190         Object currentDirs = Reflect.getField(dexPathList, "nativeLibraryDirectories");
191         File[] newDirs = new File[] { nativeLibDir };
192         // Switched from an array to an ArrayList in Lollipop.
193         if (currentDirs instanceof List) {
194             List<File> dirsAsList = (List<File>) currentDirs;
195             dirsAsList.add(0, nativeLibDir);
196         } else {
197             File[] dirsAsArray = (File[]) currentDirs;
198             Reflect.setField(dexPathList, "nativeLibraryDirectories",
199                     Reflect.concatArrays(newDirs, newDirs, dirsAsArray));
200         }
201 
202         Object[] nativeLibraryPathElements;
203         try {
204             nativeLibraryPathElements =
205                     (Object[]) Reflect.getField(dexPathList, "nativeLibraryPathElements");
206         } catch (NoSuchFieldException e) {
207             // This field doesn't exist pre-M.
208             return;
209         }
210         Object[] additionalElements = makeNativePathElements(newDirs);
211         Reflect.setField(dexPathList, "nativeLibraryPathElements",
212                 Reflect.concatArrays(nativeLibraryPathElements, additionalElements,
213                         nativeLibraryPathElements));
214     }
215 
copyChangedFiles(File srcDir, File dstDir)216     private static void copyChangedFiles(File srcDir, File dstDir) throws IOException {
217         int numUpdated = 0;
218         File[] srcFiles = srcDir.listFiles();
219         for (File f : srcFiles) {
220             // Note: Tried using hardlinks, but resulted in EACCES exceptions.
221             File dest = new File(dstDir, f.getName());
222             if (copyIfModified(f, dest)) {
223                 numUpdated++;
224             }
225         }
226         // Delete stale files.
227         int numDeleted = 0;
228         for (File f : dstDir.listFiles()) {
229             File src = new File(srcDir, f.getName());
230             if (!src.exists()) {
231                 numDeleted++;
232                 f.delete();
233             }
234         }
235         String msg = String.format(Locale.US,
236                 "copyChangedFiles: %d of %d updated. %d stale files removed.", numUpdated,
237                 srcFiles.length, numDeleted);
238         Log.i(TAG, msg);
239     }
240 
241     @SuppressLint("SetWorldReadable")
copyIfModified(File src, File dest)242     private static boolean copyIfModified(File src, File dest) throws IOException {
243         long lastModified = src.lastModified();
244         if (dest.exists() && dest.lastModified() == lastModified) {
245             return false;
246         }
247         Log.i(TAG, "Copying " + src + " -> " + dest);
248         FileInputStream istream = new FileInputStream(src);
249         FileOutputStream ostream = new FileOutputStream(dest);
250         ostream.getChannel().transferFrom(istream.getChannel(), 0, istream.getChannel().size());
251         istream.close();
252         ostream.close();
253         dest.setReadable(true, false);
254         dest.setExecutable(true,  false);
255         dest.setLastModified(lastModified);
256         return true;
257     }
258 
ensureAppFilesSubDirExists()259     private void ensureAppFilesSubDirExists() {
260         mAppFilesSubDir.mkdir();
261         mAppFilesSubDir.setExecutable(true, false);
262     }
263 
createSymlink(String to, File from)264     private void createSymlink(String to, File from) throws ReflectiveOperationException {
265         Reflect.invokeMethod(mLibcoreOs, "symlink", to, from.getAbsolutePath());
266     }
267 
makeNativePathElements(File[] paths)268     private static Object[] makeNativePathElements(File[] paths)
269             throws ReflectiveOperationException {
270         Object[] entries = new Object[paths.length];
271         if (Build.VERSION.SDK_INT >= 26) {
272             Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$NativeLibraryElement");
273             for (int i = 0; i < paths.length; ++i) {
274                 entries[i] = Reflect.newInstance(entryClazz, paths[i]);
275             }
276         } else {
277             Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element");
278             for (int i = 0; i < paths.length; ++i) {
279                 entries[i] = Reflect.newInstance(entryClazz, paths[i], true, null, null);
280             }
281         }
282         return entries;
283     }
284 
addDexElements(File[] files, File optimizedDirectory, Object[] curDexElements)285     private Object[] addDexElements(File[] files, File optimizedDirectory, Object[] curDexElements)
286             throws ReflectiveOperationException {
287         Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element");
288         Class<?> clazz = Class.forName("dalvik.system.DexPathList");
289         Object[] ret =
290                 Reflect.concatArrays(curDexElements, curDexElements, new Object[files.length]);
291         File emptyDir = new File("");
292         for (int i = 0; i < files.length; ++i) {
293             File file = files[i];
294             Object dexFile;
295             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
296                 // loadDexFile requires that ret contain all previously added elements.
297                 dexFile = Reflect.invokeMethod(clazz, "loadDexFile", file, optimizedDirectory,
298                                                mClassLoader, ret);
299             } else {
300                 dexFile = Reflect.invokeMethod(clazz, "loadDexFile", file, optimizedDirectory);
301             }
302             Object dexElement;
303             if (Build.VERSION.SDK_INT >= 26) {
304                 dexElement = Reflect.newInstance(entryClazz, dexFile, file);
305             } else {
306                 dexElement = Reflect.newInstance(entryClazz, emptyDir, false, file, dexFile);
307             }
308             ret[curDexElements.length + i] = dexElement;
309         }
310         return ret;
311     }
312 }
313