// 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.chrome.browser;
import android.app.ApplicationErrorReport;
import android.os.Build;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.StrictMode;
import android.support.annotation.UiThread;
import org.chromium.base.BuildConfig;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.library_loader.LibraryLoader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Initialize application-level StrictMode reporting.
*/
public class ChromeStrictMode {
private static final String TAG = "ChromeStrictMode";
private static final double UPLOAD_PROBABILITY = 0.01;
private static final double MAX_UPLOADS_PER_SESSION = 3;
private static boolean sIsStrictModeAlreadyConfigured = false;
private static List<Object> sCachedStackTraces =
Collections.synchronizedList(new ArrayList<Object>());
private static AtomicInteger sNumUploads = new AtomicInteger();
private static class SnoopingArrayList<T> extends ArrayList<T> {
@Override
public void clear() {
for (int i = 0; i < size(); i++) {
// It is likely that we have at most one violation pass this check each time around.
if (Math.random() < UPLOAD_PROBABILITY) {
// Ensure that we do not upload too many StrictMode violations in any single
// session. To prevent races, we allow sNumUploads to increase beyond the
// limit, but just skip actually uploading the stack trace then.
if (sNumUploads.getAndAdd(1) >= MAX_UPLOADS_PER_SESSION) {
break;
}
sCachedStackTraces.add(get(i));
}
}
super.clear();
}
}
/**
* Always process the violation on the UI thread. This ensures other crash reports are not
* corrupted. Since each individual user has a very small chance of uploading each violation,
* and we have a hard cap of 3 per session, this will not affect performance too much.
*
* @param violationInfo The violation info from the StrictMode violation in question.
*/
@UiThread
private static void reportStrictModeViolation(Object violationInfo) {
try {
Field crashInfoField = violationInfo.getClass().getField("crashInfo");
ApplicationErrorReport.CrashInfo crashInfo =
(ApplicationErrorReport.CrashInfo) crashInfoField.get(violationInfo);
String stackTrace = crashInfo.stackTrace;
if (stackTrace == null) {
Log.d(TAG, "StrictMode violation stack trace was null.");
} else {
Log.d(TAG, "Upload stack trace: " + stackTrace);
JavaExceptionReporter.reportStackTrace(stackTrace);
}
} catch (Exception e) {
// Ignore all exceptions.
Log.d(TAG, "Could not handle observed StrictMode violation.", e);
}
}
/**
* Replace Android OS's StrictMode.violationsBeingTimed with a custom ArrayList acting as an
* observer into violation stack traces. Set up an idle handler so StrictMode violations that
* occur on startup are not ignored.
*/
@SuppressWarnings({"unchecked", "rawtypes" })
@UiThread
private static void initializeStrictModeWatch() {
try {
Field violationsBeingTimedField =
StrictMode.class.getDeclaredField("violationsBeingTimed");
violationsBeingTimedField.setAccessible(true);
ThreadLocal<ArrayList> violationsBeingTimed =
(ThreadLocal<ArrayList>) violationsBeingTimedField.get(null);
ArrayList replacementList = new SnoopingArrayList();
violationsBeingTimed.set(replacementList);
} catch (Exception e) {
// Terminate watch if any exceptions are raised.
Log.w(TAG, "Could not initialize StrictMode watch.", e);
return;
}
sNumUploads.set(0);
// Delay handling StrictMode violations during initialization until the main loop is idle.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// Will retry if the native library has not been initialized.
if (!LibraryLoader.isInitialized()) return true;
// Check again next time if no more cached stack traces to upload, and we have not
// reached the max number of uploads for this session.
if (sCachedStackTraces.isEmpty()) {
// TODO(wnwen): Add UMA count when this happens.
// In case of races, continue checking an extra time (equal condition).
return sNumUploads.get() <= MAX_UPLOADS_PER_SESSION;
}
// Since this is the only place we are removing elements, no need for additional
// synchronization to ensure it is still non-empty.
reportStrictModeViolation(sCachedStackTraces.remove(0));
return true;
}
});
}
private static void turnOnDetection(StrictMode.ThreadPolicy.Builder threadPolicy,
StrictMode.VmPolicy.Builder vmPolicy) {
threadPolicy.detectAll();
if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
vmPolicy.detectAll();
} else {
// Explicitly enable detection of all violations except file URI leaks, as that
// results in false positives when file URI intents are passed between Chrome
// activities in separate processes. See http://crbug.com/508282#c11.
vmPolicy.detectActivityLeaks()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects();
}
}
private static void addDefaultPenalties(StrictMode.ThreadPolicy.Builder threadPolicy,
StrictMode.VmPolicy.Builder vmPolicy) {
threadPolicy.penaltyLog().penaltyFlashScreen().penaltyDeathOnNetwork();
vmPolicy.penaltyLog();
}
private static void addThreadDeathPenalty(StrictMode.ThreadPolicy.Builder threadPolicy) {
threadPolicy.penaltyDeath();
}
private static void addVmDeathPenalty(StrictMode.VmPolicy.Builder vmPolicy) {
vmPolicy.penaltyDeath();
}
/**
* Turn on StrictMode detection based on build and command-line switches.
*/
@UiThread
// FindBugs doesn't like conditionals with compile time results
@SuppressFBWarnings("UCF_USELESS_CONTROL_FLOW")
public static void configureStrictMode() {
assert ThreadUtils.runningOnUiThread();
if (sIsStrictModeAlreadyConfigured) {
return;
}
sIsStrictModeAlreadyConfigured = true;
StrictMode.ThreadPolicy.Builder threadPolicy =
new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy());
StrictMode.VmPolicy.Builder vmPolicy =
new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy());
CommandLine commandLine = CommandLine.getInstance();
if ("eng".equals(Build.TYPE)
|| BuildConfig.DCHECK_IS_ON
|| ChromeVersionInfo.isLocalBuild()
|| commandLine.hasSwitch(ChromeSwitches.STRICT_MODE)) {
turnOnDetection(threadPolicy, vmPolicy);
addDefaultPenalties(threadPolicy, vmPolicy);
if ("death".equals(commandLine.getSwitchValue(ChromeSwitches.STRICT_MODE))) {
addThreadDeathPenalty(threadPolicy);
addVmDeathPenalty(vmPolicy);
} else if ("testing".equals(commandLine.getSwitchValue(ChromeSwitches.STRICT_MODE))) {
addThreadDeathPenalty(threadPolicy);
// Currently VmDeathPolicy kills the process, and is not visible on bot test output.
}
}
// Enroll 1% of dev sessions into StrictMode watch. This is done client-side rather than
// through finch because this decision is as early as possible in the browser initialization
// process. We need to detect early start-up StrictMode violations before loading native and
// before warming the SharedPreferences (that is a violation in an of itself). We will
// closely monitor this on dev channel.
boolean enableStrictModeWatch =
(ChromeVersionInfo.isDevBuild() && Math.random() < UPLOAD_PROBABILITY);
if ((ChromeVersionInfo.isLocalBuild() && !BuildConfig.DCHECK_IS_ON)
|| enableStrictModeWatch) {
turnOnDetection(threadPolicy, vmPolicy);
initializeStrictModeWatch();
}
StrictMode.setThreadPolicy(threadPolicy.build());
StrictMode.setVmPolicy(vmPolicy.build());
}
}