CVE-2026-14395
Overview
Background
- Regexp bytecode peephole optimizer
- a pass (
BytecodePeepholeOptimization::OptimizeBytecode) that rewrites short bytecode sequences into fused single opcodes and rewrites all affected jump/label positions accordingly. - `kAdvanceCpAndGoto` fusion
- the peephole rule that replaced an
kAdvanceCurrentPositionimmediately followed bykGoTowith the single fusedkAdvanceCpAndGotoopcode. - `BytecodeWriter` / jump edges / label fixups
- the writer holds the emitted bytecode buffer plus
jump_edges_bookkeeping; when a sequence is replaced, downstream label positions and jump targets must be fixed up by the size delta of the replacement. - Multi-pass ("ping-pong") optimization
- a fixed-point loop introduced by crrev.com/c/7274603 that repeatedly ran the optimizer,
std::swap-ing a source and destinationBytecodeWriterand callingBytecodeWriter::Reset()between passes until no rule fired.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Commit states a performance revert, not a security fix; diff shows no OOB sink. Claimed miscomputed-fixup values (e.g. '4 was 0') are not visible in the provided test diff; the OOB mechanism is inferred.
The reverted change (crrev.com/c/7274603) turned the single peephole pass into a fixed-point loop that ping-ponged bytecode between two BytecodeWriters, calling BytecodeWriter::Reset() (clearing jump_edges_ and PC) before each subsequent pass. Across successive passes, the label-position and jump-target fixups produced by BytecodePeephole::OptimizeBytecode were miscomputed — the updated test expectations show that pre-patch pos_fixups/target_fixups and label positions were wrong (e.g. a fixup that should have been 4 was 0, and after-replacement positions differed by the wrong size deltas). Incorrect fixups mean the optimized regexp bytecode contains jump offsets and current-position advances that no longer match the real buffer layout, so during matching the interpreter/JIT can advance or index out of the intended bounds, corresponding to the reported out-of-bounds write (inferred — the specific OOB sink is not in the diff).
The fix reverts to a single peephole pass (did_optimize selecting either the single dst_writer or the untouched src_writer), removes BytecodeWriter::Reset(), and moves the kAdvanceCurrentPosition+kGoTo → kAdvanceCpAndGoto fusion out of the peephole table and back into BytecodeGenerator (tracking advance_current_start_/advance_current_offset_/advance_current_end_), so the fragile multi-pass fixup accumulation never happens.
Attack Path
- Craft a triggering regexp
Script compiles a regular expression whose generated bytecode contains sequences the peephole optimizer rewrites (e.g.
kAdvanceCurrentPosition+kGoToplus a further fusable pattern) so that more than one optimization pass fires. - Multi-pass fixup corruption
In the vulnerable code,
OptimizeBytecodeloops,Reset()-ing and swapping writers; the second pass computes label positions/jump targets from state whose fixups are already wrong, emitting bytecode with mismatched offsets. - Emit malformed bytecode
The corrupted buffer is copied into the
TrustedByteArrayand used as the regexp’s executable bytecode with off-by-size jump targets and advance offsets. - Out-of-bounds write during matching Executing the regexp against a subject drives the interpreter/JIT through the bad offsets, producing an out-of-bounds write (inferred — the diff shows the incorrect fixups and the revert, not the concrete write location).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/regexp/regexp-bytecode-generator.cc |
modified | |
forsrc/regexp/regexp-bytecode-peephole.cc |
modified | |
ifsrc/regexp/regexp-bytecode-peephole.cc |
modified |
Files Changed
src/regexp/regexp-bytecode-generator.ccsrc/regexp/regexp-bytecode-generator.hsrc/regexp/regexp-bytecode-peephole.cctest/unittests/regexp/regexp-unittest.cc
Audit Directions
- Iterative optimizer fixupsAudit any bytecode/IR pass that runs to a fixed point and rewrites positions/offsets, verifying that label and jump fixups compose correctly across repeated passes rather than only within one pass.
- Buffer ping-pong with partial resetReview passes that swap source/destination buffers and reset bookkeeping (
jump_edges_, PC) between iterations for state that must, but does not, survive the swap. - Generation-time vs. peephole fusionWhere an optimization was moved between the generator and a later peephole pass, confirm both paths compute identical sizes/offsets and that
CHECK/unit-test position expectations match the real emitted layout.
Patch
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) +
Regression Test / PoC
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++) {
Original Bug Report
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.ccv8/src/regexp/regexp-bytecode-peephole.ccv8/src/regexp/regexp-bytecode-generator.ccv8/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
- Modify
BytecodePeepholeOptimizationto ensure that offsets generated byEmitOffsetAfterSequenceare properly added to thejump_edges_tracking map so they are adjusted during subsequent shrinking passes. - 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.