Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in V8
DescriptionInteger overflow in V8
ComponentV8
Sub-componentRegexp
Bug ClassInteger Overflow
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Diff confirms int to uint32_t change, static_assert on kMaxLength*5, and length used for allocation. OOB write well supported; triggering needs a near-kMaxLength source (large memory) but default-config reachable.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker420697404
Fix commit62ee3244f3b2 (v8/v8) +18/-12
CWECWE-472, CWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedShaheen Fazim
Reported2025-05-27 · bounty $7000 (per vendor advisory)
Fix landed2025-06-03 (public git)
Disclosed2025-06-17
Patch gap14 days — fix public in source before the advisory

Background

`EscapeRegExpSource`
the routine that produces the escaped textual source of a RegExp (e.g. for RegExp.prototype.source), escaping characters like unescaped / and line terminators.
`CountAdditionalEscapeChars`
a first pass that counts how many extra characters escaping will add, so the destination string can be sized before a second write pass fills it.
Growth factor
the worst per-character expansion is 5 (line separators `
`String::kMaxLength`
V8’s maximum string length; a source can approach it, so intermediate counts can be large enough to overflow a 32-bit signed accumulator.

Root Cause Analysis

In src/objects/js-regexp.cc, CountAdditionalEscapeChars accumulated the number of added escape characters in a signed int escapes. For a regexp source near String::kMaxLength consisting largely of characters with the maximum 5× growth factor, the true additional count can exceed INT_MAX, overflowing the signed accumulator (undefined behavior / a wrapped, possibly negative value). The caller then computed int length = source->length() + additional_escape_chars, also in signed int, and used that truncated/negative length to allocate the result SeqOneByteString/SeqTwoByteString; the second pass then wrote all the real escaped output into that under-sized buffer, writing out of bounds.

The fix changes escapes, the WriteStringToCharVector cursor d, the write-pass indices s/d, and additional_escape_chars/length to uint32_t, and adds a static_assert(uint64_t{String::kMaxLength} * 5 < numeric_limits<uint32_t>::max()) proving a uint32_t can always hold the maximum escaped length, plus a DCHECK_LE bounding source->length() + additional_escape_chars to uint32_t range. This guarantees the counted length matches the buffer that is actually allocated, eliminating the under-allocation.

Key insight
The single mistake was accumulating and summing the escaped-length in signed int, which can overflow for near-maximum-length sources and yield an under-sized allocation; the fix moves the counter and length arithmetic to uint32_t and statically proves that type always suffices for kMaxLength * 5.

Attack Path

  1. Build a huge escapable source Script constructs a RegExp whose source string is close to String::kMaxLength and dominated by characters that escape to the 5× worst case (e.g. line separators).
  2. Trigger escaping Script reads the escaped source (e.g. via RegExp.prototype.source / a path calling EscapeRegExpSource), invoking CountAdditionalEscapeChars.
  3. Overflow the count The signed int escapes/length arithmetic overflows, producing a wrapped (too-small or negative) length used to allocate the result string.
  4. Out-of-bounds write The write pass emits the full escaped output into the under-allocated Seq{One,Two}ByteString, writing past its end and corrupting adjacent heap memory (the diff shows the overflow and the sizing fix; the corruption is the direct consequence of the under-sized allocation).

Impact Assessment

An attacker with script execution in the renderer can trigger a heap out-of-bounds write via integer overflow in EscapeRegExpSource (advisory: “Integer overflow in V8”), a corruption primitive suitable for further exploitation. The precondition is the ability to allocate and escape a regexp source near String::kMaxLength, which requires substantial memory but is reachable from ordinary JavaScript. The advisory does not report in-the-wild exploitation, and any escalation to a read/write primitive or code execution beyond the OOB write is not demonstrated by this diff (inferred).

Changed Functions

FunctionChangeNotes
if
src/objects/js-regexp.cc
modified

Files Changed

  • src/objects/js-regexp.cc

Audit Directions

  • Two-pass size-then-write
    Audit count-then-allocate-then-fill patterns for accumulators or length sums in int that can overflow when inputs approach String::kMaxLength or other large bounds.
  • Growth-factor arithmetic
    Review string transformations (escaping, encoding, normalization) whose output can be a multiple of the input length and confirm the size type can hold maxInput * maxGrowth (ideally enforced by static_assert).
  • Signed length mixing
    Search for int length = a + b sizing an allocation where a/b derive from lengths; prefer unsigned/size_t and range-checking DCHECKs as added here.
From 62ee3244f3b212b92e22b8e2651afbed35de9768 Mon Sep 17 00:00:00 2001
From: pthier <[email protected]>
Date: Tue, 03 Jun 2025 09:16:02 +0200
Subject: [PATCH] [regexp] Use uint32_t over int in EscapeRegExpSource

While escaping the regexp source, a regular signed int might overflow,
while an uint32_t can always hold the maximum length for the escaped
source.

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

diff --git a/src/objects/js-regexp.cc b/src/objects/js-regexp.cc
index e365ac4..6049036 100644
--- a/src/objects/js-regexp.cc
+++ b/src/objects/js-regexp.cc
@@ -189,10 +189,14 @@
 // WriteEscapedRegExpSource into a single function to deduplicate dispatch logic
 // and move related code closer to each other.
 template <typename Char>
-int CountAdditionalEscapeChars(DirectHandle<String> source,
-                               bool* needs_escapes_out) {
+uint32_t CountAdditionalEscapeChars(DirectHandle<String> source,
+                                    bool* needs_escapes_out) {
   DisallowGarbageCollection no_gc;
-  int escapes = 0;
+  uint32_t escapes = 0;
+  // The maximum growth-factor is 5 (for \u2028 and \u2029). Make sure that we
+  // won't overflow |escapes| given the current constraints on string length.
+  static_assert(uint64_t{String::kMaxLength} * 5 <
+                std::numeric_limits<decltype(escapes)>::max());
   bool needs_escapes = false;
   bool in_character_class = false;
   base::Vector<const Char> src = source->GetCharVector<Char>(no_gc);
@@ -231,14 +235,14 @@
     }
   }
   DCHECK(!in_character_class);
-  DCHECK_GE(escapes, 0);
   DCHECK_IMPLIES(escapes != 0, needs_escapes);
   *needs_escapes_out = needs_escapes;
   return escapes;
 }
 
 template <typename Char>
-void WriteStringToCharVector(base::Vector<Char> v, int* d, const char* string) {
+void WriteStringToCharVector(base::Vector<Char> v, uint32_t* d,
+                             const char* string) {
   int s = 0;
   while (string[s] != '\0') v[(*d)++] = string[s++];
 }
@@ -249,13 +253,13 @@
   DisallowGarbageCollection no_gc;
   base::Vector<const Char> src = source->GetCharVector<Char>(no_gc);
   base::Vector<Char> dst(result->GetChars(no_gc), result->length());
-  int s = 0;
-  int d = 0;
+  uint32_t s = 0;
+  uint32_t d = 0;
   bool in_character_class = false;
-  while (s < src.length()) {
+  while (s < src.size()) {
     const Char c = src[s];
     if (c == '\\') {
-      if (s + 1 < src.length() && IsLineTerminator(src[s + 1])) {
+      if (s + 1 < src.size() && IsLineTerminator(src[s + 1])) {
         // This '\' is ignored since the next character itself will be escaped.
         s++;
         continue;
@@ -263,7 +267,7 @@
         // Escape. Copy this and next character.
         dst[d++] = src[s++];
       }
-      if (s == src.length()) break;
+      if (s == src.size()) break;
     } else if (c == '/' && !in_character_class) {
       // Not escaped forward-slash needs escape.
       dst[d++] = '\\';
@@ -303,11 +307,13 @@
   if (source->length() == 0) return isolate->factory()->query_colon_string();
   bool one_byte = String::IsOneByteRepresentationUnderneath(*source);
   bool needs_escapes = false;
-  int additional_escape_chars =
+  uint32_t additional_escape_chars =
       one_byte ? CountAdditionalEscapeChars<uint8_t>(source, &needs_escapes)
                : CountAdditionalEscapeChars<base::uc16>(source, &needs_escapes);
   if (!needs_escapes) return source;
-  int length = source->length() + additional_escape_chars;
+  DCHECK_LE(static_cast<uint64_t>(source->length()) + additional_escape_chars,
+            std::numeric_limits<uint32_t>::max());
+  uint32_t length = source->length() + additional_escape_chars;
   if (one_byte) {
     DirectHandle<SeqOneByteString> result;
     ASSIGN_RETURN_ON_EXCEPTION(isolate, result,
Loading diff…

Original Bug Report

reported by [email protected]

Debug check failed: escapes >= 0 (-2005397586 vs. 0)

VULNERABILITY DETAILS

#
# Fatal error in ..\..\src\objects\js-regexp.cc, line 234
# Debug check failed: escapes >= 0 (-2005397586 vs. 0).
#
#
#
#FailureMessage Object: 00000012179FD550
==== C stack trace ===============================

	v8::base::debug::StackTrace::StackTrace [0x0x7fff2bb5b685+37] (D:\Browser\v8\v8\src\base\debug\stack_trace_win.cc:173)
	v8::platform::`anonymous namespace'::PrintStackTrace [0x0x7fff2bcdab89+57] (D:\Browser\v8\v8\src\libplatform\default-platform.cc:28)
	V8_Fatal [0x0x7fff2bb35463+323] (D:\Browser\v8\v8\src\base\logging.cc:214)
	v8::base::`anonymous namespace'::DefaultDcheckHandler [0x0x7fff2bb34e8c+44] (D:\Browser\v8\v8\src\base\logging.cc:59)
	V8_Dcheck [0x0x7fff2bb35571+81] (D:\Browser\v8\v8\src\base\logging.cc:228)
	v8::internal::`anonymous namespace'::CountAdditionalEscapeChars<unsigned short> [0x0x7ffeaae0b824+740] (D:\Browser\v8\v8\src\objects\js-regexp.cc:234)
	v8::internal::`anonymous namespace'::EscapeRegExpSource [0x0x7ffeaae08824+420] (D:\Browser\v8\v8\src\objects\js-regexp.cc:306)
	v8::internal::JSRegExp::Initialize [0x0x7ffeaae07dfa+618] (D:\Browser\v8\v8\src\objects\js-regexp.cc:344)
	v8::internal::JSRegExp::Initialize [0x0x7ffeaae08581+529] (D:\Browser\v8\v8\src\objects\js-regexp.cc:179)
	v8::internal::__RT_impl_Runtime_RegExpInitializeAndCompile [0x0x7ffeab3fe615+549] (D:\Browser\v8\v8\src\runtime\runtime-regexp.cc:2147)
	v8::internal::Runtime_RegExpInitializeAndCompile [0x0x7ffeab3fe10d+413] (D:\Browser\v8\v8\src\runtime\runtime-regexp.cc:2137)
	Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit [0x0x7ffeaf6b5b41+65]
	Builtins_RegExpConstructor [0x0x7ffeaf5f40db+7579]
	Builtins_InterpreterPushArgsThenFastConstructFunction [0x0x7ffeaf247501+1089]
	Builtins_ConstructHandler [0x0x7ffeafe522c5+8261]
	Builtins_InterpreterEntryTrampoline [0x0x7ffeaf2467f2+370]
	Builtins_InterpreterEntryTrampoline [0x0x7ffeaf2467f2+370]
	Builtins_JSEntryTrampoline [0x0x7ffeaf239ce7+103]
	Builtins_JSEntry [0x0x7ffeaf23983f+255]
	v8::internal::GeneratedCode<unsigned long long,unsigned long long,unsigned long long,unsigned long long,unsigned long long,long long,unsigned long long **>::Call [0x0x7ffea9f5438c+108] (D:\Browser\v8\v8\src\execution\simulator.h:212)
	v8::internal::`anonymous namespace'::Invoke [0x0x7ffea9f503cd+5469] (D:\Browser\v8\v8\src\execution\execution.cc:441)
	v8::internal::Execution::CallScript [0x0x7ffea9f50c61+513] (D:\Browser\v8\v8\src\execution\execution.cc:542)
	v8::Script::Run [0x0x7ffea978b5cf+1135] (D:\Browser\v8\v8\src\api\api.cc:1964)
	v8::Script::Run [0x0x7ffea978b148+120] (D:\Browser\v8\v8\src\api\api.cc:1929)
	v8::Shell::ExecuteString [0x0x7ff6ae1d21b6+3142] (D:\Browser\v8\v8\src\d8\d8.cc:1030)
	v8::SourceGroup::Execute [0x0x7ff6ae1f7e26+1174] (D:\Browser\v8\v8\src\d8\d8.cc:5073)
	v8::Shell::RunMainIsolate [0x0x7ff6ae1ff287+631] (D:\Browser\v8\v8\src\d8\d8.cc:6027)
	v8::Shell::RunMain [0x0x7ff6ae1feced+205] (D:\Browser\v8\v8\src\d8\d8.cc:5935)
	v8::Shell::Main [0x0x7ff6ae201901+3873] (D:\Browser\v8\v8\src\d8\d8.cc:6801)
	main [0x0x7ff6ae202333+35] (D:\Browser\v8\v8\src\d8\d8.cc:6893)
	invoke_main [0x0x7ff6ae368489+57] (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:79)
	__scrt_common_main_seh [0x0x7ff6ae3685c2+306] (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288)
	__scrt_common_main [0x0x7ff6ae36864e+14] (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:331)
	mainCRTStartup [0x0x7ff6ae36866e+14] (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:17)
	BaseThreadInitThunk [0x0x7fff6adfe8d7+23]
	RtlUserThreadStart [0x0x7fff6cafc5dc+44]

VERSION

V8 version 13.9.0 (candidate)

REPRODUCTION CASE

Build: python3 tools/dev/gm.py x64.debug

Run: ./d8 poc.js


Reporter credit: Shaheen Fazim

View on issue tracker
Uploaded PoC files