Commit 0d39760b authored by Torne (Richard Coles)'s avatar Torne (Richard Coles) Committed by Commit Bot

Test workaround for bug with split APK handling on O.

Android O's WebView zygote code doesn't handle split APKs correctly when
preloading the code for the current WebView implementation, creating an
incorrect entry in the Java classloader cache. This causes a crash when
the renderer process initializes, since it tries to load the native
library into two different classloaders as a result, which is not
permitted.

We're hoping to work around this using reflection to correct the cache
entry during the zygote preloading phase, so that when the renderer
starts up, it correctly reuses the preloaded classloader instead of
creating a new one. However, we're not sure if this reflection will work
on all devices due to vendor changes.

To validate that the reflection works, this CL will attempt the
reflection but not actually change anything - if the reflection fails
due to a framework change, it will catch the exception instead of
crashing, and simply record that in a static variable which can later be
logged with UMA.

Bug: 891452
Change-Id: I41dabb9c4cb4151676229a07abd37c803dbc3967
Reviewed-on: https://chromium-review.googlesource.com/1255388
Commit-Queue: Richard Coles <torne@chromium.org>
Reviewed-by: default avatarTibor Goldschwendt <tiborg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#596293}
parent 9417920f
......@@ -43,6 +43,7 @@ android_library("glue") {
"java/src/com/android/webview/chromium/ServiceWorkerSettingsAdapter.java",
"java/src/com/android/webview/chromium/SharedStatics.java",
"java/src/com/android/webview/chromium/SharedTracingControllerAdapter.java",
"java/src/com/android/webview/chromium/SplitApkWorkaround.java",
"java/src/com/android/webview/chromium/TokenBindingManagerAdapter.java",
"java/src/com/android/webview/chromium/TracingControllerAdapter.java",
"java/src/com/android/webview/chromium/WebBackForwardListChromium.java",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.android.webview.chromium;
import android.support.annotation.IntDef;
import dalvik.system.BaseDexClassLoader;
import org.chromium.base.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
/**
* WebView-side workaround for the Android O framework bug described in https://crbug.com/889954
* which affects us if we are the current WebView provider and we were installed as a split APK.
*/
public class SplitApkWorkaround {
private static final String TAG = "SplitApkWorkaround";
@IntDef({Result.NOT_RUN, Result.NO_ENTRIES, Result.ONE_ENTRY, Result.MULTIPLE_ENTRIES,
Result.TOPLEVEL_EXCEPTION, Result.LOOP_EXCEPTION})
@interface Result {
int NOT_RUN = 0;
int NO_ENTRIES = 1;
int ONE_ENTRY = 2;
int MULTIPLE_ENTRIES = 3;
int TOPLEVEL_EXCEPTION = 4;
int LOOP_EXCEPTION = 5;
}
/**
* There is a framework bug in O that causes an incorrect classloader cache entry to be created
* when the WebView provider is installed as multiple split APKs.
* Use reflection to correct the cache entry during WebView zygote startup.
* This function runs in the WebView zygote, which cannot make any binder calls to the framework
* and is a very restricted environment.
*
* @param dryRun If true, don't actually change any state in the framework; just verify that
* the reflection succeeds.
* @return a value from Result describing what happened.
*/
@SuppressWarnings("unchecked")
public static @Result int apply(boolean dryRun) {
int matchingEntries = 0;
int exceptionEntries = 0;
try {
// Retrieve all the required classes and fields first, such that if any of the lookups
// fail, we won't have done anything yet.
Class<?> alClass = Class.forName("android.app.ApplicationLoaders");
Method getDefaultMethod = alClass.getDeclaredMethod("getDefault");
Field mLoadersField = alClass.getDeclaredField("mLoaders");
mLoadersField.setAccessible(true);
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Class<?> dplClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dplClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Class<?> elClass = Class.forName("dalvik.system.DexPathList$Element");
Field pathField = elClass.getDeclaredField("path");
pathField.setAccessible(true);
// Retrieve the ApplicationLoaders singleton and get the cache from inside it.
Object alInstance = getDefaultMethod.invoke(null);
Object rawLoaders = mLoadersField.get(alInstance);
Map<String, ClassLoader> loaders = (Map<String, ClassLoader>) rawLoaders;
// Synchronize on the map while trying to update it, as the framework does.
synchronized (loaders) {
for (Map.Entry<String, ClassLoader> entry : loaders.entrySet()) {
try {
if (!(entry.getValue() instanceof BaseDexClassLoader)) {
// If it's some other type it can't be the right one.
continue;
}
String cacheKey = entry.getKey();
BaseDexClassLoader cl = (BaseDexClassLoader) entry.getValue();
// Get the list of files that this classloader uses as its classpath.
Object pathList = pathListField.get(cl);
Object dexElements = dexElementsField.get(pathList);
int elementCount = Array.getLength(dexElements);
if (elementCount <= 1) {
// If there's only one file, then this classloader cannot be affected by
// the bug, so ignore it.
continue;
}
// If there's more than one file, get the first file in the path.
// If it's the same as our cache key, then the cache key must, by
// definition, be incomplete - listing only one file when it should list
// all of them.
Object firstElement = Array.get(dexElements, 0);
File firstPath = (File) pathField.get(firstElement);
if (cacheKey.equals(firstPath.getPath())) {
// Build a new, correct cache key by concatenating all the files in the
// list together in order, separated by colons.
String newCacheKey = cacheKey;
for (int i = 1; i < elementCount; i++) {
Object element = Array.get(dexElements, i);
File path = (File) pathField.get(element);
newCacheKey += ":" + path.getPath();
}
matchingEntries++;
if (!dryRun) {
// Add a new entry to the cache which maps the new, correct key to
// the same classloader object. We do not remove the previous entry
// from the cache, in case something attempts to look it up by the
// old key for some reason - it shouldn't cause a problem for there
// to be multiple entries mapping to the same classloader.
loaders.put(newCacheKey, cl);
Log.i(TAG, "Fixed classloader cache entry for " + newCacheKey);
}
}
} catch (Exception e) {
// We log and ignore it here so we can continue looping through the cache,
// in the hope that the one that threw an exception wasn't the one we
// were looking for.
exceptionEntries++;
Log.w(TAG, "Caught exception while attempting to fix classloader cache", e);
}
}
}
} catch (Exception e) {
// If we got an exception at this point we assume that we failed to fix it, since we
// didn't get as far as iterating over the cache entries.
Log.w(TAG, "Caught exception while attempting to fix classloader cache", e);
return Result.TOPLEVEL_EXCEPTION;
}
// If we found at least one matching entry, then don't worry about exceptions that happened
// during the loop; we likely found the correct classloader, and the one that triggered an
// exception was probably not relevant. Distinguish one vs multiple entries, though,
// because multiple matches is unexpected (only one case in the code is supposed to create
// this situation).
if (matchingEntries == 1) return Result.ONE_ENTRY;
if (matchingEntries > 1) return Result.MULTIPLE_ENTRIES;
// If we didn't find any matching entries, but did get an exception during the loop, then
// report this, as we might have taken the exception while trying to access the entry we
// needed to fix.
if (exceptionEntries > 0) return Result.LOOP_EXCEPTION;
// Otherwise, we just didn't find any entries at all, which is probably fine; not all
// configurations actually trigger the bug.
return Result.NO_ENTRIES;
}
}
......@@ -13,7 +13,6 @@ import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
......@@ -40,6 +39,7 @@ import org.chromium.android_webview.command_line.CommandLineUtil;
import org.chromium.base.BuildInfo;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PackageUtils;
import org.chromium.base.PathUtils;
import org.chromium.base.StrictModeContext;
......@@ -62,7 +62,7 @@ import java.util.concurrent.TimeUnit;
*/
@SuppressWarnings("deprecation")
public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
private static final String TAG = "WebViewChromiumFactoryProvider";
private static final String TAG = "WVCFactoryProvider";
private static final String CHROMIUM_PREFS_NAME = "WebViewChromiumPrefs";
private static final String VERSION_CODE_PREF = "lastVersionCodeUsed";
......@@ -362,7 +362,19 @@ public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
}
}
private static @SplitApkWorkaround.Result int sSplitApkWorkaroundResult =
SplitApkWorkaround.Result.NOT_RUN;
public static boolean preloadInZygote() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// If we're on O, where the split APK handling bug exists, then go through the motions
// of applying the workaround - don't actually change anything, but do the reflection
// to check for compatibility issues. The result will be logged to UMA later, because
// we can't do very much in the restricted environment of the WebView zygote process.
sSplitApkWorkaroundResult = SplitApkWorkaround.apply(/* dryRun */ true);
}
for (String library : NativeLibraries.LIBRARIES) {
System.loadLibrary(library);
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment