home intel cve-2025-48619-contentprovider-readonly-truncation-privilege-escalation
CVE Analysis 2026-03-02 · 9 min read

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.

#privilege-escalation#logic-error#file-truncation#content-provider#authorization-bypass
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48619 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2025-48619HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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.

Affected Component

  • File: frameworks/base/core/java/android/content/ContentProvider.java
  • 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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →