CVE-2026-15776
Overview
Files Changed
src/builtins/builtins-regexp-gen.cctest/mjsunit/mjsunit.statustest/mjsunit/regress/regress-532595489.js
Patch
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();
Regression Test / PoC
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();
Original Bug Report
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:
- A fake receiver passes
flags: "g"andlastIndex: 1073741823(Smi::kMaxValueon x64 pointer-compression builds). constructor[Symbol.species]returns a different fastJSRegExp.matchAlliterator-next handles an empty match and advances the species regexp’slastIndex.AdvanceStringIndexFastproduces1073741824, which is aHeapNumber.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)