Android System Securing, Hardening, & Privacy

How To: Customize SELinux Policies on Android – A Step-by-Step Guide for Rooted Devices

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SELinux on Android

Security-Enhanced Linux (SELinux) is a mandatory access control (MAC) system that provides a robust security layer on top of Linux’s traditional discretionary access control (DAC). On Android, SELinux policies are crucial for enforcing application isolation, protecting system resources, and mitigating vulnerabilities. Every process, file, and system resource is labeled, and access decisions are made based on a predefined policy database. By default, Android runs in “enforcing” mode, meaning all policy violations are blocked and logged. Understanding and customizing these policies gives rooted users unparalleled control over their device’s security posture.

Why Customize SELinux Policies?

While Android’s default SELinux policies are robust, there are several reasons why an advanced user might need to customize them:

  • Specific App Needs: Granting custom permissions for unique applications or services that the default policy might restrict.
  • Security Hardening: Further restricting access to specific system components beyond the default, tailored to your threat model.
  • Custom Kernels/ROMs: Accommodating new drivers, daemons, or system modifications that introduce new contexts not covered by the stock policy.
  • Avoiding Permissive Mode: Instead of setting SELinux to “permissive” mode (which logs violations but doesn’t block them, effectively disabling a major security layer), you can precisely allow only the necessary operations.

Customizing SELinux requires a rooted Android device, familiarity with ADB, and basic Linux command-line knowledge.

Prerequisites

  • A rooted Android device (Magisk is highly recommended for its systemless approach).
  • Android Debug Bridge (ADB) installed on your computer.
  • A basic understanding of Linux commands and file systems.
  • Access to a Linux environment (VM or WSL) for compiling policies, though direct on-device compilation is also possible with appropriate binaries.

Understanding SELinux Modes: Enforcing vs. Permissive

SELinux operates in two primary modes:

  • Enforcing: This is the default and recommended mode. All policy violations are blocked and logged to the kernel audit log.
  • Permissive: Policy violations are only logged, not blocked. This mode is useful for debugging and identifying necessary policy rules without breaking functionality, but it significantly reduces security.

You can check the current SELinux status using:

adb shell getenforce

To temporarily switch to permissive mode (for debugging, not recommended for long-term use):

adb shell su -c 'setenforce 0'

To revert to enforcing:

adb shell su -c 'setenforce 1'

Identifying AVC Denials

The first step in customizing policies is understanding what’s being denied. SELinux denials are logged in the kernel’s audit log. You can view these logs using `dmesg` or `logcat`.

adb shell dmesg | grep 'avc: denied'

Or, for a continuous stream of logs:

adb logcat | grep 'avc: denied'

An typical AVC denial might look like this:

avc: denied { read } for pid=1234 comm="my_daemon" name="myfile" dev="mmcblk0pXX" ino=XXXX scontext=u:r:my_daemon_type:s0 tcontext=u:object_r:system_file:s0 tclass=file permissive=0

From this, you can extract crucial information:

  • { read }: The operation being denied.
  • pid=1234 comm="my_daemon": The process (command) attempting the operation.
  • scontext=u:r:my_daemon_type:s0: The security context of the source process (domain).
  • tcontext=u:object_r:system_file:s0: The security context of the target object (e.g., file, directory).
  • tclass=file: The class of the target object.

Creating Custom Policy Rules (.te Files)

SELinux policies are written in Type Enforcement (TE) language, typically in `.te` files. Based on the AVC denial, you’ll write rules to permit the denied action.

For the example denial above, if `my_daemon` needs to read `myfile` which is labeled `system_file`, you’d create a rule:

# In a new .te file, e.g., my_daemon.teallow my_daemon_type system_file:file { read };

This rule explicitly allows processes running under `my_daemon_type` to `read` files labeled `system_file` where the target is a `file` class.

Here are some common types of rules:

  • allow source_type target_type:class { permissions }; (most common)
  • type source_type; (declares a new type)
  • file_type target_type; (declares a new file type)
  • domain_auto_trans(source_domain, exec_type, new_domain); (process transition)

Example Scenario: Allowing a Custom Service Access to a Directory

Let’s say you have a custom daemon (`mydaemon`) that needs to write to `/data/local/mydata`.

Step 1: Create the directory and daemon

adb shell su -c 'mkdir /data/local/mydata'adb shell su -c 'touch /data/local/mydata/testfile'adb shell su -c 'chmod 777 /data/local/mydata'# Simulate your daemon trying to write to the fileadb shell su -c 'echo "hello" > /data/local/mydata/testfile'

Step 2: Check for AVC denials

adb logcat | grep 'avc: denied'

You might see a denial like this (assuming `mydaemon` runs under `untrusted_app_27` context initially, or perhaps `init` if spawned directly):

avc: denied { write } for pid=XXXX comm="mydaemon" name="mydata" dev="dm-0" ino=YYYY scontext=u:r:untrusted_app_27:s0 tcontext=u:object_r:app_data_file:s0 tclass=dir permissive=0

Step 3: Define a new type for your data and daemon

First, we need to label our custom directory correctly. We’ll define a new file type `my_data_file` and associate it with `/data/local/mydata`.

Create `my_data.te`:

type my_data_file, file_type, data_file_type;type my_daemon_type;

Create `file_contexts` entry (for persistent labeling):

/data/local/mydata(/.*)? u:object_r:my_data_file:s0

Now, create `my_daemon_policy.te` to allow `my_daemon_type` to write to `my_data_file`:

allow my_daemon_type my_data_file:dir { create search add_name write remove_name rmdir };allow my_daemon_type my_data_file:file { create read write getattr open append };

You’d also need a rule to transition `mydaemon` from its initial context (e.g., `init`) to `my_daemon_type`. This is often done via `domain_auto_trans` if `mydaemon` is an executable labeled `my_daemon_exec`.

type my_daemon_exec, exec_type, file_type;domain_auto_trans(init, my_daemon_exec, my_daemon_type);

Then, ensure your `mydaemon` binary is labeled `my_daemon_exec`.

Compiling and Loading Custom Policies

Custom policies are typically compiled into a Common Intermediate Language (CIL) file and then injected into the running policy.

Step 1: Compile .te files to .cil

You’ll need the `secilc` compiler, which is part of the AOSP build tools. You can build it from source or find pre-compiled binaries. Assume you have `secilc` available on your Linux machine.

secilc my_data.te my_daemon_policy.te -o my_custom_policy.cil

Step 2: Create a Magisk Module to Load Policy

Magisk offers a robust systemless way to inject custom policies. Create a Magisk module with the following structure:

my_selinux_module/├── module.prop├── customize.sh├── post-fs-data.sh├── service.sh└── system/    └── etc/        └── selinux/            └── my_custom_policy.cil

In `module.prop`, define module info.

In `post-fs-data.sh` (or `service.sh` for policies loaded later), you will use `magiskpolicy` to inject your custom CIL file.

# post-fs-data.sh#!/system/bin/sh# Ensure /data/local/mydata has the correct SELinux contextchcon -R u:object_r:my_data_file:s0 /data/local/mydata# Inject the custom CIL policymagiskpolicy --live --load /data/adb/modules/my_selinux_module/system/etc/selinux/my_custom_policy.cil# If your daemon executable needs a specific labelchcon u:object_r:my_daemon_exec:s0 /path/to/mydaemon

The `magiskpolicy –live` command injects the policy into the running kernel. For persistent file contexts (like `/data/local/mydata`), you’d typically handle them in `post-fs-data.sh` using `chcon` after the directory is created, or modify `file_contexts` directly via Magisk’s `system/etc/selinux/precompiled_sepolicy` patching. For simple file labels, `chcon` is sufficient.

Step 3: Install the Magisk Module

Zip your `my_selinux_module` directory and install it via the Magisk Manager app. Reboot your device.

Step 4: Test Your Changes

After reboot, try running your custom daemon or accessing the resource again. Check `dmesg` or `logcat` to ensure no new denials related to your changes appear. Verify the file contexts:

adb shell ls -Z /data/local/mydata

It should show `u:object_r:my_data_file:s0`.

Best Practices and Warnings

  • Start in Permissive (for testing): Temporarily set SELinux to permissive to capture all denials without breaking your system. Gather all denials, switch back to enforcing, and then write policies.
  • Least Privilege: Grant only the absolutely necessary permissions. Avoid broad `allow` rules.
  • Test Iteratively: Make small changes, test, and then repeat.
  • Backup: Always have a full system backup before making significant SELinux policy changes. Incorrect policies can soft-brick your device.
  • Understand Contexts: Pay close attention to source (scontext) and target (tcontext) types in denial messages.
  • Review Existing Policies: For complex scenarios, examine Android’s existing AOSP SELinux policies to understand how different components interact.

Customizing SELinux policies is a powerful way to fine-tune your Android device’s security. While it requires a learning curve, the ability to precisely control system access ensures a more secure and tailored environment than simply disabling SELinux.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner