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
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 HeapObject
s 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 Map
s 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:
- We have some sort of "value"
- 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:
JavaScriptd8> 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
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:
JavaScriptlet 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
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:
JavaScriptd8> 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.
WarningIf you prefer to write JavaScript without semicolons, beware the
%
prefix can sometimes be interpreted as a modulo operator, giving an error thatDebugPrint
(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