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 17package android.webkit; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.app.ActivityManagerInternal; 22import android.content.pm.ApplicationInfo; 23import android.content.pm.PackageInfo; 24import android.os.Build; 25import android.os.Process; 26import android.os.RemoteException; 27import android.os.SystemProperties; 28import android.text.TextUtils; 29import android.util.Log; 30 31import com.android.internal.annotations.VisibleForTesting; 32import com.android.server.LocalServices; 33 34import dalvik.system.VMRuntime; 35 36import java.io.File; 37import java.io.IOException; 38import java.util.Arrays; 39import java.util.zip.ZipEntry; 40import java.util.zip.ZipFile; 41 42/** 43 * @hide 44 */ 45@VisibleForTesting 46public class WebViewLibraryLoader { 47 private static final String LOGTAG = WebViewLibraryLoader.class.getSimpleName(); 48 49 private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_32 = 50 "/data/misc/shared_relro/libwebviewchromium32.relro"; 51 private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_64 = 52 "/data/misc/shared_relro/libwebviewchromium64.relro"; 53 private static final long CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES = 100 * 1024 * 1024; 54 55 private static final boolean DEBUG = false; 56 57 private static boolean sAddressSpaceReserved = false; 58 59 /** 60 * Private class for running the actual relro creation in an unprivileged child process. 61 * RelroFileCreator is a static class (without access to the outer class) to avoid accidentally 62 * using any static members from the outer class. Those members will in reality differ between 63 * the child process in which RelroFileCreator operates, and the app process in which the static 64 * members of this class are used. 65 */ 66 private static class RelroFileCreator { 67 // Called in an unprivileged child process to create the relro file. 68 public static void main(String[] args) { 69 boolean result = false; 70 boolean is64Bit = VMRuntime.getRuntime().is64Bit(); 71 try { 72 if (args.length != 1 || args[0] == null) { 73 Log.e(LOGTAG, "Invalid RelroFileCreator args: " + Arrays.toString(args)); 74 return; 75 } 76 Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), lib: " + args[0]); 77 if (!sAddressSpaceReserved) { 78 Log.e(LOGTAG, "can't create relro file; address space not reserved"); 79 return; 80 } 81 result = nativeCreateRelroFile(args[0] /* path */, 82 is64Bit ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 : 83 CHROMIUM_WEBVIEW_NATIVE_RELRO_32); 84 if (result && DEBUG) Log.v(LOGTAG, "created relro file"); 85 } finally { 86 // We must do our best to always notify the update service, even if something fails. 87 try { 88 WebViewFactory.getUpdateServiceUnchecked().notifyRelroCreationCompleted(); 89 } catch (RemoteException e) { 90 Log.e(LOGTAG, "error notifying update service", e); 91 } 92 93 if (!result) Log.e(LOGTAG, "failed to create relro file"); 94 95 // Must explicitly exit or else this process will just sit around after we return. 96 System.exit(0); 97 } 98 } 99 } 100 101 /** 102 * Create a single relro file by invoking an isolated process that to do the actual work. 103 */ 104 static void createRelroFile(final boolean is64Bit, @NonNull WebViewNativeLibrary nativeLib) { 105 final String abi = 106 is64Bit ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]; 107 108 // crashHandler is invoked by the ActivityManagerService when the isolated process crashes. 109 Runnable crashHandler = new Runnable() { 110 @Override 111 public void run() { 112 try { 113 Log.e(LOGTAG, "relro file creator for " + abi + " crashed. Proceeding without"); 114 WebViewFactory.getUpdateService().notifyRelroCreationCompleted(); 115 } catch (RemoteException e) { 116 Log.e(LOGTAG, "Cannot reach WebViewUpdateService. " + e.getMessage()); 117 } 118 } 119 }; 120 121 try { 122 if (nativeLib == null || nativeLib.path == null) { 123 throw new IllegalArgumentException( 124 "Native library paths to the WebView RelRo process must not be null!"); 125 } 126 boolean success = LocalServices.getService(ActivityManagerInternal.class) 127 .startIsolatedProcess( 128 RelroFileCreator.class.getName(), new String[] { nativeLib.path }, 129 "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler); 130 if (!success) throw new Exception("Failed to start the relro file creator process"); 131 } catch (Throwable t) { 132 // Log and discard errors as we must not crash the system server. 133 Log.e(LOGTAG, "error starting relro file creator for abi " + abi, t); 134 crashHandler.run(); 135 } 136 } 137 138 /** 139 * Perform preparations needed to allow loading WebView from an application. This method should 140 * be called whenever we change WebView provider. 141 * @return the number of relro processes started. 142 */ 143 static int prepareNativeLibraries(PackageInfo webviewPackageInfo) 144 throws WebViewFactory.MissingWebViewPackageException { 145 WebViewNativeLibrary nativeLib32bit = 146 getWebViewNativeLibrary(webviewPackageInfo, false /* is64bit */); 147 WebViewNativeLibrary nativeLib64bit = 148 getWebViewNativeLibrary(webviewPackageInfo, true /* is64bit */); 149 updateWebViewZygoteVmSize(nativeLib32bit, nativeLib64bit); 150 151 return createRelros(nativeLib32bit, nativeLib64bit); 152 } 153 154 /** 155 * @return the number of relro processes started. 156 */ 157 private static int createRelros(@Nullable WebViewNativeLibrary nativeLib32bit, 158 @Nullable WebViewNativeLibrary nativeLib64bit) { 159 if (DEBUG) Log.v(LOGTAG, "creating relro files"); 160 int numRelros = 0; 161 162 if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { 163 if (nativeLib32bit == null) { 164 Log.e(LOGTAG, "No 32-bit WebView library path, skipping relro creation."); 165 } else { 166 if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro"); 167 createRelroFile(false /* is64Bit */, nativeLib32bit); 168 numRelros++; 169 } 170 } 171 172 if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { 173 if (nativeLib64bit == null) { 174 Log.e(LOGTAG, "No 64-bit WebView library path, skipping relro creation."); 175 } else { 176 if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro"); 177 createRelroFile(true /* is64Bit */, nativeLib64bit); 178 numRelros++; 179 } 180 } 181 return numRelros; 182 } 183 184 /** 185 * 186 * @return the native WebView libraries in the new WebView APK. 187 */ 188 private static void updateWebViewZygoteVmSize( 189 @Nullable WebViewNativeLibrary nativeLib32bit, 190 @Nullable WebViewNativeLibrary nativeLib64bit) 191 throws WebViewFactory.MissingWebViewPackageException { 192 // Find the native libraries of the new WebView package, to change the size of the 193 // memory region in the Zygote reserved for the library. 194 long newVmSize = 0L; 195 196 if (nativeLib32bit != null) newVmSize = Math.max(newVmSize, nativeLib32bit.size); 197 if (nativeLib64bit != null) newVmSize = Math.max(newVmSize, nativeLib64bit.size); 198 199 if (DEBUG) { 200 Log.v(LOGTAG, "Based on library size, need " + newVmSize 201 + " bytes of address space."); 202 } 203 // The required memory can be larger than the file on disk (due to .bss), and an 204 // upgraded version of the library will likely be larger, so always attempt to 205 // reserve twice as much as we think to allow for the library to grow during this 206 // boot cycle. 207 newVmSize = Math.max(2 * newVmSize, CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES); 208 Log.d(LOGTAG, "Setting new address space to " + newVmSize); 209 setWebViewZygoteVmSize(newVmSize); 210 } 211 212 /** 213 * Reserve space for the native library to be loaded into. 214 */ 215 static void reserveAddressSpaceInZygote() { 216 System.loadLibrary("webviewchromium_loader"); 217 long addressSpaceToReserve = 218 SystemProperties.getLong(WebViewFactory.CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY, 219 CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES); 220 sAddressSpaceReserved = nativeReserveAddressSpace(addressSpaceToReserve); 221 222 if (sAddressSpaceReserved) { 223 if (DEBUG) { 224 Log.v(LOGTAG, "address space reserved: " + addressSpaceToReserve + " bytes"); 225 } 226 } else { 227 Log.e(LOGTAG, "reserving " + addressSpaceToReserve + " bytes of address space failed"); 228 } 229 } 230 231 /** 232 * Load WebView's native library into the current process. 233 * 234 * <p class="note"><b>Note:</b> Assumes that we have waited for relro creation. 235 * 236 * @param clazzLoader class loader used to find the linker namespace to load the library into. 237 * @param libraryFileName the filename of the library to load. 238 */ 239 public static int loadNativeLibrary(ClassLoader clazzLoader, String libraryFileName) { 240 if (!sAddressSpaceReserved) { 241 Log.e(LOGTAG, "can't load with relro file; address space not reserved"); 242 return WebViewFactory.LIBLOAD_ADDRESS_SPACE_NOT_RESERVED; 243 } 244 245 String relroPath = VMRuntime.getRuntime().is64Bit() ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 : 246 CHROMIUM_WEBVIEW_NATIVE_RELRO_32; 247 int result = nativeLoadWithRelroFile(libraryFileName, relroPath, clazzLoader); 248 if (result != WebViewFactory.LIBLOAD_SUCCESS) { 249 Log.w(LOGTAG, "failed to load with relro file, proceeding without"); 250 } else if (DEBUG) { 251 Log.v(LOGTAG, "loaded with relro file"); 252 } 253 return result; 254 } 255 256 /** 257 * Fetch WebView's native library paths from {@param packageInfo}. 258 * @hide 259 */ 260 @Nullable 261 @VisibleForTesting 262 public static WebViewNativeLibrary getWebViewNativeLibrary(PackageInfo packageInfo, 263 boolean is64bit) throws WebViewFactory.MissingWebViewPackageException { 264 ApplicationInfo ai = packageInfo.applicationInfo; 265 final String nativeLibFileName = WebViewFactory.getWebViewLibrary(ai); 266 267 String dir = getWebViewNativeLibraryDirectory(ai, is64bit /* 64bit */); 268 269 WebViewNativeLibrary lib = findNativeLibrary(ai, nativeLibFileName, 270 is64bit ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS, dir); 271 272 if (DEBUG) { 273 Log.v(LOGTAG, String.format("Native %d-bit lib: %s", is64bit ? 64 : 32, lib.path)); 274 } 275 return lib; 276 } 277 278 /** 279 * @return the directory of the native WebView library with bitness {@param is64bit}. 280 * @hide 281 */ 282 @VisibleForTesting 283 public static String getWebViewNativeLibraryDirectory(ApplicationInfo ai, boolean is64bit) { 284 // Primary arch has the same bitness as the library we are looking for. 285 if (is64bit == VMRuntime.is64BitAbi(ai.primaryCpuAbi)) return ai.nativeLibraryDir; 286 287 // Secondary arch has the same bitness as the library we are looking for. 288 if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) { 289 return ai.secondaryNativeLibraryDir; 290 } 291 292 return ""; 293 } 294 295 /** 296 * @return an object describing a native WebView library given the directory path of that 297 * library, or null if the library couldn't be found. 298 */ 299 @Nullable 300 private static WebViewNativeLibrary findNativeLibrary(ApplicationInfo ai, 301 String nativeLibFileName, String[] abiList, String libDirectory) 302 throws WebViewFactory.MissingWebViewPackageException { 303 if (TextUtils.isEmpty(libDirectory)) return null; 304 String libPath = libDirectory + "/" + nativeLibFileName; 305 File f = new File(libPath); 306 if (f.exists()) { 307 return new WebViewNativeLibrary(libPath, f.length()); 308 } else { 309 return getLoadFromApkPath(ai.sourceDir, abiList, nativeLibFileName); 310 } 311 } 312 313 /** 314 * @hide 315 */ 316 @VisibleForTesting 317 public static class WebViewNativeLibrary { 318 public final String path; 319 public final long size; 320 321 WebViewNativeLibrary(String path, long size) { 322 this.path = path; 323 this.size = size; 324 } 325 } 326 327 private static WebViewNativeLibrary getLoadFromApkPath(String apkPath, 328 String[] abiList, 329 String nativeLibFileName) 330 throws WebViewFactory.MissingWebViewPackageException { 331 // Search the APK for a native library conforming to a listed ABI. 332 try (ZipFile z = new ZipFile(apkPath)) { 333 for (String abi : abiList) { 334 final String entry = "lib/" + abi + "/" + nativeLibFileName; 335 ZipEntry e = z.getEntry(entry); 336 if (e != null && e.getMethod() == ZipEntry.STORED) { 337 // Return a path formatted for dlopen() load from APK. 338 return new WebViewNativeLibrary(apkPath + "!/" + entry, e.getSize()); 339 } 340 } 341 } catch (IOException e) { 342 throw new WebViewFactory.MissingWebViewPackageException(e); 343 } 344 return null; 345 } 346 347 /** 348 * Sets the size of the memory area in which to store the relro section. 349 */ 350 private static void setWebViewZygoteVmSize(long vmSize) { 351 SystemProperties.set(WebViewFactory.CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY, 352 Long.toString(vmSize)); 353 } 354 355 static native boolean nativeReserveAddressSpace(long addressSpaceToReserve); 356 static native boolean nativeCreateRelroFile(String lib, String relro); 357 static native int nativeLoadWithRelroFile(String lib, String relro, ClassLoader clazzLoader); 358} 359