How to render garbage on your iPhone

…and where did the slug go?

[Part of a series of stories on GPU shader compiler bugs.]

[Previous stop: AMD]

We have a surprise stop on our tour: Apple.*

I’ll go through two Apple PowerVR driver issues — one that causes garbage to be rendered, and one that makes a slug disappear. We have reported these issues to Apple. In a parallel story, I give an overview of the various metamorphic transformations that GLFuzz employs.

Rendering garbage

This WebGL fragment shader, from GLSLsandbox, should lead to this pretty image being rendered:

Image rendered on an iPhone via WebGL in Safari, with a PowerVR GPU

Try it for yourself — see whether your browser supports WebGL 1 by going here. If it does then you should be able to see the shader being rendered by visiting this page [disclaimer: this is a regular WebGL page, so should be safe to visit, but you do so at your own risk!]:

GLFuzz creates variant shaders that take an extra runtime parameter, injectionSwitch, which is set to (0.0, 1.0) at runtime. As discussed in some detail last post, this means that a conditional statement:

if(injectionSwitch.x > injectionSwitch.y) {
// Arbitrary syntactically valid code

should have no effect on what a fragment shader causes to be rendered — the condition will be false at runtime.

Using GLFuzz, we found that a simple injection causes the above shader to render garbage on an iPhone SE in Safari, with WebGL.

This is the variant shader, and the difference is really simple — GLFuzz injects two instances of the following code snippet:

if(injectionSwitch.x > injectionSwitch.y) {

Obviously this should have no impact on what the shader renders, but we find that garbage is rendered. Here is a still of what we see:

The rendered image should be the same as the blue image above, but instead we find that garbage is rendered

And here is a video Paul took of the problem manifesting on his iPhone:

Rendering garbage on Paul’s iPhone

This kind of issue is potentially security critical: information from, say, the notification center can leak into the garbage. The garbage could then be captured by the web page via JavaScript and sent to a server. Theoretically, it might then be possible to reassemble, from the garbage, the original information that leaked in.

However, in this case we believe the bug carries very low risk as the garbage is indecipherable. So if you are curious, you can try this out on your iPhone via this WebGL page [usual disclaimer that you visit at your own risk]:

You should see the same nice image associated with the original shader, but perhaps (if you’ve got an iPhone with the same drivers as us) you’ll see garbage too. Let us know!

This example is a bit similar to the AMD wrong image bugs we reported last post: the bug is induced by injecting an unreachable jump statement — two returns in this case, and a combination of return and discard statements in the case of the AMD bugs. This fits with the hypothesis of Vu Le et al. in their paper on EMI testing, that complicating the control-flow graph of a program can be effective in revealing edge-case compiler bugs.

If we remove either injection, or simplify them in any way, the problem disappears.

Killing slugs

Our next example is neat because it showcases three GLFuzz transformations simultaneously — a good buildup to the overview of the transformations that we’ll give in the next section.

This shader, again from GLSLsandbox, renders what we think looks like a slug:

Slug or worm?

OK, so the source code of the shader talks about “worm_color”, but we reckon it’s a slug (as well as reckoning that colour is the correct spelling of colour! :-)).

But, worm or slug, GLFuzz sets about killing the poor creature as follows.

First, GLFuzz takes this piece of code:

if(j == 0) {
return vec4(0.8, 0.5, 0.5, 1.0);

and replaces it with:

for(int c = 0; c < 1; c++) {
if(j == 0) {
return vec4(0.8, 0.5, 0.5, 1.0);

Evidently this should have no impact, right? The “c” loop does just one iteration so, functionally, it may as well not be there.

Next, GLFuzz takes this return statement:

return vec4(0.8, 0.7, 0.4, 1.0);

and multiplies the first component of the vector by 1.0:

return vec4(0.8 * injectionSwitch.y, 0.7, 0.4, 1.0);

Now, injectionSwitch.y may not look like 1.0, but at runtime this is the value it will hold.

Finally, right at the start of main, GLFuzz injects an empty conditional with a false guard:

if(injectionSwitch.x > injectionSwitch.y) { }

The guard happens to be false (at runtime) due to the way in which we initialise injectionSwitch, but that shouldn’t even matter here since the body of the conditional is empty.

Here is the variant shader with all three transformations applied. The result is that the poor slug gets killed:

Where did the slug go?!

Again, you can try this out for yourself:

If you’ve got an iPhone then it would be interesting to know whether you can reproduce the issue. If you can reproduce the issue on another device, that would be even more interesting!

Undoing any one of these transformations causes the problem to go away.

Learning about the GLFuzz transformations

If you’d like to know a bit more about how these transformations work in general, then please read the first in a parallel series of posts on how GLFuzz works.

Next stop: ARM

* As discussed in my first post, the plan had been to alphabetically tour the six main GPU designers, testing the shader compiler components of their drivers. As part of this, we were keen to test drivers for Imagination TechnologiesPowerVR GPU series, which are incorporated in most iPhones.

We found and reported some intriguing issues. In the process, we learned that Apple themselves actually write the PowerVR drivers for iPhone, so really it’s Apple’s, not Imagination’s, shader compilers we are testing.

Coincidentally, Apple follow AMD alphabetically, so we’ll do the Apple stop now, and postpone ARM until the next stop. Hopefully we’ll cover Imagination later if we can get our hands on some devices that use their drivers.