Zygote
Storageintermediate

The history of Android storage

From a real SD card that any app could read to a virtualised filesystem with per-app views: how WRITE_EXTERNAL_STORAGE became a privacy disaster, and how Android unwound it.

11 minAPI 1–37 · 17interactivequizverified 2026-10-27

Early Android had a simple storage model: there was internal storage, private to each app, and there was external storage, shared by everyone. That simplicity became a privacy problem, and the last decade of Android storage work has been the slow, painful process of undoing it.

TL;DR#

  • Internal storage was always per-app and private. External storage was a shared volume, historically a real SD card.
  • WRITE_EXTERNAL_STORAGE gave an app access to essentially every user file. It was the most over-granted permission on the platform.
  • As SD cards disappeared, Android emulated external storage on the same flash — first with a kernel filesystem, later with FUSE.
  • The shared model was replaced in stages: scoped storage in API 29Android 10, enforced in API 30Android 11, and refined by the photo picker in API 33Android 13.
  • Each stage moved the boundary from capability (you may write files) to per-item consent (you may read this file).

Mental model#

Think of a shared office fridge with no labels.

Everyone's lunch went in, and everyone could take anything. It worked while there were five people. With fifty, the fridge became a free-for-all: things disappeared, allergies were ignored, and nobody could tell whose food was whose.

The fix was not a bigger fridge. It was labelled shelves, a per-person shelf, and a rule that you can only take from someone else's shelf with their permission.

Where the analogy breaks#

A fridge is physical and the shelves are obvious. In a filesystem, an app's "shelf" is a directory whose path changes between Android versions, whose permissions are enforced by the kernel, and which on modern Android is presented through a userspace filesystem that can show different views to different apps. None of that is visible from the path.

The big picture#

Loading diagram…
Text description of this diagram

A treemap of what an app can read and write in shared storage, split into four groups: its own directories, other apps' media, documents and downloads, and blocked or special paths. Own directories are always writable without permission. Other apps' media needs MediaStore or a picker. Documents need the Storage Access Framework or a broad grant. Other apps' data and all-files access are blocked or require a special permission.

The treemap's four groups are the modern model: private, media, documents, blocked. The historical story is how the middle two moved out of the first.

Deep dive#

The two storages#

The original model, which still shapes the API names:

  • Internal storage is the app's private directory on the data partition. No permission needed; no other app can read it.
  • External storage is a shared volume. Historically this was a physical SD card; it was world-readable with the right permission, and it survived app uninstall.

The names persisted long after the physical distinction did. A phone with no SD card still has "external storage" — it is just a directory on the same flash, presented as a separate volume.

The permission problem#

READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE were normal permissions: granted at install time, with no runtime dialog. An app that asked for write access could read every photo, every document and every other app's files on the shared volume.

The consequences were predictable:

  • Photo and file-manager apps read everything, whether or not they needed to.
  • An app could modify another app's files.
  • Uninstalling an app did not remove what it had written to shared storage.
  • There was no record of what had been read.

By the time Android 6.0 made permissions runtime-granted, the damage was structural: the permission was too broad to be meaningful, so users granted it without thinking.

Emulating the SD card#

When manufacturers stopped shipping SD cards, Android kept the /sdcard path by emulating it on the data partition. The first implementations used a kernel filesystem, sdcardfs, which presented per-app views of the same underlying directory and enforced UID-based access in the kernel.1

sdcardfs was an out-of-tree Android addition: efficient, but another kernel patch to maintain. Android 11 replaced it with FUSE, a userspace filesystem that could implement the same per-app views with far more flexibility — and, crucially, could implement scoped storage's rules, which a simple UID check could not.

The move to FUSE cost performance; the platform mitigated it with FUSE passthrough, which lets reads and writes bypass the userspace daemon for files the app is allowed to access. The trade was deliberate: policy in userspace is easier to change than policy in the kernel.

Scoped storage#

API 29Android 10 introduced scoped storage: an app gets its own external directory freely, and accesses other apps' media through MediaStore. The first release allowed requestLegacyExternalStorage as an opt-out, because too many apps would have broken.

API 30Android 11 enforced it for apps targeting 30. The opt-out stopped working. From that point, the model was: private directory, MediaStore for media, SAF for documents.2

API 33Android 13 added the system photo picker: the user selects specific photos and the app receives URIs for those items only. API 34Android 14 added selected-photos access, where the user grants access to a chosen subset of their library that the app can re-read.

The arc is now complete: from "this app may read all storage" to "this app may read these three photos". Each step reduced the scope of a grant and increased the visibility of what was granted.

Why it took so long#

Storage is the most compatibility-sensitive part of the platform. A file manager genuinely needs broad access; a backup tool needs to enumerate; a photo editor needs to write back to the original. A model that works for all three without opening the door for everyone is hard, and the platform iterated in public for years.

What an SD card actually was#

A removable SD card was a block device shared by every app. It had no per-app quota, no ownership model and no journaling requirement. An app could write a file and another app could read it, by path, with no grant.

That model was convenient for the user — the card worked in a laptop — and catastrophic for isolation. Deleting an app did not delete its files; uninstalling a photo app left its cache in the shared volume forever.

Emulation changed the storage, not the API#

When phones stopped shipping removable cards, Android kept the /sdcard path by emulating the volume on the data partition. Apps kept working because the path did not change; only the device behind it did.

The emulation layer had to present a per-app view: the same path, but with different contents depending on which app was asking. That is a policy requirement a plain filesystem cannot express, which is why it needed a special filesystem in the kernel and, later, a userspace daemon.

The performance tax#

sdcardfs was fast because it was in the kernel. FUSE is slower because every file operation is a round trip to a userspace process.

FUSE passthrough recovers most of the loss by allowing direct access to files the app is permitted to open, so the common case does not pay the round trip. The design goal was to keep the policy in userspace while keeping the hot path in the kernel.

What the transition cost developers#

Every storage migration broke some apps. The scoped-storage transition was especially hard because:

  • Apps had built features around arbitrary paths, which no longer worked.
  • File managers genuinely needed broad access, which required a special permission.
  • The opt-out made it possible to ignore the change, which delayed the migration rather than avoiding it.

The lesson the platform drew was to stage the change over multiple releases, and to provide a migration path (MediaStore.Downloads, SAF trees, app-specific directories) before enforcing it.

The compatibility surface#

Every storage change has a compatibility cost, and the platform has learned to stage them:

  • Provide a migration API before enforcing.
  • Allow an opt-out for one release.
  • Enforce for new target SDKs first.
  • Backport the safe alternative (the picker) so apps do not have to wait.

The scoped-storage transition used all four, and it still took three releases. The lesson is that storage is the slowest part of the platform to change, because every app touches it.

What the user sees#

For the user, the changes are mostly invisible until something breaks: an app that can no longer find its files, or a picker that appears where a permission dialog used to.

The visible improvement is in Settings, where the app's access is enumerated as items and folders rather than as a capability. That is the privacy model becoming legible.

The remaining gap#

Storage is bounded now, but use is not. An app with a grant can still upload what it reads. The platform's answer to that is the data-access auditing and the privacy dashboard, which log access rather than preventing it.

The two mechanisms are complementary: scoped storage decides what is reachable, auditing makes what is reached visible.

How it evolved#

API 1–API 18SD card eraa real shared volumeAPI 1
API 19 →Emulated storage/sdcard on the data partitionAPI 19
API 29 →Scoped storageopt-out, then enforced for targeting 30API 29
API 33 →Picker eraper-item consentAPI 33
Storage changes10 releases
APIVersionWhat changed
31.5 Cupcake
  • android.widget video and media improvements
123.1 Honeycomb
  • ParcelFileDescriptor improvements
154.0.3 Ice Cream Sandwich
  • New calendar and media provider APIs
194.4 KitKat
  • Storage Access Framework
247.0 Nougat
  • FileProvider and StrictMode for file URLs
2910
  • Scoped storage with requestLegacyExternalStorage opt-out
3011
  • Scoped storage enforced for targeting apps
3313 Tiramisu
  • READ_MEDIA_IMAGES/VIDEO/AUDIO replace storage reads
3414 Upside Down Cake
  • READ_MEDIA_VISUAL_USER_SELECTED
3717 Cinnamon Bun
  • ACTION_PICK_CONTACTS gives session-scoped contact field access

Under the hood#

Curiosity corner#

  • The path /sdcard still exists on phones with no SD card. It is a compatibility alias for the emulated volume, and apps that hardcoded it kept working long after the hardware disappeared.
  • An app's external directory is deleted on uninstall, while files it wrote to the shared volume historically were not — one of the concrete privacy problems scoped storage fixed.
  • FUSE is slower than a kernel filesystem, which is why passthrough exists. Every storage redesign since has had to answer the performance question.

Try it#

List volumes
adb shell sm list-volumes all
Example output
emulated;0 mounted
public:179,1 mounted
Storage state and paths
adb shell dumpsys mount | grep -i "volume|path" | head -20
Example output
Internal storage: /data
External storage: /storage/emulated/0
An app's directories
adb shell run-as com.example.app ls -la /data/data/com.example.app/
Example output
/data/user/0/com.example.app/files
/storage/emulated/0/Android/data/com.example.app/files

Check yourself#

Check yourself

1 / 12

Why did WRITE_EXTERNAL_STORAGE become a privacy problem?

0 answered · 0 correct

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.

  1. docsStorage overviewsource.android.comaccessed 2026-10-27↩
  2. docsStorage updates in Android 11developer.android.comaccessed 2026-10-27↩
  3. docsData and file storage overview | App data and filesdeveloper.android.comaccessed 2026-10-27↩
  4. docs10 storagedeveloper.android.comaccessed 2026-10-27↩
  5. docsBehavior changes: Apps targeting Android 11developer.android.comaccessed 2026-10-27↩
  6. docsEROFS filesystemsource.android.comaccessed 2026-10-27↩
  7. docsPerubahan perilaku: aplikasi yang menargetkan API level 28+developer.android.comaccessed 2026-10-27↩