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