Android System Securing, Hardening, & Privacy

Building a Secure Environment: Practical SELinux Policy Examples for Custom Android Features

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SELinux in Android

Security-Enhanced Linux (SELinux) is a mandatory access control (MAC) system that provides a mechanism for supporting security policies, including confidentiality, integrity, and availability. In the Android ecosystem, SELinux is a critical component for hardening the operating system, isolating applications, and preventing privilege escalation. For custom Android ROM developers or those implementing custom features, understanding and correctly writing SELinux policies is not just good practice—it’s essential for maintaining a secure and stable device.

Unlike traditional Discretionary Access Control (DAC) systems (like standard Linux file permissions), which allow the owner of a resource to define who can access it, SELinux operates on a principle of least privilege. Every process and file has a security context, and explicit rules (policies) must exist for any interaction to be allowed. If a rule is missing, access is denied by default, leading to an AVC (Access Vector Cache) denial.

Understanding SELinux Denials

Identifying AVC Denials

When a custom service or feature attempts an action that isn’t explicitly permitted by the current SELinux policy, the Android kernel logs an AVC denial. Identifying and analyzing these denials is the first step in crafting or modifying an SELinux policy. You can typically find these in the kernel log buffer using dmesg or by filtering logcat.

Connect your device via ADB and run:

adb shell su -c "dmesg | grep 'avc: denied'"

Or for live monitoring:

adb logcat | grep 'avc: denied'

Decoding a Denial Message

An AVC denial message provides crucial information needed to write a policy rule. Here’s a typical example:

avc: denied { read } for pid=1234 comm="my_custom_svc" name="my_device" dev="tmpfs" ino=5678 scontext=u:r:my_custom_service_t:s0 tcontext=u:object_r:device:s0 tclass=chr_file permissive=0

Let’s break down the components:

  • denied { read }: The specific permission that was denied (e.g., read, write, execute, open).
  • pid=1234 comm="my_custom_svc": The process ID and command name of the requesting process.
  • name="my_device": The name of the resource being accessed.
  • scontext=u:r:my_custom_service_t:s0: The source context, identifying the domain of the process attempting the action. Here, my_custom_service_t is the type of our custom service.
  • tcontext=u:object_r:device:s0: The target context, identifying the type of the resource being accessed. Here, device is the type of the target resource.
  • tclass=chr_file: The class of the target resource (e.g., file, dir, socket, chr_file for character device).

Developing Custom SELinux Policies

SELinux policies are typically written in .te (Type Enforcement) files, which define types, domains, and rules. For Android, these files are usually located in device/<vendor>/<device>/sepolicy or external/sepolicy within the AOSP source tree.

Defining New Types and Domains

First, we need to define a new type for our custom service and declare it as a domain. This typically goes into a new .te file, for example, my_custom_service.te.

# my_custom_service.te
type my_custom_service_t; # Declare a new type
type my_custom_service_exec; # Type for the service's executable
domain_type(my_custom_service_t); # Declare it as a domain

Next, we need to transition from the init domain to our new service domain. This usually happens when init starts our service.

init_daemon_domain(my_custom_service_t)

This macro essentially expands to rules that allow init to execute my_custom_service_exec and transition to my_custom_service_t.

Granting Permissions

Based on our AVC denial, our my_custom_service_t needed read permission on a chr_file with type device. We can add a rule:

allow my_custom_service_t device:chr_file { read open };

Always grant the least privilege necessary. If open is implicitly required for read in some contexts, it’s good to include it to prevent another denial.

For a custom device node, say /dev/my_sensor, you’d typically define a new type for it and then allow access. Assuming my_sensor_device is the new type for /dev/my_sensor:

type my_sensor_device, dev_type; # Declare a new device type
allow my_custom_service_t my_sensor_device:chr_file { read write ioctl open };

If your service needs to interact with files in its own data directory (e.g., /data/misc/my_service_data/):

type my_service_data_file, file_type, data_file_type;
type my_service_data_dir, file_type, data_file_type;

allow my_custom_service_t my_service_data_dir:dir { search create write add_name };
allow my_custom_service_t my_service_data_file:file { create read write open getattr setattr unlink };

Applying File Contexts

For SELinux to apply the correct types to files and directories, you need to specify their contexts. This is done in file_contexts files (e.g., file_contexts or property_contexts).

For our service executable /vendor/bin/my_custom_service and its data directory /data/misc/my_service_data:

# In a file_contexts file (e.g., device/<vendor>/<device>/sepolicy/file_contexts)
/vendor/bin/my_custom_service u:object_r:my_custom_service_exec:s0
/dev/my_sensor u:object_r:my_sensor_device:s0
/data/misc/my_service_data(/.*)? u:object_r:my_service_data_file:s0
/data/misc/my_service_data u:object_r:my_service_data_dir:s0

The (/.*)? regex ensures all files and subdirectories within /data/misc/my_service_data get the my_service_data_file context, while the directory itself gets my_service_data_dir.

Integrating and Testing Your Policy

Compiling and Flashing

Once you’ve added your .te and updated file_contexts, these changes need to be compiled into the SELinux policy binary (sepolicy) and flashed to your device. This is typically part of the Android build process. Simply rebuild your Android image and flash it:

source build/envsetup.sh
lunch <target>
mka sepolicy # Or just mka to build everything
# Then flash the updated boot.img or vendor.img containing the new policy

On some devices, you might be able to push only the updated sepolicy.ko or vendor_sepolicy.img. However, a full reflash of the relevant partitions is the most reliable way to ensure the policy takes effect.

Debugging and Refinement

After flashing, reboot your device and test your custom feature. Monitor dmesg and logcat for any new AVC denials. This iterative process of identifying denials, writing rules, and re-flashing is crucial. Always prioritize specific rules over broad ones to maintain the principle of least privilege.

While tools like audit2allow exist (e.g., audit2allow -i <log_file>), they should be used with extreme caution. They often generate overly permissive rules. It’s better to manually analyze each denial and craft the most restrictive rule possible.

Avoid using dontaudit rules in production policies. While they suppress AVC denial messages, they also hide potential security issues. They are primarily for temporary use during development to reduce log spam from known, benign issues.

Conclusion

Crafting effective SELinux policies for custom Android features is a cornerstone of building a robust and secure system. It requires a deep understanding of your service’s behavior, meticulous denial analysis, and a commitment to the principle of least privilege. By following a structured approach—identifying denials, defining types and domains, granting minimal permissions, and correctly applying file contexts—you can ensure your custom features integrate seamlessly and securely into the Android framework, significantly enhancing the overall security posture of your custom ROM.

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