Webkit Exploitation Tutorial(Part 3): JavaScriptCore Overview

8 minute read

Overview

The Webkit primarily includes:

  • JavaScriptCore: JavaScript executing engine.
  • WTF: Web Template Library, replacement for C++ STL lib. It has string operations, smart pointer, and etc. The heap operation is also unique here.
  • DumpRenderTree: Produce RenderTree
  • WebCore: The most complicated part. It has CSS, DOM, HTML, render, and etc. Almost every part of the browser despite components mentioned above.

And the JSC has:

  • lexer
  • parser
  • start-up interpreter (LLInt)
  • three javascript JIT compiler, their compile time gradually becomes longer but run faster and faster:
    • baseline JIT, the initial JIT
    • a low-latency optimizing JIT (DFG)
    • a high-throughput optimizing JIT (FTL), final phase of JIT
  • two WebAssembly execution engines:
    • BBQ
    • OMG

Still a disclaimer, this post might be inaccurate or wrong in explaining WebKit mechanisms

If you have learn basic compile theory courses, lexer and parser are as usual as what taught in classes. But the code generation part is frustrating. It has one interpreter and three compilers, WTF? JSC also has many other unconventional features, let’s have a look:

JSC Value Representation

To easier identifying, JSC’s value represents differently:

  • pointer : 0000:PPPP:PPPP:PPPP (begins with 0000, then its address)
  • double (begins with 0001 or FFFE):
    • 0001:****:****:****
    • FFFE:****:****:****
  • integer: FFFF:0000:IIII:IIII (use IIII:IIII for storing value)
  • false: 0x06
  • true: 0x07
  • undefined: 0x0a
  • null: 0x02

0x0, however, is not a valid value and can lead to crash.

JSC Object Model

Unlike java, which has fix class member, JavaScript allows people to add properties any time.

So, despite traditionally statically align properties, JSC has a butterfly pointer for adding dynamic properties. It’s like an additional array. Let’s explain it with several situations.

Also, JSArray will always be allocated to butterfly pointer since they change dynamically.

We can understand the concept easily with following graph:

0x0 Fast JSObject

The properties are initialized:

var o = {f: 5, g: 6};

The butterfly pointer will be null here since we only have static properties:

--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|    NULL    | --> Butterfly Pointer
--------------
|  0xffff000 | --> 5 in JS format
|  000000005 | 
--------------
|  0xffff000 |
|  000000006 | --> 6 in JS format
--------------

Let’s expand our knowledge on JSObject. As we see, each structure ID has a matched structure table. Inside the table, it contains the property names and their offsets. In our previous object o, the table looks like: | property name| location | |————-|———-| | “f” | inline(0) | | “g” | inline(1) |

When we want to retrieve a value(e.g. var v = o.f), following behaviors will happen:

if (o->structureID == 42)
    v = o->inlineStorage[0]
else
    v = slowGet(o, f)

You might wonder why the compiler will directly retrieve the value via offset when knowing the ID is 42. This is a mechanism called inline caching, which helps us to get value faster. We won’t talk about this much, click here for more details.

0x1 JSObject with dynamically added fields

var o = {f: 5, g: 6};
o.h = 7;

Now, the butterfly has a slot, which is 7.

--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | --> -------------
--------------     | 0xffff000 |
|  0xffff000 |     | 000000007 |
|  000000005 |     -------------
--------------     |    ...    |
|  0xffff000 |
|  000000006 | 
--------------

0x2 JSArray with room for 3 array elements

var a = [];

The butterfly initializes an array with estimated size. The first element 0 means number of used slots. And 3 means the max slots:

--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | --> -------------
--------------     |     0     |
                   -------------
                   |     3     |
                   -------------
                   |   <hole>  |
                   -------------
                   |   <hole>  |
                   -------------
                   |   <hole>  |
                   -------------

0x3 Object with fast properties and array elements

var o = {f: 5, g: 6};
o[0] = 7;

We filled a element of the array, so 0(used slots) increases to 1 now:

--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | --> -------------
--------------     |     1     |
|  0xffff000 |     -------------
|  000000005 |     |     3     |
--------------     -------------
|  0xffff000 |     | 0xffff000 |
|  000000006 |     | 000000007 |
--------------     -------------
                   |   <hole>  |
                   -------------
                   |   <hole>  |
                   -------------

0x4 Object with fast and dynamic properties and array elements

var o = {f: 5, g: 6};
o[0] = 7;
o.h = 8;

The new member will be appended before the pointer address. Arrays are placed on the right and attributes are on the left of butterfly pointer, just like the wing of butterfly:

--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  | 0xffff000 |
|  0xffff000 |  |  | 000000008 |
|  000000005 |  -> -------------  (pointer address)
--------------     |     1     |
|  0xffff000 |     -------------
|  000000006 |     |     2     |
--------------     -------------
                   | 0xffff000 |
                   | 000000007 |
                   -------------
                   |   <hole>  |
                   -------------

0x5 Exotic object with dynamic properties and array elements

var o = new Date();
o[0] = 7;
o.h = 8;

We extends the butterfly with a build-in class, the static properties will not change:

--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  | 0xffff000 |
|    < C++   |  |  | 000000008 |
|   State >  |  -> -------------
--------------     |     1     |
|    < C++   |     -------------
|   State >  |     |     2     |
--------------     -------------
                   | 0xffff000 |
                   | 000000007 |
                   -------------
                   |   <hole>  |
                   -------------

Type Inference

JavaScript is a weak, dynamic type language. Compiler will do a lot of works in type inference, causing it becomes extreme complicated.

Watchpoints

Watchpoints can happen in following cases:

  • haveABadTime
  • Structure transition
  • InferredValue
  • InferredType
  • and many others…

When above situations happen, it will check whether watchpoint has optimized. In WebKit, it represents like this:

class Watchpoint {
public:
    virtual void fire() = 0;
};

For example, the compiler wants to optimize 42.toString() to "42" (return directly rather than use code to convert), it will check if it’s already invalidate. Then, If valid, register watchpoint and do the optimization.

Compilers

0x0. LLInt

At the very beginning, the interpreter will generate byte code template. Use JVM as example, to executes .class file, which is another kind of byte code template. Byte code helps executing easier:

parser -> bytecompiler -> generatorfication
-> bytecode linker -> LLInt 

0x1. Baseline JIT and Byte Code Template

Most basic JIT, it will generate byte code template here. For example, this is add in javascript:

function foo(a, b)
{
return a + b;
}

This is bytecode IL, which is more straightforward without sophisticated lexes and more convenient to convert to asm:

[ 0] enter
[ 1] get_scope loc3
[ 3] mov loc4, loc3
[ 6] check_traps
[ 7] add loc6, arg1, arg2
[12] ret loc6

Code segment 7 and 12 can result following DFG IL (which we talk next). we can notice that it has many type related information when operating. In line 4, the code will check if the returning type matches:

GetLocal(Untyped:@1, arg1(B<Int32>/FlushedInt32), R:Stack(6), bc#7);
GetLocal(Untyped:@2, arg2(C<BoolInt32>/FlushedInt32), R:Stack(7), bc#7);
ArithAdd(Int32:@23, Int32:@24, CheckOverflow, Exits, bc#7);
MovHint(Untyped:@25, loc6, W:SideState, ClobbersExit, bc#7, ExitInvalid);
Return(Untyped:@25, W:SideState, Exits, bc#12);

The AST looks like this:

   +----------+
   |  return  |
   +----+-----+
        |
        |
   +----+-----+
   |   add    |
   +----------+
   |          |
   |          |
   v          v
+--+---+    +-+----+
| arg1 |    | arg2 |
+------+    +------+

0x2. DFG

If JSC detects a function running a few times. It will go to next phase. The first phase has already generated byte code. So, DFG parser parses byte code directly, which it’s less abstract and easier to parse. Then, DFG will optimize and generate code:

DFG bytecode parser -> DFG optimizer 
-> DFG Backend

In this step, the code runs many times; and they type is relative constant. Type check will use OSR.

Imagine we will optimize from this:

int foo(int* ptr)
{
int w, x, y, z;
w = ... // lots of stuff

x = is_ok(ptr) ? *ptr : slow_path(ptr);
y = ... // lots of stuff
z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z;
}

to this:

int foo(int* ptr)
{
int w, x, y, z;
w = ... // lots of stuff

if (!is_ok(ptr))
  return foo_base1(ptr, w);
x = *ptr;
y = ... // lots of stuff
z = *ptr;
return w + x + y + z;
}

The code will run faster because ptr will only do type check once. If the type of ptr is always different, the optimized code runs slower because of frequent bailing out. Thus, only when the code runs thousands of times, browser uses OSR to optimize it.

0x3. FLT

A function, if, runs a hundred or thousands of time, the JIT will use FLT . Like DFG, FLT will reuse the byte code template, but with a deeper optimization:

DFG bytecode parser -> DFG optimizer
-> DFG-to-B3 lowering -> B3 Optimizer ->
Instruction Selection -> Air Optimizer ->
Air Backend

0x4. More About Optimization

Let’s have a look on change of IR in different optimizing phases: | IR | Style | Example | |—|——-|———| | Bytecode | High Level Load/Store | bitor dst, left, right | | DFG | Medium Level Exotic SSA | dst: BitOr(Int32:@left, Int32:@right, ...) | | B3 | Low Level Normal SSA | Int32 @dst = BitOr(@left, @right)| | Air | Architectural CISC | Or32 %src, %dest |

Type check is gradually eliminated. You may understand why there are so many type confusions in browser CVE now. In additional, they are more and more similar to machine code.

Once the type check fails, the code will return to previous IR (e.g. a type check fails in B3 stage, compiler will return to DFG and execute in this stage).

Garbage Collector (TODO)

The heap of JSC is based on GC. The objects in heap will have a counter about their references. GC will scan the heap to collect the useless memory.

…still need more materials…

Reference

Most of the post is based on Filip Pizlo’s presentations, great thanks!

Also, “Attacking JavaScript Engine” by Saelo guides me a lot.

Leave a comment