Floating-Poison Math in ChakraAugust 22, 2018 | Simon Zuckerbraun
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.
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,
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.
There is a special bit pattern that Chakra uses to indicate missing (
undefined) elements within a
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
The answer is that the specialized class
0x8000000280000002 into an element of a
undefined versus a floating-point number with binary representation
Now we have enough background knowledge to understand the vulnerability. As explained above, Chakra JIT compiles the second array assignment statement (
a = 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
a = b) may have resulted in the conversion of array “a” from a
To trigger this vulnerability, b 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:
0x0000000000001234, which is the binary representation of 2.3023e-320. This PoC will crash when it executes the code
alert(a) and Chakra tries to dereference address