At [1], the values for both the arguments a0
and a1
are loaded. The numbers 5/2
and 6/3
refer to Node 5/Variable 2
and Node 6/Variable 3
. Nodes are used in the initial Maglev IR graphs and the variables are used when the final register allocation graphs are being generated. Therefore, the arguments will be referred by their respective Nodes and Variables. At [2], two CheckedSmiUntag
operations are performed on the values loaded at [1]. This operation checks that the argument is a small integer and removes the tag. These untagged values are now fed into Int32AddWithOverflow
that takes the operands from v9/n9
and v10/n10
(the results from the CheckedSmiUntag
operations) and places the result in n11/v11
. Finally, at [4], the graph converts the resulting operation into a JavaScript number via Int32ToNumber
of n11/v11
, and places the result into v13/n13
which is then returned by the Return
operation.
Ubercage
Ubercage, also known as the V8 Sandbox (not to be confused with the Chrome Sandbox), is a new mitigation within V8 that tries to enforce memory read and write bounds even after a successful V8 vulnerability has been exploited.
The design involves relocating the V8 heap into a pre-reserved virtual address space called the sandbox, assuming an attacker can corrupt V8 heap memory. This relocation restricts memory accesses within the process, preventing arbitrary code execution in the event of a successful V8 exploit. It creates an in-process sandbox for V8, transforming potential arbitrary writes into bounded writes with minimal performance overhead (roughly 1% on real-world workloads).
Another mechanism of Ubercage is Code Pointer Sandboxing, in which the implementation removes the code pointer within the JavaScript object itself, and turns it into an index in a table. This table will hold type information and the actual address of the code to be run in a separate isolated part in memory. This prevents attackers from modifying JavaScript function code pointers as during an exploit, initially, only bound access to the V8 heap is attained.
Finally, Ubercage also signified the removal of full 64bit pointers on Typed Array objects. In the past the backing store (or data pointer) of these objects was used to craft arbitrary read and write primitives but, with the implementation of Ubercage, this is now no longer a viable route for attackers.
Garbage Collection
JavaScript engines make intensive use of memory due to the freedom the specification provides while making use of objects, as their types and references can be changed at any point in time, effectively changing their in-memory shape and location. All objects that are referenced by root objects (objects pointed by registers or stack variables) either directly, or through a chain of references, are considered live. Any object that is not in any such reference is considered dead and subject to be free’d by the Garbage Collector.
This intensive and dynamic usage of objects has led to research which proves that most objects will die young, known as the “The Generational Hypothesis”, which is used by V8 as a basis for its garbage collection procedures. In addition it uses a semi-space approach, in order to prevent traversing the entire heap-space in order to mark alive/dead objects, where it considers a “Young Generation” and an “Old Generation” depending on how many garbage collection cycles each object has managed to survive.
In V8 there exist two main garbage collectors, Major GC and Minor GC. The Major GC traverses the entire heap space in order to mark object status (alive/dead), sweep the memory space to free the dead objects, and finally, compact the memory depending on fragmentation. The Minor GC, traverses only the Young Generation heap space and does the same operations but including another semi-space scheme, taking surviving objects from the “From-space” to the “To-space” space, all in an interleaved manner.
Orinoco is part of the V8 Garbage Collector and tries to implement state-of-the-art garbage collection techniques, including fully concurrent, parallel, and incremental mechanisms for marking and freeing memory. Orinoco is applied to the Minor GC as it uses parallelization of tasks in order to mark and iterate the “Young generation”. It is also applied to the Major GC by implementing concurrency in the marking phases. All of this prevents previously observable jank and screen stutter caused by the Garbage Collector stopping all tasks with the intention of freeing memory, known as Stop-the-World approach.
Object Representation
V8 on 64-bit builds uses pointer compression. This is, all the pointers are stored in the V8 heap as 32-bit values. To distinguish whether the current 32-bit value is a pointer or a small integer (SMI), V8 uses another technique called pointer tagging:
- If the value is a pointer, it will set the last bit of the pointer to 1.
- If the value is a SMI, it will bitwise left shift (
<<
) the value by 1. Leaving the last bit unset. Therefore, when reading a 32-bit value from the heap, the first thing that is checked is whether it has a pointer tag (last bit set to 1) and if so the value of a register (r14
on x86 systems) is added, which corresponds to the V8 heap base address, therefore decompressing the pointer to its full value. If it is a SMI it will check that the last bit is set to 0 and then bitwise right shift (>>
) the value before using it.
The best way to understand how V8 represents JavaScript objects internally is to look at the output of a DebugPrint
statement, when executed in a d8
shell with an argument representing a simple object.