SELinux
Linux has discretionary access control: if you own a file, you decide who else may read it. SELinux adds mandatory access control: a policy written by someone else decides, and even root is subject to it.
Discretionary access control has a structural weakness: the owner of a resource decides who else may
use it. If an attacker convinces a process to chmod a file, or compromises the process that owns it,
the restriction is gone. SELinux exists to remove that discretion.
TL;DR#
- SELinux is mandatory access control: a policy, written outside the process, decides every access. Ownership does not grant permission.
- Every process runs in a domain; every object has a type. A rule is
allow source target:class permissions;. - Android ships in enforcing mode on production devices. Permissive mode is a development tool and fails CTS.
- A denial appears as an
avc: deniedline naming the source context, target context, object class and requested permissions. - Since API 26Android 8.0, the policy is split between system and vendor, so an OEM can change vendor policy without forking platform policy.
Mental model#
Think of a building where every door has a lock controlled by a central register.
In a normal office, whoever holds the key to a room can copy it and give it to anyone. That is discretionary access control: the key holder decides.
In this building, the register decides. Holding a key is not enough: the door checks the register, and the register says "this badge, at this time, for this door". You cannot copy your badge into a different one. That is mandatory access control.
SELinux is the register. The is your badge, and the domain is the role the badge identifies.
Where the analogy breaks#
A register is a single authority; SELinux is a policy compiled into the kernel and consulted on every access. There is no runtime negotiation and no way to ask for an exception: the policy either has a rule or it does not. That rigidity is the point, and it is also why policy development is slow.
The big picture#
Text description of this diagram
The security onion. SELinux sits in the kernel ring, between the hardware and boot rings outside it and the app sandbox inside it. The kernel ring's description lists UIDs, GIDs, capabilities, SELinux and seccomp. Its position shows that SELinux is enforced below the framework: a framework compromise does not disable it, and an app cannot opt out of it.
Deep dive#
Domains, types and contexts#
Every SELinux object has a type. For a process, the type is called its domain. A file, a socket, a Binder service and a process all have types, and the policy is a set of rules about which domain may do what to which type.
A security context has four fields:
user:role:type:levelAndroid largely ignores the first two: only the u user is defined, and only r (for subjects) and
object_r (for objects) roles are used. The type is what matters. The level field carries
MLS categories, which Android uses for per-app isolation.
A real app context looks like this:
u:r:untrusted_app:s0:c123,c256,c512,c768Same domain (untrusted_app) as every other app, but a different category set. That is how two apps
get different mandatory policies without the platform shipping a domain per app.
Rules and macros#
The rule syntax is allow source target:class permissions;.1 A typical line:
allow appdomain app_data_file:file rw_file_perms;That says: any domain in the appdomain attribute may read and write files of type app_data_file.
rw_file_perms is a macro that expands to the set of permissions common for read/write access.
Two things make real policy readable:
- Attributes group types.
appdomaincontains every app domain;file_typecontains every file type;domaincontains every process type. - Macros bundle common permission sets.
rw_file_perms,create_file_perms, and so on.
The consequence is that most policy lines are short and declarative, and the complexity lives in the type definitions.
Denials#
When the policy has no rule permitting an access, the kernel denies it and writes an audit record:
avc: denied { read write } for pid=177 comm="rmt_storage" name="mem"
dev="tmpfs" ino=6004 scontext=u:r:rmt:s0
tcontext=u:object_r:kmem_device:s0 tclass=chr_fileFive fields do the work:
scontext— the actor's context. Its type is the domain.tcontext— the target's context. Its type is the object's type.tclass— the object class:file,chr_file,dir,unix_stream_socket,service_manager, and so on.- The permission set in braces —
read write,connectto,find. pidandcomm— who it was.
Reading a denial is a skill, and it is worth practising. The tool below parses a line and labels the fields.
Text description of this diagram
An interactive AVC decoder. A text box holds a sample avc: denied line. Below it, the parser shows the verdict (denied), the requested permission set, and a two-node chain from the source context to the target context. Each context is broken into its user, role, type and level fields. A definitions list explains scontext, tcontext and tclass. A final panel shows the allow rule that audit2allow would generate from the denial, with a warning that the naive rule is usually too broad.
Permissive versus enforcing#
There are exactly two modes:
- Permissive — policy is not enforced; denials are logged.
- Enforcing — policy is enforced; denials fail with
EPERM.
The mode is global (getenforce reports it), and per-domain permissive mode is available as a
development aid so one domain can be debugged without loosening everything.
A device can be booted with androidboot.selinux=permissive, which is documented as being for
initial policy development only.2 On a production device it is not merely
discouraged: CTS fails.
The Treble split#
Before Android 8.0, there was one policy tree, and an OEM's device-specific rules were edits to it. That is the same coupling problem Treble solved everywhere else: a vendor change to policy required a platform change, and vice versa.
Since API 26Android 8.0, policy is split:3
- Platform policy comes from the system image (
system/sepolicy). - Vendor policy comes from the vendor image.
- Product policy comes from the product partition where applicable.
The two sides are kept compatible by versioned policy rules and by a compatibility layer. A vendor can add a domain for its own HAL without touching platform policy; the platform can tighten a platform domain without breaking the vendor's additions.
What SELinux is good at, and what it is not#
It is very good at containing a compromised process: an attacker who gets code execution in a media codec finds themselves in a domain that can talk to almost nothing. That is why browser renderers and media parsers are separated into their own processes with narrow domains.
It is not a substitute for correct code. A policy that allows a domain to read the user's photos still allows it; the policy cannot know whether the read was legitimate. And policy bugs are real: an over-broad rule is a security hole that looks like a feature.
It is also expensive to develop. Every new service needs a domain, a set of rules, and a SELinux test. That cost is why the platform does not put every process in its own domain.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 18 | 4.3 Jelly Bean |
|
| 23 | 6.0 Marshmallow |
|
| 28 | 9 Pie |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 37 | 17 Cinnamon Bun |
|
Five milestones: permissive introduction (4.2–4.3), enforcement (5.0), Treble split (8.0), per-app sandboxes (9), and the continuous tightening of vendor policy compatibility since.
Under the hood#
- the policy API that vendor and product policy may rely on
- platform policy that vendors must not depend on
- seapp_contextsapp attributes to domain mapping
- the analysis tools, including sepolicy-analyze
Curiosity corner#
- The audit record is called AVC because SELinux was derived from Flask, which used an access-vector cache. The name stuck.
- Android uses almost none of SELinux's features. One user, two roles, one sensitivity, no booleans. It is a deliberately small subset.
getenforceis the fastest sanity check on a device. A production device that reportsPermissivehas been tampered with or built for development.
Try it#
adb shell getenforce
adb shell dmesg | grep "avc: " | head -20
adb logcat -b events | grep avc | head -20
On a production device the denial list is usually short. On an engineering device it is a to-do list.
Check yourself#
Check yourself
Which layer enforces the app sandbox?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsSELinux conceptssource.android.comaccessed 2026-10-27↩
- docsImplement SELinuxsource.android.comaccessed 2026-10-27↩
- docsSELinux compatibilitysource.android.comaccessed 2026-10-27↩
- docsSELinuxsource.android.comaccessed 2026-10-27↩
- docsValidate SELinuxsource.android.comaccessed 2026-10-27↩
- docsApplication sandboxsource.android.comaccessed 2026-10-27↩
- docsAndroid security overviewsource.android.comaccessed 2026-10-27↩