chromium/chrome/android/java/src/org/chromium/chrome/browser/ChromeStrictMode.java

// Copyright 2015 The Chromium Authors
// 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.os.Build;
import android.os.Looper;
import android.os.StrictMode;
import android.text.TextUtils;

import androidx.annotation.UiThread;

import org.chromium.base.CommandLine;
import org.chromium.base.JavaExceptionReporter;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.version_info.VersionInfo;
import org.chromium.build.BuildConfig;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.components.strictmode.StrictModePolicyViolation;
import org.chromium.components.strictmode.Violation;

import java.io.PrintWriter;
import java.io.StringWriter;
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;
    private static List<Violation> sCachedViolations =
            Collections.synchronizedList(new ArrayList<>());
    private static AtomicInteger sNumUploads = new AtomicInteger();

    /**
     * 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(Violation violation) {
        StringWriter stackTraceWriter = new StringWriter();
        new StrictModePolicyViolation(violation).printStackTrace(new PrintWriter(stackTraceWriter));
        String stackTrace = stackTraceWriter.toString();
        if (TextUtils.isEmpty(stackTrace)) {
            Log.d(TAG, "StrictMode violation stack trace was empty.");
        } else {
            Log.d(TAG, "Upload stack trace: " + stackTrace);
            JavaExceptionReporter.reportStackTrace(stackTrace);
        }
    }

    /** Set up an idle handler so StrictMode violations that occur on startup are not ignored. */
    @UiThread
    private static void initializeStrictModeWatch() {
        sNumUploads.set(0);
        // Delay handling StrictMode violations during initialization until the main loop is idle.
        Looper.myQueue()
                .addIdleHandler(
                        () -> {
                            // Will retry if the native library has not been initialized.
                            if (!LibraryLoader.getInstance().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 (sCachedViolations.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(sCachedViolations.remove(0));
                            return true;
                        });
    }

    private static void turnOnDetection(StrictMode.VmPolicy.Builder vmPolicy) {
        // Do not enable detectUntaggedSockets(). It does not support native (the vast majority
        // of our sockets), and we have not bothered to tag our Java uses.
        // https://crbug.com/770792
        // Also: Do not enable detectCleartextNetwork(). We can't prevent websites from using
        // non-https.
        vmPolicy.detectActivityLeaks()
                .detectLeakedClosableObjects()
                .detectLeakedRegistrationObjects()
                .detectLeakedSqlLiteObjects();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Introduced in O.
            vmPolicy.detectContentUriWithoutPermission();
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Introduced in Q.
            vmPolicy.detectCredentialProtectedWhileLocked().detectImplicitDirectBoot();
        }

        // File URI leak detection, has false positives when file URI intents are passed between
        // Chrome activities in separate processes. See http://crbug.com/508282#c11.
        vmPolicy.detectFileUriExposure();
    }

    private static void addDefaultVmPenalties(StrictMode.VmPolicy.Builder vmPolicy) {
        vmPolicy.penaltyLog();
    }

    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
    public static void configureStrictMode() {
        assert ThreadUtils.runningOnUiThread();
        if (sIsStrictModeAlreadyConfigured) {
            return;
        }
        sIsStrictModeAlreadyConfigured = true;

        // Check is present so that proguard deletes the strict mode detection code for non-local
        // non-dev-channel release builds. This is needed because ChromeVersionInfo#isLocalBuild()
        // is not listed with -assumenosideeffects in the proguard configuration
        if (!ChromeStrictModeSwitch.ALLOW_STRICT_MODE_CHECKING) return;

        CommandLine commandLine = CommandLine.getInstance();
        boolean shouldApplyPenalties =
                BuildConfig.ENABLE_ASSERTS
                        || VersionInfo.isLocalBuild()
                        || commandLine.hasSwitch(ChromeSwitches.STRICT_MODE);

        // 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 =
                (VersionInfo.isLocalBuild() && !BuildConfig.ENABLE_ASSERTS)
                        || (VersionInfo.isDevBuild() && Math.random() < UPLOAD_PROBABILITY);
        if (!shouldApplyPenalties && !enableStrictModeWatch) return;

        StrictMode.VmPolicy.Builder vmPolicy =
                new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy());
        turnOnDetection(vmPolicy);

        if (shouldApplyPenalties) {
            addDefaultVmPenalties(vmPolicy);
            if ("death".equals(commandLine.getSwitchValue(ChromeSwitches.STRICT_MODE))) {
                addVmDeathPenalty(vmPolicy);
            } else if ("testing".equals(commandLine.getSwitchValue(ChromeSwitches.STRICT_MODE))) {
                // Currently VmDeathPolicy kills the process, and is not visible on bot test output.
            }
        }
        if (enableStrictModeWatch) {
            initializeStrictModeWatch();
        }

        StrictMode.setVmPolicy(vmPolicy.build());
    }
}