Mobile Computing · Mobile Platforms

Binder IPC: Parcels, Handles, and One-Way Calls

How a single-copy parcel transfer works, what a binder handle actually names, and why one-way calls have ordering guarantees but no completion signal.

Nearly every interaction between a app and the system services it depends on crosses a process boundary through the same transport. The transport is a kernel driver with a character device interface, a single ioctl for the send operation, and a memory-mapping trick that removes one of the two copies a naive message-passing system would pay. It is small enough to describe completely, and the details explain both its speed and its most awkward constraints.

One copy instead of two

A conventional message queue copies the payload from the sender’s buffer into a kernel buffer, then from the kernel buffer into the receiver’s buffer. Two copies, and the payload size bounds both.

Binder’s send path maps the receiver’s memory into the kernel’s address space for the duration of the transaction. The driver allocates pages, installs them in the receiver’s page tables at a location the receiver has already mapped, and then copies the sender’s payload once into those pages. The sender never sees the kernel buffer; the receiver reads directly from memory it already owns. The second copy is eliminated by making the kernel write into the destination rather than into an intermediate.

The cost of the trick is that the mapping must be established before the copy, which means the receiver must have told the driver how much address space to reserve and where. Each process registers a buffer at startup, and that buffer’s capacity is the hard ceiling on any single transaction. A transaction larger than the remaining space fails with a transaction-too-large error; it does not fragment and it does not fall back to a slower path.

The parcel layout

The payload is a parcel: a flat byte buffer with a position pointer and a length. Writing appends; reading consumes. There is no schema and no field tags. The writer and reader must agree on the sequence of types, and the agreement is expressed by both sides using the same generated or hand-written marshalling code.

A parcel stores its data in host byte order and word-aligned, so a four-byte integer occupies four bytes and a string occupies a four-byte length, the UTF-16 code units, and a terminator, padded to a four-byte boundary. Objects are written as a four-byte presence marker followed by their fields, recursively. Because the format is positional, a reader that consumes one field too few will misinterpret everything after it, usually producing a plausible but wrong value rather than an error.

The symmetry requirement is easiest to see in the minimal case: a writer and a reader that disagree by one field.

/* Writer: fixed order, no field names. */
put_i32(&p, record.id);
put_i32(&p, record.flags);
put_str(&p, record.label);

/* Reader in a later version that added a field in the middle. */
int32_t id    = get_i32(&p);
int32_t extra = get_i32(&p);   /* actually reads flags */
char   *label = get_str(&p);   /* actually reads the label's length as a pointer */

The third read does not fail cleanly. It interprets the first four bytes of the string’s length prefix as a pointer, and the parcel’s bounds check either rejects it or, worse, accepts a length that happens to fall inside the buffer. Appending new fields at the end is safe because the reader stops when it has what it needs; inserting a field anywhere else is not.

File descriptors are the exception to the flat-buffer rule. A descriptor cannot be copied into a byte buffer; the driver intercepts descriptors written to the parcel, removes them from the sender’s table, and installs them into the receiver’s table at a new number, recording the mapping in a side table attached to the transaction. The receiver reads back a descriptor that refers to the same open file description, with the same offset and the same flags. The sender loses its copy, which is why the sender must duplicate a descriptor before sending it if it still needs it, and why the platform’s parcel API marks descriptors as owned once written.

Handles are not pointers

A binder reference in a parcel is a 32-bit handle. The handle is an index into a per-process table maintained by the driver, and it names a node in the driver’s own namespace. The sender’s handle value and the receiver’s handle value for the same object are usually different numbers, because each process has its own table.

When a process sends a reference to one of its own objects, the driver creates a node for that object, assigns it a handle in the receiver’s table, and the receiver’s later calls to that handle are routed back to the sender’s process. When a process forwards a reference it already holds, the driver translates the sender’s handle into the node identity and then into a handle in the receiver’s table, so the receiver talks to the original object’s owner rather than to the forwarder. This translation is why handles are opaque and why storing a raw handle value across processes is meaningless.

The driver also tracks the liveness of each node. When the process owning a node exits, the node is marked dead, and any transaction sent to it fails immediately rather than blocking. Receivers can register for a notification when a node dies, which is the mechanism behind the death-recipient pattern used to detect that a service has crashed. The notification is delivered as a transaction from a special context, so it arrives on the same thread pool as ordinary calls and must not be mistaken for one.

Two call modes

A synchronous transaction sends the payload and blocks the calling thread until the receiver replies. The driver parks the caller on a wait queue associated with the transaction, and the reply wakes it. The payload of the reply is copied back through the same one-copy path, in the reverse direction.

A one-way transaction sends the payload and returns immediately. The receiver processes it asynchronously on a thread from its pool, and no reply is sent. Three properties follow, and all three are load-bearing.

Ordering is preserved between one-way calls from the same sender to the same receiver, because the driver queues them per sender-receiver pair. Two threads in the same process sending one-way calls to the same object may be serialised in either order relative to each other, but each thread’s calls arrive in the order it made them.

No completion signal exists. A one-way call that the receiver crashes on is not retried, and the sender learns nothing. This is deliberate: the point of the mode is to let a sender continue without waiting, which is incompatible with reporting a result.

Backpressure is invisible. If the receiver’s queue is full, the driver blocks the sender at the point of sending, so a one-way call can still block even though the call itself is asynchronous. The block is a flow-control measure rather than a semantic one, and it means a sender cannot assume that a one-way call returns in bounded time.

Thread pools and the re-entrancy problem

Each process that receives binder calls runs a pool of threads that read transactions from the driver and dispatch them to the local object named by the handle. The pool grows on demand up to a per-process maximum, and the maximum is a fixed number rather than a function of load.

A synchronous call occupies a thread in the receiver’s pool for its whole duration. If the receiver, while handling the call, makes a synchronous call back to the original caller, the driver can deliver it to the very thread that is blocked waiting for the reply: the driver tracks the chain of transactions each thread is waiting on, and a thread blocked on a call from process A is a valid target for a call from A. That reuse is what keeps the common callback pattern from deadlocking.

The reuse does not extend indefinitely. Each level of nesting consumes a thread at each end, and once the pool is exhausted a further nested call cannot be delivered. The symptom is a stall rather than an error: the outermost caller waits for a reply that will never be produced, until the transaction times out. The standard avoidance is to keep nesting shallow, to avoid making synchronous calls while holding a lock that the incoming call also needs, and to use one-way calls for notifications that need no result.

Security properties of the transport

The driver supplies the receiver with the sender’s UID and PID for every transaction, taken from the kernel’s own record of the calling process rather than from anything the sender wrote in the payload. A receiver that trusts a UID field inside the parcel instead of the one the driver provides can be spoofed trivially.

A reply can carry a reference to an object the receiver owns, which is how a call returns a handle to a newly created object. The driver creates a node for the object and installs a handle for it in the sender’s table, so the sender’s subsequent calls reach the object directly rather than through the original node.

Permissions are not implied by the transport. Holding a handle means the holder may send transactions to the node, nothing more. Any authorisation must be performed by the receiving object, which is why the checks described in Permission Grants and Where They Are Enforced appear at the top of nearly every system service method, and why the labels described in Mandatory Access Control Above the UID Boundary are consulted by the driver for every transaction rather than by the service.

Where the transport fails

A parcel that is written and never read leaves its file descriptors in the transaction’s side table, and the driver releases them when the transaction is freed. A parcel that is read twice, or read after being recycled, produces undefined values, because the position pointer is not bounds-checked against the original length.

A transaction that exceeds the buffer fails before any of it is delivered, so there is no partial-delivery case to handle. The sender must catch the failure and, if the payload is genuinely large, use a different channel: a shared memory file, a descriptor to a pipe, or a temporary file whose path is passed instead of its contents. This is why the saved-state channel, described in Saved State, Process Death, and the Bundle Size Limit, carries identifiers rather than data.

A receiver that blocks its pool threads on a slow operation starves the pool. Because the pool size is fixed, enough concurrent slow calls exhaust it, and subsequent calls queue in the driver until the sender times out. The symptom is a service that works under light load and stalls under moderate load, with no error in the service’s own logs.

Finally, a node’s death notification can arrive after the receiver has already replaced its reference to the node. The notification names the node, not the handle, so the receiver must compare identities rather than handle values, and must tolerate notifications for objects it no longer cares about.

References

  1. Android Open Source Project — Binder IPC — AOSP documentation
  2. ioctl(2) — Linux manual page — Linux man-pages project
  3. Android Developers — Parcelable and Parcel — Android Developers