Why Traditional Permissions Fail
Imagine a malicious app tricks a system service into executing arbitrary code. Linux Discretionary Access Control (DAC) evaluates permissions based entirely on the user ID. If that compromised service runs as root, DAC grants it unrestricted access to everything the root user owns. A single vulnerability in a privileged process can compromise the entire device.
Android requires a stricter boundary. The system needs a way to enforce the principle of least privilege independently of the user ID. This requirement introduces Mandatory Access Control (MAC).
Under MAC, administrators define rigid security policies that the kernel enforces universally. Even if a process gains root privileges, it remains confined by the rules of the MAC policy. Android uses Security-Enhanced Linux (SELinux) to implement this architecture.
SELinux acts as an independent gatekeeper that evaluates every interaction on the system. Before it can evaluate anything, it needs a way to identify the participants.
How SELinux Identifies Participants
The kernel cannot understand abstract concepts like the camera app or the audio hardware. It needs a standardized vocabulary to label every active process and every passive resource. Writing rules to control interactions would be impossible without these labels.
SELinux divides the world into subjects and objects. Subjects are the active entities requesting access, which are almost always processes. Objects are the passive entities being acted upon, like files, directories, or device nodes.
The kernel assigns a specific security attribute to every subject and object. We call the attribute for a subject a domain. An object receives an attribute known as a type. When a process attempts to read a file, SELinux sees a specific domain trying to access a specific type.
You can visualize this interaction using a simple flowchart. This diagram shows the request flow. It helps clarify where the kernel sits in the access chain. Look for how the kernel intercepts the request before the target object is reached.
The kernel sits directly between the subject and the object. SELinux ignores the user ID of the subject entirely. It only cares if the security policy explicitly allows the subject's domain to interact with the object's type. That explicit permission requires a mechanism to define the rules.
Defining Rules with Type Enforcement
By default, SELinux denies everything. If a domain tries to read a type and no rule explicitly permits it, the kernel blocks the action. You must map out the exact permissions required for the system to function.
Type Enforcement (TE) provides this mapping mechanism. TE forms the core of the Android SELinux policy. It defines the exact operations permitted between specific domains and types.
You write these rules in .te files located in the system/sepolicy/ directory of the AOSP source tree. A rule explicitly states the domain, the type, and the allowed operations. During the build process, the system compiles these files into a binary policy that the kernel loads at boot.
Consider the media server process. It needs to read media files from storage. You must define a rule allowing the mediaserver domain to perform a read operation on files labeled with the media_rw_data_file type. Without this rule, the media server crashes when it tries to open a photo.
Common Mistake: Engineers often waste hours debugging permission denied errors by changing file ownership with
chown. If DAC permissions look correct and the access still fails, you are almost certainly missing a Type Enforcement rule.
With thousands of processes and files on an Android device, writing these rules quickly becomes complex. You need a way to keep the policy readable.
Reading the Labels
A typical AOSP build contains thousands of domains and types. If engineers named these labels randomly, debugging policy violations would be impossible. You would never know if a label represented a process, a file, or a hardware device.
Android enforces strict naming conventions within the sepolicy directory. These conventions make the labels instantly recognizable.
Domains typically map directly to the process name or its primary function. Labels like init, system_server, or hal_audio_default are almost certainly domains. Types follow a suffix convention that denotes the resource class. Files usually end in _file, device nodes end in _device, system properties end in _prop, and Binder services end in _service.
Understanding these conventions allows you to read a TE rule and instantly understand the architectural boundary it crosses. You can look at a rule and immediately know whether a hardware process is trying to read a system property or write to a data file. This skill is critical for debugging the policy violations you will encounter when bringing up new hardware.