Skip to main content

Android Offensive Security Blog

Binder Fuzzing

Table of Contents


In our previous blog posts, we explored Android Binder’s intricacies, from exploiting a vulnerability (CVE-2023-20938) for kernel code execution to examining its inner workings. In this post, we shift our focus to finding vulnerabilities in the Binder kernel driver through fuzzing.

This post provides a practical guide to fuzzing the Binder kernel driver using the Linux Kernel Library (LKL). To demonstrate the advantages of this approach, we first explore existing fuzzing efforts using Syzkaller, a state-of-the-art kernel fuzzer, and highlight its challenges for this use case. Then, we dive into how LKL overcomes these limitations and our improvements, such as randomized scheduling.

If you’d like to jump straight into the fuzzer source code, you can find it here, along with an example test case and instructions on reproducing CVE-2023-20938. You can also check out our latest presentations on LKL fuzzing.


This is the last post of a multi-part series where we discuss our journey into Binder:


Our initial approach for Binder fuzzing was to leverage syzkaller. syzkaller has successfully uncovered several critical vulnerabilities in Binder before, such as CVE-2019-2215 (Bad Binder), as documented by Project Zero’s blog post.

Using syzkaller’s readily available syscall descriptions for the Binder driver, we quickly started fuzzing the driver on a Linux VM instance. Syzkaller employs structure-aware and coverage-guided fuzzing to generate test cases that interact with the Binder driver. These test cases contain various system calls and their arguments that aim to maximize code coverage within the driver.

The latest kernel code coverage for the Android Common Kernel (ACK) can be found on the syzbot dashboard. The following screenshot shows the code coverage achieved within the Binder kernel driver while fuzzing ACK 6.1.

Syzkaller code coverage for binder

Despite Binder’s complexity, syzkaller achieves decent code coverage and successfully covers most code paths for each ioctl. The remaining uncovered paths primarily consist of error handling and edge cases.

However, we knew syzkaller had missed some vulnerabilities in the past. We decided to analyze these vulnerabilities to understand syzkaller’s limitations and identify areas for improvement.

This case study uses CVE-2020-0423 to highlight potential obstacles a syzkaller might encounter when attempting to trigger one of the past vulnerabilities.

CVE-2020-0423 is a use-after-free vulnerability caused by improper locking. Instead of delving into the vulnerability’s details, we will focus on the steps required to trigger it. For reference, Longterm Security has published a detailed blog post on exploiting CVE-2020-0423.

This use-after-free occurs when two clients T1 and T2 interact through Binder in a specific sequence, as demonstrated in the diagram below.

Binder clients interaction diagram

  1. T1 sends a binder transaction containing a BINDER_TYPE_WEAK_BINDER object to T2.
  2. T1 initiates the BINDER_THREAD_EXIT ioctl.
  3. T1 (Kernel) Binder calls binder_release_work to clean up all binder_work belonging to T1 before exiting.
  4. T2 initiates a BC_FREE_BUFFER command to free the received binder transaction.
  5. T2 (Kernel) Binder calls binder_transaction_buffer_release to free the transaction buffer created in step 1. The BINDER_TYPE_WEAK_BINDER object in the transaction buffer contains a binder_work object.
  6. T1 (Kernel) In binder_release_work, Binder accesses the already freed binder_work object, resulting in a use-after-free vulnerability.

To successfully trigger this use-after-free vulnerability, syzkaller would need to:

  1. Spawn two clients (i.e. two threads running in different processes) and execute a sequence of ioctl calls.
  2. Establish a connection between those clients (as detailed in Part 1 of our blog post)
  3. Trigger a race condition on the binder_work object.

This highlights a limitation in syzkaller’s fuzzing approach for binder. Although syzkaller achieves high code coverage, covering the very lines of code where the vulnerability lies, it still has difficulty triggering the bug. This confirms the issue is not about missing code paths, but about creating the specific conditions within Binder’s complex state space that lead to memory corruption.

Many memory corruptions arise from similar logic bugs within the Binder driver’s complex state space. Much of Binder’s code involves complex state management, such as object reference counts across multiple clients, a dimension that code coverage does not capture. Although mutating transaction data can achieve high code coverage, this approach often misses specific logic bugs. Those bugs are often triggered by the specific sequence of IPC events, not by malformed data alone.

To target these specific blind spots, a more focused approach is required. Binder transactions are synchronous, demanding specific replies from clients to navigate the driver’s complex state space. An effective fuzzing approach should be aware of this. Instead of only mutating data, it must understand and manipulate the sequence of Binder operations. This involves simulating valid multi-client interactions and generating the stateful transactions to properly cover the driver’s logic.

Our analysis revealed several challenges that might prevent syzkaller from effectively navigating Binder’s state space and triggering deep logic bugs. We designed our fuzzer specifically to address these issues.

Many Binder transactions have complex dependencies between different parts of their input data. A great example is the scatter-gather mechanism used for passing objects that contain pointers.

To pass a complex data structure, the sending client must first flatten the object into a linear buffer and provide a series of binder_buffer_object structures as metadata. This metadata describes how Binder should reconstruct the object, including its internal pointer relationships and offsets, in the receiving client’s memory.

For example, consider a simple object with two pointers:

struct object {
  char *x_ptr;
  char *y_ptr;
}

Binder object with pointers Fuzzing this correctly requires generating three distinct binder_buffer_object’s that accurately describe the parent object and its two child pointers.

Beyond data, many operations depend on the state of the Binder driver and prior interactions.

First, Binder imposes strict protocol rules:

  • Transactions are synchronous.
  • A client cannot send a transaction to itself.
  • Multiple pending transactions to the same target are not allowed.

We designed our fuzzer to be aware of these rules to avoid repeatedly hitting simple error conditions. To do that, we implement default handlers so that when a client receives a transaction, it sends back a valid reply. This allows the interaction to proceed, making it more likely to explore deeper states. Binder transaction handler

Second, some Binder operations are stateful across multiple calls. For example, the BC_FREE_BUFFER operation requires a valid pointer to a transaction buffer received in a prior ioctl call.
To invoke this operation correctly, our fuzzer must maintain its own state across operations, capturing the pointer from one ioctl call and using it as valid input for a subsequent one. Capturing pointer from previous request

Simulating valid interactions requires coordinating multiple processes. As detailed in Part 1, clients need a context manager to establish connections with each other. Another key Binder rule is that only one process can register as the context manager per system boot, even if that process later dies.

To address this, our fuzzing harness implements a three-client model. One client is designated as the context manager and initialized by calling BINDER_SET_CONTEXT_MGR at startup. This context manager process is kept alive and reused across test cases. This setup enables the simulation of multi-process interactions, which could explore most parts of the Binder state space. Binder three-client model

To overcome the challenges of fuzzing Binder, we developed a specialized fuzzer built on the Linux Kernel Library (LKL). LKL compiles the entire Linux kernel into a userspace library, which allows a program to link against it and interact with the kernel through simple function calls. The entire syscall interface is exported via the lkl_syscall function. This setup allows our fuzzer to run entirely in userspace while still fuzzing the real kernel code.

This approach gave us an advantage over using tools like syzkaller because it offered us a way to develop a specialized fuzzing harness. In this section, we will only focus on the key implementation that enabled us to fuzz Binder’s complex state space. For a full breakdown of the LKL fuzzer, please refer to one of our talks here. LKL fuzzing harness

Our approach was to define a “grammar” for the interactions within our three-client model (one context manager and two clients). This is similar to structure-aware fuzzing, but our grammar also describes the sequence of events between multiple clients. This idea is inspired by Project Zero’s SockFuzzer.

For easier debugging and structured test case generation, we used Protocol Buffers (protobuf) to define this grammar and libprotobuf-mutator to generate and mutate test cases. The following textproto example shows a test case generated by our fuzzer. It describes a sequence of Binder operations across different clients, allowing us to reproduce and debug complex interactions between clients.

Sample test case using protobuf syntax

Furthermore, the fuzzer harness understands the Binder’s rules and maintains the state of each client, focusing on navigating complex state space rather than repeatedly hitting simple error paths.

In addition, we aimed to catch race condition bugs that arise from thread interleaving. This was not possible in LKL’s single-threaded kernel execution model. A LKL kernel thread handles a system call from start to finish without interruption.

To simulate thread interleaving, we inserted schedule() calls at key locations within the Binder driver’s source code, primarily around lock and unlock operations. When a client thread encounters one of these calls during a Binder operation, it yields to the scheduler and allows another client thread to run. This enables us to simulate the thread interleaving that occurs when multiple clients interact with Binder simultaneously. Deterministic thread interleaving

We also implemented a randomized scheduler controlled by our fuzzer harness to intercept these schedule() calls. When a schedule() call is triggered within the Binder code, the harness randomly selects the next client thread to run.

This approach improves the fuzzer’s ability to discover bugs that arise from thread interleaving.

The Binder inter-process communication (IPC) system is a cornerstone of Android’s architecture, and its complexity demands robust testing. Our research successfully enhanced fuzzing techniques for Binder by integrating the standard syzkaller approach with a customized Linux Kernel Library (LKL) based method. This combined strategy significantly improved code coverage and, critically, allowed us to uncover deep-seated logic bugs that are only triggered by precise interactions and timing sequences among multiple clients.

We’re pleased to announce that our Binder fuzzer has been upstreamed to the LKL (with the exception of the Randomized Scheduler part) project and is now publicly available. You can find it at lkl/linux#564. Additionally, the specific test case for CVE-2023-20938, a vulnerability we discovered using this fuzzer, and instructions on how to reproduce it are also accessible.

We encourage you to explore and utilize this fuzzer to further strengthen the security and stability of Android systems. If you would like to know more about LKL fuzzing in general, we have presented on this topic in GeekCon 2024 and Linux Security Summit 2025, feel free to check the presentation page for details.



For technical questions about content of this post, contact androidoffsec-external@google.com. All press inquiries should be directed to press@google.com.