A proxy-to-stub diagram explains one call. A working service must also survive simultaneous calls, callbacks, process death, delayed work, and retries whose first outcome is unknown. Those are not unusual corner cases: they follow directly from IPC and process lifetime.
Before you start#
Read Binder: app to system service, Concurrency and lifetime, and Android's security model. The implementation baseline is AOSP android-14.0.0_r1, with a separately identified representative Android common-kernel tag. Required practice uses source and synthetic timelines; do not flood real services or send arbitrary transactions.
Learning goals#
Explain handle/reference transfer, death notification, Binder thread pools, synchronous reentrancy, oneway ordering, deferred caller identity, and ambiguous retries. Diagnose a lock cycle and design a safer request lifecycle without assuming that more threads or asynchronous calls automatically solve it.
Handles are process-local references#
A client-side integer handle identifies a Binder reference in that client's Binder context. It is not a globally meaningful object address. When a Parcel transfers a Binder object, Binder maintains/translates the corresponding reference for the recipient. Sending the number as an ordinary integer does not perform that transfer.
File descriptors have a similar distinction: Binder can transfer references to open files through the supported object protocol, assigning recipient-local descriptor numbers. Sending the sender's raw descriptor integer does not let the recipient open that file. The recipient must also respect ownership and closure rules. Permission to receive a descriptor does not make every ioctl on its object safe or authorized.
Why this machinery exists: references let processes communicate without exposing each other's virtual pointers, while the kernel tracks objects and work. Reference lifetime can keep an endpoint object referenced; it cannot keep the owning process immortal. See the Binder overview and representative binder.c.
Death notification is information, not rollback#
A remote endpoint can die between lookup and the next call. A death recipient (linkToDeath in the Java API) lets a client respond to that lifetime event. It is not a guarantee that the last operation did not execute. Death may occur before dispatch, during work, after a side effect, or after a reply was queued but before the client consumed it.
Worked ambiguous outcome: client C asks server S to append one record. S appends, then dies before replying. C sees a failed call. Retrying without a request identifier can append twice after restart. A correct interface may need an idempotent operation, deduplication identifier with defined retention, or a queryable operation state. A blind retry loop cannot manufacture exactly-once semantics.
Removing an app's authorization also differs from killing an already-started operation. A Binder death notification is not permission revocation, and permission revocation is not an automatic undo of earlier side effects. State the lifecycle semantics in the service contract. The IBinder API defines remote-death behavior; application recovery remains the client's responsibility.
A thread pool is not a queue of independent safe jobs#
Binder servers receive work on Binder threads. A synchronous caller normally waits for the reply; meanwhile a server thread can block on a lock, downstream I/O, another service, or a callback. If enough threads block, new independent requests can wait even while CPU usage is low. That is thread-pool starvation, not necessarily a slow CPU or broken driver.
Synchronous nested calls can re-enter a process. A call from A to B followed by a synchronous callback from B to A can dispatch while A's outer work is incomplete. Even designs with few serving threads must consider nested entry and partially updated state. The Binder threading documentation explains nested transactions and warns about holding locks during calls.
Synthetic teaching data, not a device capture.
- A holding LAService B
synchronous request; A retains LA
- Service BCallback in A
synchronous callback before reply
- Callback in AA holding LA
callback needs LA; outer A still waits
Time runs downward. Dashed arrows show returns.
Read the text version
- A holding LA → Service B: synchronous request; A retains LA
- Service B → Callback in A: synchronous callback before reply
- Callback in A → A holding LA: callback needs LA; outer A still waits
Work through a deadlock
Synthetic scenario:
- A thread in service A holds lock
LAand calls service B synchronously. - B handles the call and synchronously calls A's callback.
- The callback path tries to acquire
LAwhile the outer call has not released it. - The outer A call is waiting for B; B is waiting for the callback; the callback is waiting for A's lock.
Depending on dispatch/thread/lock semantics, this can deadlock or expose unsafe reentrancy. A recursive lock can avoid one same-thread acquisition failure while still exposing state halfway through an invariant or a cycle involving other threads. “Make the mutex recursive” is not a general repair.
A safer pattern is to validate and snapshot protected state, release the lock, make the external call, then reacquire the lock and verify that the state/generation still matches before committing a result. This is not free: another request can change state while the lock is released, so generation checking or another explicit protocol is needed. Eliminating the external call, using a truly asynchronous contract, or redesigning ownership can be better for a particular service. Do not copy a universal lock recipe without analyzing its state transitions.
Why adding Binder threads may not fix it
More threads can reduce some queueing, but cannot release LA in a logical lock cycle. They can increase simultaneous access to unsafe state and make a race more reproducible. First identify whether the delay is queueing, CPU work, lock wait, downstream I/O, or callback dependency. Then change the resource or protocol that actually limits progress.
Oneway is not unlimited, parallel, or complete#
A oneway remote call does not wait for an ordinary method reply. It still marshals data and submits work; submission can encounter transport/resource failure. The receiver must eventually process the work. Oneway methods on the same Binder object have ordering/serialization behavior described by Binder's contract; different objects are not one globally ordered stream. A local implementation can also behave differently from the remote asynchronous route.
Changing a synchronous method to oneway changes observable API semantics. Errors/results need another channel; cancellation, completion, and outstanding-work limits must be designed. A producer that submits work faster than it can be completed can still build a backlog. Oneway removes a particular wait, not the need for flow control.
Also do not assume a synchronous transaction is an ordering barrier for all oneway work sent through other objects. If a later operation depends on completion, represent that dependency explicitly with a supported acknowledgement/token/state protocol. Sleeping for a guessed number of milliseconds is not such a protocol.
Caller identity is scoped to transaction handling#
During an incoming transaction, Binder supplies caller identity. A method argument named uid is merely data until checked against that authority. If the service posts a Runnable to a Handler and later calls Binder.getCallingUid() in the worker, it does not recover the original client's identity. The worker is outside the original transaction context.
Original algorithm sketch, not a complete Android service:
incomingRequest(arguments):
capture actual Binder caller identity
validate arguments and authorize that caller
create an immutable work record with permitted attribution
enqueue record
worker(record):
enforce any execution-time state/revocation requirements
execute only the already-defined operation
Capturing identity is not enough if the worker later trusts unchecked package/UID fields or if authorization must be revalidated at execution time. A queue introduces time between consent and action; the contract must decide what a state change during that delay means.
clearCallingIdentity() changes the calling-identity state used by local Binder-aware checks on the executing thread; it is not a kernel setuid and does not impersonate an arbitrary caller. A subsequent remote transaction normally presents the sending service process's identity, not a transparently delegated chain of all earlier callers. Attribution chains and explicit delegated-authority protocols require their own validation. Inspect Android 14 IPCThreadState::clearCallingIdentity and restoreCallingIdentity and the Java Binder API.
The usual try/finally restoration pattern prevents cleared thread-local identity from leaking into later local work. It does not undo an unauthorized action performed before restoration. Authorize the intended operation using the correct original identity before adopting service-owned execution context.
Bulk data and bounded work#
Binder's transaction buffer is a constrained resource shared by in-flight work in a process; the familiar transaction-size guidance is not a promise that every message just below a fixed number always succeeds. Other transactions, metadata, and implementation details matter. Large pixel arrays or media payloads usually need a contract designed around supported shared memory/file descriptors/buffers, not ever-larger Parcels. See TransactionTooLargeException.
Moving payloads into shared memory does not eliminate validation. The receiver still needs size/bounds/lifetime checks, ownership rules, and synchronization. A descriptor gives access to an object; it does not prove untrusted bytes inside it are safe to parse.
Practice#
Draw a wait-for graph for the lock cycle. Name one edge removed by each proposed repair, and explain why “add ten Binder threads” removes none. Then classify these synthetic cases:
| Case | Strongest justified conclusion |
|---|---|
| Client gets remote-death error after a mutating request | Outcome may be unknown; do not infer no side effect |
| Worker sees its own UID after enqueue | Transaction identity did not follow the work automatically |
| Oneway returned, completion callback absent | Submission and completion are separate; inspect receiver/lifetime/backlog |
| Low CPU, all Binder threads waiting on one lock | Lock/contention/dependency evidence, not proof of CPU saturation |
For a source trace, locate the BR_DEAD_BINDER branch in pinned IPCThreadState.cpp and follow the death-notification dispatch into the referenced Binder proxy path. State what it reports and why it cannot roll back arbitrary service side effects. Source inspection is not an induced service-death test.
Knowledge check#
- Does sending handle integer 7 give the receiver your Binder reference?
- Can process death happen after an operation's side effect but before its reply?
- Does oneway remove the need for queue limits and completion semantics?
- What is lost when a worker tries to reconstruct a caller with
getCallingUid()later?
Answers#
- No. Use the supported Binder-object transfer protocol; the number is process-local.
- Yes. Retry safety must account for that ambiguous outcome.
- No. It changes waiting/return semantics, not the service's capacity or lifetime.
- The original transaction context; capture and validate required identity/attribution at the entry boundary.
Checkpoint#
Ready when your wait-for graph explains the cycle, the retry case includes an idempotency/deduplication strategy with stated limits, and the worker design preserves validated identity. Revise if more threads, recursive locks, oneway, or automatic retries are used as universal fixes. Return to the exact dependency or lifetime edge and remove it deliberately. No Android deadlock, death, or load test is claimed by the synthetic exercise.
Sources#
- AOSP Binder threading and overview — transactions, reentrancy, references, and thread pools.
- Android 14
IPCThreadState.cpp— identity state andBR_DEAD_BINDERdispatch. - Representative ACK
binder.c— reference/transaction implementation, not every Android 14 product kernel. - Android API IBinder, Binder, and TransactionTooLargeException — death, caller identity, and constrained transaction storage.