Skip to content

Car Service & Framework

How Binder and AIDL actually work

Every car API call crosses a process boundary. What that costs, what a Parcel is, why thread pools run out, and why a slow Vehicle HAL shows up as jank in an unrelated app.

Advanced10 minBinder · AIDL · IPC · Performance

You write this:

val speed = properties.getFloatProperty(PERF_VEHICLE_SPEED, 0)

It looks like a method call. It is not. That line crosses a process boundary, goes through the Linux kernel, wakes a thread in another process, and blocks your thread until an answer comes back.

Understanding what actually happens explains a surprising number of automotive bugs — including ones that look like rendering problems and are not.

Why processes are separate at all#

Android isolates things. Your app is one process. is another. The is a third. None of them can read each other's memory.

That mechanism is .

What AIDL actually generates#

is a small language for describing what one process will let another ask it. You write an interface; the build generates the plumbing.

What you write
interface ICarComfort {
    int getMassageIntensity(int seatAreaId);
    void setMassageIntensity(int seatAreaId, int level);
}

From that, the build produces two classes you never write by hand:

A Proxy, used on the calling side. It takes your arguments, packs them into a buffer, and sends them.

A Stub, used on the receiving side. It unpacks the buffer, works out which method was meant, and calls your real implementation.

What the generated Proxy does, roughly
public int getMassageIntensity(int seatAreaId) throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    try {
        data.writeInterfaceToken(DESCRIPTOR);   // "you are talking to ICarComfort"
        data.writeInt(seatAreaId);              // pack the argument
 
        // This is where the process boundary is crossed. It BLOCKS.
        mRemote.transact(TRANSACTION_getMassageIntensity, data, reply, 0);
 
        reply.readException();
        return reply.readInt();                 // unpack the result
    } finally {
        reply.recycle();
        data.recycle();
    }
}
What the generated Stub does, roughly
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
    switch (code) {
        case TRANSACTION_getMassageIntensity: {
            data.enforceInterface(DESCRIPTOR);
            int seatAreaId = data.readInt();       // unpack
            int result = this.getMassageIntensity(seatAreaId);   // YOUR method
            reply.writeNoException();
            reply.writeInt(result);                // pack the answer
            return true;
        }
    }
}

A Parcel is a byte buffer, not an object#

A Parcel is a flat sequence of bytes with a cursor. writeInt appends four bytes; readInt consumes four bytes.

Crossing the boundary#

One method call, two processes, one copy through the kernelyour app processYour codemanager.getProperty()Proxy (generated)writes args into a Parcelbinder driver/dev/bindersingle copy intothe target’s bufferCar Service processBinder thread poola free thread picks it upStub → onTransact()unpacks, calls the real methodreply travels back the same way — your thread was blocked the whole timepool is finite (~15 threads) · one transaction buffer per process (~1 MB)exhaust either and unrelated callers stall — which looks like UI jank
One AIDL call, end to endYour thread blocks from the moment transact() is called until the reply comes back. The kernel copies the data once, directly into a buffer the target process has already mapped.

Step by step:

  1. The Proxy packs arguments into a Parcel.
  2. It calls transact(), which issues an ioctl on /dev/binder.
  3. The kernel copies the data once into a memory region the target process mapped at startup. One copy, not two — this is the main reason Binder is fast.
  4. The kernel wakes a thread in the target's binder thread pool.
  5. That thread runs onTransact, which calls the real method.
  6. The reply is packed and travels back the same way.
  7. Your thread, which has been blocked this whole time, wakes up with the answer.

The two limits that cause real bugs#

The thread pool is finite#

Every process that serves Binder calls has a pool of threads to handle them. It is not unlimited — commonly around 15, and configurable per process.

When every thread is busy, new incoming calls queue.

Seeing pool pressure
adb shell dumpsys activity service com.android.car | grep -i binder
adb shell perfetto -o /data/misc/perfetto-traces/t -t 15s sched binder_driver am

The transaction buffer is about 1 MB — per process#

Each process has one shared buffer for all in-flight transactions. Roughly 1 MB, shared with everything else happening at that moment.

oneway: the escape from blocking#

Marking a method or an interface oneway makes it fire-and-forget.

A callback interface that must not block the caller
oneway interface ICarComfortListener {
    void onIntensityChanged(int seatAreaId, int level);
}

The call is queued and returns immediately. There is no return value and no way to know it arrived.

Identity: who is calling?#

Binder carries the caller's identity into the receiving process.

Inside a service
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
 
// This is what permission enforcement is built on
mContext.enforceCallingOrSelfPermission(PERMISSION, "setMassageIntensity");

There is a trap:

clearCallingIdentity — necessary and dangerous
long token = Binder.clearCallingIdentity();
try {
    // Now running as the SERVICE's identity, not the caller's.
    // Needed to reach something the caller cannot — but you have just
    // dropped the caller's permission context entirely.
    doPrivilegedThing();
} finally {
    Binder.restoreCallingIdentity(token);   // ALWAYS in a finally
}

Forgetting the restore leaves the thread running with elevated identity for whatever it handles next. It is a genuine security bug and it is easy to write.

Death: knowing when the other side is gone#

Processes die. If you hold a reference to a dead one, calls throw.

Being told rather than finding out
binder.linkToDeath({
    // Car Service died. Every manager you cached is now useless.
    reconnect()
}, 0)

The three binder domains#

Android runs more than one binder driver, to keep boundaries honest.

Device nodeUsed for
/dev/binderFramework ↔ apps — the ordinary case
/dev/hwbinderHIDL interfaces, the older HAL mechanism
/dev/vndbinderVendor process ↔ vendor process

AIDL at the two levels you will meet#

The same technology appears twice in the automotive stack, and it is worth seeing that it is the same thing.

At the app level#

Your app to Car Service
val properties = car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
val speed = properties.getFloatProperty(PERF_VEHICLE_SPEED, 0)

CarPropertyManager is a friendly wrapper. Underneath it is an AIDL proxy for ICarProperty, sending a binder transaction to Car Service.

At the HAL level#

Car Service to the Vehicle HAL
interface IVehicle {
    void getValues(in IVehicleCallback callback, in GetValueRequests requests);
    void setValues(in IVehicleCallback callback, in SetValueRequests requests);
    VehiclePropConfigs getAllPropConfigs();
}

Same mechanism, different interface, and one notable difference: the VHAL's methods take arrays and answer through a callback rather than returning a value.

Practical rules#

  • Never make a blocking Binder call on the UI thread. It is a call into another process, and you cannot bound how long it takes.
  • Make callbacks oneway.
  • Never hold a lock across a Binder call, in either direction.
  • Do not pass large payloads. Pass a descriptor.
  • Check permission on the first line of every Binder method.
  • Always restoreCallingIdentity in a finally.
  • Enable binder_driver in traces. Automotive problems frequently live in a process you are not profiling.

Next#

Car Service itself — the process on the other end of most of these calls.

References & further reading

Code links target the main branch on cs.android.com. AOSP moves — if a path 404s, search the symbol instead.