🔗 V8 Engine Internals

In this module, we will discuss engine-specific internals of Chromium's V8 JavaScript engine. In particular, we will try to take the concepts we introduced in the introductory JavaScript Engine Module and concretize them in the context of V8.

Having a grasp of these concepts will make understanding (and writing!) exploits much more straightforward.

🔗 V8 Pointer Tagging

Previously, we mentioned that V8 uses pointer-tagging to differentiate pointers from JavaScript numbers. This "tag" is placed in the Least Significant Bit (LSB):

  • 0 = 31 Bit Integer (called an SMI - small integer)
  • 1 = Pointer
[bits representation]
SMI:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX00000000000000000000000000000000
Pointer:
00000000000000000XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX01

[bytes representation]
SMI:
XXXXXXXX00000000|0
Pointer:
0000XXXXXXXXXXXX|1

uint64_t smi = (0x414243 << 32) | 0;
uint64_t pointer  = (uint64_t)(obj) | 1;

Object pointers will always be aligned, so the lowest bits can be used freely without "interfering" with the pointer value.

Using this method, V8 can efficiently differentiate between SMIs and pointers at runtime while maintaining all the memory-saving techniques we discussed previously.

🔗 V8 Pointer Tagging Exercise

[open exercise]

🔗 V8 Pointer Compression

As of Chrome 80, pointers are 32-bit by default:

SMI:                [xx...31...xxx]0
Strong HeapObject:  [xx...30...xx]01
Weak HeapObject:    [xx...30...xx]11

The general idea is instead of pointers being actual pointers, they are offsets into the heap (from a global heap base pointer).

This can lead to significant improvements for performance and memory usage, but adds complexity and makes things trickier for exploitation. For our purposes, we will leave it disabled.

If you're interested, this is the "Design Doc".

🔗 Objects in V8

In V8, the simplest Object class is called a HeapObject:

C++
class HeapObject : public Object { ... // [map]: Contains a map which contains the object's reflective // information. DECL_GETTER(map, Map) ... // Layout description. #define HEAP_OBJECT_FIELDS(V) \ V(kMapOffset, kTaggedSize) \ ... }

These HeapObjects are fairly simple; they define an object on the heap with a particular type:

HeapObject Structure:
00: [        Map*        ] (Tagged Ptr) -> Type information

All other Object classes then extend this class to add other values:

🔗 V8 Map

The Map is one of the most important data structures in V8. Broadly speaking, these Maps are used by V8 to store a variety of type information such as:

  • Object Type
  • Property Keys and Value Indexes
  • Prototype Pointer
  • Allocation Size
  • Number Of inline properties
  • Type Transition links

You can see a very detailed breakdown of the Map structure here.

 Normal*| Compress | Size | Info
 Offset |  Offset  |      |
--------+-----------------+---------------
 0      |  0       | ptr  | Meta Map (Tagged)
 8      |  4       |  1   | instance_size
                     ...
 0xc    |  8       |  2   | Instance Type
                     ...
 0xf    |  0xb     |  1   | (Bits 3..7) elements_kind
 0x10   |  0xc     |  4   | (Bit 21) is_dictionary_map
 0x18   |  0x10    | ptr  | Prototype (Tagged)
 0x28   |  0x18    | ptr  | instance_descriptors (Tagged)
 0x40   |  0x24    | ptr  | Transitions (Tagged)
 ...
 *64 bit non-compressed

These maps are used to describe type information, which can be exceptionally dynamic in JavaScript. This dynamism is implemented via "Map Transitions", which govern how one kind of Map can be turned into another, or whether a new map is required:

C++
Handle<Map> Map::TransitionToDataProperty(Isolate* isolate, Handle<Map> map, Handle<Name> name, Handle<Object> value, PropertyAttributes attributes, PropertyConstness constness, StoreOrigin store_origin) { ... // Find if transition exists Map maybe_transition = TransitionsAccessor(isolate, map) .SearchTransition(*name, kData, attributes); if (!maybe_transition.is_null()) { return UpdateDescriptorForValue(isolate, transition, descriptor, constness, value); } // Otherwise make a new map MaybeHandle<Map> maybe_map; maybe_map = Map::CopyWithField(isolate, map, name, type, attributes, constness, representation, flag); return result; }

In V8, you can watch these transitions happen "in real time" with the --trace-maps debug flag.

🔗 V8 Type Generalization

V8 implements the principle of "type generalization". As the property type changes, the type "widens" towards "more general". Importantly however, it never "shrinks" back towards "more specific"; once the type becomes generalized, it stays at that level or generalizes further.

The diagram below illustrates this, types move from "most specific" (SMI/double) up towards most general:

Once we get to Tagged Value, we cannot get any more general. All we know is that:

  1. We have some sort of "value"
  2. We can tell whether it is a pointer or not by checking the LSB

A property type can get "more general" if a different type is stored in that property. When this happens, it triggers a transition.

We can use the --trace-generalization flag to log these as they happen:

JavaScript
d8> let a = {a: 1.1} d8> a.a = false; // Generalize from double to tagged value [generalizing]a:d{Any}->t{Any} (+1 maps) // d{Any} is double, t{Any} is tagged value d8> a.a = 1.1; // Doesn't go back to double

🔗 V8 JSObjects

Every JavaScript object in V8 is represented by a JSObject. In this section, we will use what we know so far to 'build up' to one.

Fundamentally, each object in V8 is a HeapObject as mentioned previously. However, we will need some additional features to build a full JSObject, as HeapObject only allows room for a Map:

HeapObject Structure:
00: [        Map*        ] (Tagged Ptr) -> Type information

The first step towards JSObject is JSReceiver. JSReceiver includes some more features we need, such as a Property Array/Hashmap and "Fast Properties", an inline list of properties.

We can see part of the definition below:

C++
// JSReceiver includes types on which properties can be defined, i.e., // JSObject and JSProxy. class JSReceiver : public TorqueGeneratedJSReceiver<JSReceiver, HeapObject> { ... // 1) EmptyFixedArray/EmptyPropertyDictionary - This is the standard // placeholder. ... // 3) PropertyArray - This is similar to a FixedArray but stores // the hash code of the object in its length field. This is a fast // backing store. // 4) NameDictionary - This is the dictionary-mode backing store. DECL_ACCESSORS(raw_properties_or_hash, Object) ... }

Now let's take a look at the structure of JSReceiver like we did with HeapObject:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [ Property* or Hash  ] (TagPtr) -> Property FixedArray 
10 | 08: [  Fast Properties   ] (TagPtr[]) -> In-Object List of Properties
                  ...

From our description of Objects previously, this is starting to look "feature-complete" but we still need a few final features, such as Elements.

The JSObject class in V8 handles this, among other things, and we can see the relevant snippet of its declaration below:

C++
// The JSObject describes real heap allocated JavaScript objects with // properties. // Note that the map of JSObject changes during execution to enable inline // caching. class JSObject : public TorqueGeneratedJSObject<JSObject, JSReceiver> { ... // [elements]: The elements (properties with names that are integers). // Elements can be in two general modes: fast and slow. Each mode // corresponds to a set of object representations of elements that // have something in common. DECL_ACCESSORS(elements, FixedArrayBase) ... }

For completion's sake, we can also take a final look at our structure diagrams:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [ Property* or Hash  ] (TagPtr)
10 | 08: [      Elements*     ] (TagPtr) -> Elements FixedArray or Sparse Array
18 | 0c: [  Fast Properties   ] (TagPtr[])
                  ...

🔗 V8 JSObject Exercise

[open exercise]

🔗 V8 JSArrays

Just like JSObject expands on JSReceiver, JSArray expands on JSObject by introducing a length property:

C++
// The JSArray describes JavaScript Arrays // Such an array can be in one of two modes: // - fast, backing storage is a FixedArray and length <= elements.length(); // Please note: push and pop can be used to grow and shrink the array. // - slow, backing storage is a HashTable with numbers as keys. class JSArray : public TorqueGeneratedJSArray<JSArray, JSObject> { // [length]: The length property. DECL_ACCESSORS(length, Object) ... }

The associated structure diagram:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [ Property* or Hash  ] (TagPtr)
10 | 08: [      Elements*     ] (TagPtr) -> Elements FixedArray or Hash Map
18 | 0c: [       Length       ] (SMI) -> Current length of the JSArray
20 | 10: [  Fast Properties   ] (TagPtr[])
                  ...

🔗 V8 Elements

Elements in JavaScript are quite similar to properties, with the major distinguishing feature being that Elements are accessed "by index" rather than "by key". In practice, if you access it "like an array", you are accessing an "Element" rather than a "Property".

This opens up some opportunities for optimization, but these all hinge on what "kind" of values are getting stored as Elements in any given object. V8 uses the concept of "Element Kind" to describe what it knows about the current Elements and to decide whether it can apply certain optimizations.

🔗 Element Kind

Element Kind is the term used to describe the overall type of all elements in a JSObject. The main idea is that if all elements are of a particular type, we can apply an optimization:

  • If we know all elements are doubles, we can inline them
  • If we know all elements are SMI, we can skip tag-checking

The different "types" of element kind are most easily seen and described by the Chromium source code:

C++
enum ElementsKind : uint8_t { // The "fast" kind for elements that only contain SMI values. Must be first // to make it possible to efficiently check maps for this kind. PACKED_SMI_ELEMENTS, HOLEY_SMI_ELEMENTS, // The "fast" kind for tagged values. Must be second to make it possible to // efficiently check maps for this and the PACKED_SMI_ELEMENTS kind // together at once. PACKED_ELEMENTS, HOLEY_ELEMENTS, // The "fast" kind for unwrapped, non-tagged double values. PACKED_DOUBLE_ELEMENTS, HOLEY_DOUBLE_ELEMENTS, ... }

🔗 ELEMENTS

When elements_kind is (PACKED|HOLEY)_ELEMENTS, we don't know anything at all about the elements' types. More precisely, we do not know anything beyond the fact that they will be tagged values. This is the most "general" of the element kinds.

d8> a=[1.1,1.1,1.1,false]
d8> %DebugPrint(a)
DebugPrint: 0xab14ed93989: [JSArray]
 - elements: 0x0ab14ed93929 <FixedArray[4]> [PACKED_ELEMENTS (COW)]

pwndbg> x/6xg 0x0ab14ed93929-1
0xab14ed93928:	0x00001afdbcb00801	0x0000000400000000
0xab14ed93938:	0x00001d673eaa3ec1	0x00001d673eaa3ed1
0xab14ed93948:	0x00001d673eaa3ee1	0x00001afdbcb00709

An important observation here is that doubles will have to use HeapNumbers instead of being able to be inlined.

🔗 *_DOUBLE_ELEMENTS

When elements_kind is (PACKED|HOLEY)_DOUBLE_ELEMENTS, we know that all elements are doubles.

That means we no longer have to use HeapNumber to represent double-values. Because the engine knows that every element will be a double, it can inline raw "un-boxed" doubles and skip the machinery associated with pointer-tagging:

d8> a=[1.1,1.1,1.1,1.1]
d8> %DebugPrint(a)
DebugPrint: 0xab14ed93fe1: [JSArray]
 - elements: 0x0ab14ed93fb1 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]

pwndbg> x/6xg 0x0ab14ed93fb1-1
0xab14ed93fb0:	0x00001afdbcb01459	0x0000000400000000
0xab14ed93fc0:	0x3ff199999999999a	0x3ff199999999999a
0xab14ed93fd0:	0x3ff199999999999a	0x3ff199999999999a

🔗 Packed vs Holey

"Packed" and "Holey" elements simply refer to whether or not there is "space" between elements.

  • Packed: No "empty space" / Dense
  • Holey: Some empty space / Sparse

Empty elements are filled with a special value called the_hole which is simply a placeholder.

In HOLEY_DOUBLE_ELEMENTS: 0xfff7fffffff7ffff

We can see this value in the spots for indexes one and two in the output below:

d8> a=[1.1]; a[3]=1.1
d8> %DebugPrint(a)
DebugPrint: 0xab14ed94b61: [JSArray]
 - elements: 0x0ab14ed94b81 <FixedDoubleArray[22]> [HOLEY_DOUBLE_ELEMENTS]
 - elements: 0x0ab14ed94b81 <FixedDoubleArray[22]> {
           0: 1.1
         1-2: <the_hole>
           3: 1.1
        4-21: <the_hole>
 }

pwndbg> x/6xg 0x0ab14ed94b81-1
0xab14ed94b80:	0x00001afdbcb01459	0x0000001600000000
0xab14ed94b90:	0x3ff199999999999a	0xfff7fffffff7ffff
0xab14ed94ba0:	0xfff7fffffff7ffff	0x3ff199999999999a

🔗 DICTIONARY_ELEMENTS

Eventually, if the elements are "Holey", it may be more efficient to switch to a hashmap or dictionary rather than continuing to use the_hole.

For example, imagine the following scenario:

JavaScript
let a = [1.1,1.1,1.1] a[10000] = 1.1;

Clearly, having thousands of indices with the_hole is wasteful. Instead, V8 switches to DICTIONARY_ELEMENTS in such situations. The below debug output shows this switch occurring as we try to insert 1.1 at index 10000:

d8> a=[1.1,1.1,1.1]
d8> %DebugPrint(a)
DebugPrint: 0x349c6ca8bab9: [JSArray]
 - elements: 0x349c6ca8ba91 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
d8> a[10000] = 1.1;
DebugPrint: 0x349c6ca8bab9: [JSArray]
 - elements: 0x349c6ca8bc09 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]

pwndbg> x/16xg 0x349c6ca8bc09-1
0x349c6ca8bc08: 0x00001bbf2e9016d9  0x0000001c00000000
0x349c6ca8bc18: 0x0000000400000000  0x0000000000000000
0x349c6ca8bc28: 0x0000000800000000  0x00004e2000000000
0x349c6ca8bc38: 0x0000000100000000  0x0000349c6ca8bbe9
0x349c6ca8bc48: 0x000000c000000000  0x00001bbf2e9004d1
0x349c6ca8bc58: 0x00001bbf2e9004d1  0x00001bbf2e9004d1
0x349c6ca8bc68: 0x0000271000000000  0x00001cef3a4df621
0x349c6ca8bc78: 0x000000c000000000  0x00001bbf2e9004d1

🔗 Element Kind Exercise

[open exercise]

🔗 V8 Typed Arrays

JavaScript includes support for "native style" arrays via Typed Arrays. These are used to store arbitrary binary data in a memory buffer:

JavaScript
d8> let a = new ArrayBuffer(0x100); d8> let b = new Uint8Array(a); d8> b[0] = 0x41;

Uint8Array is a JSTypedArray, which wraps JSArrayBuffer. There are also versions for most of the commonly used binary data-types such as Uint16, Uint32, Float64, etc.

As we did for JSObject, we can "build up" to JSTypedArray by taking a look at the intermediate structures.

🔗 V8 JSArrayBuffer

Looking at the definitions below, we can see that a JSArrayBuffer is just an Object that is augmented with a few special fields:

C++
class JSArrayBuffer: public TorqueGeneratedJSArrayBuffer<JSArrayBuffer, JSObject> { ... }

And the corresponding "Torque" definition (a custom language for parts of V8):

V8 Torque
@generateCppClass extern class JSArrayBuffer extends JSObject { byte_length: uintptr; backing_store: ExternalPointer; extension: RawPtr; bit_field: JSArrayBufferFlags; }

We can also take a look at the structure diagram:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [ Property* or Hash  ] (TagPtr)
10 | 08: [      Elements*     ] (TagPtr)
18 | 0c: [     Byte Length    ] (size_t) -> Size of buffer in bytes
20 | 14: [   Backing Store*   ] (void*) -> Allocated memory (not tagged!)
28 | 1c: [     Extension*     ] (void*) -> Used by GC
30 | 24: [     Bit Field      ] (uint32) -> Flags 
                  ...

For our purposes, JSArrayBuffer is a great corruption target because with some creativity, we can turn them into a stable way of performing arbitrary read/write operations.

🔗 V8 JSArrayBufferView

The JSArrayBufferView is the next step towards JSTypedArray. In JavaScript, "Views" are used as a way of actually interacting with data in an ArrayBuffer. You can think of an ArrayBuffer as the actual "chunk" of memory, while a "View" is your tool for manipulating it.

C++
class JSArrayBufferView : public TorqueGeneratedJSArrayBufferView<JSArrayBufferView, JSObject> {
V8 Torque
@abstract @generateCppClass extern class JSArrayBufferView extends JSObject { buffer: JSArrayBuffer; // NOTE this is a pointer TO an array buffer byte_offset: uintptr; byte_length: uintptr; }

Below, we can see some of the additions JSArrayBufferView makes to a standard JSObject:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [ Property* or Hash  ] (TagPtr)
10 | 08: [      Elements*     ] (TagPtr)
18 | 0c: [    Array Buffer*   ] (TagPtr) -> Pointer to array buffer
20 | 10: [     Byte Offset    ] (size_t) -> Start of view
28 | 18: [     Byte Length    ] (size_t) -> Size of view
                  ...

🔗 V8 JSTypedArray

Finally, we are ready to take a look at JSTypedArray. As before, we can see the definition below. Note that JSTypedArray inherits from JSArrayBufferView rather than directly from ArrayBuffer.

C++
class JSTypedArray : public TorqueGeneratedJSTypedArray<JSTypedArray, JSArrayBufferView> { ... }
V8 Torque
@generateCppClass extern class JSTypedArray extends JSArrayBufferView { length: uintptr; external_pointer: ExternalPointer; base_pointer: ByteArray|Smi; }

Finally, we have our structure diagram:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [ Property* or Hash  ] (TagPtr)
10 | 08: [      Elements*     ] (TagPtr)
18 | 0c: [    Array Buffer*   ] (TagPtr) -> Pointer to array buffer
20 | 10: [     Byte Offset    ] (size_t) -> Start of view
28 | 18: [     Byte Length    ] (size_t) -> Size of view
30 | 20: [   Element Length   ] (size_t) -> Byte Length divided by Typed Array element size
                  ...

🔗 Strings

Strings are less commonly used for exploitation, but we'll briefly cover them here.

C++
// The Name abstract class captures anything that can be used as a property // name, i.e., strings and symbols. All names store a hash value. class Name : public TorqueGeneratedName<Name, PrimitiveHeapObject> { ... } // The String abstract class captures JavaScript string values: // // Ecma-262: // 4.3.16 String Value // A string value is a member of the type String and is a finite // ordered sequence of zero or more 16-bit unsigned integer values. // // All string values have a length field. class String : public TorqueGeneratedString<String, Name> { ... }
V8 Torque
@generateCppClass extern class Name extends PrimitiveHeapObject { raw_hash_field: NameHash; } @generateCppClass @reserveBitsInInstanceType(6) extern class String extends Name { const length: int32; }

This forms an abstract base class for various string representations:

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [        Hash        ] (uint32)
0c | 08: [       Length       ] (int32)
                  ...

🔗 Strings: Representation Types

The "type" of the string (i.e. which subclass of String it is) can be found in the Map structure, in bits of the instance_type field.

V8 Torque
@reserveBitsInInstanceType(6) extern class String extends Name { macro StringInstanceType(): StringInstanceType { return %RawDownCast<StringInstanceType>( Convert<uint16>(this.map.instance_type)); } } bitfield struct StringInstanceType extends uint16 { representation: StringRepresentationTag: 3 bit; is_one_byte: bool: 1 bit; is_uncached: bool: 1 bit; is_not_internalized: bool: 1 bit; } extern enum StringRepresentationTag extends uint32 { kSeqStringTag, kConsStringTag, kExternalStringTag, kSlicedStringTag, kThinStringTag }

Note that strings can have either 8-bit or 16-bit characters in memory, as indicated by is_one_byte. This optimizes for the common case of strings with only 8-bit code points (e.g. ASCII).

SeqString is the simplest representation, with the variably-sized character buffer inlined after the String fields:

V8 Torque
@abstract extern class SeqString extends String {} extern class SeqOneByteString extends SeqString { const chars[length]: char8; } extern class SeqTwoByteString extends SeqString { const chars[length]: char16; }
np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [        Hash        ] (uint32)
0c | 08: [       Length       ] (int32)
10 | 0c: [       Buffer       ] (char8[] / char16[]) variably-sized array

The other types are:

V8 Torque
// concatenation of two strings // e.g. cons = str1 + str2 extern class ConsString extends String { first: String; second: String; } // substring of existing string // e.g. sliced = str.slice(i) extern class SlicedString extends String { parent: String; offset: Smi; } // alias / reference to other string extern class ThinString extends String { actual: String; } // string backed by memory not on the v8 heap // not used internally extern class ExternalString extends String { resource: ExternalPointer; resource_data: ExternalPointer; }

🔗 Key Points

V8 Objects use Map pointers to store type information

  • Prototype, Element Type, Property Slots, etc

Maps have transitions

  • Transitions are triggered when types "generalize"

V8 uses separate property and element arrays

  • Can be dictionaries if too many entries or sparse array

Element kind dictates how values are stored in element arrays

🔗 V8 Asides

In this section, we've placed useful tidbits and trivia about V8 that is, from experience, important, but doesn't quite fit into "the story" of the rest of this training module.

🔗 V8 Boxed Doubles

V8 cannot use pointer tagging to differentiate pointers and doubles like it can with SMIs. With JavaScript's 31 bit integers, we had a spare-bit to store a tag in, but the format of doubles does not allow for an easy trick like that.

Therefore, "native" double-values are "boxed" into entirely separate objects:

C++
class HeapNumber : public TorqueGeneratedHeapNumber<HeapNumber, PrimitiveHeapObject> { ... }
V8 Torque
@generateCppClass extern class HeapNumber extends PrimitiveHeapObject { value: float64; }

Unfortunately, this means that V8 must allocate 24 or 16 bytes of memory (TagPtr included) instead of just the 8 normally required to represent a double.

np | cp  (np = normal pointer, cp = compressed pointer)
---+----
00 | 00: [        Map*        ] (TagPtr)
08 | 04: [    Double Value    ] (double)

🔗 V8 Torque

Torque is a language developed by Google that is "TypeScript-like". It is higher-level for easier development, but still has access to internal C++ functions. You can find this code by looking for .tq files in the Chromium source tree.

It is most commonly used to implement builtin functions defined by the JS standard. For example, here is the torque implementation of Number.prototype.toString:

V8 Torque
// https://tc39.github.io/ecma262/#sec-number.prototype.tostring transitioning javascript builtin NumberPrototypeToString( js-implicit context: NativeContext, receiver: JSAny)( ...arguments): String { // 1. Let x be ? thisNumberValue(this value). const x = ThisNumberValue(receiver, 'Number.prototype.toString'); // 2. If radix is not present, let radixNumber be 10. // 3. Else if radix is undefined, let radixNumber be 10. // 4. Else, let radixNumber be ? ToInteger(radix). const radix: JSAny = arguments[0]; const radixNumber: Number = radix == Undefined ? 10 : ToInteger_Inline(radix); // 5. If radixNumber < 2 or radixNumber > 36, throw a RangeError exception. if (radixNumber < 2 || radixNumber > 36) { ThrowRangeError(MessageTemplate::kToRadixFormatRange); } // 6. If radixNumber = 10, return ! ToString(x). if (radixNumber == 10) { return NumberToString(x); } // 7. Return the String representation of this Number // value using the radix specified by radixNumber. if (TaggedIsSmi(x)) { return IntToString( Convert<int32>(x), Unsigned(Convert<int32>(radixNumber))); } if (x == -0) { return ZeroStringConstant(); } else if (::NumberIsNaN(x)) { return NaNStringConstant(); } else if (x == V8_INFINITY) { return InfinityStringConstant(); } else if (x == MINUS_V8_INFINITY) { return MinusInfinityStringConstant(); } return runtime::DoubleToStringWithRadix(x, radixNumber); }

For vulnerability research purposes, Torque is not particularly interesting but it's good to be aware of its existence.

You can find detailed information here: https://v8.dev/docs/torque

🔗 %DebugPrint

V8 exposes some additional debugging functionality to JavaScript with the command line flag --allow-natives-syntax.

One of the most useful functions is %DebugPrint, which will dump a huge amount of metadata and internal book-keeping for just about any construct you throw at it. If we try the following:

JavaScript
> let obj = {a:1}; > %DebugPrint(obj);

We will get:

DebugPrint: 0x1845aea0dbd1: [JS_OBJECT_TYPE]
 - map: 0x2358d0c8aa49 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x068254b82001 <Object map = 0x2358d0c80229>
 - elements: 0x1a7eedd80c21 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1a7eedd80c21 <FixedArray[0]> {
    #a: 1 (const data field 0)
 }
0x2358d0c8aa49: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 32
 - inobject properties: 1
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
...

Having quick & easy access to this native information will be extremely useful when writing and debugging exploits.

Warningwarning

If you prefer to write JavaScript without semicolons, beware the % prefix can sometimes be interpreted as a modulo operator, giving an error that DebugPrint (or whichever native function) is an invalid identifier. A quick fix is to prepend an extra semicolon e.g. ;%DebugPrint(...)

🔗 Release Mode DebugPrint

In the past, V8 would not include the "full" version of %DebugPrint in release builds.

This behavior is now controlled by the build option: v8_enable_object_print.

For older builds of V8, you could modify src/runtime/runtime-test.cc

  • Change #ifdef DEBUG to #if 1