Medium watch ✔︎ verified CVSS 4.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOff by one error in V8
DescriptionOff by one error in V8
ComponentV8
Sub-componentRegexp
Bug ClassLogic Error
Action tierwatch — ub-class only — no demonstrated corruption path — 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: Mechanism correct, but the loop initializer `SChar c = *block` also reads before any bound check (OOB when the range is empty). Bug class 'LogicError' understates it; commit title calls it an OOB read.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker439758498
Fix commit5666bca708c0 (v8/v8) +2/-1
CWECWE-193
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle Big Sleep
Reported2025-08-19 (per vendor advisory)
Fix landed2025-08-21 (public git)
Disclosed2025-09-30
Patch gap40 days — fix public in source before the advisory

Background

`RegExpMatchGlobalAtom_OneCharPattern`
a V8 runtime fast path that counts/locates all matches of a single-character atom pattern in a subject string during a global regexp match.
Atom pattern
a regexp with no special operators, i.e. a plain substring (here a one-character substring) that can be matched by a simple scan rather than the full regexp engine.
`block` / `end` scan pointers
block walks the subject character buffer and end marks one past the last valid character; the scan must only dereference block while block < end.
SIMD/highway scan (`hw::`)
the surrounding routine uses vectorized helpers (e.g. hw::FirstN) to accelerate the character scan, with a scalar loop involved in extracting the match.

Root Cause Analysis

The scalar loop was written as for (SChar c = *block; block < end; c = *(++block)), which reads a character through block in both the initializer and the update expression before the block < end condition is re-evaluated. In the loop-update step c = *(++block), block is first incremented and then immediately dereferenced, so on the final iteration block is advanced to end and *end is loaded before the condition check fails and the loop exits. That load reads one character past the end of the allocated subject string buffer — an off-by-one out-of-bounds read, even though (per the commit message) the value is loaded but not otherwise accessed.

The fix restructures the loop as for (; block < end; ++block) { SChar c = *block; ... }, so the block < end bound is checked before any dereference of block, eliminating the read past the buffer.

Key insight
The single mistake was placing the pointer dereference in the for-loop’s update expression (c = *(++block)), so the character load happened before the block < end bound was re-checked; moving the load inside the body after the condition (SChar c = *block;) restores the check-before-read ordering and removes the off-by-one OOB read.

Attack Path

  1. Invoke a global one-char atom match An attacker runs JavaScript that performs a global regexp match with a single-character pattern against a subject string, reaching RegExpMatchGlobalAtom_OneCharPattern.
  2. Scan to the buffer end The scalar loop iterates over the subject’s character buffer using block/end.
  3. Read one past the end On the last iteration, the pre-patch update c = *(++block) advances block to end and dereferences it, loading one SChar beyond the allocated subject buffer before the loop condition rejects it.
  4. Out-of-bounds read The load touches memory just past the string buffer; the commit notes the loaded character is not accessed further, and any information disclosure or crash consequence is (inferred).

Impact Assessment

An attacker running crafted JavaScript in the renderer can trigger a one-character out-of-bounds read past the end of a subject string buffer via a global single-character atom regexp match; the advisory classifies this as an off-by-one error in V8. The out-of-bounds character is loaded but, per the fix’s own description, not accessed, so escalation to meaningful information disclosure or a crash is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
for
src/runtime/runtime-regexp.cc
modified

Files Changed

  • src/runtime/runtime-regexp.cc

Audit Directions

  • Dereference in loop update/init
    Search for for loops that dereference a pointer in the initializer or update expression (*(++p), *p++) where the bound check occurs only in the condition, since the read can precede the check.
  • Pointer-walk scans against `end`
    Audit V8 string/buffer scan loops using ptr < end bounds to confirm no dereference happens after the final increment before re-testing the bound.
  • Vectorized fast paths with scalar tails
    Review SIMD/hw::-accelerated routines and their scalar cleanup loops in runtime-regexp.cc and similar files for off-by-one boundary handling at buffer ends.
From 5666bca708c0a2f888b78e34281b00ed20356cb2 Mon Sep 17 00:00:00 2001
From: pthier <[email protected]>
Date: Thu, 21 Aug 2025 18:08:08 +0200
Subject: [PATCH] [regexp] Fix OOB read in RegExpMatchGlobalAtom_OneCharPattern

We were loading (just not accessing) a character potentially beyond
the allocated string subject buffer.

Fixed: 439758498
Change-Id: I250670122c765640602b20e6640d604c9d33be03
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6871496
Commit-Queue: Patrick Thier <[email protected]>
Commit-Queue: Igor Sheludko <[email protected]>
Auto-Submit: Patrick Thier <[email protected]>
Reviewed-by: Igor Sheludko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#101982}
---

diff --git a/src/runtime/runtime-regexp.cc b/src/runtime/runtime-regexp.cc
index dbc9ae1..e2fe84e 100644
--- a/src/runtime/runtime-regexp.cc
+++ b/src/runtime/runtime-regexp.cc
@@ -2236,7 +2236,8 @@
   // last_match_block already contains the last match position, so use a special
   // vector with lane 0 set to extract the last_match_index later.
   const auto scalar_last_match_vec = hw::FirstN(tag, 1);
-  for (SChar c = *block; block < end; c = *(++block)) {
+  for (; block < end; ++block) {
+    SChar c = *block;
     if (c != static_cast<const SChar>(pattern)) continue;
     matches++;
     last_match_block = block;
Loading diff…

Original Bug Report

reported by [email protected]

V8: off-by-one error in RegExp matching leads to OOB read

Bug Details

There is an off-by-one error in the implementation of RegExp matching for the case of a single-character atom, specifically in the function RegExpMatchGlobalAtom_OneCharPattern [1]. This function is optimized for SIMD and processes multiple characters at the same time. In case the input string is not an exact multiple of the stride, a scalar loop processes the remaining characters at the end [2]:

  for (SChar c = *block; block < end; c = *(++block)) {
    ...
  }

Here, since the pointer is first incremented and then dereferenced, it will cause an out-of-bounds read in the last iteration. The below test case demonstrated this by causing a segmentation fault when reading past the end of a page. However, as the bug only leads to an out-of-bounds read, and the read value is never used, this bug should not have any security implications apart from the potential to cause a crash.

[1] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/runtime/runtime-regexp.cc;l=2165;drc=efb8f67f80edf4fa39c5488d07b45b1e690c0c6f

[2] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/runtime/runtime-regexp.cc;l=2241;drc=efb8f67f80edf4fa39c5488d07b45b1e690c0c6f

Affected Version(s)

The issue has been successfully reproduced:

  • at HEAD (commit 625de227c339c41b4a947a03fb74a30eba0afe3f)
  • in stable release 13.9.205.16 (b07b4e9376489c7f7c0ff2af5eceb4261b3bb784)

Reproduction

Test Case

const length = 4;
const pattern = /a/g;
for (let i = 0; i < 500000; i++) {
    let subject = 'b'.repeat(length);
    subject.match(pattern);
}

Build Instructions

Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:

gm.py x64.debug

Command

./out/x64.debug/d8 crash.js

ASan Report

AddressSanitizer:DEADLYSIGNAL
=================================================================
==754958==ERROR: AddressSanitizer: SEGV on unknown address 0x79fd002c0000 (pc 0x7f3e7493423f bp 0x7fff9cec6140 sp 0x7fff9cec5d00 T0)
==754958==The signal is caused by a READ memory access.
    #0 0x7f3e7493423f in void v8::internal::(anonymous namespace)::RegExpMatchGlobalAtom_OneCharPattern<unsigned char, unsigned char>(v8::internal::Isolate*, v8::base::Vector<unsigned char const>, unsigned char, int, int*, int*, v8::internal::PerThreadAssertScope<false, (v8::internal::PerThreadAssertType)1, (v8::internal::PerThreadAssertType)2> const&) src/runtime/runtime-regexp.cc:2241:43
    #1 0x7f3e74933595 in v8::internal::(anonymous namespace)::RegExpMatchGlobalAtom_Dispatch(v8::internal::Isolate*, v8::internal::String::FlatContent const&, v8::internal::String::FlatContent const&, bool, int, int*, int*, v8::internal::PerThreadAssertScope<false, (v8::internal::PerThreadAssertType)1, (v8::internal::PerThreadAssertType)2> const&) src/runtime/runtime-regexp.cc:2326:9
    #2 0x7f3e7491f871 in v8::internal::__RT_impl_Runtime_RegExpMatchGlobalAtom(v8::internal::Arguments<(v8::internal::ArgumentsType)0>, v8::internal::Isolate*) src/runtime/runtime-regexp.cc:2404:5
    #3 0x7f3e7491ea48 in v8::internal::Runtime_RegExpMatchGlobalAtom(int, unsigned long*, v8::internal::Isolate*) src/runtime/runtime-regexp.cc:2358:1
    #4 0x7f3e710e5f7c in Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit setup-isolate-deserialize.cc
    #5 0x7f3e71541280 in Builtins_RegExpMatchFast setup-isolate-deserialize.cc
    #6 0x7f3e7169f8b8 in Builtins_StringPrototypeMatch setup-isolate-deserialize.cc
    #7 0x7f3ed0c00fee  (<unknown module>)
    #8 0x7f3e70c72966 in Builtins_JSEntryTrampoline setup-isolate-deserialize.cc
    #9 0x7f3e70c726aa in Builtins_JSEntry setup-isolate-deserialize.cc
    #10 0x7f3e727468d6 in v8::internal::GeneratedCode<unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, long, unsigned long**>::Call(unsigned long, unsigned long, unsigned long, unsigned long, long, unsigned long**) src/execution/simulator.h:212:12
    #11 0x7f3e7273eeee in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/execution.cc:442:22
    #12 0x7f3e7273fd96 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSFunction>, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>) src/execution/execution.cc:542:10
    #13 0x7f3e71aefb98 in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) src/api/api.cc:1968:7
    #14 0x7f3e71aef41d in v8::Script::Run(v8::Local<v8::Context>) src/api/api.cc:1932:10
    #15 0x55c8d09def2c in v8::Shell::ExecuteString(v8::Isolate*, v8::Local<v8::String>, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*) src/d8/d8.cc:1033:44
    #16 0x55c8d0a14f7b in v8::SourceGroup::Execute(v8::Isolate*) src/d8/d8.cc:5322:10
    #17 0x55c8d0a20ec0 in v8::Shell::RunMainIsolate(v8::Isolate*, bool) src/d8/d8.cc:6274:37
    #18 0x55c8d0a2075e in v8::Shell::RunMain(v8::Isolate*, bool) src/d8/d8.cc:6182:18
    #19 0x55c8d0a23544 in v8::Shell::Main(int, char**) src/d8/d8.cc:7049:18
    #20 0x55c8d0a23f81 in main src/d8/d8.cc:7141:43
    #21 0x7f3e66143ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
==754958==Register values:
rax = 0x000079fd002c0000  rbx = 0x00007fff9cec5d00  rcx = 0x0000000000000061  rdx = 0xf5f5f5f5f5f5f5f5  
rdi = 0x00007b3e63d717e0  rsi = 0x00007fff9cec5bc0  rbp = 0x00007fff9cec6140  rsp = 0x00007fff9cec5d00  
 r8 = 0x00000ffff39d8b74   r9 = 0x00007fff9cec5baf  r10 = 0x00000ffff39d8b75  r11 = 0x00001000739d0b70  
r12 = 0x00007b3e053c0000  r13 = 0x00007e4e651e1080  r14 = 0x00000f67cc7ae2d0  r15 = 0xf3f30000f1f1f1f1  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV src/runtime/runtime-regexp.cc:2241:43 in void v8::internal::(anonymous namespace)::RegExpMatchGlobalAtom_OneCharPattern<unsigned char, unsigned char>(v8::internal::Isolate*, v8::base::Vector<unsigned char const>, unsigned char, int, int*, int*, v8::internal::PerThreadAssertScope<false, (v8::internal::PerThreadAssertType)1, (v8::internal::PerThreadAssertType)2> const&)
==754958==ABORTING

Reporter Credit

Google Big Sleep

Disclosure Policy

Our assessment concluded that this finding has NO security impact. Therefore, we are NOT applying a disclosure deadline to this report.

However, if your internal review indicates a security risk to your users, please email [email protected] immediately to arrange a standard disclosure deadline.

For more information, visit https://goo.gle/bigsleep

View on issue tracker