diff --git a/test/unittests/regexp/regexp-unittest.cc b/test/unittests/regexp/regexp-unittest.cc index 66873c1acf2..126aea3b3e5 100644 --- a/test/unittests/regexp/regexp-unittest.cc +++ b/test/unittests/regexp/regexp-unittest.cc @@ -22,6 +22,7 @@ #include "src/regexp/regexp-ast-printer.h" #include "src/regexp/regexp-bytecode-generator.h" #include "src/regexp/regexp-bytecodes-inl.h" +#include "src/regexp/regexp-code-generator.h" #include "src/regexp/regexp-compiler.h" #include "src/regexp/regexp-interpreter.h" #include "src/regexp/regexp-macro-assembler-arch.h" @@ -2452,6 +2453,149 @@ TEST_F(RegExpTest, PeepholeLabelFixupsComplex) { } } +// ----------------------------------------------------------------------------- +// POC: Stale kJumpTarget from peephole kOffsetAfterSequence reaches the +// CodeGenerator with no release-build bounds check. +// +// The peephole optimizer writes the SkipUntilOneOfMasked.on_match2 and +// SkipUntilOneOfMasked3.fallthrough_jump_target operands via raw +// Emit/OverwriteValue, bypassing jump_edges_ tracking. When a later +// peephole pass shrinks bytecode that precedes such an instruction and the +// instruction itself is carried over verbatim via EmitRawBytecodeStream, the +// untracked operand is not remapped by FixJumps and ends up pointing past the +// end of the final bytecode buffer. CodeGenerator::PreVisitBytecodes then +// indexes labels_[] / jump_targets_ with that stale offset. +// +// This test crafts bytecode that takes three optimizing passes to reach a +// fixed point: +// pass 1: AdvanceCurrentPosition+GoTo -> AdvanceCpAndGoto +// pass 2: header (LCC+CBIT+ACAG) -> SkipUntilBitInTable (region A) +// CP+L4CU+AC4C+ACAG+AC4C+ACN4C -> SkipUntilOneOfMasked (region B) +// ^ on_match2 written via kOffsetAfterSequence (untracked) +// pass 3: SkipUntilBitInTable+... -> SkipUntilOneOfMasked3 (region A) +// ^ shrinks 124 -> 84 bytes, twice (-80 total) +// SkipUntilOneOfMasked -> copied verbatim +// ^ on_match2 stays at the pass-2 absolute offset +// +// Final length is 220, but on_match2 is left at 296 (>= length). +// ----------------------------------------------------------------------------- +namespace { + +void EmitMasked3PrecursorRegion(regexp::RegExpMacroAssembler* m, + Handle bit_table) { + // Region A precursor for SkipUntilOneOfMasked3. The SkipUntilBitInTable + // header is itself peephole-derived (LCC+CBIT+ACAG) so that the Masked3 fold + // is delayed to pass 3. + Label start, after_loop, bc4, bc5; + // header: LoadCurrentCharacter / CheckBitInTable / Adv+GoTo + m->Bind(&start); + m->LoadCurrentCharacter(0, &after_loop, true); // on_failure -> bc1 + m->CheckBitInTable(bit_table, nullptr); // on_bit_set -> backtrack + m->AdvanceCurrentPosition(1); + m->GoTo(&start); + // bc1..bc8 + m->Bind(&after_loop); + m->CheckPosition(4, nullptr); // bc1 + m->LoadCurrentCharacter(0, nullptr, false, 4); // bc2: L4CU + m->CheckCharacterAfterAnd(0x61626364, 0xffffffff, &bc5); // bc3 + m->Bind(&bc4); + m->AdvanceCurrentPosition(1); // bc4a + m->GoTo(&start); // bc4b -> ACAG(start) + m->Bind(&bc5); + m->LoadCurrentCharacter(0, &bc4, true, 4); // bc5: L4C + m->CheckCharacterAfterAnd(0x65666768, 0xffffffff, nullptr); // bc6 + m->CheckCharacterAfterAnd(0x696a6b6c, 0xffffffff, nullptr); // bc7 + m->CheckNotCharacterAfterAnd(0x6d6e6f70, 0xffffffff, &bc4); // bc8 +} + +void EmitMaskedPrecursorRegion(regexp::RegExpMacroAssembler* m) { + // Region B precursor for SkipUntilOneOfMasked. Folds in pass 2 and emits + // on_match2 via kOffsetAfterSequence (untracked in jump_edges_). + Label start, second, adv; + m->Bind(&start); + m->CheckPosition(4, nullptr); // bc0 + m->LoadCurrentCharacter(0, nullptr, false, 4); // bc1: L4CU + m->CheckCharacterAfterAnd(0x61616161, 0xffffffff, &second); // bc2 + m->Bind(&adv); + m->AdvanceCurrentPosition(1); // bc3a + m->GoTo(&start); // bc3b + m->Bind(&second); + m->CheckCharacterAfterAnd(0x62626262, 0xffffffff, nullptr); // bc4 + m->CheckNotCharacterAfterAnd(0x63636363, 0xffffffff, &adv); // bc5 +} + +void CreatePeepholeStaleOffsetAfterSequenceBytecode( + regexp::RegExpMacroAssembler* m, Factory* factory) { + Handle bit_table = factory->NewByteArray( + regexp::RegExpMacroAssembler::kTableSize, AllocationType::kOld); + for (uint32_t i = 0; i < regexp::RegExpMacroAssembler::kTableSize; i++) { + bit_table->set(i, 0); + } + // Two region-A copies so the pass-3 shrink (-80 total) overshoots length. + EmitMasked3PrecursorRegion(m, bit_table); + EmitMasked3PrecursorRegion(m, bit_table); + EmitMaskedPrecursorRegion(m); + // GetCode() appends Bind(&backtrack_)+Backtrack() as the tail. +} + +} // namespace + +TEST_F(RegExpTest, PeepholeStaleOffsetAfterSequence) { + Zone zone(i_isolate()->allocator(), ZONE_NAME); + Factory* factory = i_isolate()->factory(); + HandleScope scope(i_isolate()); + + regexp::BytecodeGenerator opt(i_isolate(), &zone, + regexp::RegExpMacroAssembler::LATIN1); + CreatePeepholeStaleOffsetAfterSequenceBytecode(&opt, factory); + + DirectHandle source = factory->NewStringFromStaticChars("dummy"); + + v8_flags.regexp_peephole_optimization = true; + DirectHandle array_optimized = + TrustedCast( + opt.GetCode(CreateRegExpData(i_isolate(), source), {})); + const uint32_t length_optimized = array_optimized->length().value(); + + // Locate the SkipUntilOneOfMasked instruction in the optimized output and + // read its on_match2 operand. Demonstrate that it points past the end of the + // bytecode array, i.e. the peephole produced a stale absolute jump target. + uint32_t pc = 0; + uint32_t on_match2 = 0; + bool found = false; + while (pc < length_optimized) { + REB bc = GetBytecode(array_optimized, pc); + if (bc == REB::kSkipUntilOneOfMasked) { + using Ops = regexp::BytecodeOperands; + constexpr int kOnMatch2Off = Ops::Offset(Ops::Operand::on_match2); + on_match2 = *reinterpret_cast(array_optimized->begin() + pc + + kOnMatch2Off); + found = true; + fprintf(stderr, + "[POC] SkipUntilOneOfMasked @%u on_match2=%u length=%u\n", pc, + on_match2, length_optimized); + } + pc += regexp::Bytecodes::Size(bc); + } + CHECK(found); + // *** This is the bug: on_match2 is a kJumpTarget that exceeds the bytecode + // length, because it was carried unchanged through the pass-3 shrink. *** + CHECK_GE(on_match2, length_optimized); + + // Now feed the optimized bytecode into the tier-up CodeGenerator. + // PreVisitBytecodes() reads on_match2 as a kJumpTarget and indexes + // jump_targets_ (a BitVector sized to length_optimized) and labels_[] with + // it. Under ASAN this crashes with a use-after-poison / heap-buffer-overflow + // in BitVector::Contains/Add inside the Zone redzone. + regexp::BytecodeGenerator dummy_masm(i_isolate(), &zone, + regexp::RegExpMacroAssembler::LATIN1); + regexp::CodeGenerator code_gen(i_isolate(), &dummy_masm, array_optimized); + // ASAN aborts inside PreVisitBytecodes before masm_ is ever touched. + auto result = + code_gen.Assemble(CreateRegExpData(i_isolate(), source), regexp::Flags{}); + USE(result); +} + TEST_F(RegExpTestWithContext, UnicodePropertyEscapeCodeSize) { FlagScope f(&v8_flags.regexp_tier_up, false);