Commit 71f7a125 authored by Ran Ji's avatar Ran Ji Committed by Commit Bot

Add a fallback for load library failures in Android K-.

Sometime Android system fails to extract library. Manually extract them to
cache directory and load as needed.

Bug: 806998
Change-Id: I7ccff04435cb5201e410c71bde741e52687329ab
Reviewed-on: https://chromium-review.googlesource.com/930332
Commit-Queue: Ran Ji <ranj@chromium.org>
Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarYaron Friedman <yfriedman@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548237}
parent 59359a52
......@@ -2725,6 +2725,7 @@ if (is_android) {
deps = [
"//third_party/android_tools:android_support_annotations_java",
"//third_party/android_tools:android_support_multidex_java",
"//third_party/android_tools:android_support_v4_java",
"//third_party/jsr-305:jsr_305_javalib",
]
......
......@@ -4,10 +4,15 @@
package org.chromium.base;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.content.ContextCompat;
import java.io.File;
import java.io.FileOutputStream;
......@@ -18,44 +23,42 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipFile;
/**
* Handles extracting the necessary resources bundled in an APK and moving them to a location on
* the file system accessible from the native code.
*/
public class ResourceExtractor {
// Experience shows that on some devices, the PackageManager fails to properly extract
// native shared libraries to the /data partition at installation or upgrade time,
// which creates all kind of chaos (https://crbug.com/806998).
//
// We implement a fallback when we detect the issue by manually extracting the library
// into Chromium's own data directory, then retrying to load the new library from here.
//
// This will work for any device running K-. Starting with Android L, render processes
// cannot access the file system anymore, and extraction will always fail for them.
// However, the issue doesn't seem to appear in the field for Android L.
//
// Also, starting with M, the issue doesn't exist if shared libraries are stored
// uncompressed in the APK (as Chromium does), because the system linker can access them
// directly, and the PackageManager will thus never extract them in the first place.
static public final boolean PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION =
Build.VERSION.SDK_INT <= VERSION_CODES.KITKAT;
private static final String TAG = "base";
private static final String ICU_DATA_FILENAME = "icudtl.dat";
private static final String V8_NATIVES_DATA_FILENAME = "natives_blob.bin";
private static final String V8_SNAPSHOT_DATA_FILENAME = "snapshot_blob.bin";
private static final String FALLBACK_LOCALE = "en-US";
private static final String LIBRARY_DIR = "native_libraries";
private static final int BUFFER_SIZE = 16 * 1024;
private class ExtractTask extends AsyncTask<Void, Void, Void> {
private static final int BUFFER_SIZE = 16 * 1024;
private final List<Runnable> mCompletionCallbacks = new ArrayList<Runnable>();
private void extractResourceHelper(InputStream is, File outFile, byte[] buffer)
throws IOException {
OutputStream os = null;
File tmpOutputFile = new File(outFile.getPath() + ".tmp");
try {
os = new FileOutputStream(tmpOutputFile);
Log.i(TAG, "Extracting resource %s", outFile);
int count = 0;
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
os.write(buffer, 0, count);
}
} finally {
StreamUtil.closeQuietly(os);
StreamUtil.closeQuietly(is);
}
if (!tmpOutputFile.renameTo(outFile)) {
throw new IOException();
}
}
private void doInBackgroundImpl() {
final File outputDir = getOutputDir();
if (!outputDir.exists() && !outputDir.mkdirs()) {
......@@ -85,8 +88,7 @@ public class ResourceExtractor {
for (String assetName : mAssetsToExtract) {
File output = new File(outputDir, assetName + extractSuffix);
TraceEvent.begin("ExtractResource");
try {
InputStream inputStream = assetManager.open(assetName);
try (InputStream inputStream = assetManager.open(assetName)) {
extractResourceHelper(inputStream, output, buffer);
} catch (IOException e) {
// The app would just crash later if files are missing.
......@@ -138,6 +140,68 @@ public class ResourceExtractor {
return sInstance;
}
// Android system sometimes fails to extract libraries from APK (https://crbug.com/806998).
// This function manually extract libraries as a fallback.
@SuppressLint({"SetWorldReadable"})
public static String extractFileIfStale(
Context appContext, String pathWithinApk, File destDir) {
assert PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION;
String apkPath = appContext.getApplicationInfo().sourceDir;
String fileName =
(new File(pathWithinApk)).getName() + BuildInfo.getInstance().extractedFileSuffix;
File libraryFile = new File(destDir, fileName);
if (!libraryFile.exists()) {
try (ZipFile zipFile = new ZipFile(apkPath);
InputStream inputStream =
zipFile.getInputStream(zipFile.getEntry(pathWithinApk))) {
if (zipFile.getEntry(pathWithinApk) == null)
throw new RuntimeException("Cannot find ZipEntry" + pathWithinApk);
extractResourceHelper(inputStream, libraryFile, new byte[BUFFER_SIZE]);
libraryFile.setReadable(true, false);
libraryFile.setExecutable(true, false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return libraryFile.getAbsolutePath();
}
public static File makeLibraryDirAndSetPermission() {
if (!ContextUtils.isIsolatedProcess()) {
File cacheDir = ContextCompat.getCodeCacheDir(ContextUtils.getApplicationContext());
File libDir = new File(cacheDir, LIBRARY_DIR);
cacheDir.mkdir();
cacheDir.setExecutable(true, false);
libDir.mkdir();
libDir.setExecutable(true, false);
}
return getLibraryDir();
}
private static File getLibraryDir() {
return new File(
ContextCompat.getCodeCacheDir(ContextUtils.getApplicationContext()), LIBRARY_DIR);
}
private static void extractResourceHelper(InputStream is, File outFile, byte[] buffer)
throws IOException {
File tmpOutputFile = new File(outFile.getPath() + ".tmp");
try (OutputStream os = new FileOutputStream(tmpOutputFile)) {
Log.i(TAG, "Extracting resource %s", outFile);
int count = 0;
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
os.write(buffer, 0, count);
}
}
if (!tmpOutputFile.renameTo(outFile)) {
throw new IOException();
}
}
private static String[] detectFilesToExtract() {
Locale defaultLocale = Locale.getDefault();
String language = LocaleUtils.getUpdatedLanguageForChromium(defaultLocale.getLanguage());
......@@ -247,6 +311,21 @@ public class ResourceExtractor {
deleteFile(new File(getAppDataDir(), ICU_DATA_FILENAME));
deleteFile(new File(getAppDataDir(), V8_NATIVES_DATA_FILENAME));
deleteFile(new File(getAppDataDir(), V8_SNAPSHOT_DATA_FILENAME));
if (PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION) {
String suffix = BuildInfo.getInstance().extractedFileSuffix;
File[] files = getLibraryDir().listFiles();
if (files != null) {
for (File file : files) {
// The delete can happen on the same time as writing file from InputStream, use
// contains() to avoid deleting the temp file.
if (!file.getName().contains(suffix)) {
deleteFile(file);
}
}
}
}
if (existingFileNames != null) {
for (String fileName : existingFileNames) {
deleteFile(new File(getOutputDir(), fileName));
......
......@@ -4,6 +4,8 @@
package org.chromium.base.library_loader;
import static org.chromium.base.metrics.CachedMetrics.EnumeratedHistogramSample;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
......@@ -13,13 +15,13 @@ import android.os.Process;
import android.os.StrictMode;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.system.Os;
import org.chromium.base.BuildConfig;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResourceExtractor;
import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
......@@ -66,6 +68,9 @@ public class LibraryLoader {
// The singleton instance of LibraryLoader.
private static volatile LibraryLoader sInstance;
private static final EnumeratedHistogramSample sRelinkerCountHistogram =
new EnumeratedHistogramSample("ChromiumAndroidLinker.RelinkerFallbackCount", 2);
// One-way switch becomes true when the libraries are loaded.
private boolean mLoaded;
......@@ -356,6 +361,26 @@ public class LibraryLoader {
}
}
static void incrementRelinkerCountHitHistogram() {
sRelinkerCountHistogram.record(1);
}
static void incrementRelinkerCountNotHitHistogram() {
sRelinkerCountHistogram.record(0);
}
// Experience shows that on some devices, the system sometimes fails to extract native libraries
// at installation or update time from the APK. This function will extract the library and
// return the extracted file path.
static String getExtractedLibraryPath(Context appContext, String libName) {
assert ResourceExtractor.PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION;
Log.w(TAG, "Failed to load libName %s, attempting fallback extraction then trying again",
libName);
String libraryEntry = LibraryLoader.makeLibraryPathInZipFile(libName, false, false);
return ResourceExtractor.extractFileIfStale(
appContext, libraryEntry, ResourceExtractor.makeLibraryDirAndSetPermission());
}
// Invoke either Linker.loadLibrary(...), System.loadLibrary(...) or System.load(...),
// triggering JNI_OnLoad in native code.
// TODO(crbug.com/635567): Fix this properly.
......@@ -396,9 +421,18 @@ public class LibraryLoader {
try {
// Load the library using this Linker. May throw UnsatisfiedLinkError.
loadLibraryWithCustomLinker(linker, zipFilePath, libFilePath);
incrementRelinkerCountNotHitHistogram();
} catch (UnsatisfiedLinkError e) {
Log.e(TAG, "Unable to load library: " + library);
throw(e);
if (!Linker.isInZipFile()
&& ResourceExtractor
.PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION) {
loadLibraryWithCustomLinker(
linker, null, getExtractedLibraryPath(appContext, library));
incrementRelinkerCountHitHistogram();
} else {
Log.e(TAG, "Unable to load library: " + library);
throw(e);
}
}
}
......@@ -416,11 +450,19 @@ public class LibraryLoader {
for (String library : NativeLibraries.LIBRARIES) {
try {
if (!Linker.isInZipFile()) {
// The extract and retry logic isn't needed because this path is
// used only for local development.
System.loadLibrary(library);
} else {
// Load directly from the APK.
boolean is64Bit = Process.is64Bit();
String zipFilePath = appContext.getApplicationInfo().sourceDir;
String libraryName = makeLibraryPathInZipFile(library, zipFilePath);
// In API level 23 and above, it’s possible to open a .so file
// directly from the APK of the path form
// "my_zip_file.zip!/libs/libstuff.so". See:
// https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md#opening-shared-libraries-directly-from-an-apk
String libraryName = zipFilePath + "!/"
+ makeLibraryPathInZipFile(library, true, is64Bit);
Log.i(TAG, "libraryName: " + libraryName);
System.load(libraryName);
}
......@@ -445,16 +487,15 @@ public class LibraryLoader {
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
/**
* @param library The library name that is looking for.
* @param crazyPrefix true iff adding crazy linker prefix to the file name.
* @param is64Bit true if the caller think it's run on a 64 bit device.
* @return the library path name in the zip file.
*/
@NonNull
private static String makeLibraryPathInZipFile(String library, String zipFilePath) {
assert Linker.isInZipFile();
// Determine whether the process is running in 32bit mode. The API is available starting
// from M, on L- there is no need to construct the full path inside the APK, so this
// path is omitted.
boolean is32BitProcess = !Process.is64Bit();
public static String makeLibraryPathInZipFile(
String library, boolean crazyPrefix, boolean is64Bit) {
// Determine the ABI string that Android uses to find native libraries. Values are described
// in: https://developer.android.com/ndk/guides/abis.html
// The 'armeabi' is omitted here because it is not supported in Chrome/WebView, while Cronet
......@@ -462,20 +503,27 @@ public class LibraryLoader {
String cpuAbi;
switch (NativeLibraries.sCpuFamily) {
case NativeLibraries.CPU_FAMILY_ARM:
cpuAbi = is32BitProcess ? "armeabi-v7a" : "arm64-v8a";
cpuAbi = is64Bit ? "arm64-v8a" : "armeabi-v7a";
break;
case NativeLibraries.CPU_FAMILY_X86:
cpuAbi = is32BitProcess ? "x86" : "x86_64";
cpuAbi = is64Bit ? "x86_64" : "x86";
break;
case NativeLibraries.CPU_FAMILY_MIPS:
cpuAbi = is32BitProcess ? "mips" : "mips64";
cpuAbi = is64Bit ? "mips64" : "mips";
break;
default:
throw new RuntimeException("Unknown CPU ABI for native libraries");
}
// Combine the above into the final path to the library in the APK.
return zipFilePath + "!/lib/" + cpuAbi + "/crazy." + System.mapLibraryName(library);
// When both the Chromium linker and zip-uncompressed native libraries are used,
// the build system renames the native shared libraries with a 'crazy.' prefix
// (e.g. "/lib/armeabi-v7a/libfoo.so" -> "/lib/armeabi-v7a/crazy.libfoo.so").
//
// This prevents the package manager from extracting them at installation/update time
// to the /data directory. The libraries can still be accessed directly by the Chromium
// linker from the APK.
String crazyPart = crazyPrefix ? "crazy." : "";
return String.format("lib/%s/%s%s", cpuAbi, crazyPart, System.mapLibraryName(library));
}
// The WebView requires the Command Line to be switched over before
......
......@@ -4,13 +4,16 @@
package org.chromium.base.library_loader;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResourceExtractor;
import org.chromium.base.annotations.AccessedByNative;
import java.util.HashMap;
......@@ -446,13 +449,23 @@ public abstract class Linker {
/**
* Load the Linker JNI library. Throws UnsatisfiedLinkError on error.
*/
@SuppressLint({"UnsafeDynamicallyLoadedCode"})
protected static void loadLinkerJniLibrary() {
LibraryLoader.setEnvForNative();
String libName = "lib" + LINKER_JNI_LIBRARY + ".so";
if (DEBUG) {
String libName = "lib" + LINKER_JNI_LIBRARY + ".so";
Log.i(TAG, "Loading " + libName);
}
System.loadLibrary(LINKER_JNI_LIBRARY);
try {
System.loadLibrary(LINKER_JNI_LIBRARY);
LibraryLoader.incrementRelinkerCountNotHitHistogram();
} catch (UnsatisfiedLinkError e) {
if (ResourceExtractor.PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION) {
System.load(LibraryLoader.getExtractedLibraryPath(
ContextUtils.getApplicationContext(), LINKER_JNI_LIBRARY));
LibraryLoader.incrementRelinkerCountHitHistogram();
}
}
}
/**
......
......@@ -3882,6 +3882,11 @@ uploading your change for review. These are checked by presubmit scripts.
<int value="1" label="Is Tagged"/>
</enum>
<enum name="BooleanIsUseRelinker">
<int value="0" label="Didn't use relinker"/>
<int value="1" label="Used relinker"/>
</enum>
<enum name="BooleanLatched">
<int value="0" label="Not latched"/>
<int value="1" label="Latched"/>
......@@ -10420,6 +10420,17 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries.
<summary>Load at fixed address failed.</summary>
</histogram>
<histogram name="ChromiumAndroidLinker.RelinkerFallbackCount"
enum="BooleanIsUseRelinker">
<owner>agrieve@chromium.org</owner>
<owner>ranj@chromium.org</owner>
<owner>yfriedman@chromium.org</owner>
<summary>
The total number of times Chrome uses relinker fallback to extract and load
native libraries.
</summary>
</histogram>
<histogram name="ChromiumAndroidLinker.RendererLoadTime" units="ms">
<owner>rsesek@chromium.org</owner>
<summary>
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