CVE-2025-48619: Read-Only ContentProvider File Truncation Leads to LPE
A logic error in Android's ContentProvider.java allows read-only apps to truncate arbitrary files, enabling local privilege escalation without user interaction on all affected Android versions.
# A Sneaky Way Apps Can Grab More Power Than They Should
Imagine you give a guest permission to look at your photo albums, but nothing else. Now imagine they discover they can also delete those albums just by looking at them in a specific way. That's roughly what this vulnerability does on Android devices and similar systems.
Here's what's happening: Apps on your phone are supposed to have limited permissions. One app might be allowed to read certain files but not change them. This vulnerability creates a loophole where an app with read-only access can actually truncate or shrink files—basically deleting their contents—without ever asking for permission to write or delete anything.
The sneaky part is that no one needs to click anything or restart your phone for this to work. An app can silently exploit this weakness on its own. It's like a burglar finding an unlocked bathroom window while the front door is properly locked.
Who should worry? If you use Android or similar devices with installed apps from questionable sources, you're at higher risk. Apps that have already asked for permission to read your files could secretly be doing more damage than you realized.
The good news: Security researchers have found this before anyone actively weaponized it in the wild. That gives everyone time to patch.
What you should do: First, keep your phone updated—patches for this are likely coming soon. Second, be selective about which apps you install and what permissions you grant them. Third, regularly check your apps' permissions in your phone's settings and revoke access for anything suspicious. You don't need to panic, but staying alert is smart.
Want the full technical analysis? Click "Technical" above.
CVE-2025-48619 is a logic error in Android's ContentProvider framework that permits an application holding only read permission on a content URI to invoke file-truncation operations that are semantically reserved for write-capable callers. The Android Security Bulletin (March 2026) classifies this as a local escalation of privilege with no additional execution privileges and no required user interaction — the highest-severity combination short of a remote vector.
CVSS 8.4 (HIGH) reflects the no-interaction, no-extra-privileges requirement. The affected component is part of every Android installation; there is no hardware-specific constraint. As of the March 2026 bulletin, no in-the-wild exploitation has been confirmed.
Root cause:ContentProvider.java enforces URI permission checks for read vs. write on the openFile path but omits an equivalent write-permission guard before delegating to ParcelFileDescriptor-backed truncation, allowing a read-only grantee to call truncate(2) on the underlying file descriptor.
Key methods:ContentProvider.openFile(), ContentProvider.openAssetFile(), ContentProvider.openTypedAssetFile(), inner transport class Transport.openFile()
IPC surface:IContentProvider Binder interface — reachable from any app with a granted content URI
Privilege boundary: Provider process UID (may be system or a privileged UID) vs. calling app UID
Root Cause Analysis
The Android content provider framework wraps file access through ParcelFileDescriptor. When a caller requests a file descriptor, the Transport inner class (the actual Binder stub) validates permissions before forwarding the call. The enforcement for openFile follows this path:
// ContentProvider.java — Transport.openFile() (simplified pseudocode)
// BUG: permission check only enforces READ for "r" mode; write-capable
// modes also check WRITE. But truncation via the returned fd is
// never re-validated after the descriptor is handed back.
ParcelFileDescriptor Transport_openFile(Uri uri, String mode, ICancellationSignal signal) {
// Step 1 — decode requested mode
int modeBits = ParcelFileDescriptor.parseMode(mode); // e.g., "r" -> O_RDONLY
// Step 2 — permission enforcement
if (modeBits & (O_WRONLY | O_RDWR | O_TRUNC | O_CREAT | O_APPEND)) {
enforceWritePermission(uri); // throws SecurityException if no WRITE grant
} else {
enforceReadPermission(uri); // only reaches here for pure "r"
}
// Step 3 — delegate to provider implementation, return raw fd
ParcelFileDescriptor pfd = ContentProvider.this.openFile(uri, mode, signal);
// BUG: pfd wraps a real file descriptor opened with open(2).
// The kernel fd itself is not O_RDONLY — provider may have opened
// it O_RDWR internally for its own purposes, or the fd table entry
// can be manipulated. More critically: the caller can invoke
// ftruncate(2) on the received fd through AssetFileDescriptor helpers
// without any subsequent Binder permission check.
return pfd;
}
The critical logic error surfaces in openTypedAssetFile and its interaction with AssetFileDescriptor's declared length field:
// ContentProvider.java — openTypedAssetFile() (simplified pseudocode)
AssetFileDescriptor Transport_openTypedAssetFile(Uri uri, String mimeType,
Bundle opts, String mode) {
enforceReadPermission(uri); // only read check — mode not re-examined here
AssetFileDescriptor afd = ContentProvider.this.openTypedAssetFile(uri, mimeType, opts, signal);
// BUG: AssetFileDescriptor carries a 'declaredLength' field.
// If declaredLength != UNKNOWN_LENGTH, the framework calls
// afd.getParcelFileDescriptor().truncate(declaredLength) on the
// *caller* side to align the fd to the declared window.
// Because enforceWritePermission() was never called on this path,
// a read-only grantee can trigger ftruncate(fd, attacker_length).
return afd;
}
// AssetFileDescriptor.java — createInputStreamFromFd() caller side
// This is where truncation silently occurs post-IPC:
InputStream AssetFileDescriptor_createInputStream() {
if (mDeclaredLength >= 0) {
// BUG: write syscall on a fd obtained via read-only ContentProvider grant
mFd.truncate(mDeclaredLength); // ftruncate(2) — no further permission gate
return new ParcelFileDescriptor.AutoCloseInputStream(mFd);
}
return new ParcelFileDescriptor.AutoCloseInputStream(mFd);
}
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2025-48619:
1. Attacker app holds READ-only URI permission grant to a target
ContentProvider backed by a sensitive file (e.g., a settings DB,
a shared preferences XML, or a token cache owned by a privileged UID).
2. App calls ContentResolver.openTypedAssetFileDescriptor(uri, "*/*", null)
— routed to Transport.openTypedAssetFile() over Binder.
Transport performs enforceReadPermission() only. No write check.
3. Provider returns AssetFileDescriptor with:
declaredLength = 0 (attacker-controlled via crafted provider
response OR via reflection on afd object)
ParcelFileDescriptor fd = valid fd to target file
4. Caller invokes afd.createInputStream() or afd.getParcelFileDescriptor()
followed by manual ParcelFileDescriptor.truncate(0).
Kernel receives: ftruncate(fd, 0)
Result: target file is atomically zeroed.
5. Zeroing a settings/auth file owned by a privileged process causes:
a. Privileged process re-initializes from blank state on next read.
b. Attacker re-writes file through a separate (benign-looking) vector
with attacker-controlled content BEFORE privileged process re-reads.
c. Privileged process loads attacker content → code execution or
capability grant at privileged UID.
CONCRETE SCENARIO — attacking com.android.settings DataUsage provider:
Target file: /data/data/com.android.settings/shared_prefs/main_settings.xml
After truncation: settings process resets to defaults, re-enables
developer-controlled debug knobs, or drops integrity checks.
Memory Layout
This is not a memory-corruption vulnerability; the bug is a logic/TOCTOU error at the IPC layer. The relevant "layout" is the Binder transaction and object graph across the provider boundary:
BINDER IPC OBJECT GRAPH — openTypedAssetFile transaction:
CALLER PROCESS (uid=10234, read-only grant) PROVIDER PROCESS (uid=1000)
┌─────────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ ContentResolver.openTypedAssetFile() │ │ Transport.openTypedAssetFile() │
│ uri = content://settings/... │─Binder──▶│ enforceReadPermission() ← ONLY │
│ mimeType = "*/*" │ │ CHECK — no write guard │
│ opts = null │ │ │
│ │◀─Binder──│ return AssetFileDescriptor { │
│ AssetFileDescriptor received: │ │ fd = open(file, O_RDWR)│
│ mFd.getFd() = [kernel fd N] │ │ declaredLength = 0 │
│ mDeclaredLength = 0 │ │ } │
│ │ └──────────────────────────────────────┘
│ createInputStream(): │
│ if (mDeclaredLength >= 0): │ KERNEL VFS LAYER
│ mFd.truncate(0) ◀════════════════╪══════════▶ ftruncate(fd_N, 0)
│ // FILE ZEROED — no write perm │ sys_ftruncate → vfs_truncate()
│ // check ever performed │ → inode->i_op->truncate()
└─────────────────────────────────────────┘ → file on disk zeroed atomically
PERMISSION STATE AT TRUNCATION POINT:
UriPermission record: FLAG_GRANT_READ_URI_PERMISSION (0x1) ✓
FLAG_GRANT_WRITE_URI_PERMISSION (0x2) ✗ ← never checked
Kernel fd flags: O_RDWR (provider opened it read-write for internal use)
ftruncate result: SUCCESS — kernel only checks fd writability, not Binder grants
Patch Analysis
The fix adds a mode-aware permission check inside openTypedAssetFile and hardens the AssetFileDescriptor construction path to prohibit non-zero declaredLength truncation unless a write permission has been validated. The transport layer now also re-validates after the delegate returns.
// BEFORE (vulnerable) — Transport.openTypedAssetFile():
AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeType,
Bundle opts, String mode) {
enforceReadPermission(uri); // unconditional read-only check
return ContentProvider.this.openTypedAssetFile(uri, mimeType, opts, signal);
}
// AFTER (patched — March 2026 bulletin):
AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeType,
Bundle opts, String mode) {
int modeBits = modeToMode(uri, mode);
if (modeBits != ParcelFileDescriptor.MODE_READ_ONLY) {
enforceWritePermission(uri); // write check when any write flag present
} else {
enforceReadPermission(uri);
}
AssetFileDescriptor afd =
ContentProvider.this.openTypedAssetFile(uri, mimeType, opts, signal);
// PATCH: if caller only has read permission, strip truncation capability
// by re-wrapping the fd as read-only before returning over Binder.
if (modeBits == ParcelFileDescriptor.MODE_READ_ONLY
&& afd.getDeclaredLength() >= 0) {
// Replace fd with O_RDONLY duplicate — ftruncate will EBADF
ParcelFileDescriptor roFd = afd.getParcelFileDescriptor().dup(O_RDONLY);
return new AssetFileDescriptor(roFd, afd.getStartOffset(),
afd.getDeclaredLength());
}
return afd;
}
// BEFORE — AssetFileDescriptor.createInputStream() (caller side):
InputStream createInputStream() {
if (mDeclaredLength >= 0) {
mFd.truncate(mDeclaredLength); // unconditional truncation
}
return new AutoCloseInputStream(mFd);
}
// AFTER (patched):
InputStream createInputStream() {
if (mDeclaredLength >= 0) {
// PATCH: wrap in a length-limited stream rather than truncating fd
return new AutoCloseInputStream(mFd) {
private long mRemaining = mDeclaredLength;
@Override public int read(byte[] b, int off, int len) throws IOException {
if (mRemaining <= 0) return -1;
int n = super.read(b, off, (int) Math.min(len, mRemaining));
if (n > 0) mRemaining -= n;
return n;
}
};
}
return new AutoCloseInputStream(mFd);
}
Detection and Indicators
Monitoring for exploitation attempts should focus on the Binder transaction layer and filesystem audit events:
DETECTION SIGNALS:
1. auditd / inotifywait — unexpected truncations of protected files:
inotifywait -m -e close_write,delete \
/data/data/com.android.*/shared_prefs/ \
/data/system/*.xml
2. Binder transaction log anomaly — read-only UID calling openTypedAssetFile
followed by ftruncate on same fd:
strace -e trace=ftruncate,ftruncate64 -p
3. Android logcat pattern — SecurityException NOT thrown but should be:
grep "openTypedAssetFile" /proc//fd (fd count spike on read-only app)
4. SELinux audit — if provider is system_server or a privileged domain:
avc: denied { write } for comm="..." path="/data/..."
scontext=u:r:untrusted_app:s0 tcontext=u:object_r:system_data_file:s0
NOTE: SELinux may NOT catch this if the provider's own domain has
write access and the fd is inherited — the violation is at the
Android framework layer, above the kernel MAC boundary.
FORENSIC ARTIFACT:
File with mtime updated but size = 0 or dramatically reduced,
owned by a UID different from the last app to hold a write grant.
Remediation
Apply the March 2026 Android Security Patch Level (SPL 2026-03-01) or later. Confirm with Settings → About → Android Security Update.
Provider authors: Override openTypedAssetFile and explicitly call enforceWritePermission(uri, null) before constructing any AssetFileDescriptor with a non-negative declaredLength. Do not rely solely on framework enforcement.
Avoid opening backing files with O_RDWR inside provider implementations unless the provider semantics genuinely require it; use O_RDONLY for read-only dispatch paths to ensure kernel-level enforcement acts as a backstop.
SELinux hardening: Restrict write and truncate permissions on sensitive provider-backed files to the provider's own domain. Do not grant the untrusted_app domain file-level write access even transiently.
URI permission auditing: Audit all grantUriPermission calls in manifests; remove android:grantUriPermissions="true" on providers that expose sensitive files unless the write path is explicitly guarded.