Commit 07932885 authored by mnaganov's avatar mnaganov Committed by Commit bot

[Android WebView] Forbid launching of two browser processes in the app

Use FileLock in the data directory to enforce exclusive access to it.

Attempting to share the data directory between two browser processes
will most likely lead to data corruption.

BUG=558377

Review URL: https://codereview.chromium.org/1555513003

Cr-Commit-Position: refs/heads/master@{#367120}
parent 2c86c0a1
......@@ -12,9 +12,10 @@ import org.chromium.content.browser.ContentViewStatics;
/**
* Java side of the Browser Context: contains all the java side objects needed to host one
* browing session (i.e. profile).
* Note that due to running in single process mode, and limitations on renderer process only
* being able to use a single browser context, currently there can only be one AwBrowserContext
* instance, so at this point the class mostly exists for conceptual clarity.
*
* Note that historically WebView was running in single process mode, and limitations on renderer
* process only being able to use a single browser context, currently there can only be one
* AwBrowserContext instance, so at this point the class mostly exists for conceptual clarity.
*/
public class AwBrowserContext {
private static final String HTTP_AUTH_DATABASE_FILE = "http_auth.db";
......
......@@ -8,6 +8,7 @@ import android.content.Context;
import org.chromium.android_webview.policy.AwPolicyProvider;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
......@@ -16,6 +17,11 @@ import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.content.browser.BrowserStartupController;
import org.chromium.policy.CombinedPolicyProvider;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
/**
* Wrapper for the steps needed to initialize the java and native sides of webview chromium.
*/
......@@ -23,6 +29,8 @@ public abstract class AwBrowserProcess {
public static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "webview";
private static final String TAG = "AwBrowserProcess";
private static final String EXCLUSIVE_LOCK_FILE = "webview_data.lock";
private static FileLock sExclusiveFileLock;
/**
* Loads the native library, and performs basic static construction of objects needed
......@@ -50,6 +58,7 @@ public abstract class AwBrowserProcess {
* @param context The Android application context
*/
public static void start(final Context context) {
tryObtainingDataDirLockOrDie(context);
// We must post to the UI thread to cover the case that the user
// has invoked Chromium startup by using the (thread-safe)
// CookieManager rather than creating a WebView.
......@@ -71,4 +80,23 @@ public abstract class AwBrowserProcess {
}
});
}
private static void tryObtainingDataDirLockOrDie(Context context) {
String dataPath = PathUtils.getDataDirectory(context);
File lockFile = new File(dataPath, EXCLUSIVE_LOCK_FILE);
boolean success = false;
try {
// Note that the file is not closed intentionally.
RandomAccessFile file = new RandomAccessFile(lockFile, "rw");
sExclusiveFileLock = file.getChannel().tryLock();
success = sExclusiveFileLock != null;
} catch (IOException e) {
Log.w(TAG, "Failed to create lock file " + lockFile, e);
}
if (!success) {
throw new RuntimeException(
"Could not obtain an exclusive lock on the data dir. The app may have "
+ "another WebView opened in a separate process");
}
}
}
// Copyright 2015 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 org.chromium.android_webview.test;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.test.suitebuilder.annotation.LargeTest;
import org.chromium.android_webview.AwBrowserProcess;
import org.chromium.base.test.util.Feature;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Tests to ensure that it is impossible to launch two browser processes within
* the same application. Chromium is not designed for that, and attempting to do that
* can cause data files corruption.
*/
public class AwSecondBrowserProcessTest extends AwTestBase {
private CountDownLatch mSecondBrowserProcessLatch;
private int mSecondBrowserServicePid;
@Override
protected boolean needsBrowserProcessStarted() {
return false;
}
@Override
protected void tearDown() throws Exception {
stopSecondBrowserProcess(false);
super.tearDown();
}
@LargeTest
@Feature({"AndroidWebView"})
public void testCreatingSecondBrowserProcessFails() throws Throwable {
startSecondBrowserProcess();
assertFalse(tryStartingBrowserProcess());
}
@LargeTest
@Feature({"AndroidWebView"})
public void testLockCleanupOnProcessShutdown() throws Throwable {
startSecondBrowserProcess();
assertFalse(tryStartingBrowserProcess());
stopSecondBrowserProcess(true);
assertTrue(tryStartingBrowserProcess());
}
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Parcel result = Parcel.obtain();
try {
assertTrue(service.transact(
SecondBrowserProcess.CODE_START, Parcel.obtain(), result, 0));
} catch (RemoteException e) {
fail("RemoteException: " + e);
}
result.readException();
mSecondBrowserServicePid = result.readInt();
assertTrue(mSecondBrowserServicePid > 0);
mSecondBrowserProcessLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName className) {
}
};
private void startSecondBrowserProcess() throws Exception {
Context context = getActivity();
Intent intent = new Intent(context, SecondBrowserProcess.class);
mSecondBrowserProcessLatch = new CountDownLatch(1);
assertNotNull(context.startService(intent));
assertTrue(context.bindService(intent, mConnection, 0));
assertTrue(mSecondBrowserProcessLatch.await(
AwTestBase.WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
mSecondBrowserProcessLatch = null;
}
private void stopSecondBrowserProcess(boolean sync) throws Exception {
if (mSecondBrowserServicePid <= 0) return;
assertTrue(isSecondBrowserServiceRunning());
// Note that using killProcess ensures that the service record gets removed
// from ActivityManager after the process has actually died. While using
// Context.stopService would result in the opposite outcome.
Process.killProcess(mSecondBrowserServicePid);
if (sync) {
poll(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return !isSecondBrowserServiceRunning();
}
});
}
mSecondBrowserServicePid = 0;
}
private boolean tryStartingBrowserProcess() {
final Context context = getActivity();
final Boolean success[] = new Boolean[1];
// runOnMainSync does not catch RuntimeExceptions, they just terminate the test.
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
try {
AwBrowserProcess.start(context);
success[0] = true;
} catch (RuntimeException e) {
success[0] = false;
}
}
});
assertNotNull(success[0]);
return success[0];
}
// Note that both onServiceDisconnected and Binder.DeathRecipient fire prematurely for our
// purpose. We need to ensure that the service process has actually terminated, releasing all
// the locks. The only reliable way to do that is to scan the process list.
private boolean isSecondBrowserServiceRunning() {
ActivityManager activityManager =
(ActivityManager) getActivity().getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo si : activityManager.getRunningServices(65536)) {
if (si.pid == mSecondBrowserServicePid) return true;
}
return false;
}
}
......@@ -49,5 +49,10 @@
android:exported="false" />
<meta-data android:name="org.chromium.content.browser.NUM_PRIVILEGED_SERVICES"
android:value="0"/>
<!-- See AwSecondBrowserProcessTest -->
<service android:name="org.chromium.android_webview.test.SecondBrowserProcess"
android:process=":second_browser_process"
android:isolatedProcess="false"
android:exported="false" />
</application>
</manifest>
// Copyright 2015 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 org.chromium.android_webview.test;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import org.chromium.android_webview.AwBrowserProcess;
import org.chromium.android_webview.AwResource;
import org.chromium.android_webview.shell.R;
import org.chromium.base.CommandLine;
import org.chromium.base.annotations.SuppressFBWarnings;
/**
* This is a service for imitating a second browser process in the application.
*/
public class SecondBrowserProcess extends Service {
public static final int CODE_START = IBinder.FIRST_CALL_TRANSACTION;
private IBinder mBinder = new Binder() {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
switch (code) {
case CODE_START:
reply.writeNoException();
try {
startBrowserProcess();
reply.writeInt(Process.myPid());
} catch (Exception e) {
reply.writeInt(0);
}
return true;
}
return false;
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
private void startBrowserProcess() throws Exception {
CommandLine.initFromFile("/data/local/tmp/android-webview-command-line");
AwResource.setResources(this.getResources());
AwResource.setConfigKeySystemUuidMapping(R.array.config_key_system_uuid_mapping);
AwBrowserProcess.loadLibrary(this);
AwBrowserProcess.start(this);
}
}
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