This is an isolate-and-bounds lab, not a renderer 0-day. Target is d8 from a Chromium build I keep for notes (V8 version 10.9 era, matching a 2023 Chrome 109 lab image) and a 40-line JS file that allocates a Uint8Array and refuses out-of-range indexes in a lab assert. Goal: print the typed-array dump, throw on i >= length, and optionally %DebugPrint under --allow-natives-syntax. I do not JIT-spray, I do not confuse maps into a fake object, I do not turn an OOB into a backing-store primitive.
1Figure 1. Untrusted JS hits V8, then the renderer sandbox. This lab stops at a thrown assert inside d8.
2script -> typed array bounds -> LAB_ASSERT throw (or d8 abort)
3JIT / maps / isolates are the map; we do not break them here
Lab layout
1labs/v8_lab/
2 toy.js # Uint8Array + lab assert
3 crash.js # uncaught throw for --abort-on-uncaught-exception
4 d8-version.txt
1$ ./d8 --version
2V8 version 10.9.194.5
3
4$ ./d8 --help | egrep 'allow-natives-syntax|abort-on-uncaught|trace-maps'
5 --allow-natives-syntax (allow natives syntax)
6 --abort-on-uncaught-exception
7 --trace-maps
Host is a Linux lab VM. d8 is the V8 shell, not Chrome. A crash here is process abort of the shell, not a sandbox escape.
Lab binary (JS)
1// toy.js — typed array, explicit bounds, no OOB primitive
2const a = new Uint8Array(8);
3a.fill(0x41);
4
5function read_at(i) {
6 if ((i >>> 0) >= a.length) {
7 throw new Error("LAB_ASSERT oob i=" + i + " len=" + a.length);
8 }
9 return a[i];
10}
11
12function write_at(i, v) {
13 if ((i >>> 0) >= a.length) {
14 throw new Error("LAB_ASSERT oob i=" + i + " len=" + a.length);
15 }
16 a[i] = v & 0xff;
17}
18
19print("len " + a.length);
20print("a[0] " + read_at(0));
21write_at(7, 0x5a);
22print("a[7] " + read_at(7));
23try {
24 print(read_at(8));
25} catch (e) {
26 print(String(e));
27}
1$ ./d8 toy.js
2len 8
3a[0] 65
4a[7] 90
5Error: LAB_ASSERT oob i=8 len=8
That is the whole happy path. Index 8 on length 8 is rejected in JS, not in a missing CheckBounds node. Engine bugs in this class are “the JIT deleted a check the interpreter still did”. I do not delete the check.
Optional: --allow-natives-syntax
Natives are a debug surface. I use them to print the object, not to call %SetAllocationTimeout tricks.
1// toy_native.js — same array, DebugPrint, then the assert
2const a = new Uint8Array(8);
3a.fill(0x41);
4%DebugPrint(a);
5function read_at(i) {
6 if ((i >>> 0) >= a.length) throw new Error("LAB_ASSERT oob i=" + i);
7 return a[i];
8}
9read_at(8);
1$ ./d8 --allow-natives-syntax toy_native.js
2DebugPrint: 0x1a0[REDACTED]: [JSTypedArray]
3- map: 0x1a0[REDACTED] <Map(UINT8_ELEMENTS)> [FastProperties]
4- prototype: 0x1a0[REDACTED] <Object map = 0x...>
5- elements: 0x1a0[REDACTED] <FixedUint8Array[8]> [UINT8_ELEMENTS]
6- embedder fields: 2
7- length: 8
8- byte_length: 8
9- byte_offset: 0
10- buffer: 0x1a0[REDACTED] <ArrayBuffer map = 0x...>
11Error: LAB_ASSERT oob i=8
Addresses are redacted. What I actually need from %DebugPrint:
UINT8_ELEMENTS— the elements kind. A real type-confusion write-up would claim this kind is no longer true. I do not force a transition.length: 8/byte_length: 8— the bounds the assert used.ArrayBufferbacking store — a separate object. Length mismatch between the view and the buffer is a class of bug; both are 8 here.
I do not call %OptimizeFunctionOnNextCall + %NeverOptimize dances to land a check-elimination. Optional natives in this note stop at DebugPrint.
Sanitized reproduction: abort is a lab crash
Uncaught throw, then the shell flag that turns it into a fatal:
1// crash.js
2const a = new Uint8Array(4);
3function poke(i, v) {
4 if ((i >>> 0) >= a.length)
5 throw new Error("LAB_ASSERT oob i=" + i + " len=" + a.length);
6 a[i] = v;
7}
8poke(4, 1); // uncaught
1$ ./d8 crash.js
2toy: uncaught:
3Error: LAB_ASSERT oob i=4 len=4
4 at poke (crash.js:5:11)
5 at crash.js:8:1
6
7$ ./d8 --abort-on-uncaught-exception crash.js
8#
9# Fatal error in , line 0
10# unreachable code
11#
12#
13#
14#FailureMessage Object: 0x7ff[REDACTED]
15==== C stack trace ===============================
16 d8 [REDACTED]
17# aborted (core dumped) ; lab only, same box
1$ gdb -q ./d8 core
2(gdb) bt
3#0 abort () from /lib/x86_64-linux-gnu/libc.so.6
4#1 V8_Fatal ...
5#2 v8::internal::Isolate::Throw ...
6# ... frames into d8 main [REDACTED]
Tombstone: input poke(4,1) on Uint8Array(4), message LAB_ASSERT oob i=4 len=4, process abort with --abort-on-uncaught-exception. That is a lab assert, not an in-engine CHECK about a corrupted map.
ASan: I do not rebuild V8 with ASan for this note. A userspace analog — a C typed-buffer with the same bounds — does:
1/* ta_lab.c — mirror of crash.js, ASan on the C side */
2#include <stdint.h>
3#include <stdio.h>
4#include <stdlib.h>
5int main(void) {
6 uint8_t a[4] = {0};
7 int i = 4; /* the JS poke(4) */
8 if ((unsigned)i >= 4) {
9 fprintf(stderr, "LAB_ASSERT oob i=%d len=4\n", i);
10 return 1;
11 }
12 a[i] = 1; /* not reached */
13 return 0;
14}
1$ cc -O0 -fsanitize=address -g -o ta_lab ta_lab.c
2$ ./ta_lab
3LAB_ASSERT oob i=4 len=4
4# ASan silent — we did not store. Remove the if to see heap/stack overflow;
5# I keep the if. The engine bug is when the equivalent if is gone after JIT.
Isolates, JIT, renderer (the map, still no exploit)
1untrusted script / Wasm
2 -> V8 isolate (heap, maps, typed-array views)
3 -> interpreter / Sparkplug / Maglev / TurboFan
4 -> renderer process
5 -> sandbox + IPC brokers
6 -> OS
What I write next to a Chrome V8 CVE, using this lab as vocabulary:
- Isolate / context — embedder wiring. Node and Electron often have no renderer sandbox. Same engine, different wrap.
- Map / elements kind —
%DebugPrintshowedUINT8_ELEMENTS. Type confusion = trusted kind was wrong. - JIT check elimination —
read_atintoy.jsis the check TurboFan must not drop. - Typed array view vs ArrayBuffer — length 8 / byte_length 8 in the dump. Mismatch is a class.
- Patch the embedder (Chrome/Electron/Node), not a handwritten
d8flag, in production.
--trace-maps on the toy, truncated:
1$ ./d8 --trace-maps toy.js
2[trace-maps] [0x...] Initial: ... JSObject
3[trace-maps] [0x...] Transition: ... UINT8_ELEMENTS
4len 8
5...
Useful when a crash dump mentions a map id. Not useful as a primitive.
Mitigation
- Chrome auto-update; Electron rebuilt on current Chromium; Node on a supported line. Inventory V8 version, not only the product major.
- Site isolation / renderer sandbox on. A
d8abort in this lab is unsandboxed by design — do not run untrusted scripts ind8. - Crash telemetry: cluster renderer crashes in V8 JIT ranges. This lab’s abort is
V8_Fatalfrom an uncaught JS exception — different bucket than a SIGSEGV in generated code. - Custom embeds: one isolate per trust domain; no privileged bindings on the untrusted context.
--allow-natives-syntaxis off in production. It is a debug switch. I used it for%DebugPrintonly.
What I file after this lab
d8 --version→V8 version 10.9.194.5toy.js:len 8,a[0]=65,LAB_ASSERT oob i=8 len=8%DebugPrint:UINT8_ELEMENTS, length 8, ArrayBuffer backing, addresses redacted- Crash:
d8 --abort-on-uncaught-exception crash.js→V8_Fatal/ abort, messageLAB_ASSERT oob i=4 len=4 - C mirror
ta_lab.cwith ASan: assert prints, no overflow - Out of scope: map confusion, JIT check elimination, sandbox escape,
d8natives other thanDebugPrint
Commands appendix
1./d8 --version
2./d8 toy.js
3./d8 --allow-natives-syntax toy_native.js
4./d8 --abort-on-uncaught-exception crash.js
5cc -O0 -fsanitize=address -g -o ta_lab ta_lab.c && ./ta_lab