The app sandbox
Every Android app is a Linux process with its own UID, its own data directory and its own SELinux domain. That single design decision is why installing a stranger's app is a reasonable thing to do.
The Android app model is unusual in one specific way: it treats every app as a separate user of the operating system. Not a user in the sense of a person — a user in the sense of a Linux UID, with all the file permission machinery that implies.
TL;DR#
- Each app gets a unique UID and runs in its own process. The kernel enforces isolation, not the framework.
- Its private data lives in a directory owned by that UID, with mode 0700 for apps targeting API 24Android 7.0 or higher.
- Since API 28Android 9, non-privileged apps targeting that level also get individual SELinux domains, so mandatory access control distinguishes one app from another.
- Since API 26Android 8.0, all apps run under a seccomp-bpf filter that restricts the syscalls they can make.
- An is a stronger sandbox: no UID privileges, no storage access, no permissions.
Mental model#
Think of an apartment building where each tenant is legally a separate company.
Not a tenant — a company. Each has its own tax ID, its own bank account, its own lease. The building cannot accidentally pay one company's invoice from another's account, because the accounts are different legal entities, not just different labels.
An Android app's UID is that tax ID. It is not a name the system uses to identify the app; it is the identity the kernel uses to decide what the process may touch.
Where the analogy breaks#
Companies can sign contracts with each other and share resources deliberately. Apps can too, but only
through mediated channels: Binder, intents, content providers, and file providers. There is no
equivalent of two companies merging their bank accounts — sharedUserId, the old mechanism for
sharing a UID between apps, is deprecated for exactly this reason.
The big picture#
Text description of this diagram
The security onion, with the app sandbox as the fourth ring from the outside. Outside it are the hardware root of trust, verified boot and the kernel. Inside it are permissions and consent, then keys and secrets. The ring's description notes that the sandbox stops one app from reading another's files or touching its memory, but cannot stop an app from misusing the permissions the user granted it.
Read the sandbox's position carefully. It is inside the kernel, which means it is enforced by mechanisms the app cannot reach, and outside permissions, which means a sandboxed app can still be dangerous with the wrong grants.
Deep dive#
UID allocation#
Android assigns UIDs from a reserved range, one per app per user. The familiar form is u0_a123,
which decomposes as:
u0— user zero, the primary user.a— an application UID.123— the app's offset within that user's range.
A work profile's copy of the same app has a different UID, because it is a different user. That is what makes work and personal data separable without a separate sandbox mechanism: they are already different UIDs.
The data directory#
An app's private files live under /data/data/<package>, which is an alias for
/data/user/<user_id>/<package>. The directory is owned by the app's UID and, for apps targeting
API 24Android 7.0 or higher, has mode 0700 — owner read/write/execute, nobody else anything.
That mode change is worth pausing on. Before it, the default was 0751: the directory was readable
and traversable by other apps, and individual files were protected only by their own modes. An app
that accidentally created a world-readable file leaked it. The change to 0700 made the default
private, which is the correct default for a sandbox.
Since API 28Android 9, an app cannot make its own data world-accessible even if it tries. A file created with world-readable permissions is not readable by another app, because SELinux forbids the access regardless of the file mode. Sharing has to go through a content provider or a file provider, which means it has to be deliberate.
SELinux: from one domain to many#
Android 5.0 put SELinux into enforcing mode with a coarse split: the system was one domain, and all
third-party apps shared untrusted_app. UID isolation still did most of the work between apps.
Android 9 changed that. Apps targeting API 28Android 9 run in per-app SELinux sandboxes1,
which means the mandatory policy can distinguish app A from app B. The mechanism is the MLS category
set in the security context: u:r:untrusted_app:s0:c123,c256,c512,c768 is a different context from
one with different categories, even though the domain (untrusted_app) is the same.
The practical effect is that a kernel-level bug that lets one app bypass its UID check no longer automatically gives it access to every other app's data: the SELinux check is a second, independent gate.
seccomp#
Since API 26Android 8.0, all apps run with a seccomp-bpf filter.2 The filter is a list of allowed system calls; anything not on the list returns an error without ever entering the kernel's syscall implementation.
The value is surface reduction. A compromised app process has a much smaller set of kernel entry
points to attack. Android's filter blocks syscalls that no app should need — ptrace on other
processes, mount, kernel module operations — while allowing the ones the runtime needs.
Isolated processes#
Some code needs to run with less than an app's privileges: WebView renderers, some media codecs,
certain background effects. Android provides isolatedProcess, which runs a service in a process
with:
- Its own UID, not shared with the app.
- No permissions at all.
- No access to the app's data directory.
- An SELinux domain (
isolated_app) that forbids almost everything.
The only channel back is Binder, through the interface the app defined. If the isolated code is compromised, the attacker has a process that can call one interface and nothing else.
What the sandbox does not do#
Being precise about the limits is more useful than praising the mechanism.
- It does not limit what a granted permission allows. Camera permission is full camera access.
- It does not stop side channels. Timing, power and sensor data can leak information across sandboxes; that is why sensor access is itself permissioned.
- It does not protect against a malicious system app. A privileged app is inside the sandbox model but holds permissions that make the sandbox mostly irrelevant.
- It does not protect the user from themselves. Installing an app and granting it everything is a decision the sandbox cannot override.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 18 | 4.3 Jelly Bean |
|
| 23 | 6.0 Marshmallow |
|
| 28 | 9 Pie |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 37 | 17 Cinnamon Bun |
|
The sandbox has been tightened in every release, always by narrowing a specific capability:
- Android 5.0: SELinux enforcing.
- Android 6.0: per-user SELinux separation; the
0700home directory default for apps targeting API 24 or higher. - Android 7.0: FBE, so a locked device's data is unreadable.
- Android 8.0: seccomp for all apps.
- Android 9: per-app SELinux sandboxes; world-accessible data disallowed.
- Android 10: raw filesystem paths restricted; scoped storage begins.
- Android 11: scoped storage enforced.
Under the hood#
- seapp_contextsthe mapping from app attributes to SELinux domains and categories
- te_macrosthe macros that express common access patterns, such as rw_file_perms
- Process.javathe UID constants and the process start interface
- SYSCALLS.TXTthe syscall surface seccomp filters are written against
Curiosity corner#
- An app's UID is visible in
ps.u0_a123is the UID, and it is the same identity the kernel uses for every file check. - The work profile is a user, not a container. Its isolation comes from the same UID machinery as the primary user's.
run-asworks by changing UID, not by bypassing the sandbox. It is a debug convenience that requires a debuggable app, and it demonstrates that the check is the UID.
Try it#
adb shell ps -A -o USER,PID,PPID,NAME | grep com.example
adb shell ls -ld /data/data/com.example.app
adb shell dumpsys package com.example.app | grep -E "userId|flags"
Example output
u0_a123 4821 610 com.example.app
drwx------ 4 u0_a123 u0_a123 4096 2026-10-01 12:00 /data/data/com.example.appThe USER column is the UID. The directory's owner and mode are the sandbox's file-system half. The
dumpsys output shows the numeric userId that maps to it.
Check yourself#
Check yourself
Which layer enforces the app sandbox?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsSELinuxsource.android.comaccessed 2026-10-27↩
- docsSeccomp BPFdocs.kernel.orgaccessed 2026-10-27↩
- docsApplication sandboxsource.android.comaccessed 2026-10-27↩
- docsAndroid security overviewsource.android.comaccessed 2026-10-27↩
- docsSELinux conceptssource.android.comaccessed 2026-10-27↩
- docsProcesses and threads overview | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsdumpsysdeveloper.android.comaccessed 2026-10-27↩