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.
A newly discovered flaw in Android's media button handling system could let someone with access to your phone gain full control over it without your knowledge or permission.
Here's what's happening: Your phone has a system component that manages media controls—the buttons that pause music or skip songs. A bug in this component means it can be tricked into consuming massive amounts of system resources, kind of like asking someone to organize the same filing cabinet over and over until they're too exhausted to do their actual job. When this happens, the phone's security system gets out of sync with what's actually running.
An attacker with local access—say, they've borrowed your unlocked phone for a moment—could exploit this desynchronization to sneak in unauthorized permissions or privileges. Think of it like a security guard getting so confused by repeated false alarms that they stop checking ID properly.
The vulnerability affects multiple platforms running affected versions of Android. It doesn't require the attacker to trick you into downloading something malicious or clicking a suspicious link. They just need brief physical access to your device.
Most at risk are people who share devices with others, use public charging stations, or work in environments where phones are sometimes left unattended.
What you should do: First, keep your Android device updated—manufacturers are releasing patches to fix this. Second, don't leave your phone unlocked around people you don't completely trust, and disable media control shortcuts if you're in high-risk situations. Third, consider enabling additional security features like fingerprint or face unlock that require authentication even if someone briefly accesses your phone. While no one is actively exploiting this yet, these practices protect you against this and similar threats.
Want the full technical analysis? Click "Technical" above.
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:
Resolve a ComponentName from the caller's PendingIntent via PackageManager
Write the resolved name to persistent storage (XML settings file)
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.
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.
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.