Writing SELinux Policy

The Android Open Source Project (AOSP) provides a solid base policy for the applications and services that are common across all Android devices. Contributors to AOSP regularly refine this policy. The core policy is expected to make up about 90–95% of the final on-device policy with device-specific customizations making up the remaining 5–10%. This article focuses on these device-specific customizations, how to write device-specific policy, and some of the pitfalls to avoid along the way.

Device bringup

While writing device-specific policy, follow these steps.

Run in permissive mode

When a device is in permissive mode, denials are logged but not enforced. Permissive mode is important for two reasons:

  • Permissive mode ensures that policy bringup does not delay other early device bringup tasks.
  • An enforced denial may mask other denials. For example, file access typically entails a directory search, file open, then file read. In enforcing mode, only the directory search denial would occur. Permissive mode ensures all denials are seen.

The simplest way to put a device into permissive mode is using the kernel command line. This can be added to the device’s BoardConfig.mk file: platform/device/<vendor>/<target>/BoardConfig.mk. After modifying the command line, perform make clean, then make bootimage, and flash the new boot image.

After that, confirm permissive mode with:

adb shell getenforce

Two weeks is a reasonable amount of time to be in global permissive mode. After addressing the majority of denials, move back into enforcing mode and address bugs as they come in. Domains still producing denials or services still under heavy development can be temporarily put into permissive mode, but move them back to enforcing mode as soon as possible.

Enforce early

In enforcing mode, denials are both logged and enforced. It is a best practice to get your device into enforcing mode as early as possible. Waiting to create and enforce device-specific policy often results in a buggy product and a bad user experience. Start early enough to participate in dogfooding and ensure full test coverage of functionality in real world usage. Starting early ensures security concerns inform design decisions. Conversely, granting permissions based solely on observed denials is an unsafe approach. Use this time to perform a security audit of the device and file bugs against behavior that should not be allowed.

Remove or delete existing policy

There are a number of good reasons to create device-specific policy from scratch on a new device, which include:

Address denials of core services

Denials generated by core services are typically addressed by file labeling. For example:

avc: denied { open } for pid=1003 comm=”mediaserver” path="/dev/kgsl-3d0”
dev="tmpfs" scontext=u:r:mediaserver:s0 tcontext=u:object_r:device:s0
tclass=chr_file permissive=1
avc: denied { read write } for pid=1003 name="kgsl-3d0" dev="tmpfs"
scontext=u:r:mediaserver:s0
tcontext=u:object_r:device:s0 tclass=chr_file permissive=1

is completely addressed by properly labeling /dev/kgsl-3d0. In this example, tcontext is device. This represents a default context where everything in /dev receives the “ device” label unless a more specific label is assigned. Simply accepting the output from audit2allow here would result in an incorrect and overly permissive rule.

To solve this kind of problem, give the file a more specific label, which in this case is gpu_device. No further permissions are needed as the mediaserver already has necessary permissions in core policy to access the gpu_device.

Other device-specific files that should be labeled with types predefined in core policy:

In general, granting permissions to default labels is wrong. Many of these permissions are disallowed by neverallow rules, but even when not explicitly disallowed, best practice is to provide a specific label.

Label new services and address denials

Init-launched services are required to run in their own SELinux domains. The following example puts service “foo” into its own SELinux domain and grants it permissions.

The service is launched in our device’s init.device.rc file as:

service foo /system/bin/foo
    class core
  1. Create a new domain "foo"

    Create the file device/manufacturer/device-name/sepolicy/foo.te with the following contents:

    # foo service
    type foo, domain;
    type foo_exec, exec_type, file_type;
    
    init_daemon_domain(foo)
    

    This is the initial template for the foo SELinux domain, to which you can add rules based on the specific operations performed by that executable.

  2. Label /system/bin/foo

    Add the following to device/manufacturer/device-name/sepolicy/file_contexts:

    /system/bin/foo   u:object_r:foo_exec:s0
    

    This makes sure the executable is properly labeled so SELinux runs the service in the proper domain.

  3. Build and flash the boot and system images.
  4. Refine the SELinux rules for the domain.

    Use denials to determine the required permissions. The audit2allow tool provides good guidelines, but only use it to inform policy writing. Do not just copy the output.

Switch back to enforcing mode

It’s fine to troubleshoot in permissive mode, but switch back to enforcing mode as early as possible and try to remain there.

Common mistakes

Here are a few solutions for common mistakes that happen when writing device-specific policies.

Overuse of negation

The following example rule is like locking the front door but leaving the windows open:

allow { domain -untrusted_app } scary_debug_device:chr_file rw_file_perms

The intent is clear: everyone but third-party apps may have access to the debug device.

The rule is flawed in a few ways. The exclusion of untrusted_app is trivial to work around because all apps may optionally run services in the isolated_app domain. Likewise, if new domains for third-party apps are added to AOSP, they will also have access to scary_debug_device. The rule is overly permissive. Most domains will not benefit from having access to this debugging tool. The rule should have been written to allow only the domains that require access.

Debugging features in production

Debug features should not be present on production builds nor should their policy.

The simplest alternative is to only allow the debug feature when SELinux is disabled on eng/userdebug builds, such as adb root and adb shell setenforce 0.

Another safe alternative is to enclose debug permissions in a userdebug_or_eng statement.

Policy size explosion

Characterizing SEAndroid Policies in the Wild describes a concerning trend in the growth of device policy customizations. Device-specific policy should account for 5–10% of the overall policy running on a device. Customizations in the 20%+ range almost certainly contain over privileged domains and dead policy.

Unnecessarily large policy:

  • Takes a double hit on memory as the policy sits in the ramdisk and is also loaded into kernel memory.
  • Wastes disk space by necessitating a larger bootimage.
  • Affects runtime policy lookup times.

The following example shows two devices where the manufacturer-specific policy comprised 50% and 40% of the on-device policy. A rewrite of the policy yielded substantial security improvements with no loss in functionality, as shown below. (AOSP devices Shamu and Flounder are included for comparison.)

Figure 1: Comparison of device-specific policy size after security audit.

Figure 1. Comparison of device-specific policy size after security audit.

In both cases, the policy was dramatically reduced both in size and in number of permissions. The decrease in policy size is almost entirely due to removing unnecessary permissions, many of which were likely rules generated by audit2allow that were indiscriminately added to the policy. Dead domains were also an issue for both devices.

Granting the dac_override capability

A dac_override denial means that the offending process is attempting to access a file with the incorrect unix user/group/world permissions. The proper solution is almost never to grant the dac_override permission. Instead change the unix permissions on the file or process. A few domains such as init, vold, and installd genuinely need the ability to override unix file permissions to access other processes’ files. See Dan Walsh’s blog for a more in-depth explanation.