The Android security model

UIDs, capabilities, permissions, and SELinux work together.

10 min readChapter 16 of 50Intermediate
In this chapter

Before you start#

Read Linux files, permissions and processes, Programs, memory & system calls, and ADB and a safe lab. You need the host/target distinction, process identity, and file modes. This chapter introduces the security model needed by boot, init, and later Binder source traces; it does not assume that you have already studied Binder's implementation. It uses AOSP android-14.0.0_r1 as a source-reading baseline, not a promise that every vendor device behaves identically. No device modification is needed.

Think of one app asking for a private piece of data. The app is a running process; Android gives that process a numeric Linux UID. The kernel checks file permissions and SELinux policy. If the app asks a system service instead, the service can check Android permissions and caller identity. Separately, Verified Boot describes how the device decides which code to start. These are different gates, not one master “permission.”

Small vocabulary

TermPlain meaning
HostYour computer running commands such as adb.
TargetThe Android device or emulator that adb connects to.
UID / GIDNumeric user and group identities attached to a process. They are not package names.
DACDiscretionary access control: owner, group, and file mode checks such as rw-r-----.
CapabilityA particular Linux privilege, rather than one all-or-nothing “root” flag.
SELinux contextA label attached to a process or object. A process domain and an object type are compared against policy. This label is separate from UID.
Android permissionA framework authorization, often checked by an API or service; not a replacement for Linux credentials.
BinderAndroid’s inter-process request mechanism. A service can inspect the caller and authorize an operation.

Learning goals#

By the end, you can tell apart process credentials, file permissions, capabilities, SELinux labels and policy, Android framework permissions, Binder service checks, and boot trust state. You can also explain why a Permission denied message alone does not identify which layer refused an operation.

Identity, file access, and capabilities

Authority is not one root switch
UID / groups / capabilities
Who is this process, and which specific Linux powers does it hold?
DAC and path access
Does ownership/mode and directory traversal permit the operation?
SELinux domain / type / policy
May this subject perform this operation on this object under the loaded policy?
Service authorization
Does the framework/API accept the caller, permission, app-op, and input?
Conceptual model. Independent controls, not a fixed evaluation order. Which checks apply depends on the operation and effective build/policy. UID 0 is not a decryption key.

Every Linux process has credentials, including real/effective user and group IDs, supplementary groups, and capability sets. Android ordinarily assigns apps distinct Linux UIDs, which is one important part of the app sandbox. For ordinary file access, the kernel evaluates ownership and mode bits (DAC). A process that is neither the owner nor in an allowed group usually cannot read a private app file. See AOSP’s Application Sandbox and SELinux overview.

UID 0 is conventionally called root, but “root can do anything” is an unsafe shortcut. Linux capabilities divide traditional superuser powers into distinct units; a process can have UID 0 with capabilities removed. SELinux, namespaces, kernel checks, and service-level authorization are other controls. Conversely, an app may be allowed to perform a narrow action through an authorized system API without being root.

For example, an app opening another app’s private file can fail DAC because its UID does not match the file owner. Even if DAC allows a request, SELinux may reject the process domain’s access to the object type. If instead the app asks a contacts service through Binder, that service may check the caller’s framework permission and then open its own backing data using its own credentials. Each path has different identities and checks.

SELinux: mode, context, and policy are distinct

SELinux labels processes (subject) and objects such as files or sockets (object) with security contexts. A context commonly looks like u:r:some_domain:s0 for a process; the domain is the policy identity used for access decisions. A file context commonly contains a type such as system_data_file (Android 14 private/file_contexts). Policy rules describe which domain may perform which operation on which type. Android documents SELinux as enforcing by default; the AOSP Android 14 policy source is system/sepolicy, which is not a claim about an OEM device’s compiled policy.

Do not collapse these observations: getenforce reports global SELinux mode; id reports Unix IDs and groups; id -Z (if supported) or /proc/self/attr/current reports the current process context; a policy file describes policy source. None alone establishes the whole effective authorization state. A rooted build may load altered policy or use a different domain. Thus “root is always stopped by stock SELinux” is as inaccurate as “UID 0 always bypasses SELinux.” State which policy and process context you mean. After a root framework changes policy or transitions domains, stock-policy assurances no longer automatically describe that runtime. The trusted computing base (TCB)—the components whose correctness the security claim depends on—has changed.

An avc: denied audit record is evidence of an SELinux denial, not proof that DAC was correct or that all refusals will produce a visible log. Logs can be filtered or unavailable. See the companion SELinux Policy Lab for a synthetic, read-only exercise in interpreting fields and policy excerpts. Never treat disabling SELinux as routine troubleshooting.

Android permissions and Binder

Android permissions authorize selected framework operations. Runtime, signature, privileged, and role permissions have different grant rules; a permission matters when the relevant API or service checks it. It is not a universal kernel credential. A Binder service can inspect caller UID and enforce a permission or additional service policy. A permitted Binder request does not prove the caller can directly open the service’s backing file. See AOSP Permissions overview.

Why declaring a permission is not granting authority

A manifest request says the package wants an operation; the platform's grant rules decide whether it receives it. A normal permission, a dangerous/runtime permission, a signature permission, and a privileged permission do not follow the same rule. Signature authorization depends on the accepted signing relationship, not a package choosing a familiar name. Placement in a privileged application directory is not a universal grant: privileged-permission allowlisting and product policy also matter. See permission types and privileged-permission allowlists.

AppOps supplies additional operation-specific policy and accounting. A permission grant can coexist with an operation being restricted by foreground/background state, user choices, or service rules. The precise relationship is API-specific: some operations check a permission, an app-op, package/UID ownership, user/profile, attribution, and other state. Do not infer that every API has the same stack of checks. AppOpsManager documents the operation layer.

Worked comparison: a service owns a private settings file. App A has a legitimate service-level read authorization and receives a filtered response. A still cannot necessarily open the backing file directly. App B supplies A's package name as an argument; that string cannot substitute for Binder's actual caller UID and a package-ownership check. App C once had a grant but is now restricted in the background; a current service decision can differ from its earlier result. The purpose is controlled mediation: the service can reveal only the permitted operation/data instead of granting unrestricted access to its storage.

This also explains why granting an Android permission does not change Linux file ownership, why being UID 0 does not automatically produce a particular package's signing identity, and why an SDK annotation alone cannot protect a server from another client path. The later framework walkthrough makes those checks concrete.

ADB shell is not root

adb shell normally starts a target-side shell under Android’s shell account, not UID 0. Host sudo adb ... changes privileges on the host; it does not make the Android process root. On eligible development builds, adb root asks target adbd to restart with root privileges. Production user builds generally reject that request (restart_root_service() in daemon/restart_service.cpp refuses with “cannot run as root in production builds”).

In AOSP Android 14, packages/modules/adb/daemon/main.cpp implements should_drop_privileges(). The condensed logic in the current lesson’s source baseline is: ro.secure initializes the drop decision; service.adb.root=1 cancels it only when the build is debuggable; service.adb.root=0 sets it back. drop_privileges() then changes UID/GID, configures capabilities and enters a minijail. This is a source-level AOSP description, not a claim about every OEM build. See the pinned decision function and privilege-drop path.

Learning from a failure

When an operation fails, collect evidence before changing privileges. Identify the exact host command and target, process UID/GID/groups, process SELinux context, object owner/mode/label, and whether a service/API was involved. A Unix EACCES/“Permission denied” can arise from multiple layers; an AVC narrows evidence to SELinux but does not establish every other check passed. Avoid experimenting on another app’s real private data; use a harmless file you own or the synthetic lab.

Practice#

On a fresh, disposable AOSP emulator, select the target explicitly if more than one is connected. Read-only commands:

sh
adb devices -l
adb -s SERIAL shell id
adb -s SERIAL shell getenforce
adb -s SERIAL shell getprop ro.build.type
adb -s SERIAL shell id -Z

Replace SERIAL with the emulator serial shown by adb devices -l; id -Z is not guaranteed on every image. If unavailable, inspect /proc/self/attr/current from the target shell only where permitted. Expected: a development emulator commonly reports shell UID 2000 (AID_SHELL) and Enforcing, but build and labels vary. No output or access denial is an observation, not a reason to change policy. Do not use adb root here; that is covered in the later root lab.

Knowledge check#

  1. Is UID 0 the same thing as a SELinux domain?
  2. What does getenforce tell you that id -Z does not?
  3. Does an Android permission grant change file ownership or UID?
  4. Does host sudo make the target shell UID 0?
  5. What does a visible AVC prove, and what does an absent AVC fail to prove?
  6. Why can a manifest permission request or an existing grant still be insufficient for a particular operation?

Answers#

  1. No. UID is a Unix credential; the SELinux domain is part of a separate security context.
  2. Global SELinux mode; id -Z reports the calling process context if supported.
  3. No. It may authorize a framework-mediated operation; it does not replace Linux credentials or file labels.
  4. No. Host and target are different systems.
  5. A visible AVC is evidence that SELinux denied a specific access. No visible AVC does not prove SELinux allowed it; logs may be filtered, unavailable, or the refusal may be from another layer.
  6. Requesting is not granting; grant rules differ by permission type. The service may additionally check app-ops, package/UID ownership, foreground state, user/profile, and operation-specific policy.

Checkpoint#

Submit a one-page access trace for a harmless request: identify host/target, process UID/GID, SELinux context, object label, and any framework/service boundary. Mark each fact as observed, source-derived, or unknown. Ready if those identities and layers are distinct; revise if “root,” “permission,” or “SELinux mode” is used as a single explanation; not yet evidenced if runtime details are guessed instead of observed. Remediation: repeat the read-only observations and use SELinux Policy Lab for policy evidence.

Sources#


Next: Boot and partitions

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.