chromium/components/strictmode/android/java/src/org/chromium/components/strictmode/ReflectiveThreadStrictModeInterceptor.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.app.ApplicationErrorReport;
import android.os.Build;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.os.strictmode.DiskReadViolation;
import android.os.strictmode.DiskWriteViolation;
import android.os.strictmode.ResourceMismatchViolation;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import org.chromium.base.Log;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * StrictMode whitelist installer.
 *
 * <p><b>How this works:</b><br>
 * When StrictMode is enabled without the death penalty, it queues up all ThreadPolicy violations
 * into a ThreadLocal ArrayList, and then posts a Runnable to the start of the Looper queue to
 * process them. This is done in order to set a cap to the number of logged/handled violations per
 * event loop, and avoid overflowing the log buffer or other penalty handlers with violations. <br>
 * Because the violations are queued into a ThreadLocal ArrayList, they must be queued on the
 * offending thread, and thus the offending stack frame will exist in the stack trace. The
 * whitelisting mechanism works by using reflection to set a custom ArrayList into the ThreadLocal.
 * When StrictMode is adding a new item to the ArrayList, our custom ArrayList checks the stack
 * trace for any whitelisted frames, and if one is found, no-ops the addition. Then, when the
 * processing runnable executes, it sees there are no items, and no-ops. <br>
 * However, if the death penalty is enabled, the concern about handling too many violations no
 * longer exists (since death will occur after the first one), so the queue is bypassed, and death
 * occurs instantly without allowing the whitelisting system to intercept it. In order to retain the
 * death penalty, the whitelisting mechanism itself can be configured to execute the death penalty
 * after the first non-whitelisted violation.
 */
final class ReflectiveThreadStrictModeInterceptor implements ThreadStrictModeInterceptor {
    private static final String TAG = "ThreadStrictMode";

    @NonNull private final List<Function<Violation, Integer>> mWhitelistEntries;
    @Nullable private final Consumer mCustomPenalty;

    ReflectiveThreadStrictModeInterceptor(
            @NonNull List<Function<Violation, Integer>> whitelistEntries,
            @Nullable Consumer customPenalty) {
        mWhitelistEntries = whitelistEntries;
        mCustomPenalty = customPenalty;
    }

    @Override
    public void install(ThreadPolicy detectors) {
        // Use reflection on Android P despite the existence of
        // StrictMode.OnThreadViolationListener because the listener receives a stack
        // trace with stack frames prior to android.os.Handler calls stripped out.

        interceptWithReflection();
        StrictMode.setThreadPolicy(new ThreadPolicy.Builder(detectors).penaltyLog().build());
    }

    private void interceptWithReflection() {
        ThreadLocal<ArrayList<Object>> violationsBeingTimed;
        try {
            violationsBeingTimed = getViolationsBeingTimed();
        } catch (Exception e) {
            throw new RuntimeException(null, e);
        }
        violationsBeingTimed.get().clear();
        violationsBeingTimed.set(
                new ArrayList<Object>() {
                    @Override
                    public boolean add(Object o) {
                        int violationType = getViolationType(o);
                        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
                        Violation violation =
                                new Violation(
                                        violationType,
                                        Arrays.copyOf(stackTrace, stackTrace.length));
                        if (violationType != Violation.DETECT_FAILED
                                && violation.isInWhitelist(mWhitelistEntries)) {
                            return true;
                        }
                        if (mCustomPenalty != null) {
                            mCustomPenalty.accept(violation);
                        }
                        return super.add(o);
                    }
                });
    }

    @SuppressWarnings({"unchecked"})
    private static ThreadLocal<ArrayList<Object>> getViolationsBeingTimed()
            throws IllegalAccessException, NoSuchFieldException {
        Field violationTimingField = StrictMode.class.getDeclaredField("violationsBeingTimed");
        violationTimingField.setAccessible(true);
        return (ThreadLocal<ArrayList<Object>>) violationTimingField.get(null);
    }

    /** @param o {@code android.os.StrictMode.ViolationInfo} */
    @SuppressWarnings({"unchecked", "DiscouragedPrivateApi", "PrivateApi", "BlockedPrivateApi"})
    private int getViolationType(Object violationInfo) {
        try {
            Class<?> violationInfoClass = Class.forName("android.os.StrictMode$ViolationInfo");
            if (Build.VERSION.SDK_INT == 28) {
                Method getViolationBitMethod =
                        violationInfoClass.getDeclaredMethod("getViolationBit");
                getViolationBitMethod.setAccessible(true);
                int violationType = (Integer) getViolationBitMethod.invoke(violationInfo);
                return violationType & Violation.DETECT_ALL_KNOWN;
            } else if (Build.VERSION.SDK_INT == 29) {
                Method getViolationClassMethod =
                        violationInfoClass.getDeclaredMethod("getViolationClass");
                getViolationClassMethod.setAccessible(true);
                return computeViolationTypeAndroid10(
                        (Class<?>) getViolationClassMethod.invoke(violationInfo));
            } else if (Build.VERSION.SDK_INT >= 30) {
                // ViolationInfo#getViolationClass() is inaccessible via reflection.
                // crbug.com/1240777 Ignore violation type when checking white list.
                return Violation.DETECT_ALL_KNOWN;
            }
            Field crashInfoField = violationInfoClass.getDeclaredField("crashInfo");
            crashInfoField.setAccessible(true);
            ApplicationErrorReport.CrashInfo crashInfo =
                    (ApplicationErrorReport.CrashInfo) crashInfoField.get(violationInfo);
            Method parseViolationFromMessage =
                    StrictMode.class.getDeclaredMethod("parseViolationFromMessage", String.class);
            parseViolationFromMessage.setAccessible(true);
            int mask =
                    (int)
                            parseViolationFromMessage.invoke(
                                    /* static= */ null, crashInfo.exceptionMessage);
            return mask & Violation.DETECT_ALL_KNOWN;
        } catch (Exception e) {
            Log.e(TAG, "Unable to get violation.", e);
            return Violation.DETECT_FAILED;
        }
    }

    /** Computes the violation type based on the class of the passed-in violation. */
    @RequiresApi(29)
    private static int computeViolationTypeAndroid10(Class<?> violationClass) {
        if (DiskReadViolation.class.isAssignableFrom(violationClass)) {
            return Violation.DETECT_DISK_READ;
        } else if (DiskWriteViolation.class.isAssignableFrom(violationClass)) {
            return Violation.DETECT_DISK_WRITE;
        } else if (ResourceMismatchViolation.class.isAssignableFrom(violationClass)) {
            return Violation.DETECT_RESOURCE_MISMATCH;
        }
        return Violation.DETECT_FAILED;
    }
}