Skip to main content

Android Offensive Security Blog

Binder Internals

Table of Contents


In our last blog, we talked about Binder CVE-2023-20938 and how we exploited it to get kernel code execution. As you may have already noticed, exploiting this issue is not straightforward. While it is often true that kernel race conditions are notoriously tricky to exploit, the intricacy of the Binder driver’s implementation adds another layer of complexity.

This blog post dives deeper into the inner workings of Binder, including the lifecycles of its objects and the underpinnings that keep everything running smoothly across Android. We will also introduce the libdevbinder library we developed during our engagement. This library provides simpler interfaces for researchers interact with the Binder driver for the purpose of learning and experimentation. Binder is an incredibly complicated target! You’ll notice the length of this blog post reflects that complexity, and while we try to cover salient points from the perspective of security research here, there is always more to learn. The Android Red Team believes in empowering the security researcher community; sharing knowledge helps improve security across the entire ecosystem. This blog post aims to help security researchers (like you) learn more about Binder. If you learn enough to find some vulnerabilities, our goal has been achieved (oh and please, let us know!).


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


The use of multiple reference counters and object dependencies introduces complex object lifetime management logic in Binder. When doing vulnerability research, it is helpful to understand the lifetime of every object in Binder as many past vulnerabilities have exploited flaws hidden within them.

To highlight the complexity, let’s look at some properties of the binder_node object:

  • Has 4 different reference counters
  • Can be in multiple linked lists owned by other objects, such as a binder_proc and a workqueue
  • One or more associated binder_ref objects hold a pointer to it
  • One or more associated binder_buffer objects hold a pointer to it

These properties also result in multiple code paths with different conditions to free a binder_node.

Here is a simplified diagram to show dependencies between every data structure in Binder:

Binder data structure dependencies

In the next sections, we will examine the lifetime of several data structures in Binder, focusing on when they are allocated and destroyed.

The binder_proc object represents a client in Binder. It is the first object to be allocated when a process opens the Binder device node.

Note: In contrast to the userspace Binder, a process can act as a server or a client of a service. Throughout this article, we will generally refer to the process that interacts with the Binder device as the client and the Binder device itself as the server.

It contains the following fields that determine its lifetime:

  1. threads is the root node of a red-black tree that contains all binder_threads it owns.
  2. is_dead determines whether the client is dead.
  3. tmp_ref tracks the number of local variables holding a pointer to the binder_proc.

Binder allocates and initializes a binder_proc every time a process opens the Binder device node.

// === Userspace ===
int binder_fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
// === Kernel ===
static int binder_open(struct inode *nodp, struct file *filp)
{
...
	proc = kzalloc(sizeof(*proc), GFP_KERNEL);
...
}

Note: For this blog post, we are diving into the Linux kernel codebase at commit 4df1536, specifically the files within the drivers/android folder. All code snippets are sourced from this folder and are licensed under the GNU General Public License version 2 (GPLv2). You can find the complete source code on GitHub (link). For full license details, please see LICENSE. We have occasionally omitted some code for brevity (indicated by ...) and included additional comments (marked with //).

tmp_ref tracks the number of local variables holding a pointer to the binder_proc. Binder increments the tmp_ref counter when a pointer to a binder_proc object is assigned to a local variable [1]. When the pointer variable is no longer in use, Binder decrements the tmp_ref counter with the binder_proc_dec_tmpref function [2].

static void binder_transaction(...)
{
	struct binder_proc *target_proc = NULL;
...
	target_proc = target_thread->proc;
	target_proc->tmp_ref++; // [1]
...
	binder_proc_dec_tmpref(target_proc); // [2]
...
}

static void binder_proc_dec_tmpref(struct binder_proc *proc)
{
...
	proc->tmp_ref--;
...
}

The tmp_ref is protected by the binder_proc->inner_lock spinlock to prevent data race.

Binder destroys the binder_proc object with the binder_free_proc function, which is only called by the binder_proc_dec_tmpref function. Binder invokes the binder_proc_dec_tmpref function at multiple locations where it needs to decrement the tmp_ref counter.

static void binder_free_proc(struct binder_proc *proc)
{
...
	kfree(proc);
}

static void binder_proc_dec_tmpref(struct binder_proc *proc)
{
...
	if (proc->is_dead && RB_EMPTY_ROOT(&proc->threads) &&
			!proc->tmp_ref) {
		binder_inner_proc_unlock(proc);
		binder_free_proc(proc);
		return;
	}
...
}

Then, the binder_proc object is freed only when all of the following conditions are met:

  1. threads: the red-black tree is empty after all binder_thread are released (see binder_thread.
  2. is_dead: set to true when closing the Binder file descriptor (binder_thread_release).
  3. tmp_ref: set to 0 when there is no temporary variable holding a pointer to the binder_proc.

The binder_proc_dec_tmpref is called in several code paths. One common code path is closing the Binder file descriptor, which calls the binder_deferred_released function.

// === Userspace ===
close(binder_fd);
// === Kernel ===
static void binder_deferred_release(struct binder_proc *proc)
{
...
	binder_proc_dec_tmpref(proc);
}

The binder_thread object represents a thread of a client (binder_proc) in Binder. We will delve deeper into multithreaded clients in an upcoming section (Multithreaded Client).

The binder_proc maintains a reference to each binder_thread it owns, which is stored in a red-black tree (rb_tree) and the root node is in the threads field.

A binder_proc can hold references to multiple binder_threads

It contains the following fields that determine its lifetime:

  1. is_dead determines whether the thread is dead. This is distinct from the client death status (binder_proc->is_dead).
  2. Similar to binder_proc->tmp_ref, tmp_ref tracks the number of active local variables holding a pointer to it.

When the process of a client spawns a new thread, the child thread inherits the Binder file descriptor which is associated with the same binder_proc. When the new child thread initiates an ioctl call, Binder first looks up for any existing binder_thread associated with it [1]. If none exists, Binder allocates and initializes a new binder_thread [2, 3].

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int ret;
	struct binder_proc *proc = filp->private_data;
	struct binder_thread *thread;
...
	thread = binder_get_thread(proc);
...
}

static struct binder_thread *binder_get_thread(struct binder_proc *proc)
{
	struct binder_thread *thread;
	struct binder_thread *new_thread;
...
	thread = binder_get_thread_ilocked(proc, NULL); // [1]
...
	if (!thread) {
		new_thread = kzalloc(sizeof(*thread), GFP_KERNEL); // [2]
...
		thread = binder_get_thread_ilocked(proc, new_thread); // [3]
...
	}
...
	return thread;
}

Similar to binder_proc->tmp_ref, tmp_ref tracks the number of local variables holding a pointer to the binder_thread. Binder increments the tmp_ref counter when the pointer to a binder_thread object is assigned to a local variable. When the pointer is no longer in use, Binder decrements the tmp_ref counter.

For example, the binder_get_txn_from increments the tmp_ref field before returning the pointer to a binder_thread object [2]. After the pointer is no longer in use, Binder decrements the tmp_ref counter with the binder_thread_dec_tmpref function [1].

static struct binder_thread *binder_get_txn_from_and_acq_inner(...)
{
	struct binder_thread *from;

	from = binder_get_txn_from(t);
...
	binder_thread_dec_tmpref(from); // [1]
	return NULL;
}

static struct binder_thread *binder_get_txn_from(struct binder_transaction *t)
{
	struct binder_thread *from;
...
	from = t->from;
	if (from)
		atomic_inc(&from->tmp_ref); // [2]
...
	return from;
}

static void binder_thread_dec_tmpref(struct binder_thread *thread)
{
...
	atomic_dec(&thread->tmp_ref);
...
}

Although tmp_ref is an atomic variable (atomic_t), it is also protected by the binder_proc->inner_lock spinlock to prevent data race.

Binder releases a binder_thread object with the binder_free_thread function, which is only called by the binder_thread_dec_tmpref function. Binder invokes the binder_thread_dec_tmpref function at multiple locations where it needs to decrement the tmp_ref counter.

static void binder_thread_dec_tmpref(struct binder_thread *thread)
{
...
	if (thread->is_dead && !atomic_read(&thread->tmp_ref)) {
...
		binder_free_thread(thread);
...
	}
...
}

static void binder_free_thread(struct binder_thread *thread)
{
...
	kfree(thread);
}

The two conditions above are met when:

  1. is_dead: set to true when Binder releases the thread binder_thread_release. The binder_thread_release is called when:
    1. A thread calls the BINDER_THREAD_EXIT ioctl. The binder_proc and other threads remain unaffected.
    2. A client closes the Binder file descriptor (binder_deferred_release), which releases the binder_proc and all binder_thread.
  2. tmp_ref: set to 0 when there is no temporary variable holding a pointer to the binder_thread.

A binder_node object represents a port to a client (binder_proc) and has the most complex lifetime management logic. There are three different data structures that hold a reference to a binder_node. Binder must ensure that binder_node stays in memory until all references to it have been removed. Let’s examine each data structure closely.

Objects with references to a binder_node

The binder_ref always holds a pointer to a binder_node in its node field.

A binder_ref holds a reference to a binder_node

These two objects represent a connection between two clients, allowing them to interact with each other via Binder. We will discuss more about binder_ref in the next section (binder_ref). For example, the diagram below shows Client A has a port (binder_node) which Client B and Client C each has a reference to it (binder_ref). As a result, Client B and Client C can initiate RPCs to Client A.

Both Client B and Client C have a reference to Client A

Every binder_proc holds a reference to every binder_node it owns, which is organized in a red-black tree (rb_tree) with the root node stored in the nodes field.

A binder_proc can hold references to multiple binder_node

The binder_buffer represents a buffer that holds the data of a transaction. It holds a reference to the binder_node, which is the destination port of the transaction.

A binder_buffer holds a reference to a binder_node

A binder_node also contains the following fields that determine its lifetime:

  1. work is a node in a workqueue list when the binder_node is being processed.
  2. refs holds the head of the list of all binder_ref linked to it.
  3. internal_strong_refs tracks the number of strong references acquired remotely in other clients.
  4. local_weak_refs tracks the number of weak references acquired locally.
  5. local_strong_refs tracks the number of strong references acquired locally.
  6. Similar to binder_proc->tmp_ref, tmp_ref tracks the number of local variables holding a pointer to it.

We will revisit all of the above fields later when discussing the deallocation of the binder_node object.

Note: Binder kernel driver guarantees that the binder_node object is released when all reference counters, both strong and weak, are zero. The distinction between strong and weak references is primarily significant in userspace for effective memory management. For more details about the usage of these reference types, please refer to the implementation of IBinder and its underlying RefBase class, which provide a reference-counted base class.

Binder allocates a new binder_node with the binder_new_node function when a new port is established.

static struct binder_node *binder_new_node(...)
{
	struct binder_node *node;
	struct binder_node *new_node = kzalloc(sizeof(*node), GFP_KERNEL);
...
}

Clients embed the flat_binder_object objects within the transaction data to send special data types such as file descriptors, binder and handle. To establish a new port, a client sends a transaction which contains a flat_binder_object object with the header type BINDER_TYPE_BINDER and a binder identifier.

Before forwarding the transaction to the receiving client, Binder processes every flat_binder_object in the transaction data. To process flat_binder_object with the header type BINDER_TYPE_BINDER or BINDER_TYPE_WEAK_BINDER, Binder calls the binder_translate_binder function.

static void binder_transaction(...)
{
...
		switch (hdr->type) {
		case BINDER_TYPE_BINDER:
		case BINDER_TYPE_WEAK_BINDER: {
			struct flat_binder_object *fp;

			fp = to_flat_binder_object(hdr);
			ret = binder_translate_binder(fp, t, thread); // [1]
...
		} break;
...
}

In the binder_translate_binder function, Binder searches for any existing port (binder_node) with a matching binder identifier owned by the sending client (binder_proc) [1]. If none exists, Binder allocates a new one and inserts it into the nodes red-black tree [2]. Finally, Binder converts the BINDER_TYPE_*BINDER header type to BINDER_TYPE_*HANDLE [3] and assigns a new handle identifier [4].

static int binder_translate_binder(struct flat_binder_object *fp,
				   struct binder_transaction *t,
				   struct binder_thread *thread)
{
	struct binder_node *node;
	struct binder_proc *proc = thread->proc;
...
	node = binder_get_node(proc, fp->binder); // [1]
	if (!node) {
		node = binder_new_node(proc, fp); // [2]
		if (!node)
			return -ENOMEM;
	}
...
	if (fp->hdr.type == BINDER_TYPE_BINDER)
		fp->hdr.type = BINDER_TYPE_HANDLE; // [3]
	else
		fp->hdr.type = BINDER_TYPE_WEAK_HANDLE; // [3]
...
	fp->handle = rdata.desc; // [4]
...
}

In the kernel, a binder_node and a binder_ref are created and linked together. Binder uses these two data structures to track every established connection between clients.

A binder_node and a binder_ref are assocaited with each other

In the userspace, another client will receive the transaction that contains the flat_binder_object with the header type BINDER_TYPE_HANDLE and a handle identifier. Subsequently, Client B can initiate a transaction or RPC with Client A using the designated handle identifier.

Binder translates a flat_binder_object of type BINDER_TYPE_BINDER to BINDER_TYPE_HANDLE

When updating a binder_node, the spinlock in its lock field must be acquired to prevent data race. Additionally, if the client that owns a binder_node is alive, the inner_lock of the client binder_proc must also be acquired.

The binder_inc_node_nilocked and binder_dec_node_nilocked functions are used to update the following reference counters:

  • internal_strong_refs
  • local_strong_refs
  • local_weak_refs
static int binder_inc_node_nilocked(struct binder_node *node,
				     int strong, int internal, ...)

static bool binder_dec_node_nilocked(struct binder_node *node,
				     int strong, int internal)

This table summarizes which reference counter will be updated by both functions based on the internal and strong parameters.

internal
0 1
strong 0 local_weak_refs N/A
1 local_strong_refs internal_strong_refs

Note: There is no such field as internal_weak_refs in the binder_node object, so calling the functions with strong set to 0 and internal set to 1 does not modify any reference counter. The internal_weak_refs is implicitly tracked by the length of the binder_proc->refs linked list minus local_weak_refs. Therefore, before freeing a binder_node, Binder checks whether the list is empty.

The internal_strong_refs represents the number of strong references to a binder_node held by remote clients. Binder tracks this by counting the associated binder_ref with a data.strong value greater than zero. We will cover more about the data.strong counter in the binder_ref section.

Binder increments internal_strong_refs when binder_ref has a non-zero data.strong

Binder increments the internal_strong_refs [2] when it increments the data.strong of an associated binder_ref from zero [1].

static int binder_inc_ref_olocked(struct binder_ref *ref, int strong,
				  struct list_head *target_list)
{
...
	if (strong) {
		if (ref->data.strong == 0) { // [1]
			ret = binder_inc_node(ref->node, 1, 1, target_list); // [2]
			if (ret)
				return ret;
		}
		ref->data.strong++; // [1]
	} else {
...
}

Binder decrements the internal_strong_refs [2] when the data.strong of an associated binder_ref drops to zero [1].

static bool binder_dec_ref_olocked(struct binder_ref *ref, int strong)
{
	if (strong) {
...
		ref->data.strong--; // [1]
		if (ref->data.strong == 0) // [1]
			binder_dec_node(ref->node, strong, 1); // [2]
	} else {
...
}

Upon client exit, Binder clears every binder_ref that the client owns [1]. In cases where a binder_ref has a data.strong value greater than zero [2], Binder then decrements the internal_strong_refs of the corresponding binder_node [3].

static void binder_deferred_release(struct binder_proc *proc)
{
...
	while ((n = rb_first(&proc->refs_by_desc))) {
		struct binder_ref *ref;

		ref = rb_entry(n, struct binder_ref, rb_node_desc);
...
		binder_cleanup_ref_olocked(ref); // [1]
...
	}
...
}

static void binder_cleanup_ref_olocked(struct binder_ref *ref)
{
...
	if (ref->data.strong) // [2]
		binder_dec_node_nilocked(ref->node, 1, 1); // [3]
...
}

The local_strong_refs represents the number of strong references to a binder_node held by the client locally. This count increases when the client receives a transaction that either targets the binder_node or includes a BINDER_TYPE_BINDER object that references the binder_node.

When a remote client sends a transaction, Binder creates a binder_transaction and a binder_buffer to store the transaction’s metadata. The binder_buffer contains a target_node field which identifies the recipient by pointing to the binder_node owned by the receiving client. This process increments the local_strong_refs count.

Binder increments local_strong_refs when allocating a binder_buffer linked to the binder_node

In addition, receiving a transaction that includes a BINDER_TYPE_BINDER object creates a strong reference to the binder_node that has the matching binder identifier. This also increments the local_strong_refs count.

Receiving flat_binder_object of type BINDER_TYPE_BINDER increments local_strong_refs

Like the local_strong_refs, the local_weak_refs represents the number of weak references to a binder_node held by the client locally. Receiving a transaction that includes a BINDER_TYPE_WEAK_BINDER object creates a weak reference locally to the binder_node that has the matching binder identifier.

Receiving flat_binder_object of type BINDER_TYPE_WEAK_BINDER increments local_weak_refs

tmp_refs tracks the number of active local variables holding a pointer to it. Binder increments and decrements the tmp_refs counter with the binder_inc_node_tmpref_ilocked and binder_dec_node_tmpref.

Binder destroys a binder_node object with the binder_free_node function.

static void binder_free_node(struct binder_node *node)
{
	kfree(node);
...
}

There are several code paths that frees a binder_node under different conditions:

  • binder_dec_node_nilocked
  • binder_thread_read
  • binder_deferred_release

The binder_dec_node_nilocked function decrements one of the binder_node reference counters. It also returns a boolean to notify its caller that it is safe to free the binder_node.

static bool binder_dec_node_nilocked(struct binder_node *node,
				     int strong, int internal)
{
	struct binder_proc *proc = node->proc;
...
	if (strong) {
		if (internal)
			node->internal_strong_refs--;
		else
			node->local_strong_refs--;
		if (node->local_strong_refs || node->internal_strong_refs)
			return false;
	} else {
		if (!internal)
			node->local_weak_refs--;
		if (node->local_weak_refs || node->tmp_refs ||
				!hlist_empty(&node->refs))
			return false;
	}
...
	if (proc && (node->has_strong_ref || node->has_weak_ref)) {
...
       else {
		if (hlist_empty(&node->refs) && !node->local_strong_refs &&
		    !node->local_weak_refs && !node->tmp_refs) {
...
			return true;

In summary, a binder_node can be safely freed only when all of the following conditions are true:

binder_node->proc == 0
binder_node->has_strong_ref == 0
binder_node->has_weak_ref == 0
binder_node->internal_strong_refs == 0
binder_node->local_strong_refs == 0
binder_node->local_weak_refs == 0
binder_node->tmp_refs == 0
binder_node->refs == 0 // hlist_empty(node->refs)
binder_node->work.entry = &node->work.entry // list_empty(&node->work.entry)

The binder_dec_node_nilocked is called by three functions:

  • binder_dec_node
  • binder_dec_node_tmpref
  • binder_free_ref

The binder_dec_node is a wrapper function that helps acquire relevant locks.

static void binder_dec_node(struct binder_node *node, int strong, int internal)
{
...
	binder_node_inner_lock(node); // Acquire relevant locks
	free_node = binder_dec_node_nilocked(node, strong, internal);
	binder_node_inner_unlock(node);
	if (free_node)
		binder_free_node(node);
}

The binder_dec_node is called when

  • Updating the binder_ref strong and weak references.
  • Releasing a transaction (BC_FREE_BUFER) because binder_buffer has a reference to a binder_node.
  • Cleaning up a transaction when Binder failed to process it

The binder_dec_node_tmpref is called to decrement the tmp_ref counter of a binder_node.

static void binder_dec_node_tmpref(struct binder_node *node)
{
...
	free_node = binder_dec_node_nilocked(node, 0, 1);
...
	if (free_node)
		binder_free_node(node);
...
}

Note: binder_dec_node_nilocked is called here with a strong value of 0 and an internal value of 1. Since there is no internal_weak_refs, this function call does not update any reference counters of a binder_node.

The binder_free_ref function is called to clean up a binder_ref before freeing it.

static void binder_cleanup_ref_olocked(struct binder_ref *ref)
{
...
	delete_node = binder_dec_node_nilocked(ref->node, 0, 1);
...
	if (!delete_node) {
...
		ref->node = NULL;
	}
...
}

After the creation or update of a binder_node, Binder enqueues the binder_node into the client’s workqueue as a BINDER_WORK_NODE work item. When a client reads incoming response (BR_*), Binder calls the binder_thread_read function to transform a work item from the workqueue into a response. Under certain conditions, Binder will free a binder_node when transforming the BINDER_WORK_NODE work item [1].

static int binder_thread_read(...)
{
...
		struct binder_work *w = NULL;
...
		w = binder_dequeue_work_head_ilocked(list);
...
		switch (w->type) {
...
		case BINDER_WORK_NODE: { // [1]
...
			strong = node->internal_strong_refs ||
					node->local_strong_refs;
			weak = !hlist_empty(&node->refs) ||
					node->local_weak_refs ||
					node->tmp_refs || strong;
...
			if (!weak && !strong) {
...
				binder_free_node(node);

The conditions can be summarized as follows:

binder_node->internal_strong_refs == 0
binder_node->local_strong_refs == 0
binder_node->local_weak_refs == 0
binder_node->tmp_refs == 0
binder_node->refs == 0 // hlist_empty(node->refs)

When a client closes the Binder file descriptor (binder_deferred_release), Binder traverses a rb_tree that contains all binder_node owned by the binder_proc and frees them. The binder_node_release function is used to free a binder_node only if the following conditions are met [1]:

  • The binder_node has zero binder_ref in its refs list.
  • The tmp_refs counter of the binder_node must equal 1.
static void binder_deferred_release(struct binder_proc *proc)
{
...
	while ((n = rb_first(&proc->nodes))) {
		struct binder_node *node;

		node = rb_entry(n, struct binder_node, rb_node);
...
		incoming_refs = binder_node_release(node, incoming_refs);
...
	}
...
}

static int binder_node_release(struct binder_node *node, int refs)
{
...
	if (hlist_empty(&node->refs) && node->tmp_refs == 1) { // [1]
...
		binder_free_node(node);
...
}

A binder_ref represents a reference that a client holds to the port (binder_node) of a separate client, which forms a connection. Therefore, a binder_ref can only exist when its owner (binder_proc) and its associated port (binder_node) are alive.

binder_ref points to a binder_node owned by a different client

There are two scenarios where Binder creates a new binder_ref. In the binder_node section, we examined the first scenario: a client sends a BINDER_TYPE_*BINDER to another client. In the end, Binder creates a new binder_node together with a binder_ref.

When translating the BINDER_TYPE_BINDER to a BINDER_TYPE_HANDLE [1], Binder tries to find an existing binder_ref associated with the binder_node [2]. If none exists, Binder allocates a new binder_ref.

static int binder_translate_binder(...) // [1]
{
...
	ret = binder_inc_ref_for_node(target_proc, node,
			fp->hdr.type == BINDER_TYPE_BINDER,
			&thread->todo, &rdata);
...
}

static int binder_inc_ref_for_node(...)
{
	struct binder_ref *ref;
	struct binder_ref *new_ref = NULL;
...
	ref = binder_get_ref_for_node_olocked(proc, node, NULL); // [2]
	if (!ref) {
...
		new_ref = kzalloc(sizeof(*ref), GFP_KERNEL); // [3]

...
	return ret;
}

The second scenario is when a client sends a BINDER_TYPE_HANDLE or BINDER_TYPE_WEAK_BINDER to another client.

Client B sends a flat_binder_object of type BINDER_TYPE_HANDLE

Before forwarding the transaction, Binder calls the binder_translate_handle function to process flat_binder_object of type BINDER_TYPE_HANDLE or BINDER_TYPE_WEAK_HANDLE.

static void binder_transaction(...)
{
...
		case BINDER_TYPE_HANDLE:
		case BINDER_TYPE_WEAK_HANDLE: {
			struct flat_binder_object *fp;

			fp = to_flat_binder_object(hdr);
			ret = binder_translate_handle(fp, t, thread); // [1]
...
		} break;
...
}

Binder searches for the binder_node associated with a binder_ref that has a matching handle identifier [1]. If the binder_node is not owned by the receiving client [2], Binder calls the binder_inc_ref_for_node function to get a binder_ref [3] and assign a new handle identifier [4].

static int binder_translate_handle(struct flat_binder_object *fp,
				   struct binder_transaction *t,
				   struct binder_thread *thread)
{
	struct binder_proc *proc = thread->proc;
	struct binder_proc *target_proc = t->to_proc;
	struct binder_node *node;
...
	node = binder_get_node_from_ref(proc, fp->handle,
			fp->hdr.type == BINDER_TYPE_HANDLE, &src_rdata); // [1]
...
	if (node->proc == target_proc) { // [2]
...
	} else {
...
		ret = binder_inc_ref_for_node(target_proc, node, // [3]
				fp->hdr.type == BINDER_TYPE_HANDLE,
				NULL, &dest_rdata);
...
		fp->handle = dest_rdata.desc; // [4]
...
	}
...
}

The binder_inc_ref_for_node function first searches for any existing binder_ref that is associated with that binder_node [1]. If none exists [2], Binder creates a new binder_ref for the receiving client.

static int binder_inc_ref_for_node(struct binder_proc *proc,
			struct binder_node *node,
                    ...)
{
	struct binder_ref *ref;
	struct binder_ref *new_ref = NULL;
...
	ref = binder_get_ref_for_node_olocked(proc, node, NULL); // [1]
	if (!ref) { // [2]
...
		new_ref = kzalloc(sizeof(*ref), GFP_KERNEL); // [2]
...
	}
...
}

Finally, Binder creates a new binder_ref and links it to an existing binder_node in the kernel.

Client C’s new binder_ref linked to Client A’s binder_node

In the userspace, the receiving client will receive the transaction that contains the flat_binder_object with the header type BINDER_TYPE_HANDLE and a handle identifier. Subsequently, Client C can initiate a transaction or RPC with Client A using the designated handle identifier. Through this process, Client B has granted the same communication channel to Client C, allowing it to initiate new RPC with Client A.

Binder translates a flat_binder_object of type BINDER_TYPE_HANDLE to BINDER_TYPE_HANDLE

Within a binder_ref, the data field tracks the count of strong and weak references acquired by a userspace program.

/*
 * struct binder_ref - struct to track references on nodes
 * @data:        binder_ref_data containing id, handle, and current refcounts
...
 */
struct binder_ref {
	struct binder_ref_data data;
...
}

struct binder_ref_data {
	int debug_id;
	uint32_t desc;
	int strong;
	int weak;
};

A userspace program can use the following BC_* commands to update one of the reference counters of a given handle identifier.

Increment (+1) Decrement (-1)
strong BC_ACQUIRE BC_RELEASE
weak BC_INCREFS BC_DECREFS

The binder_thread_write function processes those commands and updates the binder_ref with the given handle identifier. This update is done using the binder_update_ref_for_handle function.

static int binder_thread_write(...)
{
...
	while (ptr < end && thread->return_error.cmd == BR_OK) {
		switch (cmd) {
		case BC_INCREFS:
		case BC_ACQUIRE:
		case BC_RELEASE:
		case BC_DECREFS: {
...
			bool strong = cmd == BC_ACQUIRE || cmd == BC_RELEASE;
			bool increment = cmd == BC_INCREFS || cmd == BC_ACQUIRE;
...
			if (ret)
				ret = binder_update_ref_for_handle(
						proc, target, increment, strong,
						&rdata);
...

A client can acquire additional strong references on the binder_ref by sending the BC_ACQUIRE command. It will only increment the internal_strong_refs of the associated binder_node if no strong references (data.strong) is held before.

static int binder_inc_ref_olocked(struct binder_ref *ref, int strong,
				  struct list_head *target_list)
{
...

	if (strong) {
		if (ref->data.strong == 0) {
			ret = binder_inc_node(ref->node, 1, 1, target_list);
...
		ref->data.strong++;
...
}

A client can also release a strong reference on the binder_ref by sending the BC_RELEASE command. When there are zero strong references, Binder decrements the internal_strong_refs of the associated binder_node.

static bool binder_dec_ref_olocked(struct binder_ref *ref, int strong)
{
	if (strong) {
...
		ref->data.strong--;
		if (ref->data.strong == 0)
			binder_dec_node(ref->node, strong, 1);
...
}

The binder_free_ref function is used to free a binder_ref object.

static void binder_free_ref(struct binder_ref *ref)
{
...
	kfree(ref);
}

There are two code paths where the binder_free_ref function is called:

  1. binder_thread_write
  2. binder_deferred_release

As we discussed earlier, a client can send specific BC_* commands to update the reference counter of a binder_ref with the given handle identifier. The binder_thread_write function is responsible for processing those commands.

static int binder_thread_write(...)
{
...
		switch (cmd) {
		case BC_INCREFS:
		case BC_ACQUIRE:
		case BC_RELEASE:
		case BC_DECREFS: {
...
			bool strong = cmd == BC_ACQUIRE || cmd == BC_RELEASE;
			bool increment = cmd == BC_INCREFS || cmd == BC_ACQUIRE;
...
				ret = binder_update_ref_for_handle(
						proc, target, increment, strong,
						&rdata);
...
}

When updating the reference counter of a binder_ref with a given handle identifier, binder_update_ref_for_handle frees the binder_ref if binder_dec_ref_olocked returns true.

static int binder_update_ref_for_handle(struct binder_proc *proc,
		uint32_t desc, bool increment, bool strong,
		struct binder_ref_data *rdata)
{
	bool delete_ref = false;
...
		delete_ref = binder_dec_ref_olocked(ref, strong);
...
	if (delete_ref) {
		binder_free_ref(ref);
...
	}
...
}

The binder_dec_ref_olocked function returns true to inform the caller that the binder_ref can be safely freed, if data.strong and data.weak become zero [1].

static bool binder_dec_ref_olocked(struct binder_ref *ref, int strong)
{
...
	if (ref->data.strong == 0 && ref->data.weak == 0) { // [1]
...
		return true;
	}
...
}

When a client closes the Binder file descriptor (binder_deferred_release), Binder traverses a rb_tree that contains all binder_ref owned by the binder_proc and frees them.

static void binder_deferred_release(struct binder_proc *proc)
{
...
	while ((n = rb_first(&proc->refs_by_desc))) {
		struct binder_ref *ref;

		ref = rb_entry(n, struct binder_ref, rb_node_desc);
...
		binder_free_ref(ref);
...
	}
...
}

Binder is designed to facilitate remote procedure calls (RPC) between clients. A client initiates communication by sending commands prefixed with BC_*. These commands are accompanied by relevant data specific to the command. Then, the client waits for a response prefixed with BR_* from Binder.

In the beginning, a single-threaded client initiates a RPC by send a BC_TRANSACTION command to Binder. Then, Binder forwards the command as a BR_TRANSACTION response to the recipient client. To return the RPC result, the recipient client sends a BC_REPLY command along with the result data back to Binder. Finally, Binder forwards it back to the client as a BR_REPLY response, completing the RPC process.

RPC sequence diagram

In scenarios involving multiple RPCs, a single-threaded client receives all incoming transactions in a first-in-first-out (FIFO) order. The client cannot read the next transaction until it has replied to the current one.

Multiple RPCs sequence diagram

Binder has support for multithreaded clients, enabling them to simultaneously process multiple RPCs in separate threads. Therefore, Binder maintains a list of threads (binder_thread) owned by a client (binder_proc).

When a client process spawns a new thread, the child thread inherits the previously opened Binder file descriptor. This file descriptor is associated with the same client binder_proc as the parent thread.

int binder_fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
...
pid_t pid = fork();                                // spawns a new thread
if (pid == -1) {                                   // fork failed
  return 1;
} else if (pid > 0) {                              // child thread starts here
...
  ret = ioctl(binder_fd, BINDER_WRITE_READ, &bwr); // do ioctls on inherited
                                                   // `binder_fd`
...
} else {                                           // parent thread starts here
...
  ret = ioctl(binder_fd, BINDER_WRITE_READ, &bwr); // do ioctls on `binder_fd`
...
}

Binder identifies the thread making the ioctl calls by its process ID (task_struct->pid).

Note: In userspace, the term “thread ID” corresponds to the process ID used in the kernel (task_struct->pid). Meanwhile, the term “process ID” refers to the thread group ID (task_struct->tgid).

Binder uses several workqueues to distribute incoming transactions: a main workqueue for each client (binder_proc->todo) and a thread workqueue for each thread (binder_thread->todo). We will dive deeper into the concept of workqueues in the next section (Binder Workqueues).

Before a child thread can retrieve an incoming transaction from the main workqueue, it must first register itself as a looper. This is achieved by sending the BC_ENTER_LOOPER or BC_REGISTER_LOOPER command to Binder upon spawning. Subsequently, when the child thread performs a read operation (BINDER_WRITE_READ ioctl), Binder retrieves the next transaction from the main workqueue and passes it to the child thread for processing. The overall multithreaded client is not required to respond to every transaction in a FIFO order. However, each thread must still adhere to the FIFO order when replying to its own workqueue.

Spawn child as a looper to handle transactions concurrently

A client thread can invoke the BINDER_THREAD_EXIT ioctl to exit early. Then, Binder cleans up all pending work in the thread’s workqueue and notifies the client that initiates the transaction with the BR_DEAD_REPLY response.

Child exits early with BINDER_THREAD_EXIT ioctl

When Binder cannot find a thread with an empty workqueue, it sends a BR_SPAWN_LOOPER response to the latest thread that is performing a read operation. This response requests the client to spawn a new thread to handle more future workloads. Spawning a new thread is not mandatory for the client. However, if it does, the new thread must register itself as a looper (BC_REGISTER_LOOPER) after spawn.

Binder requests new looper (BR_SPAWN_LOOPER)

A client can configure the maximum number of threads it would like to support in advance using the BINDER_SET_MAX_THREADS ioctl. Once this limit is reached, Binder will not request any additional thread (BR_SPAWN_LOOPER).

Binder supports one-way or asynchronous transactions, which does not require the recipient client to reply to. To initiate an asynchronous transaction, the sender sets the TF_ONE_WAY flag in the binder_transaction->flags field. The recipient client will receive regular transactions and asynchronous transactions together in a FIFO order.

However, Binder manages asynchronous transactions by queuing them in a dedicated asynchronous workqueue associated with each port (binder_node->async_todo). To read the next asynchronous transaction from a port’s asynchronous workqueue (binder_node->async_todo), the receiving client must first free the current one assigned in it using the BC_FREE_BUFFER command. After all, asynchronous transactions sent to the same client but different ports (binder_node) can still be processed simultaneously.

Multiple asynchronous transactions

Binder employs multiple workqueues to enable concurrency while maintaining transaction order. Each workqueue is represented as a doubly linked list with only the head pointer (struct list_head) being stored. There are three types of workqueue in Binder:

  1. Main client workqueue (binder_proc->todo): Stores all work items assigned to a client
  2. Individual client thread workqueue (binder_thread->todo): Stores work items assigned to a specific client thread.
  3. Individual binder_node asynchronous workqueue (binder_node->async_todo): Stores only a list of work items that relate to asynchronous transactions (BINDER_WORK_TRANSACTION).

Each work item is defined by a struct binder_work that can be added to a workqueue. The struct binder_work can be used independently or incorporated as a field within an object. It contains an entry node (entry) to be linked in a workqueue and the work type enum (type).

struct binder_work {
	struct list_head entry;

	enum binder_work_type {
		BINDER_WORK_TRANSACTION = 1,
		BINDER_WORK_TRANSACTION_COMPLETE,
		BINDER_WORK_TRANSACTION_PENDING,
		BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT,
		BINDER_WORK_RETURN_ERROR,
		BINDER_WORK_NODE,
		BINDER_WORK_DEAD_BINDER,
		BINDER_WORK_DEAD_BINDER_AND_CLEAR,
		BINDER_WORK_CLEAR_DEATH_NOTIFICATION,
	} type;
};

When a client performs a read operation (BINDER_WRITE_READ ioctl), Binder processes the next work item [1] and translates it into the appropriate response (BR_*) back to userspace [2]. To retrieve the next work item, Binder first checks the current client thread’s workqueue (binder_thread->todo) before looking in the main client workqueue (binder_proc->todo).

static int binder_thread_read(...)
{
	while (1) {
...
		w = binder_dequeue_work_head_ilocked(list); // [1]
...
		switch (w->type) {
...
		case BINDER_WORK_TRANSACTION_COMPLETE:
		case BINDER_WORK_TRANSACTION_PENDING:
		case BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT: {
...
			if (proc->oneway_spam_detection_enabled &&
				   w->type == BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT)
				cmd = BR_ONEWAY_SPAM_SUSPECT;
			else if (w->type == BINDER_WORK_TRANSACTION_PENDING)
				cmd = BR_TRANSACTION_PENDING_FROZEN;
			else
				cmd = BR_TRANSACTION_COMPLETE;
...
			if (put_user(cmd, (uint32_t __user *)ptr)) // [2]
...
}

When an asynchronous transaction is released, Binder dequeues a new one from the binder_node asynchronous workqueue (binder_node->async_todo) and queues it in the workqueue associated with the client thread that initiated the release (binder_thread->todo).

There are five categories of work items, each with a designated container and specific work type enums:

Category Container Work Type Enum
Transaction binder_transaction BINDER_WORK_TRANSACTION
Transaction status update None BINDER_WORK_TRANSACTION_COMPLETE
BINDER_WORK_TRANSACTION_PENDING
BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT
Binder node update binder_node BINDER_WORK_NODE
Death notifications binder_ref_death BINDER_WORK_DEAD_BINDER
BINDER_WORK_DEAD_BINDER_AND_CLEAR
BINDER_WORK_CLEAR_DEATH_NOTIFICATION
Error binder_error BINDER_WORK_RETURN_ERROR

In the Binder Concurrency Model section, we discussed how Binder distributes incoming transactions as work items across multiple threads within a client process. Every transaction (binder_transaction) is processed and queued in either the main client workqueue (binder_proc->todo) or individual thread work queue (binder_thread->todo).

New transactions are initially assigned as a BINDER_WORK_TRANSACTION work item to the main recipient client workqueue (binder_proc->todo). Upon reading, Binder processes the work item and sends the BR_TRANSACTION response back to userspace along with the transaction data.

On the other hand, reply transactions are specifically assigned to the workqueue of the client thread (binder_thread->todo) that initiated the first transaction. This guarantees that the thread that initiated the first transaction is the same thread that receives the reply. Upon reading, Binder sends the BR_REPLY response back to the userspace along with the transaction data.

A transaction is considered complete after a reply is received or an asynchronous transaction is sent. Binder queues the BINDER_WORK_TRANSACTION_COMPLETE work item in the workqueue of the client thread that initiated the transaction (binder_thread->todo). After processing this work item, Binder returns the BR_TRANSACTION_COMPLETE response back to the userspace.

In scenarios where an asynchronous transaction is sent to a frozen thread, Binder queues the BINDER_WORK_TRANSACTION_PENDING work item in the main workqueue of the client (binder_proc->todo) that initiated the transaction.

Finally, if an asynchronous transaction is received and the binder buffer allocator is full, Binder queues the BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT work item in the main workqueue of the recipient client (binder_proc->todo).

Binder supports death notifications, which allows clients to be notified when a connected client they’re interacting with exits. Binder tracks this by creating a binder_ref_death object containing the work item (binder_work) and assigning it to the binder_ref->death. When a binder_ref is released, Binder checks for an associated binder_ref_death. If found, Binder locates the corresponding binder_node and queues it as a BINDER_WORK_DEAD_BINDER work item in the main workqueue of the owner of that binder_node. When the client performs a read, Binder will send the BR_DEAD_BINDER response, notifying which client that was registered has exited.

Upon sending the BR_DEAD_BINDER response, Binder adds the work item in the binder_proc->delivered_death list. The client is expected to send the BC_DEAD_BINDER_DONE command, indicating that it has processed the death notification. Then, Binder removes the work item from the delivered_death list.

Clients also have the option to unregister death notifications. Upon success, Binder queues the BINDER_WORK_CLEAR_DEATH_NOTIFICATION work item in the main workqueue of the client that initiates the operation. However, if the registered client dies during this process, Binder queues the BINDER_WORK_DEAD_BINDER_AND_CLEAR work item, indicating that the operation failed because the client had already exited.

The BINDER_WORK_NODE work item provides updates to the client about the presence or absence of strong or weak references of a binder_node. Depending on who initiated the operation, Binder assigns the binder_node->work as a BINDER_WORK_NODE work item to either the main client workqueue or a client thread workqueue.

For example, if a client sends a BINDER_TYPE_BINDER to another client, which results in the creation of a binder_ref and an increase in the strong reference of the binder_node, Binder then assigns the work item to the workqueue of the client thread that sends it. Meanwhile, if another client acquires a strong reference on its binder_ref, leading to an increase in the strong reference of the binder_node owned by another client, Binder assigns the work item to the main workqueue of the other client.

Binder informs the userspace changes the presence or absence of strong or weak references only once. Binder uses the has_strong_ref and has_weak_ref fields within the binder_node to monitor the changes. When Binder processes the BINDER_WORK_NODE work item, it updates these fields and returns one of four responses based on the changes:

  • BR_INCREFS: has_weak_ref transitions from zero to one
  • BR_ACQUIRE: has_strong_ref transitions from zero to one
  • BR_RELEASE: has_strong_ref transitions from one to zero
  • BR_DECREFS: has_weak_ref transitions from one to zero
static int binder_thread_read(...)
{
...
	case BINDER_WORK_NODE: {
...
		// Save the previous values
		has_strong_ref = node->has_strong_ref;
		has_weak_ref = node->has_weak_ref;
...
		// Update the presence of strong and weak references
		if (weak && !has_weak_ref) {
			node->has_weak_ref = 1;
		... }
		if (strong && !has_strong_ref) {
			node->has_strong_ref = 1;
		... }
		if (!strong && has_strong_ref)
			node->has_strong_ref = 0;
		if (!weak && has_weak_ref)
			node->has_weak_ref = 0;
...
		// Check for any changes and return approriate responses
		if (weak && !has_weak_ref)
			ret = binder_put_node_cmd(..., BR_INCREFS, "BR_INCREFS");
		if (!ret && strong && !has_strong_ref)
			ret = binder_put_node_cmd(..., BR_ACQUIRE, "BR_ACQUIRE");
		if (!ret && !strong && has_strong_ref)
			ret = binder_put_node_cmd(..., BR_RELEASE, "BR_RELEASE");
		if (!ret && !weak && has_weak_ref)
			ret = binder_put_node_cmd(..., BR_DECREFS, "BR_DECREFS");

When certain operations either complete or fail, Binder queues the BINDER_WORK_RETURN_ERROR work item into the workqueue of the client thread that initiated it. Binder processes this work item and returns either the BR_OK or BR_ERROR response back to userspace.

Binder implements a memory allocator to store incoming transaction data, which we will call the binder buffer allocator. Every client binder_proc owns a binder buffer allocator binder_alloc which allocates memory for incoming transaction data.

During initialization, a client must create a memory map for the Binder device file descriptor as follows:

#define BINDER_VM_SIZE 4 * 1024 * 1024

int binder_fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
void *map = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE, binder_fd, 0);

The mmap syscall will call binder_mmap to reserve a virtual memory area and map it to the userspace. Binder defers the allocation of physical backing pages to store incoming transaction data until needed (lazy allocation).

Binder reserves a virtual memory area and maps it to userspace

Binder relies on the integrity of the transaction data in the allocator’s memory for cleanup. If the active transaction data is corrupted, it could lead to memory corruptions and even code execution in the kernel as demonstrated in the CVE-2020-0041 and CVE-2023-20938 writeups. As a result, Binder only allows users to create a read-only memory map (PROT_READ) and prevents them from modifying its access permission later.

The binder buffer allocator implements the best-fit allocation strategy with the kernel’s red-black tree data structure (rb_tree). The binder buffer allocator starts with a single binder_buffer, which takes up the whole buffer space and is labeled as unused.

binder_alloc starts with a single, unused binder_buffer consuming the entire buffer space

When sending a transaction to a client, Binder first allocates a binder_transaction object to store information about the transaction. Then, the binder buffer allocator allocates a binder_buffer object to own a chunk of memory in the memory map and assign it to the binder_transaction.

Binder splits the unused binder_buffer to allocate a new binder_buffer

When a client (Client A) sends a transaction with data, Binder allocates a binder_buffer from the target client’s (Client B) binder buffer allocator and copies the data into it. The target client receives the transaction and reads the data from the memory map.

Binder copies binder_transaction_data between clients

To free a transaction and its data, a client sends a BC_FREE_BUFFER message with the start address of the transaction data. Binder then frees the binder_transaction and sets the free field of the binder_buffer. If adjacent memory is also freed, the binder buffer allocator merges the two binder_buffer into one.

Binder merges a freed binder_buffer with the unused one

The binder buffer allocator does not zero out the transaction data after freeing the transaction. A client has read access to the memory map, so it can still read the transaction data after freeing it (BC_FREE_BUFFER). To zero out the transaction data after free, the sender must explicitly set the TF_CLEAR_BUF flag in the binder_transaction_data when sending the BC_TRANSACTION command.

For stub and proxy code generated from the AIDL, developers can annotate the interface with @SensitiveData to explicitly set the TF_CLEAR_BUF flag in all outgoing transactions. This prevents sensitive data in the transaction from remaining in memory after free. For example, the IKeyMintOperation.aidl AIDL interface used by Keymint is annotated with @SensitiveData.

@SensitiveData
interface IKeyMintOperation { ... }

As we discussed in Binder Concurrency Model, a single-threaded client cannot retrieve the next incoming transaction until it has responded to the current one. On the caller side, a single-threaded client cannot initiate a new RPC (BC_TRANSACTION) until it receives a reply (BR_REPLY) to the previous RPC. To maintain these orders, Binder tracks every incoming and outgoing transaction between two different threads on a transaction stack. The purpose of transaction stacks is different from the Binder Workqueues whose purpose is to support concurrency among multiple threads in a client.

Note: Every transaction is only associated with one sender thread and one receiver thread during its lifetime, regardless of whether the client is multithreaded or not.

It is common to have multiple clients working with each other to serve a RPC request. The diagram below shows an example of a RPC originating from A that involves multiple single-threaded clients.

RPC chain across multiple clients

It is a chain of transactions in the following order:

A -> B -> C -> D -> B

Binder allows a client to handle multiple pending transactions at the same time. For example, client B has two pending transactions at one point, one coming from A and another coming from D. However, the client can only respond with BR_REPLY to those pending transactions in the last-in-first-out (LIFO) order. In addition, when one of the clients in the chain dies, Binder ensures proper error handling by delivering errors back to every involved client in the correct order.

Binder puts every transaction on two different stacks, which we will call the chain transaction stack and the thread transaction stack.

Let’s have an overview of the functions each stack serves:

  1. Chain transaction stack
    • The order of transactions of a chain request.
    • Binder traverses it to clean up the chain request when one of the clients exits early.
  2. Thread transaction stack
    • The order of transactions that is sent or received by a client
    • Binder traverses it to clean up and release transactions, which a client participated in before exit.

Binder implements these two stacks as two linked lists with the following fields in binder_transaction:

struct binder_transaction {
...
	struct binder_transaction *from_parent;
	struct binder_transaction *to_parent;
...
	struct binder_thread *from;
	struct binder_thread *to_thread;
...
}

The binder_thread stores a pointer to the top element of its thread transaction stack in the transaction_stack field. This top element represents the last transaction that the thread sent or received.

struct binder_thread {
...
	struct binder_transaction *transaction_stack
...
}

When a client initiates a transaction (BC_TRANSACTION), Binder pushes the transaction onto the sender’s thread transaction stack. This is achieved by setting the from_parent field of the new transaction (binder_transaction) to point to the current top of the sender’s stack (transaction_stack). Then, the top of the stack is updated to point to the new transaction (binder_transaction).

binder_transaction t;
binder_thread sender;
...
t->from_parent = sender->transaction_stack;
sender->transaction_stack = t;

When a client reads a transaction, Binder pushes the transaction onto the receiver’s thread transaction stack. This is achieved by setting the to_parent field of the new transaction (binder_transaction) to point to the current top of the sender’s stack (transaction_stack). Then, the top of the stack is updated to point to the new transaction (binder_transaction).

binder_transaction t;
binder_thread receiver;
...
t->to_parent = receiver->transaction_stack;
receiver->transaction_stack = t;

Consequently, the chain transaction stack is formed by linking transactions through their from_parent fields, creating a chain of requests.

Assuming everything is in order, when a client sends a reply (BC_REPLY), Binder pops the current top transaction of the sender’s thread transaction stack. This is achieved by updating the top of the stack to point to the to_parent of the current top transaction. The popped transaction will be the one the sender had received and needs to reply to.

static void binder_transaction(..., struct binder_thread *thread, ...) {
...
	struct binder_transaction *in_reply_to = NULL;
...
	if (reply) {
		in_reply_to = thread->transaction_stack;
...
		thread->transaction_stack = in_reply_to->to_parent;
...
}

When a client with a pending incoming transaction fails or crashes, Binder cancels the pending request by popping the current top transaction from the sender’s thread transaction stack. The popped transaction will be the one the sender had sent, but the client failed to reply to. To notify the sender of the failure, Binder queues a BINDER_WORK_RETURN_ERROR work item to the sender’s client thread. Later, when the sender tries to read a reply, Binder processes the work item and returns either BR_DEAD_REPLY or BR_FAILED_REPLY according to the cause of the failure.

static void binder_pop_transaction_ilocked(struct binder_thread *target_thread, ...)
{
...
	target_thread->transaction_stack =
		target_thread->transaction_stack->from_parent;
...
}

When a client with a pending incoming transaction fails to reply or crashes, Binder cancels the pending request by popping the current top transaction from the sender’s thread transaction stack. The popped transaction will be the one the sender had sent, but the client failed to reply to. To notify the sender of the failure, Binder queues a BINDER_WORK_RETURN_ERROR work item to the sender’s client thread. Later, when the sender tries to read a reply, Binder processes the work item and returns either BR_DEAD_REPLY or BR_FAILED_REPLY according to the cause of the failure.

The chain transaction stack tracks the order of transactions in a chain, which originates from the first request.

Let’s reuse the example above involving four transactions among A, B, C and D. Before B responds D, the chain transaction stack will look as follows:

Chain transaction stack

Binder can traverse the chain transaction stack, by following the from_parent field of any transaction, to find the previous transaction and the first transaction in the chain.

Suppose client B exits before responding to the last transaction sent by D. During cleanup, Binder traverses the chain transaction stack starting from the top transaction on B’s transaction_stack to look for its previous client in the chain. Then, it sends a BR_DEAD_REPLY to notify the client that there is a failed reply. In our case, Binder sends BR_DEAD_REPLY to D, which is the previous client before B in the chain.

A -> B -> C -> D -> B

Binder calls binder_send_failed_reply to traverse the chain transaction stack and sends an error_code (e.g BR_DEAD_REPLY) to the previous client in the chain.

static void binder_send_failed_reply(struct binder_transaction *t,
				     uint32_t error_code)
{
        while (1) {
                target_thread = binder_get_txn_from_and_acq_inner(t);
                if (target_thread) {
...
                        // Send `error_code` to `target_thread
...
                        return;
                }
                next = t->from_parent;
        }
}

The thread transaction stack tracks the order of active transactions that a client has sent and received.

Following the previous example, the thread transaction stack of each client will look as follows:

Thread transaction stack

Binder utilizes the from_parent and to_parent fields along with from and to_thread to traverse the thread transaction stack of each client thread. By checking if from or to_thread points to the target client thread, it follows either the from_parent or to_parent field to the next transaction in the stack.

For example, starting from B’s transaction_stack, Binder checks whether the from or to_thread points to B and follows either the from_parent or to_parent to the next transaction.

  • The first transaction from the top has to_thread pointing to B, so Binder follows to_parent to the next transaction.
  • The second transaction has from pointing to B, so Binder follows the from_parent to the next transaction.

When a client thread exits, Binder must remove every reference to that thread within all ongoing transactions. The binder_thread_release, which is responsible for releasing the client thread, handles that cleanup. It traverses the thread transaction stack to remove every reference to the client (binder_proc) and client thread (binder_thread).

static int binder_thread_release(struct binder_proc *proc,
				 struct binder_thread *thread)
{
        while (t) {
...
                if (t->to_thread == thread) {
...
                        t->to_proc = NULL;
                        t->to_thread = NULL;
...
                        t = t->to_parent;
                } else if (t->from == thread) {
                        t->from = NULL;
                        t = t->from_parent;
                }
        }
...
}

We would like to conclude this blog with a quick overview of the libdevbinder project we briefly mentioned in the introduction. As you might have noticed based on the information provided above Binder kernel module is a very complex target for analysis. In order to fully understand semantics of the exposed Binder kernel API to the user-space code via ioctls one would need to study the implementation of libbinder – library which sits on top of the Binder kernel module and provides a higher-level Binder IPC interface to the Android Framework. The libbinder itself consists of multiple files written in C++ & Rust and might be challenging to comprehend for the audience with no prior knowledge in this area.

Thus, to facilitate the Binder research and make understanding Binder concepts easier we developed a tiny and simple library – libdevbinder – which serves as an example on how two endpoints running in user-space could communicate with each other over the Binder. This library provides necessary minimalistic implementation sufficient to send a Binder transaction across the IPC interface.

As an example, libdevbinder additionally provides two small programs client and server where client sends user-provided data via Binder transaction to the server which prints the received data to stdout.

These programs are expected to run on top of a vanilla Linux kernel built with Binder driver enabled config (e.g. in QEMU) – not in an Android environment. Main reason for that is that the server program registers itself with the Binder driver as the context manager. There can only be one context manager per binder device node, which very likely is already claimed by the ServiceManager process on Android.

To send the transaction we would use client program which takes as input a string and sends it to the server:

./client “Hello world!”

We hope that these tiny examples remove the layer of ambiguity and complexity over the Binder kernel ioctl interface and make researching Binder easier and more convenient.

While we couldn’t cover every detail about Binder, here are some additional resources that may be helpful for learning more:

Historical context:

Special thanks to Adam Bacchus, Alice Ryhl, Carlos Llamas, Farzan Karimi, Jon Bottarini and Sindhu Shivkumar for their support with technical questions and review of this post. This post would also not have been possible without the collaborative effort of our amazing team.



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