Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in V8
DescriptionOut of bounds write in V8
ComponentV8
Bug ClassOOB
Tracker511290389
Fix commit60524d5d9f05 (v8/v8) +60/-73
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
src/regexp/regexp-bytecode-generator.cc
modified
for
src/regexp/regexp-bytecode-peephole.cc
modified
if
src/regexp/regexp-bytecode-peephole.cc
modified

Files Changed

  • src/regexp/regexp-bytecode-generator.cc
  • src/regexp/regexp-bytecode-generator.h
  • src/regexp/regexp-bytecode-peephole.cc
  • test/unittests/regexp/regexp-unittest.cc
From 60524d5d9f05320d0ddbf83959b5cfb0c404bfb4 Mon Sep 17 00:00:00 2001
From: Jakob Linke <[email protected]>
Date: Mon, 11 May 2026 14:14:42 +0200
Subject: [PATCH] [regexp] Revert multi-pass-peephole

Partially reverts crrev.com/c/7274603 (90084a5d5fd).

The peephole pass is sufficiently expensive that doing it twice is
noticeable in benchmarks. We go back to the old state: a single peephole
pass, with the AdvanceCurrentPosition+GoTo fusion moving back into the
BytecodeGenerator so existing peephole defs can remain the same.

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

diff --git a/src/regexp/regexp-bytecode-generator.cc b/src/regexp/regexp-bytecode-generator.cc
index 82bbbc4..0095942 100644
--- a/src/regexp/regexp-bytecode-generator.cc
+++ b/src/regexp/regexp-bytecode-generator.cc
@@ -42,12 +42,6 @@
   buffer_.resize(new_size);
 }
 
-void BytecodeWriter::Reset() {
-  // We keep the buffer_ storage; the next pass will overwrite its contents.
-  jump_edges_.clear();
-  ResetPc(0);
-}
-
 void BytecodeWriter::EmitRawBytecodeStream(const uint8_t* data, int len) {
   EnsureCapacity(len);
   // Must start at a bytecode boundary.
@@ -245,6 +239,7 @@
 }
 
 void BytecodeGenerator::Bind(Label* l) {
+  advance_current_end_ = kInvalidPC;
   DCHECK(!l->is_bound());
   if (l->is_linked()) {
     int pos = l->pos();
@@ -315,7 +310,16 @@
   Emit<Bytecode::kBacktrack>(error_code);
 }
 
-void BytecodeGenerator::GoTo(Label* label) { Emit<Bytecode::kGoTo>(label); }
+void BytecodeGenerator::GoTo(Label* label) {
+  if (advance_current_end_ == pc_) {
+    // Fuse the preceding AdvanceCurrentPosition into kAdvanceCpAndGoto.
+    ResetPc(advance_current_start_);
+    Emit<Bytecode::kAdvanceCpAndGoto>(advance_current_offset_, label);
+    advance_current_end_ = kInvalidPC;
+  } else {
+    Emit<Bytecode::kGoTo>(label);
+  }
+}
 
 void BytecodeGenerator::PushBacktrack(Label* label) {
   Emit<Bytecode::kPushBacktrack>(label);
@@ -329,7 +333,10 @@
 void BytecodeGenerator::Fail() { Emit<Bytecode::kFail>(); }
 
 void BytecodeGenerator::AdvanceCurrentPosition(int by) {
+  advance_current_start_ = pc_;
+  advance_current_offset_ = by;
   Emit<Bytecode::kAdvanceCurrentPosition>(by);
+  advance_current_end_ = pc_;
 }
 
 void BytecodeGenerator::CheckFixedLengthLoop(
diff --git a/src/regexp/regexp-bytecode-generator.h b/src/regexp/regexp-bytecode-generator.h
index 64bf47a..ee87fac 100644
--- a/src/regexp/regexp-bytecode-generator.h
+++ b/src/regexp/regexp-bytecode-generator.h
@@ -41,8 +41,6 @@
 
   // Update bookkeeping at bytecode boundaries.
   inline void ResetPc(int new_pc);
-  // Reset all state.
-  void Reset();
 
   // Templated code emission.
   template <Bytecode bytecode, typename... Args>
@@ -231,6 +229,13 @@
 
   Label backtrack_;
 
+  // Tracks a just-emitted AdvanceCurrentPosition so the next GoTo can fuse
+  // both into a single kAdvanceCpAndGoto bytecode.
+  static const int kInvalidPC = -1;
+  int advance_current_start_ = 0;
+  int advance_current_offset_ = 0;
+  int advance_current_end_ = kInvalidPC;
+
   Isolate* isolate_;
 
   DISALLOW_IMPLICIT_CONSTRUCTORS(BytecodeGenerator);
diff --git a/src/regexp/regexp-bytecode-peephole.cc b/src/regexp/regexp-bytecode-peephole.cc
index ec7cf8c..49397a4 100644
--- a/src/regexp/regexp-bytecode-peephole.cc
+++ b/src/regexp/regexp-bytecode-peephole.cc
@@ -583,15 +583,6 @@
   // (--trace-regexp-bytecodes) and using v8/tools/regexp-sequences.py.
 
   {
-    static constexpr auto Target = B::kAdvanceCpAndGoto;
-    CreateSequence(B::kAdvanceCurrentPosition)
-        .FollowedBy(B::kGoTo)
-        .ReplaceWith(Target)
-        .MapArgument(T(by), 0, I(B::kAdvanceCurrentPosition, by))
-        .MapArgument(T(on_goto), 1, I(B::kGoTo, label));
-  }
-
-  {
     static constexpr auto Target = B::kSkipUntilBitInTable;
     CreateSequence(B::kLoadCurrentCharacter)
         .FollowedBy(B::kCheckBitInTable)
@@ -1081,8 +1072,7 @@
 DirectHandle<TrustedByteArray> BytecodePeepholeOptimization::OptimizeBytecode(
     Isolate* isolate, Zone* zone, DirectHandle<RegExpData> re_data,
     BytecodeWriter* src_writer) {
-  BytecodeWriter second_writer(zone);
-  BytecodeWriter* dst_writer = &second_writer;
+  BytecodeWriter dst_writer(zone);
 
   // Preserve the original bytecode for tracing if needed.
   std::optional<ZoneVector<uint8_t>> original_bytecode;
@@ -1092,31 +1082,19 @@
     original_bytecode.emplace(begin, begin + src_writer->length(), zone);
   }
 
-  // Run the peephole optimizer until we've reached a fixed point. All relevant
-  // data structures ping-pong between src and dst_writer.
-  bool any_pass_optimized = false;
-  for (;;) {
-    dst_writer->Reset();
-    // TODO(jgruber): This currently recreates standard definitions for each
-    // pass. These should be global instead (or at the very least, once per
-    // compilation).
-    bool this_pass_optimized =
-        BytecodePeephole::OptimizeBytecode(zone, src_writer, dst_writer);
-    if (!this_pass_optimized) break;
-    any_pass_optimized = true;
-    std::swap(dst_writer, src_writer);
-  }
-
-  // The result is in the src_writer (not in dst, since the last pass did not
-  // change anything).
-  const uint8_t* optimized_bytecode = src_writer->buffer().data();
-  uint32_t optimized_length = src_writer->length();
+  const bool did_optimize =
+      BytecodePeephole::OptimizeBytecode(zone, src_writer, &dst_writer);
+  // The result is in dst_writer iff a peephole rule fired; otherwise the
+  // unchanged input bytecode is still in src_writer.
+  BytecodeWriter* result = did_optimize ? &dst_writer : src_writer;
+  const uint8_t* optimized_bytecode = result->buffer().data();
+  uint32_t optimized_length = result->length();
 
   DirectHandle<TrustedByteArray> array =
       isolate->factory()->NewTrustedByteArray(optimized_length);
   MemCopy(array->begin(), optimized_bytecode, optimized_length);
 
-  if (any_pass_optimized && v8_flags.trace_regexp_peephole_optimization) {
+  if (did_optimize && v8_flags.trace_regexp_peephole_optimization) {
     std::unique_ptr<char[]> pattern_cstring =
         re_data->escaped_source()->ToCString();
     PrintF("Original Bytecode:\n");
diff --git a/test/unittests/regexp/regexp-unittest.cc b/test/unittests/regexp/regexp-unittest.cc
index 66873c1..7837fa6 100644
--- a/test/unittests/regexp/regexp-unittest.cc
+++ b/test/unittests/regexp/regexp-unittest.cc
@@ -1936,8 +1936,7 @@
   const uint32_t length_expected =
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacter) +
       regexp::Bytecodes::Size(REB::kCheckCharacter) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilChar) +
@@ -1997,8 +1996,7 @@
   const uint32_t length_expected =
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacter) +
       regexp::Bytecodes::Size(REB::kCheckBitInTable) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilBitInTable) +
@@ -2053,8 +2051,7 @@
       regexp::Bytecodes::Size(REB::kCheckPosition) +
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacterUnchecked) +
       regexp::Bytecodes::Size(REB::kCheckCharacter) +
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/unittests/regexp/regexp-unittest.cc b/test/unittests/regexp/regexp-unittest.cc
index 66873c1..7837fa6 100644
--- a/test/unittests/regexp/regexp-unittest.cc
+++ b/test/unittests/regexp/regexp-unittest.cc
@@ -1936,8 +1936,7 @@
   const uint32_t length_expected =
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacter) +
       regexp::Bytecodes::Size(REB::kCheckCharacter) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilChar) +
@@ -1997,8 +1996,7 @@
   const uint32_t length_expected =
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacter) +
       regexp::Bytecodes::Size(REB::kCheckBitInTable) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilBitInTable) +
@@ -2053,8 +2051,7 @@
       regexp::Bytecodes::Size(REB::kCheckPosition) +
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacterUnchecked) +
       regexp::Bytecodes::Size(REB::kCheckCharacter) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilCharPosChecked) +
@@ -2108,8 +2105,7 @@
       regexp::Bytecodes::Size(REB::kCheckPosition) +
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacterUnchecked) +
       regexp::Bytecodes::Size(REB::kCheckCharacterAfterAnd) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilCharAnd) +
@@ -2165,8 +2161,7 @@
       regexp::Bytecodes::Size(REB::kLoadCurrentCharacter) +
       regexp::Bytecodes::Size(REB::kCheckCharacter) +
       regexp::Bytecodes::Size(REB::kCheckCharacter) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilCharOrChar) +
@@ -2232,8 +2227,7 @@
       regexp::Bytecodes::Size(REB::kCheckCharacterGT) +
       regexp::Bytecodes::Size(REB::kCheckBitInTable) +
       regexp::Bytecodes::Size(REB::kGoTo) +
-      regexp::Bytecodes::Size(REB::kAdvanceCurrentPosition) +
-      regexp::Bytecodes::Size(REB::kGoTo) +
+      regexp::Bytecodes::Size(REB::kAdvanceCpAndGoto) +
       regexp::Bytecodes::Size(REB::kBacktrack);
   const uint32_t length_optimized_expected =
       regexp::Bytecodes::Size(REB::kSkipUntilGtOrNotBitInTable) +
@@ -2291,13 +2285,13 @@
 
   CHECK_EQ(0x00, dummy_before.pos());
   CHECK_EQ(0x28, dummy_inside.pos());
-  CHECK_EQ(0x3C, dummy_after.pos());
+  CHECK_EQ(0x38, dummy_after.pos());
 
   const Label* labels[] = {&dummy_before, &dummy_after, &dummy_inside};
   const int label_positions[4][3] = {
-      {0x04, 0x40},  // dummy_before
-      {0x0C, 0x48},  // dummy after
-      {0x14, 0x50}   // dummy inside
+      {0x04, 0x3C},  // dummy_before
+      {0x0C, 0x44},  // dummy after
+      {0x14, 0x4C}   // dummy inside
   };
 
   DirectHandle<String> source = factory->NewStringFromStaticChars("dummy");
@@ -2320,13 +2314,13 @@
 
   const int pos_fixups[] = {
       0,  // Position before optimization should be unchanged.
-      0,  // Position after first replacement should be 0 (optimized size (36) -
-          // original size (36)).
+      4,  // Position after first replacement should be 4 (optimized size (20) -
+          // original size (32) + preserve length (16)).
   };
   const int target_fixups[] = {
       0,  // dummy_before should be unchanged
-      0,  // dummy_after should be unchanged
-      4   // dummy_inside should be 4
+      4,  // dummy_inside should be 4
+      4   // dummy_after should be 4
   };
 
   for (int label_idx = 0; label_idx < 3; label_idx++) {
@@ -2397,17 +2391,17 @@
                                            &dummy_after, &dummy_inside);
 
   CHECK_EQ(0x00, dummy_before.pos());
-  CHECK_EQ(0x44, dummy_between.pos());
-  CHECK_EQ(0x74, dummy_inside.pos());
-  CHECK_EQ(0x88, dummy_after.pos());
+  CHECK_EQ(0x40, dummy_between.pos());
+  CHECK_EQ(0x70, dummy_inside.pos());
+  CHECK_EQ(0x80, dummy_after.pos());
 
   const Label* labels[] = {&dummy_before, &dummy_between, &dummy_after,
                            &dummy_inside};
   const int label_positions[4][3] = {
-      {0x04, 0x48, 0x8C},  // dummy_before
-      {0x0C, 0x50, 0x94},  // dummy between
-      {0x14, 0x58, 0x9C},  // dummy after
-      {0x1C, 0x60, 0xA4}   // dummy inside
+      {0x04, 0x44, 0x84},  // dummy_before
+      {0x0C, 0x4C, 0x8C},  // dummy between
+      {0x14, 0x54, 0x94},  // dummy after
+      {0x1C, 0x5C, 0x9C}   // dummy inside
   };
 
   DirectHandle<String> source = factory->NewStringFromStaticChars("dummy");
@@ -2430,14 +2424,17 @@
 
   const int pos_fixups[] = {
       0,    // Position before optimization should be unchanged.
-      -16,  // Position after first replacement.
-      -16   // Position after second replacement.
+      -12,  // Position after first replacement should be -12 (optimized size =
+            // 20 - 32 = original size).
+      -8    // Position after second replacement should be -8 (-12 from first
+            // optimization -12 from second optimization + 16 preserved
+            // bytecodes).
   };
   const int target_fixups[] = {
-      0,    // dummy_before
-      -16,  // dummy_between
-      -16,  // dummy_after
-      -12   // dummy_inside
+      0,    // dummy_before should be unchanged
+      -12,  // dummy_between should be -12
+      -8,   // dummy_inside should be -8
+      -8    // dummy_after should be -8
   };
 
   for (int label_idx = 0; label_idx < 4; label_idx++) {
Loading diff…

Original Bug Report

reported by [email protected]

Potential OOB Write in V8 RegExp JIT Compiler via Stale Peephole Jump Offsets

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: V8’s Irregexp peephole optimizer fails to track jump offsets for kOffsetAfterSequence, causing them to become stale if preceding bytecodes are shrunk in subsequent optimization passes. During native JIT tier-up, these stale offsets trigger an out-of-bounds write in CodeGenerator::PreVisitBytecodes, which can zero out jump_targets_ data and lead to wild branches.

Affected files:

  • v8/src/regexp/regexp-code-generator.cc
  • v8/src/regexp/regexp-bytecode-peephole.cc
  • v8/src/regexp/regexp-bytecode-generator.cc
  • v8/src/regexp/regexp-bytecode-generator-inl.h

Estimated timestamp from git blame: 2026-01-08

Root Cause

V8’s Irregexp peephole optimizer (v8/src/regexp/regexp-bytecode-peephole.cc) applies templates to shorten bytecodes in a fixpoint loop until no changes occur. For certain optimizations (e.g., B::kSkipUntilOneOfMasked3 and B::kSkipUntilOneOfMasked), the jump target is calculated using EmitOffsetAfterSequence(), which records a kOffsetAfterSequence mapping type.

When emitting the optimization, EmitOptimization writes the target as an absolute PC offset via dst_writer_->OverwriteValue<uint32_t>(pc(), offset). Crucially, unlike standard jump targets handled via EmitArgument(), this raw write bypasses the jump_edges_ map in the BytecodeWriter, which is responsible for tracking all jump targets so they can be adjusted if bytecodes are moved.

The Trigger

If a subsequent pass in the peephole optimizer’s fixpoint loop matches a template that shrinks a bytecode sequence located before the SkipUntilOneOfMasked instruction (e.g., replacing a 2-byte sequence with a 1-byte sequence), all following bytecodes shift to lower PC offsets.

When the code is copied to the new buffer via EmitRawBytecodeStream, standard jump edges are adjusted. However, because the kOffsetAfterSequence offset was not added to jump_edges_, it is copied verbatim and becomes stale. Because preceding code was shrunk, this absolute offset now points beyond the final length of the shortened bytecode array.

Potential Impact

To execute RegExps efficiently, V8 tiers up to native JIT compilation after a certain number of executions. The tier-up process instantiates a CodeGenerator (v8/src/regexp/regexp-code-generator.cc).

The CodeGenerator constructor allocates an array of Label objects and a BitVector for jump targets sequentially in a deterministic bump-pointer Zone heap. Due to alignment and the 8-byte size of Label, the start of the jump_targets_ internal data array immediately follows the end of the labels_ array in memory.

CodeGenerator::PreVisitBytecodes extracts jump target offsets from the bytecode to initialize labels:

uint32_t offset = Operands::template Get<operand>(pc, no_gc);
if (!jump_targets_.Contains(offset)) {
  jump_targets_.Add(offset);
  ...
  Label* label = &labels_[offset];
  new (label) Label();
}

There are no release-build bounds checks on offset. When the stale offset (which is >= ulength) is processed, jump_targets_.Add(offset) performs an out-of-bounds bitwise write, and new (&labels_[offset]) Label() performs an out-of-bounds placement-new.

Because labels_ and jump_targets_ are adjacent in the Zone, &labels_[offset] points directly into the bit storage of jump_targets_. The Label constructor initializes its fields to 0, effectively zeroing out 8 bytes (64 bits) of jump_targets_.

This can erase the bits marking legitimate forward jump targets for other bytecodes. Later, during CodeGenerator::VisitBytecodes(), the compiler will check jump_targets_.Contains(iter_.current_offset()), see a 0, and skip binding the label (__ Bind(&labels_[iter_.current_offset()])).

Consequently, forward branches to that unbound label will remain unresolved. The JIT compiler emits them with intermediate relative link-chain displacements, creating wild branches in the generated machine code. When the RegExp is executed, these wild branches cause the CPU to jump to unintended instructions, allowing a potential attacker to achieve deterministic control flow hijacking and Remote Code Execution (RCE) in the renderer process.

(Note: These are suggested steps based on source code analysis; a working Proof of Concept has not been run.)

Suggested Fix

  1. Modify BytecodePeepholeOptimization to ensure that offsets generated by EmitOffsetAfterSequence are properly added to the jump_edges_ tracking map so they are adjusted during subsequent shrinking passes.
  2. Add release-build bounds checks in CodeGenerator::PreVisitBytecodes (e.g., CHECK_LT(offset, bytecode_->ulength().value())) to harden the JIT compiler against corrupted bytecode and prevent out-of-bounds memory accesses.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker