Introduction: The Imperative of SELinux in Android
In the landscape of custom Android ROM development, functionality often takes precedence over security during initial development. However, a truly robust and trustworthy custom ROM necessitates a hardened security posture. SELinux (Security-Enhanced Linux) is a mandatory access control (MAC) system built into the Linux kernel, and by extension, Android, designed to restrict processes from performing actions they aren’t explicitly permitted to do. While many custom ROMs begin development in a “permissive” SELinux state for ease of debugging, transitioning to an “enforcing” mode is critical for genuine device hardening.
When SELinux is in Permissive mode, violations are logged but not blocked. This allows all operations to proceed while providing valuable audit trails. In contrast, Enforcing mode actively blocks unauthorized actions, preventing potential exploits and limiting the blast radius of compromised processes. This article will guide you through the meticulous process of auditing SELinux denials, writing custom policies, and integrating them into your Android build to secure your custom ROM.
Understanding SELinux Denials: The Audit Phase
The first step in hardening your ROM is to identify every legitimate operation that is currently being denied by SELinux. This phase is crucial and often iterative. Your goal is to run your custom ROM in permissive mode, execute all intended functionalities of your custom components (apps, services, device drivers), and collect every single SELinux denial. These denials will form the basis for your custom policy rules.
Collecting Denial Logs
Ensure your custom ROM is running with SELinux in permissive mode. You can check the current status with getenforce. If it’s enforcing, you might temporarily set it to permissive via setenforce 0 (though this won’t persist across reboots).
Connect your device via ADB and collect logs. The most common sources for SELinux denials are the kernel ring buffer and logcat.
# Clear existing logs to start fresh (optional, but recommended)adb logcat -c# Perform all custom ROM operations, launch apps, interact with custom hardware, etc.# Collect kernel denialsdmesg | grep 'avc: denied' > /tmp/selinux_denials_dmesg.log# Collect logcat denials (may include more details from Android's init)adb logcat -b all -d | grep 'avc: denied' > /tmp/selinux_denials_logcat.log
Combine and review these logs. You’re looking for lines starting with avc: denied { ... }. Each line represents an action that SELinux prevented (or would prevent in enforcing mode).
Analyzing Denials with audit2allow
Manually parsing thousands of denial messages can be overwhelming. The audit2allow tool, part of the SELinux development tools, automates the generation of policy rules based on denial logs. While it’s a powerful tool, it should be used judiciously, as blindly accepting all suggestions can lead to an overly permissive policy.
First, ensure you have `audit2allow` installed on your development machine (e.g., `sudo apt-get install policycoreutils-python-utils` on Debian/Ubuntu).
Merge your collected denial logs into a single file:
cat /tmp/selinux_denials_dmesg.log /tmp/selinux_denials_logcat.log > /tmp/all_selinux_denials.log
Now, run audit2allow. It’s often best to pipe the denials into it and generate a module:
cat /tmp/all_selinux_denials.log | audit2allow -M my_custom_policy
This command will generate two files: my_custom_policy.te and my_custom_policy.if. The .te file contains the Type Enforcement rules, while the .if file defines an interface (typically for more complex scenarios where one policy needs to grant permissions to another). Review the `my_custom_policy.te` file carefully. It will contain rules like:
allow app_data_file my_daemon_data_file:dir { create search add_name write remove_name };allow my_daemon shell_exec:file { execute execute_no_trans read open getattr };
These rules need to be scrutinized. Does my_daemon *really* need to execute arbitrary `shell_exec` files? Probably not directly. You should refine these rules to be as specific as possible, adhering to the principle of least privilege.
Crafting Your Custom SELinux Policy (.te files)
Rather than relying solely on audit2allow, it’s best to manually create or refine your .te files. This allows for better organization, modularity, and adherence to security best practices. Android’s SELinux policy is typically located under `system/sepolicy/` in the AOSP tree or `device/vendor/your_device/sepolicy/` for device-specific policies.
A typical .te file defines types, domains, and rules. Key concepts include:
type: A label applied to subjects (processes) and objects (files, sockets, devices).domain: A special type assigned to processes.allow: Grants a specific permission.neverallow: Explicitly forbids a permission, even if another rule attempts to grant it. Critical for preventing over-permissiveness.type_transition: Specifies how a process transitions to a new domain when executing a file labeled with a specific type.
Example: Granting Permissions for a Custom Service
Suppose you have a custom daemon called `my_daemon` that needs to read and write to its configuration files located at `/data/misc/my_custom_service/`. You would typically define a new type for your daemon’s domain and for its data files.
First, define the type for your service’s domain (e.g., in `my_daemon.te`):
type my_daemon, domain;type my_daemon_exec, exec_type, file_type;init_daemon_domain(my_daemon) # If it's started by init
Then, define the type for its data directory and files (e.g., in `my_daemon_data_file.te`):
type my_daemon_data_file, file_type;type my_daemon_data_dir, file_type;file_type_auto_trans(my_daemon_data_dir, my_daemon_data_file, file)
Now, grant permissions. In `my_daemon.te`:
allow my_daemon my_daemon_data_dir:dir { search write add_name remove_name rmdir };allow my_daemon my_daemon_data_file:file { create read write getattr unlink open };# If it needs to bind a network port (e.g., 8888)allow my_daemon self:socket { create bind listen };# Specify the port in service_contexts or use port_type if generic.
You’d also need to ensure that the `init` script or service that launches `my_daemon` is correctly labeled and allowed to perform the `type_transition` to the `my_daemon` domain.
Finally, you need to tell SELinux to label the `/data/misc/my_custom_service` directory and its contents with your new types. This is done in a `file_contexts` file (e.g., `file_contexts` in your `sepolicy` directory):
/data/misc/my_custom_service(/.*)? u:object_r:my_daemon_data_file:s0/data/misc/my_custom_service u:object_r:my_daemon_data_dir:s0
Integrating Policy into Your Android Build System
Once you’ve crafted your custom .te and `file_contexts` files, you need to integrate them into your Android build system so they are compiled into the final SELinux policy binary (`sepolicy`).
The standard location for device-specific SELinux policy files is within your device tree, typically `device/vendor/your_device/sepolicy/`. You’ll place your new `.te` files here.
You need to modify your `BoardConfig.mk` (or the equivalent `.mk` file for your device) to inform the build system about your new policy files. You’ll use `BOARD_SEPOLICY_DIRS` and `BOARD_SEPOLICY_UNION`:
BOARD_SEPOLICY_DIRS: Specifies directories containing SELinux policy files to be included.BOARD_SEPOLICY_UNION: Explicitly lists the `.te` files from your directories to be merged with the base policy.
Example `BoardConfig.mk` entries:
# Path to your device's sepolicy directoryBOARD_SEPOLICY_DIRS += device/vendor/your_device/sepolicy# List your custom .te files to be included in the union policyBOARD_SEPOLICY_UNION += device/vendor/your_device/sepolicy/my_daemon.te device/vendor/your_device/sepolicy/my_daemon_data_file.te device/vendor/your_device/sepolicy/some_other_component.te
Also ensure your custom `file_contexts` are included. If you have a device-specific `file_contexts` file, make sure it’s listed in `BOARD_SEPOLICY_DIRS` or explicitly added via `BOARD_SEPOLICY_FILE_CONTEXTS += device/vendor/your_device/sepolicy/file_contexts`.
After making these modifications, perform a full clean build of your Android ROM:
source build/envsetup.shlunch your_device_variant-userdebugmake -j$(nproc)
This process will recompile the SELinux policy and embed it into your `boot.img` (or `system.img` depending on your Android version and partition layout). Flash the new build to your device.
Transitioning to Enforcing Mode and Beyond
After flashing the ROM with your new policy, boot your device. It will likely still be in permissive mode initially unless you’ve explicitly configured it otherwise (e.g., by ensuring `qemu.hw.mainkeys=1` or `ro.boot.selinux=enforcing` in kernel command line or a build property). For testing purposes, you can manually set it to enforcing:
adb shell su -c
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 →