High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker532595489
Fix commitde11d56041d5 (v8/v8) +22/-1
CISA KEVNot listed
CreditedSalvatore Gulizia (nickname: Serotav)
Disclosed2026-07-14

Files Changed

  • src/builtins/builtins-regexp-gen.cc
  • test/mjsunit/mjsunit.status
  • test/mjsunit/regress/regress-532595489.js
From de11d56041d5aa9c5e69b031990ad17068dbef7a Mon Sep 17 00:00:00 2001
From: pthier <[email protected]>
Date: Thu, 09 Jul 2026 17:00:27 +0200
Subject: [PATCH] [regexp] Hard Check that index + 1 is a Smi in AdvanceStringIndex

Bug: 532595489
Change-Id: I3fb2a3246ebbf801b883cc9fe22a7903cf7b1edd
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8070079
Reviewed-by: Jakob Linke <[email protected]>
Commit-Queue: Patrick Thier <[email protected]>
Auto-Submit: Patrick Thier <[email protected]>
Cr-Commit-Position: refs/heads/main@{#108562}
---

diff --git a/src/builtins/builtins-regexp-gen.cc b/src/builtins/builtins-regexp-gen.cc
index b371c20..be445c3 100644
--- a/src/builtins/builtins-regexp-gen.cc
+++ b/src/builtins/builtins-regexp-gen.cc
@@ -1603,7 +1603,8 @@
     // Must be in Smi range on the fast path. We control the value of {index}
     // on all call-sites and can never exceed the length of the string.
     static_assert(String::kMaxLength + 2 < Smi::kMaxValue);
-    CSA_DCHECK(this, TaggedIsPositiveSmi(index_plus_one));
+    // TODO(532595489): Hard CHECK for now as a quick fix.
+    CSA_CHECK(this, TaggedIsPositiveSmi(index_plus_one));
   }
 
   Label if_isunicode(this), out(this);
diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status
index 0606336..0d1d12a 100644
--- a/test/mjsunit/mjsunit.status
+++ b/test/mjsunit/mjsunit.status
@@ -86,6 +86,9 @@
   # https://crbug.com/485267831
   'regress/regress-485267831': [SKIP],
 
+  # https://crbug.com/532595489
+  'regress/regress-532595489': [FAIL, CRASH, NO_VARIANTS, ['not pointer_compression', SKIP]],
+
   ##############################################################################
   # Tests where variants make no sense.
   'd8/enable-tracing': [PASS, NO_VARIANTS],
diff --git a/test/mjsunit/regress/regress-532595489.js b/test/mjsunit/regress/regress-532595489.js
new file mode 100644
index 0000000..c80f7e0
--- /dev/null
+++ b/test/mjsunit/regress/regress-532595489.js
@@ -0,0 +1,17 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const re = /(?:)/;
+
+function Species() {
+  return re;
+}
+
+const pseudo_re = {
+  flags: 'g',
+  lastIndex: 1073741823,
+  constructor: { [Symbol.species]: Species },
+};
+
+RegExp.prototype[Symbol.matchAll].call(pseudo_re, '').next();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status
index 0606336..0d1d12a 100644
--- a/test/mjsunit/mjsunit.status
+++ b/test/mjsunit/mjsunit.status
@@ -86,6 +86,9 @@
   # https://crbug.com/485267831
   'regress/regress-485267831': [SKIP],
 
+  # https://crbug.com/532595489
+  'regress/regress-532595489': [FAIL, CRASH, NO_VARIANTS, ['not pointer_compression', SKIP]],
+
   ##############################################################################
   # Tests where variants make no sense.
   'd8/enable-tracing': [PASS, NO_VARIANTS],
diff --git a/test/mjsunit/regress/regress-532595489.js b/test/mjsunit/regress/regress-532595489.js
new file mode 100644
index 0000000..c80f7e0
--- /dev/null
+++ b/test/mjsunit/regress/regress-532595489.js
@@ -0,0 +1,17 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const re = /(?:)/;
+
+function Species() {
+  return re;
+}
+
+const pseudo_re = {
+  flags: 'g',
+  lastIndex: 1073741823,
+  constructor: { [Symbol.species]: Species },
+};
+
+RegExp.prototype[Symbol.matchAll].call(pseudo_re, '').next();
Loading diff…

Original Bug Report

reported by [email protected]

V8 RegExp.matchAll fast path stores HeapNumber lastIndex through Smi no-barrier path

VULNERABILITY DETAILS

Pure JavaScript can make RegExp.prototype[Symbol.matchAll] store a young HeapNumber into an old JSRegExp.lastIndex field through a fast path typed as Smi.

That Smi store lowers to StoreObjectFieldNoWriteBarrier, so the old-to-new edge is not recorded. A later young GC can reclaim/reuse the young HeapNumber while the old JSRegExp.lastIndex field still points at the stale young-space address.

Minimal trigger shape:

  1. A fake receiver passes flags: "g" and lastIndex: 1073741823 (Smi::kMaxValue on x64 pointer-compression builds).
  2. constructor[Symbol.species] returns a different fast JSRegExp.
  3. matchAll iterator-next handles an empty match and advances the species regexp’s lastIndex.
  4. AdvanceStringIndexFast produces 1073741824, which is a HeapNumber.
  5. FastStoreLastIndex(JSRegExp, Smi) stores it through the Smi/no-barrier overload.

Relevant current-source refs:

src/builtins/regexp-match-all.tq:178-188
  FastLoadLastIndex -> AdvanceStringIndexFast -> FastStoreLastIndex

src/builtins/builtins-regexp-gen.h:217-220
  AdvanceStringIndexFast(...) is declared TNode<Smi> but casts
  AdvanceStringIndex(...): Number

src/builtins/builtins-regexp-gen.cc:163-168
  FastStoreLastIndex(TNode<JSRegExp>, TNode<Smi>) stores JSRegExp::kLastIndexOffset

src/codegen/code-stub-assembler.cc:4043-4046
  StoreObjectField(TNode<Smi>) uses StoreObjectFieldNoWriteBarrier

Expected behavior: the fast path should not store a heap object through a Smi no-write-barrier store. A non-Smi result should use a generic tagged-object store with the normal write barrier, or the path should deopt/fallback before writing.

Actual behavior: valid JavaScript reaches the Smi store with a HeapNumber, causing stale/corrupt lastIndex state after GC.

I also paired this RegExp primitive with an already known sandbox escape and used the combined chain to flag the Chrome 150 v8CTF target.

VERSION

Chrome Version:

d8 / V8 15.2.0 (candidate)
V8 source commit: 0c7a9d0d8c05491e8053d7259a1a6b11e75c1907
Release d8 build id: 0c3af5e5a4d734b5
ASAN d8 build id: cd9b6305dec563bd

The combined exploitability check was against Chrome for Testing M150 in the v8CTF environment.

Operating System:

Ubuntu 24.04.4 LTS x86_64
Linux 6.8.0-101-generic

REPRODUCTION CASE

Please attach the files in attachments/ directly.

Invariant break, no flags

File:

attachments/poc_invariant_lastindex_heapnumber.js

Command:

d8 attachments/poc_invariant_lastindex_heapnumber.js

Observed:

1073741824 number

This shows the fast path stored Smi::kMaxValue + 1 into lastIndex as a HeapNumber.

Release crash, no flags

File:

attachments/poc_missing_barrier_crash.js

Command:

d8 attachments/poc_missing_barrier_crash.js

Observed 5/5 locally:

Received signal 11 SEGV_ACCERR ...
exit=139

Supporting files:

attachments/release_crash_summary.txt
attachments/release_crash_sample.txt
attachments/release_build_info.txt

ASAN crash

Command:

ASAN_OPTIONS=symbolize=1:detect_leaks=0 \
  out.gn/x64.asan/d8 --disable-in-process-stack-traces \
  attachments/poc_missing_barrier_crash.js

The --disable-in-process-stack-traces flag is only used so ASAN owns the crash report instead of V8’s in-process signal handler.

Supporting files:

attachments/asan_stack.txt
attachments/asan_build_info.txt

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash:

d8 process crash. In Chrome this is renderer-reachable V8 code.

Crash State:

AddressSanitizer: SEGV READ
#0 Builtins_TypeOfHandler
#3 v8::internal::Invoke(...) src/execution/execution.cc:475
#5 v8::Script::Run(...) src/api/api.cc:2048
#6 v8::Shell::ExecuteSource(...) src/d8/d8.cc:1150

Full symbolized ASAN output is in:

attachments/asan_stack.txt

Client ID:

N/A

CREDIT INFORMATION

Reporter credit:

Salvatore Gulizia (nickname: Serotav)
View on issue tracker