AOSP Framework & Internals
5 min read

Permission Control & SDK Integration

Learn about Permission Control & SDK Integration.

Why Your System Service is an Attack Surface

You just added a Manager API to expose your custom hardware. That new interface is now visible to every application running on the device. An untrusted app can easily locate your service interface and attempt to bind to it. If your service accesses custom hardware or modifies critical system state, malicious callers could abuse those capabilities.

Android relies on permissions to prevent unauthorized access. You must protect your custom service by requiring a specific permission before it executes any logic.

To implement this, define new custom permissions inside the framework manifest at frameworks/base/core/res/AndroidManifest.xml. By setting the protectionLevel to signature|privileged, you restrict who can hold it.

<permission android:name="android.permission.ACCESS_MY_CUSTOM_HW"
    android:protectionLevel="signature|privileged"
    android:label="@string/permlab_accessMyCustomHw"
    android:description="@string/permdesc_accessMyCustomHw" />

This configuration guarantees that only applications signed by the device manufacturer or pre-installed in the privileged system partition can acquire the permission. It narrows the attack surface immediately at the platform level. But declaring a permission does not automatically enforce it.

Enforcing Permissions at the Binder Boundary

Defining a permission in the manifest is just metadata. A compromised or malicious application can still bypass your SDK and invoke your service directly over Binder.

Your system service implementation must actively verify the caller privileges before performing any sensitive work. This check acts as your last line of defense.

You enforce permissions by calling Context.enforceCallingOrSelfPermission() at the very beginning of your service method. The system will inspect the Binder transaction and check the caller UID against the required permission.

@Override
public void applyConfiguration(MyCustomConfig config) {
    mContext.enforceCallingOrSelfPermission(
            "android.permission.ACCESS_MY_CUSTOM_HW",
            "Requires ACCESS_MY_CUSTOM_HW permission");

    // Proceed with sensitive operation
}

If the caller lacks the required permission, this method throws a SecurityException. The Binder transaction aborts instantly. This guarantees that unauthorized code never reaches the internal logic of your service. But this introduces a subtle issue when your service tries to talk to the hardware layer.

Bridging the Identity Gap with Binder

Hardware Abstraction Layers or the Linux kernel often restrict access based on the caller UID. When an app calls your service, the active UID during the Binder transaction belongs to that app. If your service blindly calls down into the HAL, the lower layers might reject the request because the app UID lacks hardware privileges.

You must temporarily switch the execution identity from the calling app to the system_server process. Doing so elevates your privileges for specific downstream calls.

The actual implementation uses Binder.clearCallingIdentity() and Binder.restoreCallingIdentity().

The sequence diagram below visualizes this identity transition. It helps you see exactly when the active UID changes during a single API request. Pay close attention to how the UID switches to the system process just before the HAL invocation.

The service verifies the app is allowed to make the request before adopting the system identity. It then talks to the hardware and finally restores the original app identity before returning.

To implement this, you capture the token returned by clearCallingIdentity(). You perform your hardware operations and then pass that token back to restoreCallingIdentity() inside a finally block.

long token = Binder.clearCallingIdentity();
try {
    nativeApplyConfig(config);
} finally {
    Binder.restoreCallingIdentity(token);
}

Common Mistake: Failing to restore the calling identity in a finally block can cause subsequent security checks in the same thread to execute with system privileges.

This pattern allows your service to act as a secure proxy. The app never gets direct access to the HAL. Meanwhile, the HAL only ever sees requests coming from a trusted system process. The next step is proving these defenses actually work.

Validating Defenses with Integration Tests

A missing permission check is a critical security vulnerability. Manual testing is never enough to verify that your service properly rejects unauthorized callers.

You must write device-specific integration tests using Tradefed to prove the enforcement works. Official Compatibility Test Suite tests do not cover custom OEM features, so you build a Vendor Test Suite instead.

Write a test application that attempts to call the service without the required permission. Assert that the call results in a SecurityException. Next, grant the permission to the test application and verify that the call succeeds.

Automated testing ensures your custom hardware remains protected as the platform evolves. A passing test proves your API boundaries hold strong. You have secured the new service, verified its access controls, and safely bridged the gap to the hardware layer.

Previous Lesson
API Exposure
Course Complete!
You've finished all lessons.