diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index e12181a08db377debc26615ff6b5099d9efa6162..3b6ea14ab0b6afa62d1d760ba68cdf567de85a27 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -135,6 +135,7 @@ import android.os.GraphicsEnvironment;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IBinder;
+import android.os.IBinderCallback;
 import android.os.ICancellationSignal;
 import android.os.LocaleList;
 import android.os.Looper;
@@ -7274,6 +7275,18 @@ public final class ActivityThread extends ClientTransactionHandler
         } catch (RemoteException ex) {
             throw ex.rethrowFromSystemServer();
         }
+
+        // Set binder transaction callback after finishing bindApplication
+        Binder.setTransactionCallback(new IBinderCallback() {
+            @Override
+            public void onTransactionError(int pid, int code, int flags, int err) {
+                try {
+                    mgr.frozenBinderTransactionDetected(pid, code, flags, err);
+                } catch (RemoteException ex) {
+                    throw ex.rethrowFromSystemServer();
+                }
+            }
+        });
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index d15c79f41d3f08c13c237f47f711f76353126299..24cb9ea87a12b78a12210a174c58eba46c8f64b2 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -477,6 +477,16 @@ public final class ApplicationExitInfo implements Parcelable {
      */
     public static final int SUBREASON_OOM_KILL = 30;
 
+    /**
+     * The process was killed because its async kernel binder buffer is running out
+     * while being frozen.
+     * this would be set only when the reason is {@link #REASON_FREEZER}.
+     *
+     * For internal use only.
+     * @hide
+     */
+    public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31;
+
     // If there is any OEM code which involves additional app kill reasons, it should
     // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
 
@@ -654,6 +664,7 @@ public final class ApplicationExitInfo implements Parcelable {
         SUBREASON_UNDELIVERED_BROADCAST,
         SUBREASON_EXCESSIVE_BINDER_OBJECTS,
         SUBREASON_OOM_KILL,
+        SUBREASON_FREEZER_BINDER_ASYNC_FULL,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SubReason {}
@@ -1383,6 +1394,8 @@ public final class ApplicationExitInfo implements Parcelable {
                 return "EXCESSIVE BINDER OBJECTS";
             case SUBREASON_OOM_KILL:
                 return "OOM KILL";
+            case SUBREASON_FREEZER_BINDER_ASYNC_FULL:
+                return "FREEZER BINDER ASYNC FULL";
             default:
                 return "UNKNOWN";
         }
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 03baf26713140b94506cecb8e756577dede3d329..520bf7dc890cd28b6bf8497b7a2c939534e71707 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -939,4 +939,14 @@ interface IActivityManager {
     int[] getUidFrozenState(in int[] uids);
 
     int checkPermissionForDevice(in String permission, int pid, int uid, int deviceId);
+
+    /**
+     * Notify AMS about binder transactions to frozen apps.
+     *
+     * @param debugPid The binder transaction sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err);
 }
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 218d4bb8514fce3eab493c8ff25deaf5cfd5cf77..3db1cb0500f97f1cbf71a7104a57bfe11831c99a 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -665,6 +665,32 @@ public class Binder implements IBinder {
      */
     public static final native void blockUntilThreadAvailable();
 
+
+    /**
+     * TODO (b/308179628): Move this to libbinder for non-Java usages.
+     */
+    private static IBinderCallback sBinderCallback = null;
+
+    /**
+     * Set callback function for unexpected binder transaction errors.
+     *
+     * @hide
+     */
+    public static final void setTransactionCallback(IBinderCallback callback) {
+        sBinderCallback = callback;
+    }
+
+    /**
+     * Execute the callback function if it's already set.
+     *
+     * @hide
+     */
+    public static final void transactionCallback(int pid, int code, int flags, int err) {
+        if (sBinderCallback != null) {
+            sBinderCallback.onTransactionError(pid, code, flags, err);
+        }
+    }
+
     /**
      * Default constructor just initializes the object.
      *
diff --git a/core/java/android/os/IBinderCallback.java b/core/java/android/os/IBinderCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4be5b02e7e021449c6f5ec59b7bd196ffc57968
--- /dev/null
+++ b/core/java/android/os/IBinderCallback.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * Callback interface for binder transaction errors
+ *
+ * @hide
+ */
+public interface IBinderCallback {
+    /**
+     * Callback function for unexpected binder transaction errors.
+     *
+     * @param debugPid The binder transaction sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    void onTransactionError(int debugPid, int code, int flags, int err);
+}
diff --git a/core/java/com/android/internal/os/BinderfsStatsReader.java b/core/java/com/android/internal/os/BinderfsStatsReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cc4a35b5c6591cc45ab92d7544d41fb3bd21ef1
--- /dev/null
+++ b/core/java/com/android/internal/os/BinderfsStatsReader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.os;
+
+import com.android.internal.util.ProcFileReader;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Reads and parses {@code binder_logs/stats} file in the {@code binderfs} filesystem.
+ * Reuse procFileReader as the contents are generated by Linux kernel in the same way.
+ *
+ * A typical example of binderfs stats log
+ *
+ * binder stats:
+ * BC_TRANSACTION: 378004
+ * BC_REPLY: 268352
+ * BC_FREE_BUFFER: 665854
+ * ...
+ * proc 12645
+ * context binder
+ * threads: 12
+ * requested threads: 0+5/15
+ * ready threads 0
+ * free async space 520192
+ * ...
+ */
+public class BinderfsStatsReader {
+    private final String mPath;
+
+    public BinderfsStatsReader() {
+        mPath = "/dev/binderfs/binder_logs/stats";
+    }
+
+    public BinderfsStatsReader(String path) {
+        mPath = path;
+    }
+
+    /**
+     * Read binderfs stats and call the consumer(pid, free) function for each valid process
+     *
+     * @param predicate  Test if the pid is valid.
+     * @param biConsumer Callback function for each valid pid and its free async space
+     * @param consumer   The error function to deal with exceptions
+     */
+    public void handleFreeAsyncSpace(Predicate<Integer> predicate,
+            BiConsumer<Integer, Integer> biConsumer, Consumer<Exception> consumer) {
+        try (ProcFileReader mReader = new ProcFileReader(new FileInputStream(mPath))) {
+            while (mReader.hasMoreData()) {
+                // find the next process
+                if (!mReader.nextString().equals("proc")) {
+                    mReader.finishLine();
+                    continue;
+                }
+
+                // read pid
+                int pid = mReader.nextInt();
+                mReader.finishLine();
+
+                // check if we have interest in this process
+                if (!predicate.test(pid)) {
+                    continue;
+                }
+
+                // read free async space
+                mReader.finishLine(); // context binder
+                mReader.finishLine(); // threads:
+                mReader.finishLine(); // requested threads:
+                mReader.finishLine(); // ready threads
+                if (!mReader.nextString().equals("free")) {
+                    mReader.finishLine();
+                    continue;
+                }
+                if (!mReader.nextString().equals("async")) {
+                    mReader.finishLine();
+                    continue;
+                }
+                if (!mReader.nextString().equals("space")) {
+                    mReader.finishLine();
+                    continue;
+                }
+                int free = mReader.nextInt();
+                mReader.finishLine();
+                biConsumer.accept(pid, free);
+            }
+        } catch (IOException | NumberFormatException e) {
+            consumer.accept(e);
+        }
+    }
+}
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index bfd80a9e4f741ccb1a0765e7cace57458fbd33f5..d2d5186eb8a1b5fe5a3097dfad195c41242ae49f 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -73,6 +73,7 @@ static struct bindernative_offsets_t
     jclass mClass;
     jmethodID mExecTransact;
     jmethodID mGetInterfaceDescriptor;
+    jmethodID mTransactionCallback;
 
     // Object state.
     jfieldID mObject;
@@ -1173,6 +1174,8 @@ static int int_register_android_os_Binder(JNIEnv* env)
     gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
     gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor",
         "()Ljava/lang/String;");
+    gBinderOffsets.mTransactionCallback =
+            GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V");
     gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
 
     return RegisterMethodsOrDie(
@@ -1387,7 +1390,12 @@ static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
 
     if (err == NO_ERROR) {
         return JNI_TRUE;
-    } else if (err == UNKNOWN_TRANSACTION) {
+    }
+
+    env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(),
+                              code, flags, err);
+
+    if (err == UNKNOWN_TRANSACTION) {
         return JNI_FALSE;
     }
 
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e9f6450c13aabe0b63b4a3147728f326227db68e
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.os;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.util.IntArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BinderfsStatsReaderTest {
+    private static final String BINDER_LOGS_STATS_HEADER = """
+            binder stats:
+            BC_TRANSACTION: 695756
+            BC_REPLY: 547779
+            BC_FREE_BUFFER: 1283223
+            BR_FAILED_REPLY: 4
+            BR_FROZEN_REPLY: 3
+            BR_ONEWAY_SPAM_SUSPECT: 1
+            proc: active 313 total 377
+            thread: active 3077 total 5227
+            """;
+    private static final String BINDER_LOGS_STATS_PROC1 = """
+            proc 14505
+            context binder
+              threads: 4
+              requested threads: 0+2/15
+              ready threads 0
+              free async space 520192
+              nodes: 9
+              refs: 29 s 29 w 29
+              buffers: 0
+            """;
+    private static final String BINDER_LOGS_STATS_PROC2 = """
+            proc 14461
+            context binder
+              threads: 8
+              requested threads: 0+2/15
+              ready threads 0
+              free async space 62
+              nodes: 30
+              refs: 51 s 51 w 51
+              buffers: 0
+            """;
+    private static final String BINDER_LOGS_STATS_PROC3 = """
+            proc 542
+            context binder
+              threads: 2
+              requested threads: 0+0/15
+              ready threads 0
+              free async space 519896
+              nodes: 1
+              refs: 2 s 3 w 2
+              buffers: 1
+            """;
+    private static final String BINDER_LOGS_STATS_PROC4 = """
+            proc 540
+            context binder
+              threads: 1
+              requested threads: 0+0/0
+              ready threads 1
+              free async space 44
+              nodes: 4
+              refs: 1 s 1 w 1
+              buffers: 0
+            """;
+    private File mStatsDirectory;
+    private int mFreezerBinderAsyncThreshold;
+    private IntArray mValidPids; // The pool of valid pids
+    private IntArray mStatsPids; // The pids read from binderfs stats that are also valid
+    private IntArray mStatsFree; // The free async space of the above pids
+    private boolean mHasError;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getContext();
+        mStatsDirectory = context.getDir("binder_logs", Context.MODE_PRIVATE);
+        mFreezerBinderAsyncThreshold = 1024;
+        mValidPids = IntArray.fromArray(new int[]{14505, 14461, 542, 540}, 4);
+        mStatsPids = new IntArray();
+        mStatsFree = new IntArray();
+        mHasError = false;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtils.deleteContents(mStatsDirectory);
+    }
+
+    @Test
+    public void testNoneProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER);
+        assertFalse(mHasError);
+        assertEquals(0, mStatsPids.size());
+        assertEquals(0, mStatsFree.size());
+    }
+
+    @Test
+    public void testOneProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1);
+        assertFalse(mHasError);
+        assertEquals(0, mStatsPids.size());
+        assertEquals(0, mStatsFree.size());
+    }
+
+    @Test
+    public void testTwoProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2);
+        assertFalse(mHasError);
+        assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+        assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+    }
+
+    @Test
+    public void testThreeProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3);
+        assertFalse(mHasError);
+        assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+        assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+    }
+
+    @Test
+    public void testFourProc() throws Exception {
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+        assertFalse(mHasError);
+        assertArrayEquals(mStatsPids.toArray(), new int[]{14461, 540});
+        assertArrayEquals(mStatsFree.toArray(), new int[]{62, 44});
+    }
+
+    @Test
+    public void testInvalidProc() throws Exception {
+        mValidPids = new IntArray();
+        runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+                + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+        assertFalse(mHasError);
+        assertEquals(0, mStatsPids.size());
+        assertEquals(0, mStatsFree.size());
+    }
+
+    private void runHandleBlockingFileLocks(String fileContents) throws Exception {
+        File tempFile = File.createTempFile("stats", null, mStatsDirectory);
+        Files.write(tempFile.toPath(), fileContents.getBytes());
+        new BinderfsStatsReader(tempFile.toString()).handleFreeAsyncSpace(
+                // Check if the current process is a valid one
+                mValidPids::contains,
+
+                // Check if the current process is running out of async binder space
+                (pid, free) -> {
+                    if (free < mFreezerBinderAsyncThreshold) {
+                        mStatsPids.add(pid);
+                        mStatsFree.add(free);
+                    }
+                },
+
+                // Log the error if binderfs stats can't be accesses or correctly parsed
+                exception -> {
+                    mHasError = true;
+                });
+        Files.delete(tempFile.toPath());
+    }
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e88d0c6baf268186faf882f4149c9699ae062ea3..ae79f19f70f115f05bf25a42d913a1e458042493 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20305,7 +20305,7 @@ public class ActivityManagerService extends IActivityManager.Stub
         final long token = Binder.clearCallingIdentity();
 
         try {
-            return mOomAdjuster.mCachedAppOptimizer.isFreezerSupported();
+            return CachedAppOptimizer.isFreezerSupported();
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -20433,4 +20433,21 @@ public class ActivityManagerService extends IActivityManager.Stub
             return index >= 0 && !mMediaProjectionTokenMap.valueAt(index).isEmpty();
         }
     }
+
+    /**
+     * Deal with binder transactions to frozen apps.
+     *
+     * @param debugPid The binder transaction sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    @Override
+    public void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err) {
+        final ProcessRecord app;
+        synchronized (mPidsSelfLocked) {
+            app = mPidsSelfLocked.get(debugPid);
+        }
+        mOomAdjuster.mCachedAppOptimizer.binderError(debugPid, app, code, flags, err);
+    }
 }
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 6005b64ca1bcd70ea68dc4df500eb9d7909d8c06..68af626869b775907c8cdabf5ec75d73cdf2d350 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -52,6 +52,8 @@ import android.app.ActivityManager;
 import android.app.ActivityManagerInternal.OomAdjReason;
 import android.app.ActivityThread;
 import android.app.ApplicationExitInfo;
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
 import android.app.IApplicationThread;
 import android.database.ContentObserver;
 import android.net.Uri;
@@ -67,6 +69,7 @@ import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.IntArray;
 import android.util.Pair;
@@ -75,6 +78,7 @@ import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BinderfsStatsReader;
 import com.android.internal.os.ProcLocksReader;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.ServiceThread;
@@ -131,6 +135,12 @@ public final class CachedAppOptimizer {
             "freeze_binder_offset";
     @VisibleForTesting static final String KEY_FREEZER_BINDER_THRESHOLD =
             "freeze_binder_threshold";
+    @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_ENABLED =
+            "freeze_binder_callback_enabled";
+    @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_THROTTLE =
+            "freeze_binder_callback_throttle";
+    @VisibleForTesting static final String KEY_FREEZER_BINDER_ASYNC_THRESHOLD =
+            "freeze_binder_async_threshold";
 
     static final int UNFREEZE_REASON_NONE =
             FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_NONE;
@@ -270,6 +280,9 @@ public final class CachedAppOptimizer {
     @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4;
     @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500;
     @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_THRESHOLD = 1_000;
+    @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED = true;
+    @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE = 10_000L;
+    @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD = 1_024;
 
     @VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
                 Settings.Global.CACHED_APPS_FREEZER_ENABLED);
@@ -312,6 +325,7 @@ public final class CachedAppOptimizer {
     static final int COMPACT_NATIVE_MSG = 5;
     static final int UID_FROZEN_STATE_CHANGED_MSG = 6;
     static final int DEADLOCK_WATCHDOG_MSG = 7;
+    static final int BINDER_ERROR_MSG = 8;
 
     // When free swap falls below this percentage threshold any full (file + anon)
     // compactions will be downgraded to file only compactions to reduce pressure
@@ -408,7 +422,10 @@ public final class CachedAppOptimizer {
                             } else if (KEY_FREEZER_BINDER_ENABLED.equals(name)
                                     || KEY_FREEZER_BINDER_DIVISOR.equals(name)
                                     || KEY_FREEZER_BINDER_THRESHOLD.equals(name)
-                                    || KEY_FREEZER_BINDER_OFFSET.equals(name)) {
+                                    || KEY_FREEZER_BINDER_OFFSET.equals(name)
+                                    || KEY_FREEZER_BINDER_CALLBACK_ENABLED.equals(name)
+                                    || KEY_FREEZER_BINDER_CALLBACK_THROTTLE.equals(name)
+                                    || KEY_FREEZER_BINDER_ASYNC_THRESHOLD.equals(name)) {
                                 updateFreezerBinderState();
                             }
                         }
@@ -480,7 +497,15 @@ public final class CachedAppOptimizer {
     @VisibleForTesting volatile int mFreezerBinderOffset = DEFAULT_FREEZER_BINDER_OFFSET;
     @GuardedBy("mPhenotypeFlagLock")
     @VisibleForTesting volatile long mFreezerBinderThreshold = DEFAULT_FREEZER_BINDER_THRESHOLD;
-
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting volatile boolean mFreezerBinderCallbackEnabled =
+            DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting volatile long mFreezerBinderCallbackThrottle =
+            DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE;
+    @GuardedBy("mPhenotypeFlagLock")
+    @VisibleForTesting volatile int mFreezerBinderAsyncThreshold =
+            DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD;
 
     // Handler on which compaction runs.
     @VisibleForTesting
@@ -488,6 +513,7 @@ public final class CachedAppOptimizer {
     private Handler mFreezeHandler;
     @GuardedBy("mProcLock")
     private boolean mFreezerOverride = false;
+    private long mFreezerBinderCallbackLast = -1;
 
     @VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
     @VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
@@ -790,6 +816,12 @@ public final class CachedAppOptimizer {
             pw.println("  " + KEY_FREEZER_BINDER_THRESHOLD + "=" + mFreezerBinderThreshold);
             pw.println("  " + KEY_FREEZER_BINDER_DIVISOR + "=" + mFreezerBinderDivisor);
             pw.println("  " + KEY_FREEZER_BINDER_OFFSET + "=" + mFreezerBinderOffset);
+            pw.println("  " + KEY_FREEZER_BINDER_CALLBACK_ENABLED + "="
+                    + mFreezerBinderCallbackEnabled);
+            pw.println("  " + KEY_FREEZER_BINDER_CALLBACK_THROTTLE + "="
+                    + mFreezerBinderCallbackThrottle);
+            pw.println("  " + KEY_FREEZER_BINDER_ASYNC_THRESHOLD + "="
+                    + mFreezerBinderAsyncThreshold);
             synchronized (mProcLock) {
                 int size = mFrozenProcesses.size();
                 pw.println("  Apps frozen: " + size);
@@ -1309,10 +1341,22 @@ public final class CachedAppOptimizer {
         mFreezerBinderThreshold = DeviceConfig.getLong(
                 DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
                 KEY_FREEZER_BINDER_THRESHOLD, DEFAULT_FREEZER_BINDER_THRESHOLD);
+        mFreezerBinderCallbackEnabled = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_BINDER_CALLBACK_ENABLED, DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED);
+        mFreezerBinderCallbackThrottle = DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_BINDER_CALLBACK_THROTTLE, DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE);
+        mFreezerBinderAsyncThreshold = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_BINDER_ASYNC_THRESHOLD, DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD);
         Slog.d(TAG_AM, "Freezer binder state set to enabled=" + mFreezerBinderEnabled
                 + ", divisor=" + mFreezerBinderDivisor
                 + ", offset=" + mFreezerBinderOffset
-                + ", threshold=" + mFreezerBinderThreshold);
+                + ", threshold=" + mFreezerBinderThreshold
+                + ", callback enabled=" + mFreezerBinderCallbackEnabled
+                + ", callback throttle=" + mFreezerBinderCallbackThrottle
+                + ", async threshold=" + mFreezerBinderAsyncThreshold);
     }
 
     private boolean parseProcStateThrottle(String procStateThrottleString) {
@@ -2182,6 +2226,21 @@ public final class CachedAppOptimizer {
                         Slog.w(TAG_AM, "Unable to check file locks");
                     }
                 } break;
+                case BINDER_ERROR_MSG: {
+                    IntArray pids = new IntArray();
+                    // Copy the frozen pids to a local array to release mProcLock ASAP
+                    synchronized (mProcLock) {
+                        int size = mFrozenProcesses.size();
+                        for (int i = 0; i < size; i++) {
+                            pids.add(mFrozenProcesses.keyAt(i));
+                        }
+                    }
+
+                    // Check binder errors to frozen processes with a local freezer lock
+                    synchronized (mFreezerLock) {
+                        binderErrorLocked(pids);
+                    }
+                } break;
                 default:
                     return;
             }
@@ -2487,4 +2546,115 @@ public final class CachedAppOptimizer {
                 return UNFREEZE_REASON_NONE;
         }
     }
+
+    /**
+     * Kill a frozen process with a specified reason
+     */
+    public void killProcess(int pid, String reason, @Reason int reasonCode,
+            @SubReason int subReason) {
+        mAm.mHandler.post(() -> {
+            synchronized (mAm) {
+                synchronized (mProcLock) {
+                    ProcessRecord proc = mFrozenProcesses.get(pid);
+                    // The process might have been killed or unfrozen by others
+                    if (proc != null && proc.getThread() != null && !proc.isKilledByAm()) {
+                        proc.killLocked(reason, reasonCode, subReason, true);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Sending binder transactions to frozen apps most likely indicates there's a bug. Log it and
+     * kill the frozen apps if they 1) receive sync binder transactions while frozen, or 2) miss
+     * async binder transactions due to kernel binder buffer running out.
+     *
+     * @param debugPid The binder transaction sender
+     * @param app The ProcessRecord of the sender
+     * @param code The binder transaction code
+     * @param flags The binder transaction flags
+     * @param err The binder transaction error
+     */
+    public void binderError(int debugPid, ProcessRecord app, int code, int flags, int err) {
+        Slog.w(TAG_AM, "pid " + debugPid + " " + (app == null ? "null" : app.processName)
+                + " sent binder code " + code + " with flags " + flags
+                + " to frozen apps and got error " + err);
+
+        // Do nothing if the binder error callback is not enabled.
+        // That means the frozen apps in a wrong state will be killed when they are unfrozen later.
+        if (!mFreezerBinderCallbackEnabled) {
+            return;
+        }
+
+        final long now = SystemClock.uptimeMillis();
+        if (now < mFreezerBinderCallbackLast + mFreezerBinderCallbackThrottle) {
+            Slog.d(TAG_AM, "Too many transaction errors, throttling freezer binder callback.");
+            return;
+        }
+        mFreezerBinderCallbackLast = now;
+
+        // Check all frozen processes in Freezer handler
+        mFreezeHandler.sendEmptyMessage(BINDER_ERROR_MSG);
+    }
+
+    private void binderErrorLocked(IntArray pids) {
+        // PIDs that run out of async binder buffer when being frozen
+        ArraySet<Integer> pidsAsync = (mFreezerBinderAsyncThreshold < 0) ? null : new ArraySet<>();
+
+        for (int i = 0; i < pids.size(); i++) {
+            int current = pids.get(i);
+            try {
+                int freezeInfo = getBinderFreezeInfo(current);
+
+                if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
+                    killProcess(current, "Sync transaction while frozen",
+                            ApplicationExitInfo.REASON_FREEZER,
+                            ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION);
+
+                    // No need to check async transactions in this case
+                    continue;
+                }
+
+                if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {
+                    if (pidsAsync != null) {
+                        pidsAsync.add(current);
+                    }
+                    if (DEBUG_FREEZER) {
+                        Slog.w(TAG_AM, "pid " + current
+                                + " received async transactions while frozen");
+                    }
+                }
+            } catch (Exception e) {
+                // The process has died. No need to kill it again.
+                Slog.w(TAG_AM, "Unable to query binder frozen stats for pid " + current);
+            }
+        }
+
+        // TODO: when kernel binder driver supports, poll the binder status directly.
+        // Binderfs stats, like other debugfs files, is not a reliable interface. But it's the
+        // only true source for now. The following code checks all frozen PIDs. If any of them
+        // is running out of async binder buffer, kill it. Otherwise it will be killed at a
+        // later time when AMS unfreezes it, which causes race issues.
+        if (pidsAsync == null || pidsAsync.size() == 0) {
+            return;
+        }
+        new BinderfsStatsReader().handleFreeAsyncSpace(
+                // Check if the frozen process has pending async calls
+                pidsAsync::contains,
+
+                // Kill the current process if it's running out of async binder space
+                (current, free) -> {
+                    if (free < mFreezerBinderAsyncThreshold) {
+                        Slog.w(TAG_AM, "pid " + current
+                                + " has " + free + " free async space, killing");
+                        killProcess(current, "Async binder space running out while frozen",
+                                ApplicationExitInfo.REASON_FREEZER,
+                                ApplicationExitInfo.SUBREASON_FREEZER_BINDER_ASYNC_FULL);
+                    }
+                },
+
+                // Log the error if binderfs stats can't be accesses or correctly parsed
+                exception -> Slog.e(TAG_AM, "Unable to parse binderfs stats"));
+    }
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 0d024d6a4c587e8043ce20a2696969eebd5f999a..56e385d535a0f39d1c9357acbe041ec8804faca8 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -65,6 +65,7 @@ import android.os.Environment;
 import android.os.FactoryTest;
 import android.os.FileUtils;
 import android.os.IBinder;
+import android.os.IBinderCallback;
 import android.os.IIncidentManager;
 import android.os.Looper;
 import android.os.Message;
@@ -985,6 +986,14 @@ public final class SystemServer implements Dumpable {
             }
         }
 
+        // Set binder transaction callback after starting system services
+        Binder.setTransactionCallback(new IBinderCallback() {
+            @Override
+            public void onTransactionError(int pid, int code, int flags, int err) {
+                mActivityManagerService.frozenBinderTransactionDetected(pid, code, flags, err);
+            }
+        });
+
         // Loop forever.
         Looper.loop();
         throw new RuntimeException("Main thread loop unexpectedly exited");