Introduction: Securing Android with SELinux
In the world of custom Android development, particularly with robust projects like LineageOS, understanding and correctly implementing SELinux policies is paramount. SELinux (Security-Enhanced Linux) provides a Mandatory Access Control (MAC) layer, enforcing strict permissions beyond traditional Unix Discretionary Access Control (DAC). This means even if a process runs as ‘root’, SELinux can still restrict its actions based on predefined policies. For custom ROM developers, integrating new services or modifying existing ones often leads to ‘avc: denied‘ messages in the logs, signifying an SELinux policy violation. This article will guide you through the process of identifying these denials, understanding their structure, and writing precise SELinux rules to integrate your new services seamlessly and securely into LineageOS.
Understanding SELinux in Android
Android’s security model heavily relies on SELinux to isolate applications, protect system resources, and mitigate vulnerabilities. Every file, process, and IPC mechanism in Android has an SELinux context, which is composed of a user, role, type, and sensitivity (e.g., u:object_r:system_file:s0). Policies define what interactions are allowed between different contexts.
Key SELinux Concepts:
- Contexts: Labels applied to all system objects (files, processes, sockets).
- Types: The most significant part of an SELinux context in Android, defining the purpose or classification of an object.
- Domains: A special type assigned to processes, defining their runtime permissions.
- Policy: A set of rules that dictates allowed interactions between contexts. In Android, these are compiled from
.te(type enforcement) files into a binarysepolicyfile. - MAC vs. DAC: Unlike DAC (where owners dictate permissions), MAC enforces system-wide, centrally administered policies that even root cannot bypass without a policy change.
Identifying SELinux Denials
The first step in writing new rules is to pinpoint what SELinux is denying. When a custom service or application encounters a permission issue, SELinux logs a denial. These are typically visible via logcat or dmesg.
Steps to Find Denials:
- Reproduce the Issue: Start your new service or perform the action that triggers the denial.
- Monitor Logs: Use
adb logcatoradb shell dmesgto filter for SELinux denials.
adb logcat | grep 'avc: denied'
You’ll typically see messages like this:
01-01 00:00:00.000 1234 1234 W Audit : type=1400 audit(1672531200.000:5): avc: denied { read } for pid=1234 comm="mydaemon" name="mydata.txt" dev="mmcblk0pXY" ino=12345 scontext=u:r:myservice:s0 tcontext=u:object_r:unlabeled:s0 tclass=file permissive=0
Dissecting a Denial Message:
scontext=u:r:myservice:s0: The source context (the domain of the process trying to access something). Here, it’smyservice.tcontext=u:object_r:unlabeled:s0: The target context (the label of the object being accessed). Here, it’sunlabeled, which is a red flag indicating the file needs a specific context.tclass=file: The class of the target object (e.g.,file,dir,socket,process).perm={ read }: The permission being denied.comm="mydaemon": The name of the process attempting the action.
Anatomy of an SELinux Rule
SELinux rules are defined in Type Enforcement (.te) files. The basic syntax for an allow rule is:
allow source_domain target_type:class { permissions };
source_domain: The SELinux type of the process (thescontext).target_type: The SELinux type of the object being accessed (thetcontext).class: The class of the object (thetclass).permissions: A space-separated list of operations allowed (theperm).
Other common rule types include dontaudit (suppresses denials for specific actions, useful for noisy but harmless events during debugging, but never for production), and neverallow (ensures certain actions are never permitted). For a new service, you’ll primarily be writing allow rules and defining new types.
Step-by-Step: Writing New SELinux Rules for a Custom Service
Let’s assume you have a new service called mydaemon, residing at /vendor/bin/mydaemon, and it needs to create and read/write files in /data/misc/mydata/. We’ll add this to a LineageOS build.
Prerequisites:
- A LineageOS build environment set up.
- Your custom service executable (
mydaemon) compiled and placed correctly in your device tree (e.g., viadevice/<vendor>/<device>/<device>-vendor.mk). - An
init.rcscript to start your service (e.g.,vendor/etc/init/hw/[email protected]) which typically defines a service like this:
service mydaemon /vendor/bin/mydaemon
class main
user system
group system
oneshot
seclabel u:r:myservice:s0
Note the seclabel entry – this is crucial for assigning the service its SELinux domain.
Step 1: Define a New Domain for the Service
Create a new Type Enforcement (.te) file for your service. A good location is typically device/<vendor>/<device>/sepolicy/myservice.te.
# Define the type for our service's process domain
type myservice, domain;
# Define the type for the executable that runs our service
type myservice_exec, exec_type, file_type, vendor_file_type;
# Integrate with Android's init system
init_daemon_domain(myservice)
type myservice, domain;: Declaresmyserviceas a new process domain.type myservice_exec, exec_type, file_type, vendor_file_type;: Declares a type for the executable, marking it as an executable file type and a vendor file.init_daemon_domain(myservice): A macro that grants basic permissions to run as an init daemon (e.g., access to/dev/null, interact withlogd, etc.). This greatly simplifies initial policy writing.
Step 2: Assign Context to the Executable
Next, you need to tell SELinux that your mydaemon executable should be labeled with myservice_exec. Modify or create a file_contexts file, typically located at device/<vendor>/<device>/sepolicy/file_contexts.
/vendor/bin/mydaemon u:object_r:myservice_exec:s0
This rule ensures that when mydaemon is placed in /vendor/bin, it gets the correct SELinux context.
Step 3: Define New Types for Resources (if needed)
Our service needs to access /data/misc/mydata/. If this directory and its contents don’t already have appropriate SELinux types, we need to define them. Add these to myservice.te:
# Define a type for the directory where our service stores its data
type myservice_data_file, file_type;
type myservice_data_dir, dir_type;
Then, update file_contexts to apply these new types:
/data/misc/mydata u:object_r:myservice_data_dir:s0
/data/misc/mydata/.* u:object_r:myservice_data_file:s0
This ensures the directory and any files within it are correctly labeled.
Step 4: Grant Specific Permissions
Now, we grant the myservice domain the necessary permissions to interact with its own executable (to execute itself), and its data files/directories. Add these allow rules to myservice.te:
# Allow the myservice process to execute its own binary
allow myservice myservice_exec:file { execute execute_no_trans read open getattr };
# Allow myservice to manage its data directory
allow myservice myservice_data_dir:dir { create search add_name write remove_name rmdir getattr open read };
# Allow myservice to manage its data files
allow myservice myservice_data_file:file { create read write open getattr setattr unlink };
# Basic permissions, often needed. Adjust as required.
allow myservice self:capability { setuid setgid chown sys_chroot };
allow myservice system_server:binder call;
allow myservice property_socket:sock_file write;
allow myservice init:unix_stream_socket connectto;
allow myservice self:socket { create bind listen setopt };
allow myservice logd:unix_stream_socket connectto;
allow myservice toolbox_exec:file rx_file_perms;
You will need to adjust these permissions based on actual denial logs. The goal is to grant only the minimum necessary permissions (Principle of Least Privilege).
Step 5: Integrate Rules into sepolicy
Finally, you need to ensure your new myservice.te and updated file_contexts files are included in the overall SELinux policy build. This is typically done in your device’s BoardConfig.mk file (e.g., device/<vendor>/<device>/BoardConfig.mk):
BOARD_SEPOLICY_DIRS +=
device/<vendor>/<device>/sepolicy
# Or directly add the .te file if not using a directory inclusion
BOARD_SEPOLICY_UNION +=
device/<vendor>/<device>/sepolicy/myservice.te
This tells the build system to include your custom SELinux policies when compiling the sepolicy image.
Applying and Testing Your Policies
- Recompile and Flash: After making changes to SELinux policy files, you must recompile your boot image and flash it to your device.
m bootimage # or m vendor_bootimage depending on your device's partitioning
# Then flash the image using fastboot
fastboot flash boot <path_to_boot.img>
- Reboot and Monitor: Reboot your device and immediately start monitoring
logcatfor any newavc: deniedmessages. It’s an iterative process; you’ll likely find more denials as your service performs different operations. - Refine: For each new denial, add a specific
allowrule. Avoid using overly broad permissions ordontauditduring initial development, as it can mask real security issues.
Best Practices and Debugging Tips
- Principle of Least Privilege: Always grant the absolute minimum permissions required. Over-permissive rules weaken security.
- Iterative Development: SELinux policy writing is often an iterative process of identifying denials and writing rules.
audit2allow(Use with Caution): Theaudit2allowtool can generate policy rules from denial logs. While useful for quickly drafting rules, *never* blindly apply its output. Always review and refine the generated rules to ensure they adhere to the principle of least privilege.- Avoid
permissiveMode: Running a device inpermissivemode (where SELinux logs denials but doesn’t enforce them) should only be done for debugging, never for production. - Understanding
neverallow: LineageOS and AOSP includeneverallowrules that prevent certain dangerous permissions from ever being granted. If you hit aneverallowviolation, you’ll need to rethink your service’s design or find an alternative, more secure approach. - Verify Contexts: Use
ls -Zandps -Zto verify the SELinux contexts of files and processes on your running device.
adb shell ls -Z /data/misc/mydata
adb shell ps -Z | grep mydaemon
Conclusion
Writing and applying SELinux rules is a critical skill for any custom ROM developer working on Android. While it can seem daunting at first, a methodical approach of identifying denials, understanding their structure, and crafting precise allow rules will enable you to securely integrate new services into LineageOS. By adhering to the principle of least privilege and thoroughly testing your policies, you contribute to a more robust and secure custom Android experience. Master SELinux, and you master a fundamental pillar of Android security.
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 →