
Exploiting the Math.expm1 typing bug in V8 - gok
https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/
======
tptacek
The fundamental bug here is _really_ slick. The static analyzer in the JIT
incorrectly believes

    
    
         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.

~~~
zachrose
Here's what I don't get. Why isn't Math.expm1 just implemented in JavaScript?

    
    
        Math.expm1 = (x) => Math.pow(Math.E, (x)) - 1;
    

With this monkey patched version, Math.expm1(-0) now returns 0. I've been
writing JavaScript for a long time and I didn't even know Math.expm1 was a
thing. Was it really necessary to implement this inside V8?

~~~
cozzyd
Because you lose all your precision there due to how floating point math
eorjsy. expm1 is for very small x that where exp (x) is close to 1.

~~~
cozzyd
oops, s/eorjsy/works. That's what I get for typing on my phone in bed at
night...

------
wahern
Interesting how at the end, after acquiring out-of-bounds write access, that
it was easiest to leverage the WebAssembly infrastructure to execute code than
to build a ROP chain.

Apparently WebAssembly heap memory storing generated code is not write
protected _at_ _all_. I guess whatever architecture they have for managing
typed memory chunks doesn't make it sufficiently easy to manipulate protection
bits dynamically, and the WebAssembly folks were content to leave compiled
WebAssembly chunks are RWX.

It's unfortunate that this thread hasn't been upvoted. It really drives home
the futility of expecting something as complex as V8 to ever be safe enough to
sandbox arbitrary code. And it has little to do with the language of the
implementation--the same engine re-written in Rust would have been just as
susceptible to the two exploits.

I guess it's just too inconvenient to accept reality. I'm still surprised that
Cloudflare and AWS have actually convinced themselves they can make such
architectures secure[1]. Less shocked that everybody is else is so credulous.

Though, I _am_ shocked that Netflix is so credulous. A recent blog post
described how they use AWS Lambda functions to manage their CA private key.
But, again, because of how limited KMS is I guess it's too inconvenient to not
trust Lambda. KMS really should support asymmetric key operations; it's
ridiculous it's still not supported. I guess AWS's KMS "cloud HSM" solution
just wouldn't scale (in terms of CPU) if people could do that.

[1] Absolute security is impossible, but as complex as this exploit was it's
obviously still far too trivial. It's a totally unwarranted and unreasonable
expectation that bad actors are incapable of reading (if not manipulate) co-
hosted Lambda or Cloudflare Worker projects. Writing a non-JIT'd engine with a
simplified memory architecture is not only feasible, it would be an obvious
candidate for formal verification. But the demands for performance are just
too strong and so, as always, security takes a back-seat to performance and
speed-to-market.

~~~
titzer
Hi, TLM of the WebAssembly runtime in V8 here.

TLDR: it's asm.js's fault. And yes, complexity.

The reason that WebAssembly JIT code memory is still RMW (for now) is actually
really unfortunate. As you might know, V8's JIT code memory for JS is only
writable when the application is quiesced (i.e. JS is not running) and the JIT
is either finishing a function or the garbage collector is moving JITted code.
It's read-execute otherwise. It's never both writable and executable at the
same time. We generally refer to this as WX protection (i.e.
writeable/executable exclusive).

In the case of WebAssembly, it's asm.js that's the real culprit here.
Internally in V8, asm.js code is translated to WebAssembly by a very quick
custom-built parser that validates asm.js's type system while parsing and
emits WebAssembly bytecode along the way. The WebAssembly bytecode is then fed
to the WebAssembly engine for compilation.

Well...not so fast. In order to meet our performance goals for fast asm.js
validation and startup, the WebAssembly engine does not do up-front
compilation of Wasm code coming from asm.js. Instead, it does on-demand
compilation of asm.js code, compiling the Wasm bytecode corresponding to each
asm.js function upon first execution. We call this "lazy compilation".

We originally shipped lazy compilation cooperating with the WX mechanism
executable for JS code. That is, upon every (lazy) compilation of asm.js code,
we'd flip the permission bits on the code space in order to write in the
little bit of machine code generated for each function. Problem is, that
permission flip is expensive--like really expensive. So expensive that we had
to unship WX protection because it made asm.js code unusably slow.

We're working on fixing this, as we are keenly aware of the risk exposure
here.

~~~
pcwalton
I'm not sure how asm.js is relevant here. The issue is that function-level
lazy compilation makes compilation too hot to allow for an mprotect call, no?
Wouldn't a function-level lazy Web Assembly implementation have the same
problem?

It seems to me that the solution is the same for both wasm and asm.js: make
the unit of compilation larger than the function, so as to amortize the cost
of mprotect.

~~~
titzer
You and I both know that the comments section of HN lacks sufficient space to
discuss all the things we thought of / could think of :)

------
devwastaken
I would love to know how v8 is properly secured in the instance of things like
Cloudflares. Im curious to know if this goes through v8 isolates, and if
something like lxd containment would be a viable alternative.

