Mobile Computing · Mobile Platforms
Permission Grants and Where They Are Enforced
How install-time and runtime permission models differ, which layer checks a grant at the moment of use, and why a granted permission can still fail at the call site.
A permission system has to answer two questions with one mechanism: what may this app do in general, and what may it do right now. The answers differ in lifetime, in who decides, and in the layer that enforces them. The confusion between the two is behind most of the surprising behaviour around permissions, including grants that appear in settings while the corresponding call still fails.
The declaration and the grant are separate
An app declares the permissions it needs in its manifest, at install time. The declaration is a statement of intent and a claim about the app’s own behaviour; the installer shows it to the user and, for permissions considered low-risk, records the grant immediately. The grant is a row in the package manager’s database, associated with the package name and the permission name.
At process launch, the package manager’s view of the grants is converted into kernel-visible state: the supplementary group list for permissions that map to a device node or a shared file, and the security context for permissions that map to a mandatory access policy rule. The conversion happens once per process start, which is why a permission granted while a process is running may not take effect in that process until it is restarted. It is also why revoking a permission is implemented by killing the app’s process: the kernel state cannot be edited from outside, so the platform removes the process and lets the next launch build a new one from the updated database.
That kill-on-revoke behaviour is a design decision with a cost. It guarantees that a revoked permission is not retained by a running process, at the price of the app losing all in-memory state, which the saved-state channel described in Saved State, Process Death, and the Bundle Size Limit is expected to absorb.
Two models with different enforcement points
The install-time model is a capability check. If the app holds the permission, the operation is allowed; if it does not, the operation fails. Enforcement can happen in the kernel, through a group check or a policy rule, or in a system service, through an explicit check against the caller’s identity.
The runtime model inserts a user decision between the declaration and the grant. The app may ask, the user may allow once, allow while in use, or deny, and the grant can be withdrawn later without reinstalling. Enforcement is still a capability check, but the capability is now mutable and its scope may be narrower than the declaration.
The narrower scopes are where the two models diverge most. “While in use” is not a boolean; it is a grant conditioned on the app’s process importance. A location grant scoped to use while visible is enforced by the system service that fields location requests, which checks the caller’s current importance before honouring the request. The same app, with the same grant, calling the same API from a background service, is denied. Nothing about the grant changed; the condition on it did. This is the clearest example of enforcement living above the kernel, because no permission bit can express “only while the process is in the foreground”.
What the kernel enforces and what it cannot
Kernel enforcement is limited to what the kernel can see: the caller’s UID, its groups, and the labels attached to objects. That is sufficient for coarse resources. Access to a camera device node can be a group permission. Access to a socket family can be a policy rule. Access to a specific app’s files can be a UID check.
It is not sufficient for anything that requires knowing which app is calling. A system service that receives a binder call knows the caller’s UID, because the kernel supplies it with the message, and it can map that UID to a package. The mapping is one-to-many: several packages can share a UID, and one package can have several processes. The service must therefore consult the package manager to resolve the caller, and it must decide what to do when the UID maps to more than one package, which is exactly the case that shared user IDs create.
The check performed by the service is a call to a helper that compares the caller’s UID against the permission database. It is a normal function call, not a kernel enforcement, and its correctness depends on the service remembering to make it.
// Inside a system service method: the caller's identity comes from binder.
if (context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("caller lacks ACCESS_FINE_LOCATION");
}
// The check resolves the UID that the binder driver recorded for this
// transaction, not any identity the caller put in the parcel.
A service that forgets, or that checks the wrong permission, exposes the resource to every app. This is the reason permission checks in system services are audited heavily and why the platform provides a declarative annotation that generates the check rather than relying on the author to write it.
Delegation and the identity of the caller
Some operations cannot be performed by an app at all, so the app asks a system component to perform them on its behalf. Taking a photo through the system camera app is the standard case: the camera app holds the camera permission and writes the image, and the calling app receives a reference to the result.
The result is a URI, and a URI is not access. The camera app grants the calling app a temporary permission on that specific URI, recorded as a grant against the calling app’s package. The grant is time-limited and tied to the receiving activity’s lifetime. If the calling app passes the URI to a third app without also passing the grant, the third app’s open fails, which is the intended behaviour and a frequent source of confusion: the URI is transferable, the permission attached to it is not, unless explicitly included in the transfer.
The mechanism generalises to content providers, where an app can expose a subset of its own data through a URI namespace and grant per-URI read or write access. The grant is checked by the provider process, not by the kernel, because the provider is the only component that knows which URIs it serves. A provider that trusts its caller without checking the incoming grant is the classic confused-deputy vulnerability: the caller has no permission of its own, but the provider acts with its own privileges on the caller’s request.
Revocation and the stale-state problem
Revocation is not instantaneous across all observers. The database row changes immediately, the app’s process is killed, and the next launch reflects the new state. But other components may have cached a decision. A service that recorded “this UID may access this resource” at the time of the last check will keep honouring that record until it re-checks.
The platform’s answer is that grants carry a generation or are re-validated on each use, and that any long-lived grant must be tied to the process that received it. A temporary URI grant tied to an activity’s lifetime is revoked when that activity is destroyed, which the provider observes as the grant record disappearing. A grant that outlives the process that requested it is the case where the model is weakest.
Numbers and limits that matter
Permissions are namespaced strings, conventionally in reverse-domain form, and the platform reserves a prefix for its own. Declaring a permission in another app’s namespace requires that the other app define it with a matching protection level, and the installer rejects a declaration that does not match.
The number of distinct permissions an app may hold is not bounded by the platform, but the group list is: the kernel caps the number of supplementary groups a process may carry, and older kernels used a substantially smaller cap than current ones. Permissions that map to groups compete for those slots, which is why the platform maps only a small set of permissions to groups and handles the rest in services.
Signature-level permissions are granted by comparing the signing certificates of the declaring and requesting packages, which is a check performed at install time against the certificate bytes rather than against the package name. The comparison is exact, so a re-signed app loses every signature permission it held, including the ability to share data with its own earlier version. This is one of the places where the code identity described in Code Signing and Entitlement Enforcement at Load Time has consequences outside the loader.
Grants that never take effect
A permission granted but not yet reflected in the running process produces a call that fails with a permission error immediately after the user taps allow. The app’s process was started before the grant existed, and its group list and security context do not include it. The correct behaviour is to retry after the process has been recreated, which usually means the next launch.
A permission declared with a protection level higher than the platform allows is silently downgraded or rejected at install. The app then fails at runtime with an error that names the permission, not the mismatch, which sends debugging in the wrong direction.
A runtime request made from a background process is denied without a dialog on current platform versions, because the platform will not show UI on behalf of a process the user cannot see. The request must be made from a visible activity, and the response arrives asynchronously to that activity.
Finally, a permission that the user has set to “ask every time” is not a grant at all until the dialog is answered. Code that caches the answer at startup and assumes it holds for the session will fail when the user changes the setting mid-session, because the change triggers the same revocation path described above.
References
- Android Developers — Permissions on Android — Android Developers
- Apple Developer Documentation — Requesting authorization to capture and save media — Apple Inc.
- Android Open Source Project — Permissions and access control — AOSP documentation