Inverting Your Assumptions: A Guide to JIT Comparisons
April 12, 2018 | Jasiel SpelmanSimilar to many others that have spent an unhealthy amount of their life staring at a computer screen, I have back issues. Having an office setup with proper ergonomics is an obvious first step towards avoiding back pain, but I've also found that getting exercise can help quite a bit. Rock climbing is one of the things I like to do that helps my back, but on days where I don't even have the opportunity to go to an indoor rock gym, I like to use the gravity inversion bar I have at home.
What does this have to do with anything interesting, let alone anything involving security? Between doing upside-down crunches, I'll sometimes spend some time just catching up on various feeds on my phone. This past January, after I'd already found some WebKit JIT bugs and gotten a decent understanding of one of the engines, I came across a link to a Stack Overflow post about a ridiculous interview question:
“Is it ever possible that (a== 1 && a ==2 && a==3) could evaluate to true in JavaScript?”
For many languages, this would obviously be no. And of course, logically the answer is no. But we're dealing with JavaScript here, and there are two types of comparison operators at our disposal. The first one referenced in the question is the loose comparison operator, represented by two successive equals signs (==
). The other type is the strict comparison operator, which is represented by three successive equals signs (===
). The main difference is that if the types of both values are different, the loose comparison operator will perform type coercion to see if they can become the same type before being compared, while the strict comparison operator will return that the two values are different.
As a simple example, let's compare the Integer 1 with the true
constant as well as a String containing the number 1:
Both return true despite not being equal at all from a visual inspection. Let's compare that against what happens if we use strict equality comparison:
Back to the post that spurred all of this, a curious thing happens when you compare an Object to something that isn't an Object:
We can execute JavaScript during the loose comparison against an Object!
Now that we've covered enough of the JavaScript language to understand one of its odd quirks, let's dive in to see how this might get handled by one of the sublight* engines within WebKit.
*The fastest engine within JavaScriptCore is called the Faster Than Light (FTL) engine and, as such, I refer to the rest of the engines as the sublight engines. I haven't seen others do this, so apologies if people think you're crazy as you talk about the engines this way.
One of the morbidly beautiful things about JavaScript is that it can give rise to unsafe patterns from otherwise perfectly cromulent C++ and similarly, one of the morbidly beautiful things about JIT is that it can give rise to unsafe patterns from otherwise perfectly safe JavaScript. The Data Flow Graph (DFG) JIT engine applies the same type of compiler optimizations that you'd see in any other compiler. However, it also needs to make sure that it is safe to apply certain optimizations after a given operation has been executed. For example, it is unsafe to remove bounds checks around an operation that could change the size of the underlying buffer. One of the ways this is performed within DFG is within a file called DFGAbstractInterpreterInlines.h
, which contains a method named executeEffects
responsible for changing program state based on the operation and arguments. The way to state that an operation is potentially dangerous to prevent later optimizations is to call a function called clobberWorld
which, among other things, will break all assumptions about the types of all Arrays within the graph.
The net result of this is that if an operation is improperly modelled, it is possible to trigger type confusion and have a Double interpreted as an Object to trigger code execution, or have an Object interpreted as a Double to trigger an information leak.
If we look at how the CompareEq operation is modeled, one thing that becomes clear is it only attempts to increase performance by setting the operation to have a constant value. Unfortunately, CompareEq did not take into account that JavaScript can be executed as part of the middle of the operation.
Here is a snippet of how DFGAbstractInterpreterInlines.h
handles the CompareEq
operation:
Here's a simple proof-of-concept demonstrating the issue:
This PoC demonstrates that, when comparisons are just so, fundamental assumptions made by the JIT engine can be broken. In the benign case, 'a==1 && a==2 && a==3' can evaluate to true, but in the worst case this can result in a compromise of the renderer process. Apologies if we just messed up your interview question, but now you’ll potentially get a more interesting answer!
Here is what happens when you run the proof-of-concept:
We get a crash when dereferencing 0x686374696c6c
while calling isString
on what the JavaScriptCore believes is a JavaScript Object, but was actually a Double. The value 0x686374696c6c
comes from adding 5 to 0x686374696c67
, which itself comes from the 5.67070584648226e-310
value being written in the proof-of-concept, since that is the Double representation of 0x686374696c67
.
The patch for this is rather simple – ensure clobberWorld
is called by properly modeling the loose comparison operators. You’ll still be able to have 'a==1 && a==2 && a==3' evaluate to true, however, now important checks will not get removed if you do.
Hopefully this blog post on Safari JIT has been interesting. Stay tuned for more JavaScript shenanigans! Until then, you can find me on Twitter at @WanderingGlitch, and follow the team for the latest in exploit techniques and security patches.