- Escaping the V8 Sandbox by Overwriting WebAssembly Jump Table via TypedArray or ArrayBuffer (V8 < 10.0.138)
- Escaping the V8 Sandbox by Overwriting WebAssembly Jump Table via TypedArray or ArrayBuffer: Part 2 (10.0.138 <= V8 < 10.3.163)
In this post, I will demonstrate a technique to escape the V8 sandbox using TypedArray or ArrayBuffer. This walkthrough assumes we already have standard sandboxed exploit primitives.
Setup
- Ubuntu 20.04
0ac7e1203fcb957851887fb140dc8a41139846a5(Feb 15, 2022)
Place v8setup.py and sandbox.diff into your working directory and run v8setup.py.
[sandbox] Add new Memory Corruption API (May 20, 2022)
When enabled, this API exposes a new global ‘Sandbox’ object which contains a number of functions and objects that in effect emulate typical memory corruption primitives constructed by exploits. In particular, the ‘MemoryView’ constructor can construct ArrayBuffers instances that can corrupt arbitrary memory inside the sandbox. Further, the getAddressOf(obj) and getSizeInBytesOf(obj) functions can be used respectively to obtain the address (relative to the base of the sandbox) and size of any HeapObject that can be accessed from JavaScript.
This API is useful for testing the sandbox, for example to facilitate developing PoC sandbox escapes or writing regression tests. In the future, it may also be used by custom V8 sandbox fuzzers.
sandbox.diff corresponds to the commit above, which introduces a new memory corruption API. This allows us to simulate sandboxed exploit primitives for testing purposes.
Analysis
TypedArray
The TypedArray object in JavaScript is represented internally by the JSTypedArray class.
90 | extern class JSTypedArray extends JSArrayBufferView { |
The JSTypedArray class contains two types of pointers:
- The
base_pointerpoints to theByteArrayobject that stores the data. - The
external_pointerholds the offset from thebase_pointerto the actual address where the data is stored.
let arr = new BigUint64Array(1); |


When accessing the data, V8 calculates the address by adding base_pointer and external_pointer.


Since the JSTypedArray object resides within the V8 sandbox, we can:
- Leak the V8 sandbox base address by reading the
external_pointerusing the sandboxed arbitrary address read primitive. - Access arbitrary addresses outside the V8 sandbox via the
JSTypedArrayobject by overwriting theexternal_pointerand thebase_pointerwith the sandboxed arbitrary address write primitive.
ArrayBuffer
The ArrayBuffer object in JavaScript is represented internally by the JSArrayBuffer class.
14 | extern class JSArrayBuffer extends JSObject { |
The JSArrayBuffer class contains a backing_store pointer, which points to the actual address where the data is stored.
let buf = new ArrayBuffer(8); |


When accessing the data, V8 reads from the backing store of the JSArrayBuffer object.

Since the JSArrayBuffer object resides within the V8 sandbox, we can access arbitrary addresses outside the V8 sandbox by overwriting the backing_store pointer using the sandboxed arbitrary address write primitive.
WebAssembly Code Space
2188 | // Try up to two times; getting rid of dead JSArrayBuffer allocations might |
When a WebAssembly module is constructed, WasmCodeManager::NewNativeModule() allocates a code_space for that module.
1581 | if (needs_jump_table) { |
NativeModule::AddCodeSpaceLocked() creates a jump_table at the beginning of the code space. The jump table dispatches execution to the appropriate WebAssembly functions.
1898 | // Even when we employ W^X with FLAG_wasm_write_protect_code_memory == true, |
The code space is mapped with RWX permissions to allow WebAssembly functions to be compiled and executed at runtime.
d8.file.execute("v8/test/mjsunit/wasm/wasm-module-builder.js"); |
The JavaScript code above creates a WebAssembly module, which contains three empty functions exported to JavaScript.

The jump table consists of jmp instructions, where each entry corresponds to a function in the module.
We can access the exported functions using WebAssembly.Instance.prototype.exports.
let instance = new WebAssembly.Instance(module); |

Exported WebAssembly functions are represented internally by the JSFunction class, just like any other ordinary JavaScript function.
20 | // This class does not use the generated verifier, so if you change anything |
The JSFunction class has an accessor named code pointing to a CodeDataContainer object.
86 | // Cached value of code().InstructionStart(). |
104 | // Alias for code_entry_point to make it API compatible with Code. |
The CodeDataContainer class has an accessor named code_entry_point pointing to the execution entry point of the function.
% DebugPrint(instance.exports.f3); |


instance.exports.f3(); |

At this point, rdi holds the address of the Function object. The instructions from here get the call target corresponding to the function.





The instruction pointer eventually hits the third entry of the jump table, which corresponds to f3(), the function we called.
After we implement an arbitrary address write primitive, we can overwrite the jump table with our shellcode, so that it’s executed when the WebAssembly function is called.
Exploitation
Using TypedArray
The following scripts demonstrate the full exploit chain using TypedArray:

Using ArrayBuffer
Similarly, here is the implementation using ArrayBuffer:

Patch
[wasm] Ship code protection via mprotect (Feb 15, 2022)
Even though this is not a perfect protection, it will make it harder to write to the wasm code space because it’s not permanently RWX.
The patch above mitigates the technique demonstrated in this post by preventing WebAssembly code spaces from being permanently RWX. This prevents us from simply overwriting the memory with shellcode.