V8 Heap Archaeology: Finding Exploitation Artifacts in Chrome’s Memory
Author
Liam D.
Read Time
17 mins
Published
Feb 11, 2026
TL;DR : This post aims to introduce readers to the anatomy and detection of JavaScript memory corruption exploits that target Google Chrome’s V8 JavaScript engine. We’ll dive into the primitives attackers need during the first stage of a Chrome full-chain exploit and what artifacts these primitives leave behind in memory. It is these artifacts that defenders can utilise for proactive detection purposes even when the specific CVE/Bug is unknown.
Why This Matters
Most treat memory forensics as a reactive exercise-something that happens after alerts fire and compromise is already suspected; however, this misses a proactive opportunity I believe isn’t being utilised as much as it should be: the fact that exploits fail.
Browsers, and the exploits that target them, are incredibly complex., The latter typically consists of various bugs with varying degrees of reliability and determinism. Not only that but:
- Heap layouts shift between runs
- Garbage collection triggers at unpredictable moments
- Just-in-time (JIT) compilation can introduce non-determinism
Even well-engineered Chrome exploits don’t land 100% of the time and, when they fail, they often crash the renderer process.
These crashes are observable. On Windows, renderer crashes generate Windows Error Reporting (WER) dumps. Most organisations ignore these or treat them as benign browser instability, but a crashed renderer could be a failed exploitation attempt. These crash dumps contain the V8 heap’s state at the moment of failure, complete with any artifacts the attacker left behind during primitive construction.
Instead of waiting for successful compromise and subsequent attacker activity, you’re catching exploitation attempts at the point of failure resulting in their first failed attempt becoming your alert.
The Solution
Alongside this post, I’m releasing v8-forensics: a Rust crate for extracting and analysing JavaScript objects from Chrome process memory without symbols. It works across various Chrome versions (both release and debug builds) and flags the anomalies we’ll discuss throughout this post. Point it at a renderer crash dump and it tells you whether someone was trying to corrupt V8’s heap.
This work builds on research from the paper Juicing V8: A Primary Account for the Memory Forensics of the V8 JavaScript Engine, which first demonstrated symbol-less V8 object extraction, so a big thanks to the following authors for laying the groundwork that made this tool possible:
- Enoch Wang
- Smuel Zurowski
- Orion Duffy
- Tyler Thomas
- Ibrahim Baggili
Before we dive into the theory, here’s what detection looks like in practice:

The Anatomy of a V8 Memory Corruption Exploit
JavaScript memory corruption exploits typically follow the same exploitation path regardless of the initial bug used. Whether it’s a use-after-free, type confusion, integer overflow, or logic bug-attackers typically first convert that raw vulnerability into reliable, reusable primitives such as:
- out-of-bounds read/write – read from or write to adjacent memory beyond an array’s backing store
- addrof – leak the memory address of a JavaScript object
- fakeobj – trick V8 into treating attacker-controlled data as a legitimate object
- caged read/write – read from or write (R/W) to any memory address within the V8 sandbox
- arbitrary read/write – R/W any memory address within the Chrome renderer process
Although these primitives build on each other, the starting point always depends on the type of initial bug. Below are some examples:
Type Confusion Bugs
Bug -> fakeobj/addrof -> caged R/W -> sandbox escape -> arbitrary R/W -> code execution
Out-of-bounds (OOB) Bugs (integer overflow, incorrect bounds check, etc.)
Bug -> OOB R/W -> addrof/fakeobj -> caged R/W -> sandbox escape -> arbitrary R/W -> code execution
Direct Corruption Bugs (some Use-after-free (UAF) bugs)
Bug -> corrupt object fields -> OOB or addrof/fakeobj -> ...
The rest of this post follows these primitives and for each we’ll examine:
- What the attacker is trying to achieve
- What V8 internals they’re abusing to achieve it
- What artifacts get left behind in memory
Out-of-Bounds Read/Write
Many V8 vulnerabilities (whether it be logic bugs, incorrect JIT assumptions, integer overflows, or off-by-one errors) are typically used to provide an initial out-of-bounds array access by corrupting the Length field of a JSArray. Attackers use this OOB access to R/W past the end of an array, corrupting adjacent V8 objects to establish stronger primitives like addrof and fakeobj.
Foundation: What is a JSArray?
A JSArray consists of two separate heap objects: a JSArray header containing metadata (type, length, elements pointer) and a FixedDoubleArray or FixedArray holding the actual elements. Consider this JavaScript:
V8’s memory representation of this array would be as follows:

- Map Pointer – Describes the object’s type and layout (e.g., PACKED_DOUBLE_ELEMENTS vs HOLEY_ELEMENTS). V8 uses this to know how to interpret the object and what operations are valid. The V8 maintainers have a great blog post that clearly breaks down the internals of Maps
- Properties Pointer – Points to named properties (like arr.foo = “bar”). For most arrays, this is empty since arrays typically only have indexed elements, not named properties
- Elements Pointer – Points to the backing store containing the actual array elements (the FixedDoubleArray or FixedArray). Note: This points to offset +0 (the Map field) of the backing store, not the element data at +8
- Length – The JavaScript-visible .length property. Must always be <= elements.length in legitimate code
The Artifact
The key to detecting this primitive is the fact that for all legitimate JSArray objects, JSArray.length <= elements.length must be true. In the example above, both the JSArray and elements length are 3, so the invariant holds. V8 may allocate extra capacity for growth (e.g., during.push()), making elements.length > JSArray.length normal. The reverse (JSArray.length > elements.length) never occurs in legitimate code, and so can be used as a high-fidelity signal when it comes to detecting potential exploitation attempts of the V8 JavaScript engine.
The following is output of v8-forensics searching for corrupted lengths via the CorruptedLength detection. This was done on a memory dump taken after exploitation of a Type Confusion (CVE-2025-2135) on Chrome Stable release 134.0.6998.36 with 0 false positives:
loading dump: "./chrome_release.dmp"
loaded 797 regions (2564.2 MB)
arrays with corrupted length (array_length > elements_length):
corrupted array 1:
address: 0x2f7002ccd4c
map_address: 0x2f70010e449 (instance_type: 2119)
elements_kind: 4
array_length: 128 (CORRUPTED)
elements_length: 2
oob_elements: 126
addrof: Leaking Object Addresses
With OOB access established, attackers typically escalate to addrof and fakeobj primitives. These primitives utilise two JSArray objects that point to the same backing store but with different Maps:
- float_array’s Map: PACKED_DOUBLE_ELEMENTS -> V8 reads as raw IEEE 754 doubles.
- object_array’s Map: PACKED_ELEMENTS -> V8 reads as tagged object pointers.
Same data, interpreted two different ways depending on which array is accessed, this will make more sense later.
Foundation: ElementsKind and Type Confusion
V8 doesn’t use a one-size-fits-all approach for array storage. Instead, it tracks what kind of values an array contains and optimizes storage accordingly using the ElementsKind property in the array’s Map. The most important thing to understand is the difference between arrays storing doubles versus arrays storing objects:
Behind the scenes, V8 stores these arrays completely differently:

When you write float_array[0], V8 knows from the Map’s ElementsKind that this array holds raw doubles, so it performs an 8-byte load and interprets those bits as an IEEE 754 floating-point number. When you write object_array[0], V8 knows this array holds object references, so it performs a 4-byte load and interprets those bits as a tagged pointer. The purpose of tagged pointers is because V8 needs to distinguish between different value types at runtime. It does this by using the least significant bits of every value:

For example, the JavaScript number 42 is stored as the Smi 84 (42 << 1). The object {foo: 1} is stored as a 32-bit pointer with the low bit set to 1.
Foundation: Pointer compression
On 64-bit Chrome, V8 confines all heap objects to a 4GB “cage” region. Pointers are stored as 32-bit offsets from the cage base rather than full 64-bit addresses. This halves memory usage for pointers while keeping everything sandboxed within the cage. The full address is computed as cage_base + (compressed_pointer & ~tag_bits).
The Technique
If an attacker can make V8 disagree with itself about an array’s element type, whether that be through a direct type confusion bug or by corrupting a Map pointer via OOB, then they can now leak object addresses:

The addrof primitive leaks an object’s memory address by writing it through object_array and reading it back through float_array, for example:
A complete implementation of an addrof primitive would look like the following:
The same memory location is accessed twice with different interpretations:
- object_array: V8 treats slot [0] as a 32-bit tagged pointer (writes 4 bytes)
- float_array: V8 treats slot [0] as a 64-bit IEEE 754 double (reads 8 bytes)
This results in an attacker being able to extract the compressed pointer 0x0ABC1235, which is the memory address of the victim object ({secret: 0x1337}) within the V8 heap cage.
The Artifact
The key to detecting this primitive is that, for all legitimate arrays, the Map’s elements_kind must match the backing store’s instance type. An array with elements_kind = PACKED_DOUBLE_ELEMENTS must have a FixedDoubleArray backing store and an array with elements_kind = PACKED_ELEMENTS must have a FixedArray backing store. After addrof exploitation, the heap contains arrays where this invariant is violated. The map claims one element type while the backing store is actually another. This contradiction is impossible in legitimate JavaScript execution because V8 never creates arrays where the ElementsKind disagrees with the backing store type.
The following is output of v8-forensics searching for elements kind mismatches via the ElementsMapMismatch detection on a memory dump taken after exploitation of CVE-2025-2135 on Chrome Stable release 134.0.6998.36 with 0 false positives:
loading dump: "./chrome_release.dmp"
loaded 797 regions (2564.2 MB)
elements map mismatches detected:
mismatch 1:
address: 0x2f7002ccdd8
map_address: 0x2f70010e449 (instance_type: 2119)
elements_kind: 4
array_length: 4
elements_length: 0
elements_address: 0x2f7002ccf75
elements_map_address: 0x1
fakeobj: Forging Object References
The fakeobj primitive is the inverse of addrof. Where addrof leaks an address by reading a pointer as a float, fakeobj forges an object by writing floats that V8 interprets as a valid V8 JavaScript object.
The Technique
Using the same type confusion mechanism described in the addrof section above we end up with a situation where an array of floats (float_array) that can contain arbitrary 64-bit values can be interpreted as object pointers and fields by reading those floats via an array of objects (object_array) that share the same elements pointer.
With pointer compression, each JSArray field is 32 bits, meaning you can construct an entire fake JSArray using just 2 doubles (16 bytes):
The following is what the values look like in memory:

It would then be possible to use the fakeobj function to make V8 treat this data as a real object:
By writing just two carefully chosen 64-bit floats, an attacker can construct all four 32-bit fields of a JSArray header. When V8 reads this through object_array, it sees what looks like a perfectly valid JSArray object complete with a legitimate Map pointer, property storage, element storage, and a length. V8 has no way to know the attacker forged this object rather than the engine itself allocating it.
The Artifact
The key to detecting this primitive is that legitimate JavaScript never creates heap objects inside array element slots. A FixedArray or FixedDoubleArray backing store contains values (e.g. Smis, floats, or pointers to other objects) and not the objects themselves with their own Map pointers and internal structure. The detection scans a memory dump to find any instances of a JSArray (or other HeapObject) whose address falls within the element data region of a FixedArray. If this does occur, then we’ve found a fake object which is structurally impossible in legitimate V8 execution.
The following is output of v8-forensics searching for embedded objects via the EmbeddedInElements detection on a memory dump taken after exploitation of CVE-2025-2135 on Chrome Stable release 134.0.6998.36 with 0 false positives:
loading dump: "./chrome_release.dmp"
loaded 797 regions (2564.2 MB)
fake arrays embedded in other arrays:
embedded array 1:
address: 0x2f7002ccd4c
map_address: 0x2f70010e449 (instance_type: 2119)
elements_kind: 4
array_length: 128
elements_length: 2
elements_address: 0x2f7002ccd45
elements_map_address: 0x2f7000008a1
container_address: 0x2f7002ccd28
container.array_length: 2
container.elements_length: 2
container.elements_address: 0x2f7002ccd45
offset: 8 bytes (element ~0)
Caged Arbitrary Read/Write
With addrof and fakeobj primitives having been established, attackers could escalate to arbitrary read/write within the V8 heap cage. This primitive allows reading from or writing to any address within the 4GB sandbox region by manipulating an array’s elements pointer.
The Technique
Assuming we have a corrupted array (fake_arr) with OOB access then it would be possible to reach adjacent JSArray object headers on the heap. If we can locate where another JSArray containing floats lives relative to fake_arr it would be possible to directly overwrite its elements pointer to point anywhere in the cage. Consider this heap layout:

By writing to fake_arr[18] it is possible to directly modify float_arr’s elements pointer. This particular way of setting up caged arbitrary read/write relies upon first identifying where float_arr is relative to fake_arr as seen below:
Once the location of float_arr is identified, go ahead and create a reusable write32 function such as the following:
The reasoning behind subtracting eight from target_addr is because when V8 accesses float_arr[0], it doesn’t R/W directly from/to where the elements pointer points. The elements pointer points to the start of the backing store object (FixedDoubleArray), which has its own header (Map + Length) and so V8 automatically skips past this header to reach the actual element data. A visual example demonstrating writing to the 0x1234 address can be seen below:

The Artifact
The key to detecting this primitive is recognizing that caged arbitrary read/write is built upon the same type confusion foundation as addrof and fakeobj. When an attacker overwrites float_arr‘s elements pointer to target arbitrary addresses, the array’s Map still claims elements_kind = PACKED_DOUBLE_ELEMENTS, which mandates a FixedDoubleArray backing store. However, the corrupted elements pointer now points to arbitrary memory that is almost certainly not a legitimate FixedDoubleArray with the expected Map at offset +0.
This means the ElementsMapMismatch detection remains effective here. When float_arr’s elements pointer is redirected to target_addr – 8, V8 will attempt to interpret whatever bytes exist at that location as a backing store. Unless the attacker is specifically targeting another FixedDoubleArray (which would severely limit the primitive’s usefulness), the Map pointer at the new elements address will not match what the array’s elements_kind requires. The invariant violation persists: the JSArray claims to hold packed doubles, but its elements pointer references memory with an incompatible or nonsensical Map.
Additionally, the CorruptedLength detection may trigger if the attacker has inflated the corrupted array’s length to enable OOB access in the first place. This is a common prerequisite for reaching adjacent heap objects to establish arbitrary read/write as previously discussed.
Conclusion
The primitives required for V8 exploitation (corrupted array lengths, type-confused backing stores, fake objects) violate fundamental invariants that V8 maintains during normal operation. These violations persist in memory even after successful exploitation, and they’re structurally impossible to produce through legitimate JavaScript. The v8-forensics crate detects these anomalies across Chrome versions without requiring symbols or prior knowledge of the specific vulnerability. It turns renderer crash dumps from ignored noise into actionable signals.
Note that the following detection limitations do apply:
- Although sophisticated exploits may restore corrupted fields before proceeding, this would be very hard to do after the first exploit failure before clean up could occur
- Once outside V8’s heap, different forensic techniques apply
- Some vulnerabilities don’t require array corruption, though a large majority do rely on arrays to establish primitives
Future work that utilises the forensic techniques in this blog post could apply to:
- TypedArray corruption detection
- ArrayBuffer backing store analysis
- WebAssembly memory anomalies
Your users’ browser crashes might just be browser crashes, or they might be someone’s zero-day failing to land. Now you have a way to tell the difference:
- Repository: https://github.com/medioxor/v8-forensics
- Based on research from: https://www.sciencedirect.com/science/article/pii/S2666281722000816