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:
- Part 1: Attacking Android Binder: Analysis and Exploitation of CVE-2023-20938
- Part 2: Binder Internals
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:
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:
threads
is the root node of a red-black tree that contains allbinder_threads
it owns.is_dead
determines whether the client is dead.tmp_ref
tracks the number of local variables holding a pointer to thebinder_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:
threads
: the red-black tree is empty after allbinder_thread
are released (see binder_thread.is_dead
: set to true when closing the Binder file descriptor (binder_thread_release
).tmp_ref
: set to 0 when there is no temporary variable holding a pointer to thebinder_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.
It contains the following fields that determine its lifetime:
is_dead
determines whether the thread is dead. This is distinct from the client death status (binder_proc->is_dead
).- 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:
is_dead
: set to true when Binder releases the threadbinder_thread_release
. Thebinder_thread_release
is called when:- A thread calls the
BINDER_THREAD_EXIT
ioctl. Thebinder_proc
and other threads remain unaffected. - A client closes the Binder file descriptor (
binder_deferred_release
), which releases thebinder_proc
and allbinder_thread
.
- A thread calls the
tmp_ref
: set to 0 when there is no temporary variable holding a pointer to thebinder_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.
The binder_ref
always holds a pointer to a binder_node
in its node
field.
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.
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.
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_node
also contains the following fields that determine its lifetime:
work
is a node in a workqueue list when thebinder_node
is being processed.refs
holds the head of the list of allbinder_ref
linked to it.internal_strong_refs
tracks the number of strong references acquired remotely in other clients.local_weak_refs
tracks the number of weak references acquired locally.local_strong_refs
tracks the number of strong references acquired locally.- 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 ofIBinder
and its underlyingRefBase
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.
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.
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 thebinder_node
object, so calling the functions withstrong
set to 0 andinternal
set to 1 does not modify any reference counter. Theinternal_weak_refs
is implicitly tracked by the length of thebinder_proc->refs
linked list minuslocal_weak_refs
. Therefore, before freeing abinder_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 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.
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.
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.
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
) becausebinder_buffer
has a reference to abinder_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 nointernal_weak_refs
, this function call does not update any reference counters of abinder_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 zerobinder_ref
in itsrefs
list. - The
tmp_refs
counter of thebinder_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.
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.
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.
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.
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:
binder_thread_write
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.
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.
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.
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.
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.
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.
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:
- Main client workqueue (
binder_proc->todo
): Stores all work items assigned to a client - Individual client thread workqueue (
binder_thread->todo
): Stores work items assigned to a specific client thread. - 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 oneBR_ACQUIRE
:has_strong_ref
transitions from zero to oneBR_RELEASE
:has_strong_ref
transitions from one to zeroBR_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 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.
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
.
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.
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.
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.
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:
- 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.
- 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:
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:
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 followsto_parent
to the next transaction. - The second transaction has
from
pointing to B, so Binder follows thefrom_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:
- https://www.synacktiv.com/publications/binder-transactions-in-the-bowels-of-the-linux-kernel
- https://events.static.linuxfound.org/images/stories/slides/abs2013_gargentas.pdf (YouTube)
- https://medium.com/swlh/binder-introduction-54fb90feeecb
- https://medium.com/swlh/binder-architecture-and-core-components-38089933bba
- https://medium.com/swlh/binder-threading-model-79077b7c892c
Historical context:
- https://www.osnews.com/story/13674/introduction-to-openbinder-and-interview-with-dianne-hackborn/
- http://kroah.com/log/blog/2014/01/15/kdbus-details/
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.