Not all Android apps are created equal. The Settings app on an Android device, for example, can change numerous things that no “normal” app can, regardless of how many permissions that app requests. Apps with special privileges like Settings are often called “system apps.” But what makes an app a “system app”? In answering that question for ourselves, we noticed that AOSP’s resources on the subject are disparate and assume a great deal of Android internals knowledge. We wrote this post to summarize what we learned for the benefit of security researchers, app developers, and enthusiasts alike.

In modern Android, two main things determine what an app can do—the set of permissions it’s been granted and the SELinux domain it runs in. Permissions typically gate Binder access to system services and app components on a call-by-call basis, while SELinux gates access to Linux kernel objects (e.g. files, sockets, device nodes) and to entire Binder services. Although any app on Android can define a permission, we’ll constrain our discussion to platform permissions, which are permissions built into Android that control access to system APIs.

Most platform permissions are defined in the manifest of the special android package1. Note how each one includes a protectionLevel string consisting of a base type (normal, dangerous, signature, or internal) modified by zero or more flags (denoted with a leading |, as in |privileged). A permission’s protectionLevel indirectly specifies which apps may use it.

App SELinux domains are defined as part of Android’s OS-wide SELinux policy. SELinux is used for more than just apps, so app domains (e.g. priv_app) are denoted by the appdomain attribute. A set of textual policy rules, located in /system/etc/selinux/plat_seapp_contexts and similar files on other Project Treble partitions2, tell Android which domain to assign a given app.

Although protectionLevel and seapp_contexts fully define the privileges available to each app, both are complex and have many special cases, so it can be hard to pick out which parts matter. In our experience, most apps on a device can be classified into one or more of the following five groups, each of which grants certain privileges. Every group but the first represents some flavor of “system app”:

  • Untrusted apps are apps that can be built by a third-party developer and installed to any Android device. Android will only grant them platform permissions with base type normal or dangerous, and they run in some flavor of untrusted_app SELinux domain. This is the vast majority of apps on the Play Store.
  • Preinstalled apps are all the apps that come with a device. They’re located in app/ and priv-app/ directories under /system, other Project Treble partitions, and APEX modules. Such apps can be granted |preinstalled permissions and are marked FLAG_SYSTEM. (Although that flag’s documentation says it “should not be used to make security decisions,” some APIs give it special treatment anyway.) Updates to preinstalled apps, which live in /data/app/ like untrusted apps, are still considered preinstalled.
  • Privileged apps are preinstalled apps that live in priv-app/ instead of app/. (Prior to Android 4.4, there was no priv-app/ and all preinstalled apps were privileged.) They run in the priv_app SELinux domain and can request |privileged permissions subject to the constraints of privapp-permissions.xml (introduced in Android 8).
  • Platform-signed apps are apps that share a signing key (the “platform key”) with /system/framework/framework-res.apk and so can be granted signature platform permissions. They run in the platform_app SELinux domain. Platform-signed apps don’t need to be preinstalled, but they typically are.
  • System-UID apps are platform-signed apps that run as the system user (UID 1000) by specifying android:sharedUserId="android.uid.system" in their manifest; for example, here’s where the Settings app specifies it. Only platform-signed apps can do that, as the standard rules of sharedUserId require all apps in a UID to share a signing key. System-UID apps run in the system_app SELinux domain and bypass all permission checks. Notably, system is just one of several special users that appropriately-signed apps can run as.

If an app qualifies for multiple SELinux domains, Android prioritizes them in this order: system_app, platform_app, priv_app, untrusted_app. This is theoretically the order of descending privilege, but nothing enforces that. For example, we’ve seen devices whose vendor SELinux policy lets priv_app access resources that system_app cannot!

These groups describe the most common ways apps qualify for special privileges, but they are not comprehensive. Here are just a few examples of why two apps in the same group(s) might end up with different privileges:

  • The user has opted into a dangerous permission for one but not the other.
  • Their signing keys differ, giving them access to different sets of app-defined signature permissions.
  • One has been assigned a role that grants it certain privileged permissions. (|role is one of several lesser-used protectionLevel flags we didn’t discuss.)
  • seapp_contexts contains a rule that puts one into a special SELinux domain based on its package name or UID. (Vendors often use such rules to give their own apps extra hardware access.)

Nonetheless, we find these groups very useful as a mental model. For example, they helped us quantify the security impact of two vulnerabilities we discovered that let attackers run code in the context of various apps. We hope you’ll find them equally useful.

Thanks to Yiannis Kozyrakis, Adam Sindelar, Nik Tsytsarkin, and Vlad Ionescu for improvements and corrections.

  1. Platform permissions can also be defined in the manifests of other platform-signed apps, but the majority are in the android manifest. 

  2. /system_ext, /vendor, /product, and /odm as of this writing