- 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)
The patch mitigating the exploit from Part 1 was designed to prevent overwriting the WebAssembly code space. However, because we still have access to an arbitrary address write primitive outside the V8 sandbox, we can simply overwrite the flag at runtime to disable this protection. This effectively reverts the environment to the state described in Part 1, restoring our ability to execute arbitrary code.
Setup
- Ubuntu 20.04
- [wasm] Ship code protection via mprotect (Feb 15, 2022)
Place v8setup.py and sandbox.diff in 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
WebAssembly Code Protection
82 | // static |
If FLAG_wasm_write_protect_code_memory is set to true, CodeSpaceWriteScope::SetExecutable() calls NativeModule::RemoveWriter() to revoke write permissions whenever write access to the WebAssembly code space is no longer needed.
822 | void WasmCodeAllocator::MakeWritable(base::AddressRegion region) { |
Write permissions are restored only when necessary. WasmCodeAllocator::MakeWritable() calculates the memory range requiring write access and calls WasmCodeAllocator::InsertIntoWritableRegions().
894 | new_writable_memory += region.size(); |
WasmCodeAllocator::InsertIntoWritableRegions() calls SetPermissions() to set the region’s permissions to RWX.
This mechanism prevents us from writing shellcode to the code space as we did in Part 1, since write permissions likely won’t be active at the moment we attempt our write. However, CodeSpaceWriteScope::SetExecutable() checks FLAG_wasm_write_protect_code_memory every time it is called. If we overwrite this flag to false at runtime, the protection is immediately disabled, allowing us to achieve arbitrary code execution exactly as we did in Part 1.
Exploitation
Obtaining address of FLAG_wasm_write_protect_code_memory
339 | DECL_PRIMITIVE_ACCESSORS(isolate_root, Address) |
385 | V(kIsolateRootOffset, kSystemPointerSize) \ |
2052 | // This class contains a collection of data accessible from both C++ runtime |
The WasmInstanceObject class has an accessor, isolate_root, which points to Isolate::isolate_data_.

d8.file.execute("v8/test/mjsunit/wasm/wasm-module-builder.js"); |


197 | ExternalReferenceTable external_reference_table_; |
The IsolateData class contains external_reference_table_, which holds pointers to d8 execution flags.

The order of the execution flags remains consistent as long as the V8 version does not change.
374 | DEFINE_BOOL(builtin_subclassing, true, |
940 | DEFINE_BOOL(wasm_write_protect_code_memory, true, |
Therefore, we can reliably calculate the address of FLAG_wasm_write_protect_code_memory by using a known flag address found in the IsolateData::external_reference_table_.
Using TypedArray
The following scripts demonstrate the full exploit chain using TypedArray:

Using ArrayBuffer
Similarly, here is the implementation using ArrayBuffer:

Bisection
[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 enables wasm_write_protect_code_memory by default.
Patch
[sandbox] Enable sandboxed pointers on Desktop (May 05, 2022)
Revert “[sandbox] Enable sandboxed pointers on Desktop” (May 06, 2022)
Reland “[sandbox] Enable sandboxed pointers on Desktop” (May 06, 2022)
Revert “Reland “[sandbox] Enable sandboxed pointers on Desktop”” (May 06, 2022)
Reland “Reland “[sandbox] Enable sandboxed pointers on Desktop”” (May 10, 2022)
The commits above enabled v8_enable_sandboxed_pointers by default when v8_enable_sandbox is enabled. Consequently, the external_pointer of the JSTypedArray class is no longer stored as a full 8-byte pointer, and the backing_store pointer of the JSArrayBuffer class is now allocated within the V8 sandbox.
[sandbox] Also enable the sandbox outside of Chromium builds (Jun 17, 2022)
Revert “[sandbox] Also enable the sandbox outside of Chromium builds” (Jun 20, 2022)
Reland “[sandbox] Also enable the sandbox outside of Chromium builds” (Jun 21, 2022)
[sandbox] Disable the sandbox by default outside of Chromium builds (Jul 19, 2022)
[sandbox] Enable the sandbox by default in V8 builds (Sep 23, 2022)
v8_enable_sandbox was enabled by default for standalone V8 builds in the commits above, while it was already enabled in Chromium builds in the following commit.
Turn on v8_enable_virtual_memory_cage for Chromium builds (Oct 4, 2021)