Introduction to Android SELinux and Custom Binaries
Android’s security model is robust, and a cornerstone of this strength is SELinux (Security-Enhanced Linux). Operating in enforcing mode on modern Android devices, SELinux provides mandatory access control (MAC), confining applications and system services to the minimal set of permissions required for their operation. While this significantly enhances security, it introduces a challenge when introducing custom binaries or modifying system behavior: every action, every file access, and every process interaction must be explicitly allowed by the SELinux policy.
This tutorial will guide you through the process of writing your first custom SELinux policy for a new binary on Android. We’ll cover everything from setting up your development environment to analyzing denials and integrating your policy into the Android Open Source Project (AOSP) build system. By the end, you’ll have a foundational understanding of how to debug and develop SELinux policies for your specific needs.
Prerequisites
- An AOSP build environment set up and capable of building a full Android image.
- Basic familiarity with Linux command line and Android system structure.
- A device capable of flashing custom AOSP builds (e.g., a Google Pixel device).
Step 1: Create Your Custom Binary
Let’s start by creating a simple C program that attempts to print a message and create a file in a location that typically requires specific permissions (/data/local/tmp/).
1.1. Write the C Code
Create a file named mybinary.c:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
printf("Hello from my custom binary!n");
// Attempt to create a file in /data/local/tmp/
int fd = open("/data/local/tmp/mybinary_output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Error creating file in /data/local/tmp");
return 1;
}
const char *message = "This file was created by my custom binary.n";
write(fd, message, strlen(message));
close(fd);
printf("Successfully created /data/local/tmp/mybinary_output.txtn");
return 0;
}
1.2. Create an Android.mk File
To compile this into an Android executable, create an Android.mk file in the same directory:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := mybinary.c
LOCAL_MODULE := mybinary
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_PROPRIETARY_MODULE := true # Important for placing it in /vendor/bin
include $(BUILD_EXECUTABLE)
Place both files (mybinary.c and Android.mk) in a new directory within your AOSP source tree, for example, /path/to/aosp/vendor/myproject/mybinary/.
1.3. Integrate into AOSP Build and Flash
Add your new module to the device’s BoardConfig.mk or a similar product makefile. For instance, in device/<vendor>/<device>/device.mk, add:
PRODUCT_PACKAGES +=
mybinary
Now, build your AOSP project:
source build/envsetup.sh
lunch <your_device_target>-userdebug
m mybinary # Build just your binary first to test compilation
make -j$(nproc)
Flash the resulting images to your device.
Step 2: Encountering SELinux Denials
After flashing, connect to your device via ADB and try to execute your binary:
adb shell
cd /vendor/bin # Or wherever your binary was installed
./mybinary
You will likely see an output similar to this:
Hello from my custom binary!
Error creating file in /data/local/tmp: Permission denied
And critically, if you check the kernel logs, you’ll find SELinux denials:
adb logcat | grep -i avc
Look for lines like:
01-01 00:00:00.123 123 123 I auditd : avc: denied { execute } for pid=1234 comm="sh" name="mybinary" dev="dm-0" ino=12345 scontext=u:r:shell:s0 tcontext=u:object_r:vendor_file:s0 tclass=file permissive=0
01-01 00:00:00.123 123 123 I auditd : avc: denied { create } for pid=1235 comm="mybinary" name="mybinary_output.txt" dev="dm-0" ino=67890 scontext=u:r:shell:s0 tcontext=u:object_r:data_local_tmp_file:s0 tclass=file permissive=0
Let’s break down a denial:avc: denied { <permission> } for pid=<pid> comm="<command>" name="<name>" scontext=<source_context> tcontext=<target_context> tclass=<target_class>
permission: The action that was denied (e.g.,execute,create).scontext(Source Context): The SELinux context of the process attempting the action (e.g.,u:r:shell:s0for theshelldomain).tcontext(Target Context): The SELinux context of the resource being accessed (e.g.,u:object_r:vendor_file:s0for your binary file,u:object_r:data_local_tmp_file:s0for the target directory).tclass(Target Class): The type of resource (e.g.,file,dir,socket).
The first denial (execute) indicates that the shell domain is not allowed to execute files labeled vendor_file with a direct transition. The second (create) shows that our mybinary, currently running under the shell context, isn’t allowed to create files in /data/local/tmp/ which has a data_local_tmp_file context.
Step 3: Writing Your First SELinux Policy
We need to create a new SELinux domain for our binary and define its permissions.
3.1. Define a New Type for Your Binary
Navigate to your AOSP source directory and create a new directory for your policy, e.g., vendor/myproject/sepolicy/. Inside, create mybinary.te:
# mybinary.te
# 1. Define a new type for our executable file
# exec_type: indicates it's an executable file
# file_type: it's a file
# vendor_file_type: common for executables placed in /vendor
type mybinary_exec, exec_type, file_type, vendor_file_type;
# 2. Define a new domain for our process
# domain: indicates it's a process domain
type mybinary, domain;
# 3. Allow shell to execute mybinary_exec and transition to mybinary domain
# This rule is crucial for the shell to launch our binary and change its context.
domain_auto_trans(shell, mybinary_exec, mybinary)
# 4. Allow the mybinary domain to have entrypoint to its own executable
# This is standard practice for any domain
allow mybinary mybinary_exec:file { entrypoint read getattr open };
# 5. Grant basic permissions to the mybinary domain
# Allow logging for printf output
allow mybinary devpts:chr_file { read write getattr };
# Allow creation of files in /data/local/tmp
# This covers the 'create' denial we saw earlier.
allow mybinary data_local_tmp:dir { search write add_name };
allow mybinary data_local_tmp:file { create write getattr open };
# Allow mybinary to read /dev/urandom (common for many tools)
allow mybinary urandom_device:chr_file { read getattr open };
# Allow necessary system calls
# For example, if your binary uses basic system calls (like fork, execve),
# it often needs capabilities. Be specific.
allow mybinary self:capability { net_raw dac_override }; # Example: be very careful with capabilities
# Inherit from a common type if appropriate, e.g., to get basic system permissions.
# For example, many vendor binaries inherit from vendor_init_daemon_domain.
# This might grant too many permissions initially, but helps debugging.
# Consider init_daemon_domain(mybinary) if it's a background service.
# For a simple command-line tool, explicit permissions are safer.
# Example of allowing communication with logd for proper log output beyond printf
allow mybinary logd:unix_stream_socket connectto;
# Allow mybinary to execute other shell binaries if needed
allow mybinary shell_exec:file execute_no_trans;
```
3.2. Define File Contexts
We need to tell SELinux what context to apply to our binary file on the filesystem. Create a file named file_contexts in the same directory (vendor/myproject/sepolicy/):
# file_contexts
# Apply the mybinary_exec type to our binary in /vendor/bin
/vendor/bin/mybinary u:object_r:mybinary_exec:s0
Step 4: Integrate Policy into AOSP Build
Now, integrate your policy into the AOSP build system.
4.1. Create a Policy Makefile
In vendor/myproject/sepolicy/, create an Android.mk:
LOCAL_PATH := $(call my-dir)
# Register our policy files
BOARD_SEPOLICY_DIRS += $(LOCAL_PATH)
BOARD_SEPOLICY_UNION +=
$(LOCAL_PATH)/mybinary.te
$(LOCAL_PATH)/file_contexts
4.2. Update BoardConfig.mk
Finally, ensure your policy directory is included in the device's BoardConfig.mk. Find device/<vendor>/<device>/BoardConfig.mk and add a line like:
# Include custom SELinux policy from vendor/myproject
BOARD_SEPOLICY_DIRS += vendor/myproject/sepolicy
Step 5: Build, Flash, and Test
Rebuild your AOSP images and flash them to your device:
source build/envsetup.sh
lunch <your_device_target>-userdebug
make -j$(nproc)
After flashing, connect via ADB and try running your binary again:
adb shell
cd /vendor/bin
./mybinary
If all policies are correctly applied, you should now see:
Hello from my custom binary!
Successfully created /data/local/tmp/mybinary_output.txt
You can verify the file context of your binary and the created file:
adb shell ls -Z /vendor/bin/mybinary
adb shell ls -Z /data/local/tmp/mybinary_output.txt
Expected output:
u:object_r:mybinary_exec:s0 /vendor/bin/mybinary
u:object_r:data_local_tmp_file:s0 /data/local/tmp/mybinary_output.txt
If you still encounter denials, repeat Step 2 to analyze the new AVC messages and incrementally add or refine rules in your mybinary.te. Remember to rebuild and re-flash after every policy change.
Debugging Tips
- Iterate, Don't Guess: Never add broad permissions blindly. Always analyze
logcat | grep -i avcfor specific denials and add only the necessary rules. audit2allow: A powerful tool for suggesting policy rules from audit logs. Extract your AVC denials into a file (e.g.,denials.txt) and runaudit2allow -i denials.txton your host machine. Be cautious:audit2allowcan suggest overly permissive rules; always review and refine them.sesearch: Use this utility on your host machine to query the compiled SELinux policy. For example,sesearch -A -s mybinary -t data_local_tmp -c file -p createcan show if your domain has permission to create files in that context.- Permissive Mode (for debugging): Temporarily set SELinux to permissive mode (
setenforce 0in ADB shell as root, if your build allows) to identify all potential denials without blocking execution. All denials will still be logged but not enforced. This is for debugging only and should NEVER be used in production.
Conclusion
Writing SELinux policies for custom Android binaries is an essential skill for advanced system customization and development. While it can seem daunting initially due to its strictness, the process is methodical: define types, set file contexts, analyze denials, and incrementally grant necessary permissions. By following this hands-on guide, you've taken the first step toward mastering SELinux policy development, enabling you to integrate your custom code securely and effectively into the Android ecosystem.
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 →