Math.expm1(x)
can't return -0. But if x is -0, it can. That, in turn, means it believes Object.is(Math.expm(x), -0)
must always be false. But if x is -0, it's true, not false. That, in turn, means that the JIT believes array[Object.is(Math.expm(x), -0) * INDEX]
must be array[0], no matter what INDEX is. But if x is -0, it'll be array[INDEX]. That's a big problem, because the optimizer, guided by the static analyzer, removes the bounds check if the bounds check can't ever fail.But it's not quite that simple, because another consequence of of the static analyzer's misperception is that
Object.is(Math.expm(x), -0) * INDEX
is the constant 0, no matter what value x takes and no matter what INDEX is, and the JIT will simply constant-fold 0 in for the whole expression, so the bounds check really won't matter.But constant folding is an optimization, not a security boundary, and easily "defeated": replace the -0 constant in that expression with an object reference:
obj = { o: -0 }
Object.is(Math.expm(x), obj.o)
You can read that code and see that it's the constant -0, but the static analyzer needs to do escape analysis to make sure the object reference really does just evaluate to a constant, and escape analysis happens after the typing analysis, so the constant folding doesn't happen.The end result of the bug is a sort of "black magic zero/one chimera" that, mixed into an expression, tricks the JIT into stripping bounds checks. It's an extremely cool exploit primitive. It's doubly cool because if you don't have Andrea's writeup or a really nuanced understanding of v8 internals, you could stare at that exploit for eternity and never understand why it works.