Home › Mobile OS & Software › Android SELinux
Mobile OS & Software

Android SELinux: Mandatory Access Control Policy Architecture

By Jones Consortium Editorial, Embedded Systems EngineerAugust 18, 202612 min read

SELinux is the last line of defense in Android's security model. When UID-based Discretionary Access Control fails — and it inevitably does — Mandatory Access Control ensures a compromised mediaserver or browser renderer cannot pivot to exfiltrate keystore keys or remount system partitions.

Since Android 5.0, SELinux has run in enforcing mode for all domains. Yet most developers treat sepolicy as a black box of opaque denials. Understanding its Type Enforcement architecture, security context labeling, and neverallow guarantees is essential for anyone building HALs, system apps, or custom ROMs.

1. Why DAC Is Insufficient on Android

Traditional Linux DAC assigns ownership and rwx bits to users and groups. On Android, every app gets a unique UID (e.g., u0_a123) and runs in its own sandbox. This isolates apps from each other, but DAC has two fundamental flaws for mobile:

  • Root is omnipotent: Any process running as UID 0 bypasses DAC checks entirely. A single exploited setuid binary or kernel flaw grants full device control.
  • Permission is discretionary: A compromised app can chmod its own files to be world-readable, leaking sensitive data to colluding malware.

Mandatory Access Control inverts this. The policy is centrally defined, compiled into a binary blob, loaded by the kernel at boot, and cannot be overridden by any userspace process — not even root. This principle aligns closely with modern privacy and tracking prevention models where system-enforced guarantees matter more than app-declared permissions.

2. SELinux Core: Type Enforcement and Beyond

Android SELinux is a Type Enforcement (TE) system with Role-Based Access Control (RBAC) and Multi-Level Security (MLS) largely vestigial. Four concepts govern every decision:

Mobile Connectivity Standards Comparison
Feature 5G NR Wi-Fi 7 Bluetooth 5.4
Peak Throughput20 Gbps46 Gbps2 Mbps
Latency1–4 ms1–2 ms3–6 ms
RangeSeveral km30–50 m100–400 m
Power DrawHighMediumUltra-low
Primary UseCellular dataLocal networkingPeripherals/IoT

The Security Context

Every subject (process) and object (file, socket, Binder endpoint, property) carries a context: user:role:type:level. On Android this collapses to:

u:r:hal_camera_default:s0 # process context
u:object_r:camera_device:s0 # /dev/video0 context
u:object_r:system_file:s0 # /system/bin/cameraserver

u is always SELinux user, r is role (usually r), s0 is MLS sensitivity. The type (e.g., hal_camera_default, system_file) is the sole driver of TE decisions.

The Allow Rule

Policy is a set of allow rules evaluated by the kernel's LSM hook. Syntax:

allow source_type target_type : class { permissions };

# cameraserver can read camera device node
allow hal_camera_default camera_device:chr_file { read write open ioctl map };
# system_server can call into hal_camera via binder
allow system_server hal_camera_service:service_manager { find };
allow system_server hal_camera_default:binder { call transfer };

If no allow rule matches, access is denied by default — even if DAC would permit it. This default-deny posture is why zero-day exploits like Stagefright were contained: mediaserver was confined to mediaserver domain and could not execute su or write to /data despite running as a privileged UID.

Expert Note: MLS on Android

Android uses only s0 for most domains, but leverages MLS categories (c0-c1023) to isolate apps. Each app gets unique categories like s0:c512,c768 via seapp_contexts, preventing one untrusted_app from reading another's /data/data directory even within the same TE type.

3. Android Enforcement Architecture

Figure 1 — SELinux Enforcement Path in Android Framework & Kernel
Android SELinux Architecture Block Diagram USERSPACE App / System Serviceuntrusted_app, system_server HAL / Native Daemonhal_camera_default, hal_audio libselinux + libbinderselinux_check_access() • binder_call() init (first process)loads sepolicy • sets contexts AVC CHECK KERNEL (LSM / SELINUX) SELinux LSM Hooksinode_permission • socket_create • binder_transaction AVC Cachefast allow/denycache Policy DBbinary sepolicyfrom /sepolicy Audit Subsystemavc: denied → dmesg / logcat Object Managersfile • binder • property • keystore Enforcing = 1 • Permissive = 0 (debug only) POLICY SOURCES platform sepolicysystem/sepolicy/*.te vendor sepolicydevice/vendor/*.te contexts filesfile_contexts, seapp, hwservice secilc → sepolicycompiled + tested (neverallow) On-device files/system/etc/selinux//vendor/etc/selinux/

All access decisions flow through LSM hooks; userspace cannot bypass the kernel AVC, unlike pure DAC checks.

Every system call that touches a protected object — open(), ioctl(), binder_transaction — is intercepted. The hook extracts source and target contexts, queries the Access Vector Cache (AVC), and on miss performs a full policy DB lookup. This synchronous check adds ~2–3% overhead, mitigated by the AVC's hashed caching, which is critical for Binder's high-throughput IPC.

4. AOSP Policy Structure in Practice

Since Android 8.0's Treble, policy is split to enforce vendor separation:

  • Platform policy — system/sepolicy/public/ (exported types/attributes) and private/ (platform-only rules).
  • Vendor policy — device/[vendor]/sepolicy/ may only use types exposed via public or its own vendor_ types. Vendor cannot modify system_server policy directly.

Build-time attributes group types for scalable rules:

# public/hal_camera.te
type hal_camera, domain;
hal_attribute camera;

# private construct: auto-allow binder to hal services
type hal_camera_default, domain;
hal_server_domain(hal_camera_default, hal_camera)

# one rule covers all hal clients
allow hal_client hal_camera_service:service_manager find;

Attributes like domain, hal_client, netdomain, and appdomain prevent rule explosion. Instead of 80 duplicate allows, you write one allow appdomain hal_camera_service:service_manager find; and all 60+ app domains inherit it. This is similar to how modular 5G hardware abstractions reuse interfaces — policy reuse reduces audit surface.

File Labeling Pipeline

Labels are not stored in xattrs on most partitions at build time; they are assigned via file_contexts:

# file_contexts
/dev/video[0-9]* u:object_r:camera_device:s0
/vendor/bin/hw/android\.hardware\.camera@.* u:object_r:hal_camera_default_exec:s0
/data/vendor/camera(/.*)? u:object_r:camera_data_file:s0

# hwservice_contexts (for hwbinder / AIDL)
android.hardware.camera.provider@2.6::ICameraProvider u:object_r:hal_camera_hwservice:s0

At runtime, init restores contexts via restorecon, and each daemon transitions to its domain via type_transition when execve() matches a _exec type.

5. Security Context Assignment: Where Theory Meets init

Context transitions are explicit. A process starts as init (u:r:init:s0). When it executes a binary labeled hal_camera_default_exec, the kernel checks:

type_transition init hal_camera_default_exec:process hal_camera_default;
allow init hal_camera_default:process transition;
allow hal_camera_default hal_camera_default_exec:file { entrypoint read execute };

For apps, zygote forks and seapp_contexts selects context based on UID, seinfo, and package name:

# seapp_contexts
user=_app seinfo=platform domain=platform_app type=app_data_file
user=_app domain=untrusted_app type=app_data_file levelFrom=user

This is why two apps sharing untrusted_app TE type remain isolated: MLS categories differentiate them. Removing categories (a common vendor mistake when porting) silently breaks isolation while passing naive CTS tests.

6. Comparison: DAC vs. SELinux MAC on Android

DimensionDiscretionary Access ControlSELinux Mandatory Access Control
Policy OwnerFile owner / app (chmod, ACL)System policy builder (sepolicy binary)
Root OverrideYes — UID 0 bypasses checksNo — root still confined (e.g., shell → su domain)
Default ModeAllow unless restrictedDeny unless explicitly allowed
GranularityUser/group/other + capabilitiesType × Class × Permission (e.g., binder:call)
PersistenceMutable at runtimeImmutable after boot (requires rebuild to change)
Failure ModeOver-permissive files leak dataDenial logged via AVC; fails closed
Use CaseApp sandbox isolation (UID)System service confinement, HAL isolation, exploit mitigation

The two systems are complementary, not competing. DAC handles user consent — “allow this app to read photos.” MAC handles system integrity — “even if compromised, mediaserver cannot load a kernel module.” For data path protection, see our analysis of VPN protocol security boundaries, which assumes MAC is correctly enforcing tunnel isolation.

7. Neverallow: The Policy About Policy

Android's most powerful innovation is neverallow — assertions that must never become true, checked at compile time with secilc and checkpolicy:

# Global invariants — no domain may bypass MAC
neverallow domain noblock_class:blk_file { open read write };
neverallow { domain -init } proc_security:file write;
neverallow untrusted_app kmem_device:chr_file *;
neverallow { appdomain -bluetooth } bluetooth_data_file:file *;

If a vendor adds allow untrusted_app kmem_device:chr_file read;, the build fails. Google's Compatibility Test Suite (CTS) and selinux_policy tests run ~300+ neverallow rules, ensuring Treble compliance. This compile-time guarantee is stronger than any runtime audit — it mathematically prevents whole classes of privilege escalation.

To create a custom HAL safely, replicate the pattern:

# vendor/hal_custom.te
type hal_custom_default, domain;
type hal_custom_hwservice, hwservice_manager_type;
type custom_device, dev_type;

allow hal_custom_default custom_device:chr_file rw_file_perms;
allow hal_custom_default hal_custom_hwservice:hwservice_manager { add find };
# Explicitly forbid network access
neverallow hal_custom_default self:tcp_socket *;

8. Debugging Without Disabling

On userdebug builds, denials appear in dmesg and logcat:

avc: denied { read } for pid=892 comm="cameraserver" name="video0" dev="tmpfs"
scontext=u:r:hal_camera_default:s0 tcontext=u:object_r:unlabeled:s0 tclass=chr_file permissive=0

This tells you: source needed read on chr_file labeled unlabeled (missing file_contexts), not camera_device. Fix the label, not the rule.

Workflow for engineers:

  1. adb shell dmesg | grep avc: | audit2allow -p out/target/product/*/obj/ETC/sepolicy_intermediates/sepolicy to generate candidate allows.
  2. Manually verify each allow — audit2allow often suggests over-broad rwx where read open suffices.
  3. Test with adb shell setenforce 1 and rebuild; never ship a permissive domain — it violates neverallow and fails CTS.
  4. Add userdebug_plat_sepolicy.cil exceptions via permissive hal_camera_default; only temporarily.

Remember that permissive mode is diagnostic, not a fix. A permissive domain logs but does not block, hiding real breakage until production. The hardware analogy is leaving coil shielding disabled during testing: you see better efficiency numbers but ship a device that overheats.

Conclusion

Android SELinux succeeds because it makes security architecture explicit. Every Binder call, every /dev node, every property read must justify itself in policy. For embedded engineers, the payoff is a system where a compromised HAL cannot pivot to modem interfaces, and an exploited app cannot dump credentials — not because exploits are impossible, but because the mandatory policy makes post-exploitation lateral movement computationally prohibited.

Treat sepolicy as code: review it, test it with neverallow, and version it with your hardware. The DAC sandbox gets you app isolation; SELinux MAC gets you device integrity.

Frequently Asked Questions

Is SELinux the same as Android app permissions?

No. App permissions (CAMERA, LOCATION) are DAC-like, user-granted Runtime Permissions checked by the framework. SELinux MAC operates below that, in the kernel, confining what processes can do regardless of app permissions. An app with CAMERA permission still runs as untrusted_app and cannot access camera_device directly — only cameraserver and hal_camera_default can.

Can I set SELinux to permissive to fix denials on a production device?

Never on user builds. Permissive domains are blocked by neverallow and CTS; setenforce 0 requires userdebug or eng builds and a permissive build flag. On user builds enforcing=1 is forced by the kernel cmdline and Verified Boot will reject a tampered sepolicy. Fix labeling and allow rules instead.

Where do vendor SELinux policies live after Android 8.0 Treble?

Vendor policies are in /vendor/etc/selinux/ (CIL files) and compiled together with platform policy at boot into /sys/fs/selinux/policy. Sources belong in device/[vendor]/[device]/sepolicy/. Vendor types must use vendor_ prefix or be explicitly declared in system/sepolicy/public/ to maintain Treble separation.

How do I correctly label a new hardware device node?

Add an entry to file_contexts (e.g., /dev/my_sensor u:object_r:sensor_device:s0), declare the type in a .te file with type sensor_device, dev_type;, and use genfs_contexts if the node is created dynamically via sysfs. After flashing, verify with ls -Z /dev/my_sensor — if it shows unlabeled, the pattern did not match.

References & Standards: GSMA Intelligence · IEEE 802.11