MediaSessionService: any app can name a class and have system_server construct it
Android ID 492815504 · Reported to the Android & Google Devices VRP, 15 March 2026
1. Summary
MediaSessionService exposes two Binder methods that take a class name as a string and
instantiate it inside system_server:
void setCustomMediaKeyDispatcher(String name);
void setCustomMediaSessionPolicyProvider(String name);
Neither performs a permission check. Both reach
Class.forName(name).getDeclaredConstructor(Context.class).newInstance(mContext) — running an
attacker-chosen constructor at UID 1000, with the real system Context.
Three details turn that from a bad idea into a usable primitive:
- The type check happens after construction. The cast to
MediaKeyDispatcheris applied to the result ofnewInstance(), so a constructor's side effects have already completed by the time the cast fails. ClassCastExceptionis not caught locally. The catch block lists five reflection exceptions and not that one, so the failed cast escapes as aRuntimeException— which Binder catches during transact handling.system_servertherefore survives each call.- Surviving makes it repeatable. The caller gets a failed transact and can immediately call again, as many times as it likes.
So the attacker does not need a class that is a MediaKeyDispatcher. They need a class whose
constructor does something useful, and the cast failing afterwards costs them nothing.
On the tested device, 35 of 35 candidate gadget classes constructed successfully.
2. Affected surface
| Property | Detail |
|---|---|
| Service | com.android.server.media.MediaSessionService$SessionManagerImpl |
| AIDL | frameworks/base/media/java/android/media/session/ISessionManager.aidl (lines 77–78) |
| Implementation | MediaSessionService.java — entry points at 2434 / 2439, reflection at 1205 / 1225 |
| Attacker position | Any installed app |
| Permissions required | None |
| User interaction | Install and run |
| Tested on | Redmi Note 13 Pro (garnet_in), Android 15, AQ3A.240912.001, HyperOS OS2.0.207.0.VNRINXM |
3. Root cause
3.1 No authorisation on either entry point
@Override
public void setCustomMediaKeyDispatcher(String name) {
instantiateCustomDispatcher(name);
}
@Override
public void setCustomMediaSessionPolicyProvider(String name) {
instantiateCustomProvider(name);
}
The service is not careless elsewhere. addOnMediaKeyEventDispatchedListener requires
MEDIA_CONTENT_CONTROL; setOnVolumeKeyLongPressListener and registerRemoteSessionCallback
are both permission-gated. These two methods are the exception.
3.2 The cast is after the constructor, and the wrong exception is caught
private void instantiateCustomDispatcher(String componentName) {
synchronized (mLock) {
mCustomMediaKeyDispatcher = null;
try {
if (componentName != null && !TextUtils.isEmpty(componentName)) {
Class customDispatcherClass = Class.forName(componentName);
Constructor constructor =
customDispatcherClass.getDeclaredConstructor(Context.class);
mCustomMediaKeyDispatcher =
(MediaKeyDispatcher) constructor.newInstance(mContext);
}
} catch (ClassNotFoundException | InstantiationException | InvocationTargetException
| IllegalAccessException | NoSuchMethodException e) {
mCustomMediaKeyDispatcher = null;
Log.w(TAG, "Encountered problem while using reflection", e);
}
}
}
Read the order of operations. newInstance(mContext) runs the constructor; only then is
the result cast. Whatever that constructor did — spawned threads, published a Binder service,
wrote a file, registered a receiver — has already happened.
ClassCastException is absent from the catch list. It propagates out as a RuntimeException,
Binder catches it at the transact boundary, and the process lives. The failed cast is not a
defence; it is the cleanup step that happens too late.
MediaKeyDispatcher is applied to what newInstance() returned — so by the time it fails, the attacker’s constructor has already run inside system_server. And because ClassCastException is not in the local catch list, the failure escapes to Binder rather than to the service: the process survives, and the call can be repeated without limit.3.3 What does not work, and why that matters
instantiateCustomDispatcher() does not call Binder.clearCallingIdentity() before
newInstance(). The calling identity is still the attacking app, which splits the gadget
space in a way worth being precise about:
- Reliable: side effects local to
system_server— thread creation, in-process listener registration, file I/O, Binder service publication through the system-service path. - Unreliable: constructors that immediately make permission-checked or multi-user Binder calls, because those checks still see the caller's identity.
This is why the finding rests on confirmed gadgets rather than on the existence of (Context)
constructors in general.
4. Confirmed device impact
4.1 system_server crash, and a boot loop
Gadget: InputMethodManagerService$Lifecycle — each construction spawns two service
threads, and the attack is repeatable.
At roughly 500 calls, system_server hit thread and stack exhaustion:
03-10 20:59:07.779 F libc : shadow stack read-write mprotect(...) failed: Out of memory
03-10 20:59:07.781 F libc : Fatal signal 6 (SIGABRT) ... pid 16260 (system_server)
03-10 20:59:20.283 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS
03-10 20:59:20.283 E AndroidRuntime: java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed
The PID change is the proof the process restarted: 16260 → 26025.
With a BOOT_COMPLETED / LOCKED_BOOT_COMPLETED receiver restarting the flood, a one-shot
crash becomes a boot loop until the app is removed or the device is booted into Safe Mode.
4.2 Replacing a core Binder service
Gadget: ResourcesManagerService, whose constructor publishes a Binder service before
onStart() ever runs:
public ResourcesManagerService(@NonNull Context context) {
super(context);
publishBinderService(Context.RESOURCES_SERVICE, mService);
}
publishBinderService(...) delegates to ServiceManager.addService(...). Constructing this
class through the reflection path therefore registers a phantom resources service,
displacing the real one:
SUCCESS — ResourcesManagerService phantom registered
Real 'resources' service is permanently broken until reboot.
service check resources still reports the service present; service call resources 1 fails
with an NPE inside the phantom. The service is not missing — it is wrong, which is harder to
notice.
4.3 Writes, deletes and injected receivers
| Gadget | Constructor side effect |
|---|---|
MediaQualityService |
creates a SQLite DB and shared prefs under /data/system/media_quality/ |
BitmapOffloadService |
deletes the contents of /data/system/offloaded-bitmaps/ |
AutofillManagerService |
receiver registration and listener setup inside system_server |
ProfcollectForwardingService |
registers receivers for several system intents |
SupervisionService |
user-lifecycle and role listener setup |
UsbService |
policy-state receiver registration |
StorageStatsService |
installer and listener wiring |
A six-phase run on the device:
Phase 1/6: SERVICE_HIJACK — ResourcesManagerService SUCCESS
Phase 2/6: FILE_WRITE — MediaQualityService SUCCESS
Phase 3/6: RECEIVER_INJECT — AutofillManagerService + Profcollect SUCCESS
Phase 4/6: STATE_MODIFY — SupervisionService + UsbService SUCCESS
Phase 5/6: FILE_DELETE — BitmapOffloadService SUCCESS
Phase 6/6: SYSTEM_CRASH — IMMS$Lifecycle, 500 calls SUCCESS
And a census of candidate gadgets: 35 / 35 constructed successfully, 0 blocked.
5. Why the census is the point
The individual gadgets are interchangeable. What matters is that the set is open.
Any class already on the system_server classpath with a (Context) constructor is a
candidate, and every future one becomes a candidate the moment it is added — without anybody
touching MediaSessionService. A gadget fixed in isolation removes one entry from a list
nobody maintains.
That is why this is one vulnerability with many impacts rather than a bundle of separate bugs: the defect is the unauthenticated construction primitive, not any particular class it reaches.
6. What is not being claimed
No arbitrary code execution. Follow-up work looking for a credible ACE path did not find one, and the report says so rather than leaving the implication hanging.
The accurate framing is a local elevation-of-privilege primitive plus a persistent
denial-of-service: unauthenticated execution of privileged constructor side effects inside
system_server, with confirmed integrity impact (service replacement, system-directory writes
and deletes) and confirmed availability impact (crash, and a boot loop with the persistence
variant).
Classification: CWE-862 missing authorization, CWE-470 externally-controlled class selection, CWE-400 uncontrolled resource consumption.
7. Suggested fix
The submitted report included a patch. It gates both entry points, allowing root, system and shell for platform integration and testing, and requiring a signature-level permission of anyone else:
private void enforceCustomMediaComponentPermission(String apiName, String className) {
final int callingUid = Binder.getCallingUid();
// Root/system/shell are allowed for platform integration and testing.
if (callingUid == Process.ROOT_UID
|| callingUid == Process.SYSTEM_UID
|| callingUid == Process.SHELL_UID) {
return;
}
// Signature-level gate for external callers.
mContext.enforceCallingPermission(Manifest.permission.MEDIA_CONTENT_CONTROL, apiName);
}
Two changes are worth making alongside it, because each independently breaks the primitive:
- Check the type before running the constructor.
Class.isAssignableFrom(...)on the loaded class, beforenewInstance(), means a non-conforming class never constructs. - Catch
ClassCastExceptionlocally. It does not stop the side effects, but it stops the failure escaping to Binder in a shape that leaves the caller free to retry.
8. Disclosure timeline
| Date | Event |
|---|---|
| 2026-03-15 | Reported with proof of concept, evidence logs and a proposed AOSP patch |
| 2026-03-15 | Triaged |
| 2026-07-17 | Assessed against an existing internal investigation; Google confirms the canonical issue was closed without a patch, and that there is no Security Bulletin to wait for |
| 2026-07-17 | Google invites a draft of this writeup for technical-accuracy review ahead of publication |
| 2026-09-12 | Draft shared for that review |
| 2026-09-12 | Google reply: reviewed, no objections to publishing |
Disclosure status. Published with Google's agreement, after the technical-accuracy review they offered.
Thank you for sharing the draft of your write-up. We have reviewed it and we have no objections to you publishing it. Since the canonical issue was closed as "Won't Fix", there is no security patch that we need to wait for.
Reported by Ayush Kumar to the Android & Google Devices Vulnerability Reward Program. Published under coordinated disclosure.