Digging into the Android SystemUI Crash from a JPEG

By Sophia d'Antoine, Peter Wyatt (PDF Association), Ryan Speers | July 29, 2020

In late May 2020, we were asked to help triage the root cause of a bug where an image, when parsed by Android SystemUI, caused the Android process to crash. This could cause a boot loop if, for example, the image was set as the phone’s background. We quickly identified the root cause which we found interesting from an ecosystem perspective.

This blog shares parts of our analysis, and covers our trace of the relevant code path and diagnosis of the root cause. We describe how the fixes work, and then dive into why this bug was only seen relatively recently. Finally, we break down the file’s JPEG and ICC structures, and what impacts these may have on the parsers.

Background

After the image was shared on Twitter by IceUniverse, various posts including on xdadevelopers, SKIA bug tracker, and Hacker News highlighted this, as well as popular press reporting.

The initial report was that if the image shown below was used as an Android desktop background, it causes a crash on Samsung and Pixel 3 XL devices using Android 10.

Unsafe JPEG with ICC Issue

Other people have written brief reports (such as the one here) that correctly identify the issue, but we wanted to provide a more detailed writeup which could be consumed by an audience of file-format-creators, not only Java developers or vulnerability researchers.

Digging In

Given this bug created a crash, we had a good place to start. The crash log, such as the one someone shared here, looks something like this:

AndroidRuntime: FATAL EXCEPTION: AsyncTask #2
AndroidRuntime: Process: com.android.systemui, PID: 4968
AndroidRuntime: java.lang.RuntimeException: An error occurred while executing doInBackground()
AndroidRuntime:     at android.os.AsyncTask$4.done(AsyncTask.java:399)
...
AndroidRuntime: Caused by: java.lang.ArrayIndexOutOfBoundsException: length=256; index=256
AndroidRuntime:     at com.android.systemui.glwallpaper.ImageProcessHelper$Threshold.getHistogram(ImageProcessHelper.java:139)
AndroidRuntime:     at com.android.systemui.glwallpaper.ImageProcessHelper$Threshold.compute(ImageProcessHelper.java:105)
AndroidRuntime:     at com.android.systemui.glwallpaper.ImageProcessHelper$ThresholdComputeTask.doInBackground(ImageProcessHelper.java:89)
AndroidRuntime:     at com.android.systemui.glwallpaper.ImageProcessHelper$ThresholdComputeTask.doInBackground(ImageProcessHelper.java:77)
AndroidRuntime:     at android.os.AsyncTask$3.call(AsyncTask.java:378)
AndroidRuntime:     at java.util.concurrent.FutureTask.run(FutureTask.java:266)
AndroidRuntime:     ... 4 more

Starting from this we initially computed the binary differences of the getHistogram functions taken for vulnerable devices and non-vulnerable devices, which revealed some changes we’ll get to in a moment.

We also found that the function getBitmapAsUser is called when starting the wallpaper service. This pulls the saved (crashing) wallpaper image from data/system/users/{userid}/wallpaper and redraws it, causing the phone to crash again. Since this occurs on phone boot up, the crash and re-boot cycle occurs.1 In this function, we see that there is a new parameter for the function peekWallpaperBitmap on affected devices. The different calls are shown below.

public Bitmap getBitmapAsUser(int userId, boolean hardware) {
  return WallpaperManager.sGlobals.peekWallpaperBitmap(this.mContext, true, 1, userId, hardware);
}

Code on Unaffected Devices

public Bitmap getBitmapAsUser(int userId, boolean hardware) {
  return WallpaperManager.sGlobals.peekWallpaperBitmap(this.mContext, true, 1, userId, hardware, this.getcolormanagementproxy());
}

Code on Affected Devices

As part of the wallpaper being drawn, we found that the function doColorManagement of the ColorManagementProxy is called. This function checks if the color space of the image is supported by the device and the display. If it isn’t supported, the code will replace the image’s color space with SRGB. In this image, on devices where luma2 isn’t supported, such as the Pixel 4 XL, no crash will occur as the color space is changed to SRGB.

On some vulnerable devices, the display support is not verified. This means that an image with a Color Space which isn’t supported may cause a crash on that device if that image is set as the wallpaper. The color space of the image in question appears to be RGB.

The core bug is a simple rounding error either in the color space conversion or in the luma conversion done for the image’s histogram.

The relevant code segment is:

private Bitmap toGrayscale(Bitmap bitmap) {
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    Bitmap grayscale = Bitmap.createBitmap(width, height, bitmap.getConfig());
    Canvas canvas = new Canvas(grayscale);
    ColorMatrix cm = new ColorMatrix(LUMINOSITY_MATRIX);                        // <-- LUMINOSITY_MATRIX constant used "internally"
    Paint paint = new Paint();
    paint.setColorFilter(new ColorMatrixColorFilter(cm));
    canvas.drawBitmap(bitmap, new Matrix(), paint);
    return grayscale;
}

private int[] getHistogram(Bitmap grayscale) {
    int width = grayscale.getWidth();
    int height = grayscale.getHeight();
    // ...
    int[] histogram = new int[256];                                             // <-- limited array size
    for (int row = 0; row < height; row++) {
        for (int col = 0; col < width; col++) {
            int pixel = grayscale.getPixel(col, row);                           // <-- internal matrix luminosity calculation
            int y = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel);  // <-- y can get too large
            histogram[y]++;
        }
    }
    return histogram;
}

The function toGrayscale is called before getHistogram and creates an empty canvas for the bitmap to get drawn on by getHistogram which creates a histogram from the grayscale pixels.

The histogram array has a max index of 255. However, in the vulnerable case, rounding up of the calculated values would end up with a value greater than 255s. This is because there is no consideration for float-to-int rounding behavior or bounds-checking. This issue directly causes a Java exception that is not caught, causing the critical SystemUI component to crash. In turn the system watchdog reboots the device.

Notably, the crash is not (at least directly) related to processing malicious data from an ICC profile, as seems to be alluded to in some posts. Instead, the root cause is the rounding performed in the luminosity histogram calculation which uses a fixed-value ITU BT.709 matrix3 that is hardcoded in Java.

Fixes

A number of fixes have since been released (here, here) that are instructive to review, and confirm the issue indeed was a rounding error, as described in this commit (emphasis added):

SystemUI: Manually calculate the greyscale values for histogram

The Bug:

  • Using the provided greyscale image gives us a sum of r, g and b of 255

  • This is due to the fact, that the internal matrix calculation always rounds up, resulting in maximum values of

    r: 255 * 0.2126 = 54.213 => 55

    g: 255 * 0.7152 = 182.376 => 183

    b: 255 * 0.0722 = 18.411 => 19 => r + b + g = **257**

  • This has been observed with a picture where the original RGB value is (255, 255, 243) resulting in 55 + 183 + 18 = 256, therefore crashing SystemUI due to an invalid array access …

The fix of manually calculating the pixel values, and rounding (down) as each pixel is converted, is an appropriate patch. For example:

int red = Math.round(Color.red(pixel) * LUMINOSITY_MATRIX[0]);
...

Why now?

When you review the Java root cause above, you should immediately wonder why it was not triggered by any white pixel (R=255, G=255, B-=255) a very long time ago! The Java code works on a Bitmap. This is the in-memory uncompressed equivalent of the compressed JPEG wallpaper file that has been color-converted by code elsewhere, prior to the faulty Java luminosity histogram calculation.

As noted in the online analysis by the initial researchers (see above), Android only relatively recently added ICC support to support devices with advanced display capabilities and thus only limited vendors have adopted this code. One hypothesis that is yet to be fully proven is that the ICC colorimetric color-processing code path is distinct from the non-ICC / non-colorimetric (or at least approximated color) code path such that the resultant Bitmap pixel values (in what Java sees as Bitmaps) are different. This may be because of rounding errors or because this specific ICC contains curve data which triggers unusual conditions or processing.

Sidenote on Rounding

As Marti Maria (LittleCMS) notes in his ICCDevCon’08: “ICC profile internal mechanics” presentation:

In general, Rounding is the process of reducing the number of significant digits in a number. The result of rounding is a “shorter” number having fewer non-zero digits yet similar in magnitude. The result is less precise but easier to use. ICC profiles do have a severe dependency on rounding. Why? Because ICC profiles are computer artifacts: binary digital files and as such are subjected to quantization. Quantization means: we have a fixed amount of bits, so round-off errors happen.

There are many different rounding algorithms to choose from to manage issues such as accumulated bias and rounding of negative values – yet we find that many programmers may not know the specifics of their preferred programming language! Choices include Round-Towards-Nearest, Round-Half-Up (Arithmetic Rounding), Round-Half-Up-Asymmetric, Round-Half-Down and Round-Half-Even (“Bankers Rounding”).

Marti Maria goes on:

For example, the round method of the Java Math Library provides an asymmetric implementation of the round-half-up algorithm, while the round function in MATLAB® provides a symmetric implementation. Just for giggles and grins, the round function in Visual Basic for Applications 6.0 actually implements the Round-Half-Even algorithm.

This ICC presentation seems especially relevant in light of this specific Java coding error.

Is the file compliant?

TL;DR: The JPEG is compliant but the ICC profile is not compliant. However, it is unclear if the lack of ICC profile compliance causes the crash, or if it is parsed acceptably and the color data hits the rounding condition described above separate from the malformations in the profile discussed below.

First, we look to see if the JPEG file is compliant - and it is. Checking the file with exiftool, JPEG Snoop, or other tools seems to indicate everything is set in compliance with the standard. For example, we show the exiftool output below:

$ exiftool -H 1.jpg
     - ExifTool Version Number         : 10.80
...
     - MIME Type                       : image/jpeg
0x0000 JFIF Version                    : 1.01
0x0002 Resolution Unit                 : None
0x0003 X Resolution                    : 1
0x0005 Y Resolution                    : 1
0x0004 Profile CMM Type                :
0x0008 Profile Version                 : 2.1.0
0x000c Profile Class                   : Display Device Profile
0x0010 Color Space Data                : RGB
0x0014 Profile Connection Space        : XYZ
...
0x0044 Connection Space Illuminant     : 0.9642 1 0.82491
0x0050 Profile Creator                 :
0x0054 Profile ID                      : 0
     - Profile Description             : Google/Skia/E3CADAB7BD3DE5E3436874D2A9DEE126
     - Red Matrix Column               : 0.79767 0.28804 0
     - Green Matrix Column             : 0.13519 0.71188 0
     - Blue Matrix Column              : 0.03134 9e-05 0.82491
     - Red Tone Reproduction Curve     : (Binary data 40 bytes, use -b option to extract)
     - Green Tone Reproduction Curve   : (Binary data 40 bytes, use -b option to extract)
     - Blue Tone Reproduction Curve    : (Binary data 40 bytes, use -b option to extract)
     - Media White Point               : 0.9642 1 0.82491
     - Profile Copyright               : Google Inc. 2016
...
     - Image Size                      : 1440x2560
     - Megapixels                      : 3.7

Next, we ask ourselves – based on the fact that the code path that had the issue is likely related to the introduction of ICC handling – is the ICC profile embedded in the JPEG compliant with the specification?

This profile is, according to the ICC description and copyright tags, created by the Google SKIA library (Google/SKIA/E3CADAB7BD3DE5E3436874D2A9DEE126). This ICC profile can be checked for conformance against the ICC specifications by various tools. The Argyll CMS iccdump utility can be used to dump the embedded ICC directly from the JPEG using the -s option. The LittleCMS utility jpgicc can also be used to extract the ICC profile as a standalone file. The ICC RefIccMax reference implementation has the most comprehensive iccDumpProfile when used with the -v (validate) option:

$ iccDumpProfile -v skia.icc
Profile:            'skia.icc'
Profile ID:         Profile ID not calculated.
Size:               536(0x218) bytes

Header
------
Attributes:         Reflective | Glossy
Cmm:                Unknown
Creation Date:      0/0/0  00:00:00
Creator:            NULL
...
Version:            2.10
...
          mediaWhitePointTag  'wtpt'       456        20
                copyrightTag  'cprt'       476        60

Validation Report
-----------------
Profile violates ICC specification

Warning! -  - 0: Invalid year!
Warning! -  - 0: Invalid month!
Warning! -  - 0: Invalid day!
NonCompliant! - profileDescriptionTagmultiLocalizedUnicodeType: Invalid tag type (Might be critical!).
NonCompliant! - copyrightTagmultiLocalizedUnicodeType: Invalid tag type (Might be critical!).

So, it is complaining about a few things that are just metadata and should not matter: (1) date is null but ICC requires a date, and (2) the profile says it is version 2.1 but uses Unicode description and copyright tags that were not in the specification until version 4.

We also note that this SKIA ICC uses parametricCurveType as the data type for the redTRCTag, greenTRCTag and blueTRCTag tags (TRCs are Tone Reproduction Curves). Importantly, the parametric curve data type was also not defined until version 4 even though RefIccMax does not report it. For version 2.1 ICC profiles, the correct data type is “curveType” which is also slightly smaller in size than the more powerful v4 equivalent.

While the incorrect ICC version could influence the processing pipeline, it seems likely that an ICC parser which supports version 4 profiles may just ignore the version in the header and process the data provided (e.g., treat parametricCurveType as such). This type of permissive parsing behavior is what we believe to be happening based on some limited experiments on a number of open-source ICC libraries. We note this comes with its own risks, even if they do not manifest in a crash on this specific profile.

Conclusion

We hope that this has given you a brief overview of this bug from a different perspective than other write-ups. You may also be interested in our work on other file formats and automated tooling to help identify issues and analyze differences in libraries.

River Loop Security works heavily in applying vulnerability analysis skills and techniques to solve real-world problems in cybersecurity, from research projects such as DARPA’s SafeDocs program to commercial penetration testing engagements where we audit source code or binaries to find vulnerabilities.

We encourage you to contact us if you have any questions or comments based on this post or have questions about applying advanced binary and file format analysis to solve your cybersecurity challenges.



This blog grew out of a collaboration that started with Peter Wyatt (Principal Scientist for the PDF Association) who we have worked with as part of Dr. Sergey Bratus’ DARPA Safe Documents (SafeDocs) program asked for help triaging the root cause of a bug that had surfaced on the internet. River Loop thanks Peter for his contributions to parts of this blog post.

Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the Defense Advanced Research Projects Agency (DARPA).




  1. To bypass this, the phone can be started in safe mode, setting the wallpaper image to something else from this mode, on other devices where this isn’t possible, the phone must be factory reset from the bootloader. [return]
  2. See Luma on Wikipedia for background. [return]
  3. See relative luminance on Wikipedia for background on this matrix. [return]