chromium/components/strictmode/android/java/src/org/chromium/components/strictmode/ThreadStrictModeInterceptor.java

// Copyright 2020 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.components.strictmode;

import android.os.Handler;
import android.os.Looper;
import android.os.StrictMode.ThreadPolicy;

import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

/** Installs a whitelist configuration for StrictMode's ThreadPolicy feature. */
public interface ThreadStrictModeInterceptor {
    /**
     * Install this interceptor and it's whitelists.
     *
     * Pre-P, this uses reflection.
     */
    void install(ThreadPolicy detectors);

    /**
     * Builds ThreadStrictModeInterceptor with the death penalty and with
     * {@link KnownViolations} exempted.
     */
    public static ThreadStrictModeInterceptor buildWithDeathPenaltyAndKnownViolationExemptions() {
        ThreadStrictModeInterceptor.Builder threadInterceptor =
                new ThreadStrictModeInterceptor.Builder();
        threadInterceptor.replaceAllPenaltiesWithDeathPenalty();
        KnownViolations.addExemptions(threadInterceptor);
        return threadInterceptor.build();
    }

    /**
     * Builds a configuration for StrictMode enforcement.
     *
     * The API (but not the implementation) should stay in sync with the API used by
     * 'KnownViolations' for other apps (http://go/chrome-known-violations-upstream).
     */
    final class Builder {
        private final List<Function<Violation, Integer>> mWhitelistEntries = new ArrayList<>();
        private @Nullable Consumer<Violation> mCustomPenalty;

        /**
         * Ignores all StrictMode violations for which {@link filterPackageName} is not part of the
         * stack trace.
         *
         * Also ignores StrictMode violations where:
         * 1) {@link filterPackageName} calls {@link blocklistCalleePackageName}
         * AND
         * 2) The violation is caused in code called by {@link blocklistCalleePackageName}.
         *
         * This scenario occurs when {@link blocklistCalleePackageName} registers an observer with
         * {@link filterPackageName} and the strict mode violation is in the observer code.
         */
        public Builder onlyDetectViolationsForPackage(
                final String filterPackageName, final String blocklistCalleePackageName) {
            mWhitelistEntries.add(
                    violation -> {
                        for (StackTraceElement frame : violation.stackTrace()) {
                            if (frame.getClassName().startsWith(blocklistCalleePackageName)) {
                                return Violation.DETECT_ALL_KNOWN;
                            }
                            if (frame.getClassName().startsWith(filterPackageName)) {
                                return null;
                            }
                        }
                        return Violation.DETECT_ALL_KNOWN;
                    });
            return this;
        }

        /**
         * Ignore a violation that occurs outside of your app.
         *
         * @param violationType A mask containing one or more of the DETECT_* constants.
         * @param packageName The name of the package to ignore StrictMode violations
         *     for example, "org.chromium.foo"
         */
        public Builder ignoreExternalPackage(int violationType, final String packageName) {
            mWhitelistEntries.add(
                    violation -> {
                        if ((violation.violationType() & violationType) == 0) {
                            return null;
                        }
                        return doesStackTraceContainPackage(violation, packageName)
                                ? violationType
                                : null;
                    });
            return this;
        }

        /**
         * Returns whether the passed-in {@link Violation}'s stack trace contains a stack
         * frame within the passed-in package.
         */
        private static boolean doesStackTraceContainPackage(
                Violation violation, String packageName) {
            for (StackTraceElement frame : violation.stackTrace()) {
                if (frame.getClassName().startsWith(packageName)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * Ignore a violation that occurs outside of your app.
         *
         * @param violationType A mask containing one or more of the DETECT_* constants.
         * @param className The name of the class to ignore StrictMode violations
         *     for example, "org.chromium.foo.ThreadStrictModeInterceptor"
         */
        public Builder ignoreExternalClass(int violationType, final String className) {
            mWhitelistEntries.add(
                    violation -> {
                        if ((violation.violationType() & violationType) == 0) {
                            return null;
                        }
                        for (StackTraceElement frame : violation.stackTrace()) {
                            if (frame.getClassName().equals(className)) {
                                return violationType;
                            }
                        }
                        return null;
                    });
            return this;
        }

        /**
         * Ignore a violation that occurs outside of your app.
         *
         * @param violationType A mask containing one or more of the DETECT_* constants.
         * @param classNameWithMethod The name of the class and method to ignore StrictMode
         *         violations
         *     in. The format must be "package.Class#method", for example,
         *     "com.google.foo.ThreadStrictModeInterceptor#addAllowedMethod".
         */
        public Builder ignoreExternalMethod(int violationType, final String classNameWithMethod) {
            String[] parts = classNameWithMethod.split("#");
            String className = parts[0];
            String methodName = parts[1];
            mWhitelistEntries.add(
                    violation -> {
                        if ((violation.violationType() & violationType) == 0) {
                            return null;
                        }
                        for (StackTraceElement frame : violation.stackTrace()) {
                            if (frame.getClassName().equals(className)
                                    && frame.getMethodName().equals(methodName)) {
                                return violationType;
                            }
                        }
                        return null;
                    });
            return this;
        }

        /**
         * Ignore a violation that occurs inside your app.
         *
         * @param violationType A mask containing one or more of the DETECT_* constants.
         * @param classNameWithMethod The name of the class and method to ignore StrictMode
         *         violations
         *     in. The format must be "package.Class#method", for example,
         *     "com.google.foo.StrictModeWhitelist#addAllowedMethod".
         */
        public Builder addAllowedMethod(int violationType, final String classNameWithMethod) {
            return ignoreExternalMethod(violationType, classNameWithMethod);
        }

        /** Set the custom penalty that will be notified when an unwhitelisted violation occurs. */
        public Builder setCustomPenalty(Consumer<Violation> penalty) {
            mCustomPenalty = penalty;
            return this;
        }

        /**
         * Replaces all penalties with the death penalty.
         *
         * <p>Installing whitelists requires that StrictMode does not have the death penalty. If
         * your app requires the death penalty, you can set this, which will attempt to emulate the
         * system behavior if possible.
         *
         * <p>Death is not guaranteed, since it relies on reflection to work.
         */
        public Builder replaceAllPenaltiesWithDeathPenalty() {
            mCustomPenalty =
                    info -> {
                        StrictModePolicyViolation toThrow = new StrictModePolicyViolation(info);
                        // Post task so that no one has a chance to catch the thrown exception.
                        new Handler(Looper.getMainLooper())
                                .post(
                                        () -> {
                                            throw toThrow;
                                        });
                    };
            return this;
        }

        /** Make immutable. */
        public ThreadStrictModeInterceptor build() {
            return new ReflectiveThreadStrictModeInterceptor(mWhitelistEntries, mCustomPenalty);
        }
    }
}