Error handling on Android part 5: handling obfuscation and minification in Android crash reports

Jamie Lynch

Error handling on Android

Accent divider.

How many times have you been in the middle of using a new shiny app, only to have it crash on you?

Android app displaying a crash dialog

This is the fifth in a series of posts that will investigate how the exception handling mechanism works in Java and Android, and how crash reporting SDKs can capture diagnostic information, so that you're not flying blind in production.

Handling obfuscation and minification in Android crash reports

We previously learnt how to capture other sources of errors on Android, such as caught exceptions and ANRs. Our goal now is to handle obfuscated stacktraces from a minified Android app, by uploading ProGuard mapping files to an error reporting API.

Why bother obfuscating your app?

Obfuscating your app has several advantages. Your app will tend to be smaller, as obfuscated symbols require less space in Dex files than regular naming conventions for identifiers. Obfuscated code is also harder to understand, which is a benefit if you wish to make life harder for anyone attempting to reverse engineer your production APK.

An additional benefit is that obfuscation is usually coupled with a minification process that removes dead code, and optimises any remaining Java bytecode to make your app faster. If you're interested in learning more about optimisations made by Android, Jake Wharton has written a very comprehensive series of blog posts which cover optimisations performed by D8 and R8.

Android uses R8 as the default obfuscation tool and code shrinker. Without a bit of extra work, this can cause some serious problems when it comes to debugging stacktraces. This is what a stacktrace may look like before obfuscation:

java.lang.RuntimeException: Whoops!
   at com.foo.mylib.RequestInterceptor.interceptRequest(RequestInterceptor.kt:5)
   at com.foo.mylib.HttpClient.doGet(HttpClient.kt:9)
   at com.foo.mylib.HttpClient.makeRequest(HttpClient.kt:5)
   at com.foo.mylib.DownloadManager.downloadFile(DownloadManager.kt:5)

And this is what it will look like after obfuscation:

java.lang.RuntimeException: Whoops!
   at com.a.a.c.a(Unknown Source:5)
   at com.a.a.b.b(Unknown Source:9)
   at com.a.a.b.a(Unknown Source:5)
   at com.a.a.a.a(Unknown Source:5)

I know which one I'd prefer to debug!

How to deobfuscate stacktraces using mapping files

Fortunately, it's possible to reverse the obfuscation process if we retain the mapping file. This contains a map where each obfuscated symbol is the key, and the value for each entry is the original symbol information. In a production app, this will contain thousands of entries, but a simplified mapping file may look something like the following:

com.foo.mylib.DownloadManager -> com.a.a.a:
   5:6:void downloadFile(java.lang.String) -> a
   3:3:void <init>() -> <init>
com.foo.mylib.HttpClient -> com.a.a.b:
   5:6:void makeRequest(java.lang.String) -> a
   9:10:void doGet(java.lang.String) -> b
   3:3:void <init>() -> <init>
com.foo.mylib.RequestInterceptor -> com.a.a.c:
   5:5:void interceptRequest(java.lang.String) -> a
   3:3:void <init>() -> <init>

Looking back to the first line of our obfuscated stacktrace, we can obviously see that #com.a.a.c corresponds to #com.foo.mylib.RequestInterceptor. We can then look up line number and method information in a similar way, and deobfuscate the first frame:

java.lang.RuntimeException: Whoops!
   at com.foo.mylib.RequestInterceptor.interceptRequest(RequestInterceptor:5)
   at com.a.a.b.b(Unknown Source:9)
   at com.a.a.b.a(Unknown Source:5)
   at com.a.a.a.a(Unknown Source:5)

It's then simply a case of deobfuscating the rest of the frames to gain the original stacktrace:

java.lang.RuntimeException: Whoops!
   at com.foo.mylib.RequestInterceptor.interceptRequest(RequestInterceptor:5)
   at com.foo.mylib.HttpClient.doGet(HttpClient:9)
   at com.foo.mylib.HttpClient.makeRequest(HttpClient:5)
   at com.foo.mylib.DownloadManager.downloadFile(DownloadManager:5)

Why does every crash reporting service have a gradle plugin?

Decoding each stacktrace manually is a bit painful, particularly if you receive several thousands of them each day. It also means you need to retain correct mapping file for every single build of your APK.

Bugsnag deobfuscates Android errors by automatically uploading any mapping files and using the information to deobfuscate any errors sent to our error reporting API. This is achieved using a gradle plugin which hooks into the #assemble gradle task, and uploads the mapping file whenever the app is built, whether it be locally or on CI.

Now that we've answered the eternal question of why every crash reporting service uses a gradle plugin, it's time to think about adding some useful metadata to our reports. Read on in the next post.

Would you like to know more?

Hopefully this has helped you learn a bit more about Error Handling on Android. If you have any questions or feedback, please feel free to get in touch.

———

Try Bugsnag's Android crash reporting.

Share