Floating-Poison Math in Chakra
August 22, 2018 | Simon ZuckerbraunThis post is an installment in my series of posts on security in Chakra, Microsoft’s JavaScript engine. In this installment, I present a particularly elegant type confusion bug in Chakra. This vulnerability was discovered by multiple researchers, including an anonymous researcher working with Trend Micro’s Zero Day Initiative.
As I have discussed in a previous post, Chakra uses JIT (Just-in-Time) compilation to produce high-performance native code from frequently-executed JavaScript function and loop bodies. To produce reasonably streamlined native code, the JIT engine collects type information during interpreted execution. For example, if a variable named array1
is observed to contain an array of floating-point numbers during interpreted execution, Chakra will consider it likely that this variable will continue to contain an array of that type during subsequent invocations. Accordingly, the JIT compiler will produce native code that is optimized for this assumption. The Chakra JIT compiler will emit native code prefixed with an array type check to validate the assumption. If the check fails, the native code will branch back into the interpreter. This branch is known as a bail-out. Bail-outs hopefully occur only in a minority of cases, so that most of the time execution can proceed with the efficient native implementation produced by the JIT compiler.
In some circumstances, Chakra can do better than to make an optimistic assumption. When Chakra can actually prove that a variable will be of the expected type, then it can omit the type check and bailout path. For example, consider a sequence of JavaScript statements all accessing the same array. Within the JIT compiled code, Chakra will need to insert a type check before the first array access. Once that check has passed, and execution proceeds past that point in the JIT code, there might not be any need to check the array again. If the Chakra JIT compiler can prove that none of the intervening operations could possibly alter the array’s type, then the compiler will not place array type checks in front of any of the remaining array accesses.
For a more in-depth treatment, see my blog post about bounds checking in Chakra.
Now to begin discussion of the type confusion vulnerability that is the subject of this post. Consider the following:
Suppose that function jit
is invoked many times until Chakra decides it is a candidate for JIT compilation. Chakra will notice that variable “a” always contains an array of floating-point numbers, and that variable “b” is of type Float64Array
. Those will be the assumptions that the JIT compiler makes about the types of the variables within function jit
. It can’t rely on those assumptions entirely, since it’s always possible that a future caller of function jit
will reassign those variables before invoking the function. So the JIT compiler will insert a type check before the first use of array “a”, to make sure that variable “a” really does still contain a floating point array. (It will also insert a type check for variable “b”.) If the check passes, the JIT code will proceed to execute the first assignment statement within function jit
by calling a specialized method, JavascriptOperators::OP_SetNativeFloatElementI
. This method provides a fast code path for inserting an operand known to be a floating-point value into an array known to be a native floating-point array.
Next, the JIT compiler encounters the second statement assigning into an element of array “a”. Does the JIT compiler need to insert a new type check to make sure that variable “a” is still an array of the expected type? Quite understandably, the JIT compiler reasons that it is not necessary, because the previous operation, OP_SetNativeFloatElementI
, would not change the type of an array. Accordingly, for the second statement, the JIT compiler emits native code that simply copies the data into the array. The native code for this second statement does not perform any type check beforehand.
Unfortunately, the assumption that the array type cannot change is not entirely watertight. This is due to a surprising wrinkle.
From the behavioral point of view, JavaScript arrays are completely untyped. It is perfectly valid to have an array of floating-point values, with some elements of other types sprinkled inside. From the implementation perspective, Chakra implements all mixed-typed arrays using the class JavascriptArray
, while single-typed numeric arrays, also known as native arrays, are implemented using specialized subclasses, such as JavascriptNativeFloatArray
. The data buffers maintained by a JavascriptNativeFloatArray
consist entirely of floating-point values packed into consecutive memory locations.
There is a special bit pattern that Chakra uses to indicate missing (undefined
) elements within a JavascriptNativeFloatArray
. (Most frequently, the reason why an element would have a value of undefined
is because it was never initialized.) On 64-bit Chakra, the bit pattern is 0x8000000280000002
. This bit pattern is defined in the ChakraCore source code as the constant JavascriptNativeFloatArray::MissingItem
. Somewhat problematically, this bit pattern is itself a valid (though unlikely) floating-point number. What should Chakra do if a floating-point value having this bit pattern is assigned into an element of a JavascriptNativeFloatArray
? It must not simply copy the bit pattern into the array. Otherwise, when later retrieving this element of the array, it would be interpreted as a JavaScript value of undefined
, which is not the behavior expected in JavaScript.
The answer is that the specialized class JavascriptNativeFloatArray
is incapable of handling this situation. If script assigns a floating-point value having bit pattern 0x8000000280000002
into an element of a JavascriptNativeFloatArray
, Chakra will automatically convert the array’s internal representation into the non-specialized array class JavascriptArray
. The JavascriptArray
class has the ability to store elements of varied types, and it is able to encode the difference between an element containing a value of undefined
versus a floating-point number with binary representation 0x8000000280000002
.
Now we have enough background knowledge to understand the vulnerability. As explained above, Chakra JIT compiles the second array assignment statement (a[0] = 2.3023e-320
) into native code that copies the specified floating-point value into array “a”, without any type check to first validate that “a” is still a JavascriptNativeFloatArray
. This is a fatal mistake, because the first array assignment (a[200] = b[0]
) may have resulted in the conversion of array “a” from a JavascriptNativeFloatArray
to a JavascriptArray
.
To trigger this vulnerability, b[0] must be a floating-point value whose binary representation is exactly 0x8000000280000002
. This is straightforward to accomplish using typed arrays:
Additionally, Lokihardt of Google Project Zero showed that it can even be accomplished using a carefully-chosen “poison” floating-point literal:
This vulnerability allows an attacker to specify an arbitrary 64-bit floating point value and place it into the array. When Chakra reads the value back, it will interpret the value as a pointer to a JavaScript object. In the code above, the bit pattern is 0x0000000000001234
, which is the binary representation of 2.3023e-320. This PoC will crash when it executes the code alert(a[0])
and Chakra tries to dereference address 0x0000000000001234
.
Microsoft patched this specific vulnerability in May as CVE-2018-0953 and credited Anonymous working with Trend Micro's Zero Day Initiative, Yuki Chen of Qihoo 360 Vulcan Team, and Lokihardt of Google Project Zero. JavaScript and JIT bugs continue to prevalent in modern browser vulnerability research. Keep an eye on this blog for future installments covering the state of the art in Chakra and other JavaScript engines.
Until then, you can find me on Twitter at @HexKitchen, and follow the team for the latest in exploit techniques and security patches.