Skip to content
DownloadAPK

How Android App Sandboxing Actually Works

A technical breakdown of how Android isolates every installed app using Linux UID sandboxing, SELinux mandatory access control, Binder IPC, and the runtime permissions model, plus an honest look at where the sandbox falls short.

Every time you install an Android app, the operating system quietly sets up a miniature prison for it: a dedicated Linux user ID, a private data directory no other app can read, and a set of kernel-enforced boundaries that persist for the entire lifetime of the process. Most users never think about this. Most developers take it for granted. But understanding exactly how the sandbox is constructed - and where its limits are - matters if you care about privacy, want to evaluate a sketchy APK, or are choosing between ROMs for a hardened device.

This piece walks through the full stack: Linux UID isolation, Discretionary Access Control, SELinux’s Mandatory Access Control layer, the Binder IPC mechanism, and finally the permissions model that sits on top of it all.

The Foundation: One App, One Linux UID

Android’s application sandbox is not a virtual machine or a container runtime. It is built directly on the Linux kernel’s process isolation primitives - the same Unix-lineage UID model that has been in production use since the 1970s.

When the Package Manager installs an app, it assigns that app a unique Unix user ID (UID) in the range 10000-19999. The app’s private data directory (/data/data/<package.name>/) is owned by that UID with mode 0700 - readable and writable only by its owner. Every process spawned from that app’s APK runs under the same UID.

Because the Linux kernel enforces UID boundaries on every file operation, network socket, and process signal, two apps with different UIDs simply cannot touch each other’s data without explicit kernel permission. This is Discretionary Access Control (DAC) and it works before any Android-specific code runs.

The practical consequence: a malicious app cannot open() another app’s SQLite database, read its SharedPreferences, or write to its private cache directory. The kernel returns EACCES - permission denied - at the system call level.

One important exception: apps from the same developer can declare android:sharedUserId in their manifest to share a UID. This feature is deprecated as of API level 29 and Google actively discourages it, but it still exists in older apps. Shared UID means shared sandbox - treat two apps with the same UID as a single trust unit.

SELinux: Mandatory Access Control on Top of UID Isolation

DAC is necessary but not sufficient. A process running as a given UID still has the kernel’s default capabilities for that UID, and on a complex system like Android that surface is large. SELinux (Security-Enhanced Linux), integrated into AOSP since Android 4.3 and enforced in strict mode since Android 5.0, adds a second independent access control layer: Mandatory Access Control (MAC).

Under SELinux, every process and every file is labeled with a security context - a string like u:r:untrusted_app:s0:c512,c768 for a third-party app process or u:object_r:app_data_file:s0:c512,c768 for that app’s data files. Before the kernel executes any operation, it consults the SELinux policy to check whether the source context is allowed to perform that action on the target context.

Crucially, SELinux policy is additive: the kernel must say yes at both the DAC and MAC layers. Passing DAC but failing SELinux still blocks the operation. This matters because some operations (like accessing /proc entries or calling certain ioctls) might pass DAC for a given UID but are blocked by SELinux policy for the untrusted_app domain.

AOSP ships a reference policy in system/sepolicy/ that categorizes apps into domains:

SELinux DomainWho uses itTypical extra access
untrusted_appThird-party apps (Play Store, sideloaded)Minimal - only what permissions grant
platform_appApps signed with the platform keySystem APIs, some /data paths
system_appSystem-level appsBroad system service access
priv_appPrivileged apps in /system/priv-appSensitive APIs like INSTALL_PACKAGES
isolated_appRenderer processes, sandboxed servicesNear-zero access - network blocked

Third-party apps land in untrusted_app regardless of who published them. The SELinux label is determined by the installer and the app’s signature, not by any self-declaration in the manifest.

GrapheneOS and CalyxOS ship tighter variants of these policies. GrapheneOS in particular applies stricter domain restrictions to untrusted_app beyond the AOSP baseline, reducing what the domain can access even when DAC would otherwise allow it. If you are evaluating hardened ROMs, checking GrapheneOS vs LineageOS will show you how dramatically these policies differ between projects.

Binder IPC: Crossing the Sandbox Safely

Apps often need to talk to each other or to system services - sharing a file, requesting a contact lookup, asking the camera to take a photo. If the sandbox is a wall, Binder is the guarded gate.

Binder is Android’s primary inter-process communication mechanism, implemented as a kernel driver (/dev/binder). When app A wants to call a method on app B or on a system service:

  1. App A sends a transaction to the Binder kernel driver, targeting a registered interface.
  2. The kernel copies the transaction data into a shared memory region and delivers it to the target process.
  3. The target process handles the call and returns a result the same way.

The kernel-level copy means app A never gets a direct pointer into app B’s memory space. More importantly, the Binder driver carries the caller’s UID and PID with every transaction. The receiving service can call Binder.getCallingUid() to verify who is asking and enforce its own access checks.

System services like PackageManagerService, ActivityManagerService, and CameraService all use this mechanism. When an app calls context.getSystemService(Context.CAMERA_SERVICE), it is making a Binder call to a privileged system process that checks whether the app holds android.permission.CAMERA before proceeding. The camera hardware never touches untrusted app space directly.

Android 11 introduced android:isolatedProcess="true" for app services and tighter restrictions on which processes can use certain Binder interfaces, reducing the blast radius if a component is exploited.

The Permissions Model: Doors in the Sandbox Wall

The sandbox is always on. Permissions are the mechanism by which apps are granted specific, auditable exits from that sandbox.

Android defines four protection levels that determine how a permission is granted:

  • Normal: Granted automatically at install time, no user prompt. Low-risk capabilities like setting an alarm or accessing the internet (yes, INTERNET is a normal permission, not a dangerous one - any app can make network connections by default).
  • Dangerous: Requires explicit runtime consent from the user (Android 6.0+). Covers access to contacts, location, microphone, camera, storage, call logs, and SMS.
  • Signature: Granted only to apps signed with the same certificate as the app that declared the permission. Used for inter-app communication within a suite of apps from the same developer.
  • Privileged/System: Granted only to apps pre-installed in /system/priv-app or to apps granted by the OEM. Covers capabilities like installing other packages, reading call state at a system level, or modifying system settings.

A critical point that even experienced developers sometimes miss: granting a dangerous permission does not weaken the sandbox. It extends a specific, auditable capability. An app with READ_CONTACTS can read your contacts through the ContentProvider interface - it still cannot read another app’s private files, inspect system memory, or access the microphone. Each dangerous permission is an independent, revocable door.

Since Android 11, permissions also have auto-revocation: if you have not used an app for a few months, the system automatically resets its dangerous permissions to denied. Android 12 added approximate location as an option separate from precise location, giving users finer-grained control.

For a practical checklist on tightening permissions across your device, see the Android privacy hardening checklist.

Where the Sandbox Has Real Limits

No security architecture is unlimited, and being honest about the gaps is more useful than pretending the sandbox is airtight.

Accessibility Services: An app granted Accessibility Service permission can observe virtually all UI events across all apps - text typed, screens shown, buttons pressed. This is intentional for legitimate use (screen readers, switch access), but it is also the primary mechanism used by stalkerware and credential-stealing malware. The sandbox does not prevent this because the permission explicitly grants cross-app visibility. Review which apps hold this permission regularly.

Notification access: Similar issue. An app with Notification Listener permission can read the content of every notification, including OTP codes. Android 11 tightened this by hiding OTPs from notification content in supported apps, but the surface remains.

Shared storage: Files in /sdcard (external storage) have historically used a single shared UID (sdcard_rw group) accessible to any app with READ_EXTERNAL_STORAGE. Android 10 introduced Scoped Storage, which limits apps to their own directories on external storage plus specific media collections. Apps targeting API 29+ that use requestLegacyExternalStorage or apps targeting older APIs can still access the old shared model on some devices.

Kernel exploits: A sandbox escape via a kernel vulnerability bypasses everything. CVE-2019-2215 (Binder use-after-free) and CVE-2021-0920 (Unix domain socket garbage collection race) are real examples from recent Android history. This is why monthly security patches matter, and why projects like GrapheneOS invest in compiler-level mitigations (CFI, ShadowCallStack, MTE on supported hardware) to raise the cost of kernel exploitation.

If you are sideloading APKs from outside the Play Store, understanding these limits becomes especially important. The sandbox applies equally to sideloaded apps, but you are taking on the APK vetting responsibility yourself. The sideloading security guide covers what to verify before installing a third-party APK.

How Custom ROMs Harden the Sandbox

Stock AOSP is a solid baseline. Hardened ROMs push further in several directions:

GrapheneOS ships a hardened memory allocator (hardened_malloc) that makes heap exploitation significantly harder, enables ASLR improvements beyond AOSP defaults, applies compile-time CFI and shadow call stacks more broadly, and has tightened SELinux policies. It also adds a toggle to block apps from accessing the network and sensors entirely at the OS level, independent of the permissions system - a capability that does not exist in AOSP.

LineageOS tracks AOSP fairly closely on sandbox architecture but tends to ship security patches promptly on supported devices, which matters more practically for most users than architectural changes. Its Trust interface surfaces permission and patch status clearly.

When using any ROM with root access (Magisk on LineageOS, or a rooted GrapheneOS build), remember that root processes operate outside the sandbox by definition. Magisk’s DenyList is a mitigation for specific apps’ root detection, not a security guarantee - a malicious app running as root or exploiting a root-exposed interface can read any other app’s data freely.

For a side-by-side comparison of what each ROM actually changes at the security layer, the GrapheneOS vs LineageOS comparison goes into further depth.

Putting It Together

Android’s sandbox is layered by design: Linux UID isolation handles the common case at near-zero overhead, SELinux MAC catches what DAC misses, Binder provides safe cross-process calls, and the permissions model gives users auditable control over what each app can reach. No single layer is sufficient alone, and an attacker who breaks through one (say, via a kernel CVE) still has to contend with the others.

For everyday users, the practical takeaways are: keep your device patched, audit accessibility and notification-listener permissions regularly, use Scoped Storage-aware apps, and treat any app that requests an unusual permission combination with suspicion. The sandbox does the heavy lifting automatically, but it works best when you are not handing out keys to every door.

FAQ

Can one Android app read another app's files?
No, not under normal conditions. Each app runs under a unique Linux UID and its private data directory is set to mode 0700 (owner-only). The kernel returns EACCES on any cross-UID file access attempt before any Android framework code is consulted. The only legitimate exception historically was android:sharedUserId, which is deprecated since API level 29.
What does SELinux actually add if UID isolation already exists?
UID isolation (DAC) covers file and process access by owner, but it still leaves some kernel surfaces open to any process with the right UID capabilities. SELinux adds a separate Mandatory Access Control layer where every process and file carries a security label, and the kernel checks both layers on every operation. Passing DAC but failing SELinux still blocks the operation, so SELinux catches what UID isolation misses - for example, certain /proc reads or ioctl calls.
Does granting a dangerous permission make an app less sandboxed?
No. Granting a dangerous permission extends one specific, auditable capability through a defined interface, such as reading contacts via ContentProvider. The app still cannot access another app's private files, inspect system memory, or use any other capability it was not explicitly granted. Each dangerous permission is independently revocable, and since Android 11 unused permissions are auto-revoked after a few months of inactivity.
Which Android security features does GrapheneOS add beyond stock AOSP?
GrapheneOS ships a hardened memory allocator (hardened_malloc) making heap exploitation harder, broader compile-time CFI and shadow call stacks, tighter SELinux domain policies for untrusted_app, and ASLR improvements beyond AOSP defaults. Most distinctively, it adds OS-level toggles to deny network and sensor access to any app entirely, independent of the standard permissions system - something AOSP does not offer.