home intel cve-2025-48615-mediabuttonreceiverholder-desync-lpe
CVE Analysis 2025-12-08 · 9 min read

CVE-2025-48615: MediaSession Persistence Desync Enables Local Privilege Escalation

A resource-exhaustion-induced desync in MediaButtonReceiverHolder.getComponentName() allows a local attacker to corrupt media session state and escalate privileges without user interaction.

#resource-exhaustion#privilege-escalation#persistence-desync#media-receiver#local-attack
Technical mode — for security professionals
▶ Attack flow — CVE-2025-48615 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2025-48615Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2025-48615 is a logic-level privilege escalation vulnerability in the Android media framework's MediaButtonReceiverHolder class, patched in the December 2025 Android Security Bulletin. The bug lives in getComponentName() — a method responsible for resolving and persisting the ComponentName of a media button receiver across process boundaries. Under resource exhaustion conditions, the persistence write and the in-memory state diverge, creating a window where an attacker-controlled component is accepted by the system server as a trusted media receiver.

CVSS 7.8 (HIGH). No additional privileges required. No user interaction required. The vulnerability is pre-auth from the perspective of the media_session service: any application capable of registering a MediaSession can trigger it.

Root cause: getComponentName() resolves a ComponentName from a PendingIntent and caches it in-memory before the corresponding persistence write completes, allowing resource exhaustion to abort the write while leaving the stale in-memory reference in place — a classic TOCTOU variant operating across the persistence boundary.

Affected Component

File: services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
Class: MediaButtonReceiverHolder
Method: getComponentName(Context, PendingIntent, MediaSessionRecord)
Service: MediaSessionService (runs as system_server)
Privilege context: system UID, SELinux domain system_server

The affected code path is exercised whenever a media application calls MediaSession.setMediaButtonReceiver(PendingIntent), which routes through MediaSessionService.setMediaButtonReceiver() into the vulnerable holder class.

Root Cause Analysis

The core issue is a two-phase commit without atomicity guarantees. getComponentName() performs three sequential operations:

  1. Resolve a ComponentName from the caller's PendingIntent via PackageManager
  2. Write the resolved name to persistent storage (XML settings file)
  3. Cache the resolved name in the object's mComponentName field

When step 2 fails due to resource exhaustion — a full /data partition, an OOM abort mid-write, or an IOException on the settings journal — the exception is caught, logged, and execution continues. Step 3 still runs. The in-memory mComponentName is now set to the attacker-supplied value, while persistent storage retains either the previous legitimate entry or nothing at all.

// Reconstructed Java->pseudocode of the vulnerable path
// MediaButtonReceiverHolder.java (pre-patch)

ComponentName getComponentName(Context ctx, PendingIntent pi, MediaSessionRecord record) {
    ComponentName resolved = null;

    // Phase 1: resolve from PendingIntent — attacker controls the PendingIntent content
    if (pi != null) {
        Intent intent = pi.getIntent();
        resolved = intent.getComponent();          // attacker-supplied ComponentName

        if (resolved == null) {
            // fallback: query PM for implicit intent resolution
            resolved = resolveImplicit(ctx, intent);
        }
    }

    // Phase 2: persist to disk
    try {
        persistComponentName(ctx, resolved);       // writes XML journal to /data/system/
    } catch (IOException e) {
        Slog.e(TAG, "Failed to persist component name", e);
        // BUG: exception is swallowed; execution falls through to Phase 3
        // BUG: no rollback, no null-assignment to 'resolved'
    }

    // Phase 3: update in-memory cache — runs regardless of Phase 2 success
    mComponentName = resolved;                     // BUG: desync — disk != memory
    return mComponentName;
}

The persistComponentName() call uses an AtomicFile write that journals to a temp path before renaming. If write() fails mid-rename due to ENOSPC or ENOMEM, the rename never happens, leaving the on-disk state at the previous version. The in-memory state, however, reflects the attacker's ComponentName.

// persistComponentName internals — simplified
void persistComponentName(Context ctx, ComponentName cn) throws IOException {
    AtomicFile file = getSettingsFile(ctx);           // /data/system/media_session_cb.xml
    FileOutputStream fos = file.startWrite();         // creates .xml.tmp
    XmlSerializer out = new FastXmlSerializer();
    out.startDocument(null, true);
    out.startTag(null, "component");
    out.attribute(null, "package", cn.getPackageName());  // attacker-controlled string
    out.attribute(null, "class",   cn.getClassName());    // attacker-controlled string
    out.endTag(null, "component");
    out.endDocument();
    // BUG TRIGGER: if disk is full, flush() throws IOException here
    fos.flush();                                      // may throw ENOSPC
    file.finishWrite(fos);                            // rename .tmp -> .xml (never reached)
}

Memory Layout

The desync is a logical state corruption rather than a heap overflow, but the object layout of MediaButtonReceiverHolder is relevant to understanding what the attacker controls post-desync.

MediaButtonReceiverHolder object layout (ART, 64-bit):
+0x00  klass*          mClass          // ART class pointer
+0x08  Object*         mPendingIntent  // original PendingIntent (attacker-supplied)
+0x10  ComponentName*  mComponentName  // DESYNC TARGET — points to attacker object post-bug
+0x18  int             mUserId         // user ID of the registering application
+0x20  String*         mFlatComponent  // string form of ComponentName for logging

ComponentName object layout:
+0x00  klass*          mClass
+0x08  String*         mPackage        // e.g., "com.attacker.evil"
+0x10  String*         mClass          // e.g., "com.attacker.evil.MaliciousReceiver"

SYSTEM STATE BEFORE EXPLOIT:
  disk:   /data/system/media_session_cb.xml -> { pkg: "com.legitimate.app", class: ".Receiver" }
  memory: mComponentName -> ComponentName{ "com.legitimate.app", ".Receiver" }
  [CONSISTENT]

SYSTEM STATE AFTER DESYNC (IOException swallowed):
  disk:   /data/system/media_session_cb.xml -> { pkg: "com.legitimate.app", class: ".Receiver" }
                                                 ^^ stale — write failed
  memory: mComponentName -> ComponentName{ "com.attacker.evil", ".MaliciousReceiver" }
                                           ^^ attacker-controlled — accepted by system_server
  [INCONSISTENT — attacker component dispatched on media button events]

Exploitation Mechanics

The attacker's goal is to get system_server to dispatch ACTION_MEDIA_BUTTON intents to an attacker-controlled component running as a privileged receiver. Because MediaSessionService dispatches to mComponentName directly — bypassing the normal package-visibility and permission checks that would apply to a third-party app registering a receiver — the attacker gains implicit broadcast delivery with system trust.

EXPLOIT CHAIN:
1. Install attacker APK with exported BroadcastReceiver (com.attacker.evil/.MaliciousReceiver)
   that handles ACTION_MEDIA_BUTTON.

2. Fill /data partition to near-capacity using any accessible storage API
   (e.g., allocate large files via Context.openFileOutput() in a loop until ~95% full).
   Target: leave < 4KB free to guarantee flush() throws ENOSPC on next XML write.

3. Call MediaSession.setMediaButtonReceiver(pi) where pi is a PendingIntent
   wrapping an explicit Intent targeting com.attacker.evil/.MaliciousReceiver.

4. system_server executes getComponentName():
   - Phase 1 resolves ComponentName{"com.attacker.evil", ".MaliciousReceiver"} ✓
   - Phase 2 calls persistComponentName() -> fos.flush() throws IOException (ENOSPC) ✓
   - IOException caught and swallowed ✓
   - Phase 3 sets mComponentName = attacker ComponentName ✓

5. Free allocated storage (delete large files). Disk pressure removed.

6. Trigger any media button event (headset insertion, Bluetooth AVRCP, volume key).
   MediaSessionService dispatches ACTION_MEDIA_BUTTON to mComponentName.

7. MaliciousReceiver receives broadcast in system_server's dispatch context,
   with flags granting it implicit-broadcast exemption and elevated trust level.

8. From within MaliciousReceiver: bind to system services, read protected content
   providers, or leverage the elevated dispatch context for further privilege chains
   (e.g., SIP/VOIP interception, call log access without READ_CALL_LOG).

PERSISTENCE NOTE: On reboot, system_server re-reads the XML journal.
Disk state still contains the legitimate component. The desync is lost.
Exploit window is bounded to the current boot session — but sufficient for
data exfiltration or secondary persistence implantation.

Patch Analysis

The fix enforces atomicity of the two-phase commit. If the persistence write fails, the in-memory state must not advance. Two mechanisms are applied:

// BEFORE (vulnerable) — MediaButtonReceiverHolder.java pre-December 2025 patch:
ComponentName getComponentName(Context ctx, PendingIntent pi, MediaSessionRecord rec) {
    ComponentName resolved = resolveFromPendingIntent(ctx, pi);

    try {
        persistComponentName(ctx, resolved);
    } catch (IOException e) {
        Slog.e(TAG, "Failed to persist component name", e);
        // falls through — mComponentName updated regardless
    }

    mComponentName = resolved;   // BUG: unconditional assignment
    return mComponentName;
}

// AFTER (patched) — December 2025 ASB:
ComponentName getComponentName(Context ctx, PendingIntent pi, MediaSessionRecord rec) {
    ComponentName resolved = resolveFromPendingIntent(ctx, pi);

    try {
        persistComponentName(ctx, resolved);
    } catch (IOException e) {
        Slog.e(TAG, "Failed to persist component name, retaining previous", e);
        // FIX: return existing cached value — do not update mComponentName
        return mComponentName;   // early return preserves consistency
    }

    // FIX: only reaches here on successful persist
    mComponentName = resolved;
    return mComponentName;
}

A secondary hardening change adds a pre-flight storage check before the AtomicFile write, rejecting the registration early if available space is below a threshold — preventing the resource exhaustion trigger class entirely:

// Secondary fix: pre-flight space check in persistComponentName()
void persistComponentName(Context ctx, ComponentName cn) throws IOException {
    // FIX: guard against near-full disk before attempting journal write
    StatFs stat = new StatFs(Environment.getDataDirectory().getPath());
    long available = stat.getAvailableBytes();
    if (available < MIN_PERSIST_THRESHOLD_BYTES) {  // MIN_PERSIST_THRESHOLD_BYTES = 8192
        throw new IOException("Insufficient space to persist component: " + available + "B");
    }

    AtomicFile file = getSettingsFile(ctx);
    FileOutputStream fos = file.startWrite();
    // ... remainder of write logic unchanged
}

Detection and Indicators

The exploit is largely silent, but leaves detectable traces:

Logcat signatures — the swallowed exception produces a log entry in pre-patch builds:

E MediaSessionService: Failed to persist component name
  java.io.IOException: No space left on device
    at libcore.io.Linux.write(Native Method)
    at libcore.io.BlockGuardOs.write(BlockGuardOs.java:348)
    at com.android.server.media.MediaButtonReceiverHolder.persistComponentName(:XX)
    at com.android.server.media.MediaButtonReceiverHolder.getComponentName(:XX)

Storage anomalies — rapidly filling and then freeing /data partition space in a short window is a behavioral indicator. Monitor df /data delta events with StorageManager callbacks.

MediaSession audit — compare in-memory component (dumpsys media_session) against the persisted XML at /data/system/media_session_cb.xml. Any mismatch on a running system indicates the desync has occurred.

## Detection check (adb shell, root required):
$ dumpsys media_session | grep -A2 "Media button receiver"
  Media button receiver: ComponentName{com.attacker.evil/.MaliciousReceiver}

$ cat /data/system/media_session_cb.xml
  

## MISMATCH DETECTED — desync active

Remediation

Apply the December 2025 Android Security Bulletin patch. The fix is in MediaButtonReceiverHolder.java and requires a system framework update — it cannot be mitigated at the application layer.

For devices that cannot be immediately patched:

  • Monitor /data partition utilization and alert on rapid fill/drain cycles originating from third-party UIDs.
  • Restrict setMediaButtonReceiver() calls via SELinux policy neverallow rules on high-security profiles where media session registration by untrusted apps is not required.
  • Enterprise MDM policies should audit installed applications for exported BroadcastReceiver components handling ACTION_MEDIA_BUTTON from packages not in an approved allowlist.

Patch identifier: Android December 2025 Security Bulletin — Framework severity HIGH. OEM partners received the patch under the standard 30-day pre-disclosure window. Pixel devices are addressed in the concurrent Pixel Update Bulletin.

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 →