Chrome · V8
CVE-2026-14403
UAF in V8
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/regexp/arm/regexp-macro-assembler-arm.ccsrc/regexp/arm64/regexp-macro-assembler-arm64.ccsrc/regexp/ia32/regexp-macro-assembler-ia32.ccsrc/regexp/loong64/regexp-macro-assembler-loong64.ccsrc/regexp/mips64/regexp-macro-assembler-mips64.ccsrc/regexp/ppc/regexp-macro-assembler-ppc.ccsrc/regexp/regexp-stack.hsrc/regexp/riscv/regexp-macro-assembler-riscv.ccsrc/regexp/s390/regexp-macro-assembler-s390.ccsrc/regexp/x64/regexp-macro-assembler-x64.cc
Patch
From e11fdcc64813b765779e970dc0dcba0674ffcdfc Mon Sep 17 00:00:00 2001 From: Jakob Linke <[email protected]> Date: Tue, 19 May 2026 09:17:25 +0200 Subject: [PATCH] Reland "[regexp] Refresh backtrack SP across prologue stack-guard call" This is a reland of commit 379e61d3994ddbab474d3344cfee87585c41f3b5 Only the test changed st we no longer write sentinel patterns into unmapped memory. The test is now limited to V8_ENABLE_SANDBOX_HARDWARE_SUPPORT, where it reliably triggers on the original bug. Original change's description: > [regexp] Refresh backtrack SP across prologue stack-guard call > > The native irregexp prologue's stack_limit_hit handler saved the > backtrack_stackpointer register across CallCheckStackGuardState via > push/pop (x64, ia32) or relied on the C ABI's callee-saved guarantee > (arm64, arm, loong64, mips64, ppc, riscv, s390). If that call ran an > API interrupt callback that re-entered irregexp and grew the backtrack > stack, the original buffer was freed but the register kept pointing > into it. Subsequent Push() operations during matching wrote into freed > memory. > > Mirror the check_preempt_label_ protocol on every port: store the > backtrack_stackpointer to RegExpStack::stack_pointer_ before the call, > reload it after, so GrowStack-induced relocation is observed. > > Fixed: 513298483 > Change-Id: I8ab7e850bb4963c69afa1552fc96bb1316c1c0f6 > Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7849379 > Auto-Submit: Jakob Linke <[email protected]> > Commit-Queue: Jakob Linke <[email protected]> > Reviewed-by: Patrick Thier <[email protected]> > Cr-Commit-Position: refs/heads/main@{#107370} Bug: 513298483 Cq-Include-Trybots: luci.v8.try:v8_linux64_pku_dbg,v8_linux64_pku_rel Change-Id: I230219c9602f515013efcd353423a23d882d0cc8 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7852919 Auto-Submit: Jakob Linke <[email protected]> Reviewed-by: Patrick Thier <[email protected]> Commit-Queue: Jakob Linke <[email protected]> Cr-Commit-Position: refs/heads/main@{#107475} --- diff --git a/src/regexp/arm/regexp-macro-assembler-arm.cc b/src/regexp/arm/regexp-macro-assembler-arm.cc index 5c19dd8..25294b9 100644 --- a/src/regexp/arm/regexp-macro-assembler-arm.cc +++ b/src/regexp/arm/regexp-macro-assembler-arm.cc @@ -762,10 +762,12 @@ __ jmp(&return_r0); __ bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), r1); CallCheckStackGuardState(extra_space_for_variables); __ cmp(r0, Operand::Zero()); // If returned value is non-zero, we exit with the returned value as result. __ b(ne, &return_r0); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/arm64/regexp-macro-assembler-arm64.cc b/src/regexp/arm64/regexp-macro-assembler-arm64.cc index 24d5700..f47bce4 100644 --- a/src/regexp/arm64/regexp-macro-assembler-arm64.cc +++ b/src/regexp/arm64/regexp-macro-assembler-arm64.cc @@ -1289,9 +1289,11 @@ __ B(&return_w0); __ Bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), x10); CallCheckStackGuardState(x10, extra_space_for_variables); // If returned value is non-zero, we exit with the returned value as result. __ Cbnz(w0, &return_w0); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ Bind(&stack_ok); } diff --git a/src/regexp/ia32/regexp-macro-assembler-ia32.cc b/src/regexp/ia32/regexp-macro-assembler-ia32.cc index 7206237..4136eaf 100644 --- a/src/regexp/ia32/regexp-macro-assembler-ia32.cc +++ b/src/regexp/ia32/regexp-macro-assembler-ia32.cc @@ -807,12 +807,12 @@ __ jmp(&return_eax); __ bind(&stack_limit_hit); - __ push(backtrack_stackpointer()); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), edi); CallCheckStackGuardState(ebx, extra_space_for_variables); - __ pop(backtrack_stackpointer()); __ or_(eax, eax); // If returned value is non-zero, we exit with the returned value as result. __ j(not_zero, &return_eax); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/loong64/regexp-macro-assembler-loong64.cc b/src/regexp/loong64/regexp-macro-assembler-loong64.cc index 9f451cc..b7dcd8b 100644 --- a/src/regexp/loong64/regexp-macro-assembler-loong64.cc +++ b/src/regexp/loong64/regexp-macro-assembler-loong64.cc @@ -741,10 +741,12 @@ __ jmp(&return_v0); __ bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), a1); CallCheckStackGuardState(a0, extra_space_for_variables); // If returned value is non-zero, we exit with the returned value as // result. __ Branch(&return_v0, ne, a0, Operand(zero_reg)); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/mips64/regexp-macro-assembler-mips64.cc b/src/regexp/mips64/regexp-macro-assembler-mips64.cc index 0d4f58e..b0abf9e 100644 --- a/src/regexp/mips64/regexp-macro-assembler-mips64.cc +++ b/src/regexp/mips64/regexp-macro-assembler-mips64.cc @@ -775,10 +775,12 @@ __ jmp(&return_v0); __ bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), a0); CallCheckStackGuardState(a0, extra_space_for_variables); // If returned value is non-zero, we exit with the returned value as // result. __ Branch(&return_v0, ne, v0, Operand(zero_reg)); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/ppc/regexp-macro-assembler-ppc.cc b/src/regexp/ppc/regexp-macro-assembler-ppc.cc index 9add970..2fa0fd7 100644 --- a/src/regexp/ppc/regexp-macro-assembler-ppc.cc +++ b/src/regexp/ppc/regexp-macro-assembler-ppc.cc @@ -808,11 +808,13 @@ __ b(&return_r3); __ bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), r4); CallCheckStackGuardState(r3, extra_space_for_variables); __ cmpi(r3, Operand::Zero()); // If returned value is non-zero, we exit with the returned value as // result. __ bne(&return_r3); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/regexp-stack.h b/src/regexp/regexp-stack.h index 3a5d796..cae82b0 100644 --- a/src/regexp/regexp-stack.h +++ b/src/regexp/regexp-stack.h @@ -82,7 +82,7 @@ // Ensures that there is a memory area with at least the specified size. // If passing zero, the default/minimum size buffer is allocated. - Address EnsureCapacity(size_t size); + V8_EXPORT_PRIVATE Address EnsureCapacity(size_t size); // Thread local archiving. static constexpr int ArchiveSpacePerThread() { diff --git a/src/regexp/riscv/regexp-macro-assembler-riscv.cc b/src/regexp/riscv/regexp-macro-assembler-riscv.cc index 7b59334..8abad4c 100644 --- a/src/regexp/riscv/regexp-macro-assembler-riscv.cc +++ b/src/regexp/riscv/regexp-macro-assembler-riscv.cc @@ -876,10 +876,12 @@ __ jmp(&return_a0); __ bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), a1); CallCheckStackGuardState(a0, extra_space_for_variables); // If returned value is non-zero, we exit with the returned value as // result. __ Branch(&return_a0, ne, a0, Operand(zero_reg)); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/s390/regexp-macro-assembler-s390.cc b/src/regexp/s390/regexp-macro-assembler-s390.cc index acb7993..a2f3fc3 100644 --- a/src/regexp/s390/regexp-macro-assembler-s390.cc +++ b/src/regexp/s390/regexp-macro-assembler-s390.cc @@ -827,10 +827,12 @@ __ b(&return_r2); __ bind(&stack_limit_hit); + StoreRegExpStackPointerToMemory(backtrack_stackpointer(), r3); CallCheckStackGuardState(r2, extra_space_for_variables); __ CmpS64(r2, Operand::Zero()); // If returned value is non-zero, we exit with the returned value as result. __ bne(&return_r2); + LoadRegExpStackPointerFromMemory(backtrack_stackpointer()); __ bind(&stack_ok); } diff --git a/src/regexp/x64/regexp-macro-assembler-x64.cc b/src/regexp/x64/regexp-macro-assembler-x64.cc index 8cba65e..cb36bd8 100644 --- a/src/regexp/x64/regexp-macro-assembler-x64.cc
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 0fed7fd..5d9ebbf 100644
--- a/test/unittests/regexp/regexp-unittest.cc
+++ b/test/unittests/regexp/regexp-unittest.cc
@@ -26,6 +26,7 @@
#include "src/regexp/regexp-interpreter.h"
#include "src/regexp/regexp-macro-assembler-arch.h"
#include "src/regexp/regexp-parser.h"
+#include "src/regexp/regexp-stack.h"
#include "src/strings/char-predicates-inl.h"
#include "src/strings/string-stream.h"
#include "src/strings/unicode-inl.h"
@@ -2626,6 +2627,68 @@
CHECK(IsNull(*result));
}
+// The bug below is only reliably observable under sandbox hardware support,
+// where the freed RegExpStack page is unmapped and a buggy JIT Push through
+// the dangling BSP segfaults. On other builds the success-path BSP recovery
+// hides the divergence.
+#ifdef V8_ENABLE_SANDBOX_HARDWARE_SUPPORT
+
+namespace {
+
+// Runs from the prologue's stack_limit_hit handler via CheckStackGuardState ->
+// HandleInterrupts -> InvokeApiInterruptCallbacks. Doubles the backtrack
+// stack to force GrowStack to relocate the per-isolate buffer; under sandbox
+// hardware support the freed page is unmapped (vas->FreePages -> munmap), so
+// any subsequent JIT Push through a stale BSP faults.
+void StalePrologueGrowStackInterrupt(v8::Isolate*, void* data) {
+ auto* i_iso = static_cast<i::Isolate*>(data);
+ i_iso->regexp_stack()->EnsureCapacity(i_iso->regexp_stack()->memory_size() *
+ 2);
+}
+
+} // namespace
+
+// Regression test for crbug.com/513298483. The native irregexp prologue's
+// stack_limit_hit handler used to save/restore the backtrack_stackpointer with
+// pushq/popq (x64, ia32) or via callee-saved registers (other arches) around
+// CallCheckStackGuardState, instead of round-tripping it through
+// RegExpStack::stack_pointer_. If the call ran an API interrupt that
+// re-entered irregexp and grew the backtrack stack (freeing the original
+// buffer), the restored register held a dangling pointer that subsequent
+// Push() operations wrote through. The fix replaces the push/pop with
+// Store/LoadRegExpStackPointerToMemory on every port, mirroring
+// check_preempt_label_.
+TEST_F(RegExpTestWithContext, RegExpInterruptStalePrologueBacktrackPointer) {
+ if (v8_flags.jitless) return;
+ v8_flags.regexp_tier_up_ticks = 0; // Compile to native on first exec.
+
+ v8::HandleScope scope(isolate());
+ i::Isolate* i_iso = i_isolate();
+
+ // Pre-grow the per-isolate backtrack stack so EnsureCapacity in the
+ // interrupt actually frees a heap buffer (rather than transitioning off the
+ // static stack).
+ i_iso->regexp_stack()->EnsureCapacity(2 * i::KB);
+
+ isolate()->RequestInterrupt(&StalePrologueGrowStackInterrupt, i_iso);
+
+ // Any regexp that hits the prologue's stack_limit_hit path will do.
+ i::DirectHandle<i::JSRegExp> regexp = v8::Utils::OpenDirectHandle(
+ *v8::RegExp::New(context(), NewString("(a|b)(a|b)(a|b)c"),
+ v8::RegExp::kNone)
+ .ToLocalChecked());
+ i::DirectHandle<i::String> subject =
+ v8::Utils::OpenDirectHandle(*NewString("aaac"));
+
+ i::DirectHandle<i::Object> result =
+ i::RegExp::Exec_Single(i_iso, regexp, subject, 0,
+ i_iso->regexp_last_match_info())
+ .ToHandleChecked();
+ USE(result);
+}
+
+#endif // V8_ENABLE_SANDBOX_HARDWARE_SUPPORT
+
#undef CHECK_PARSE_ERROR
#undef CHECK_SIMPLE
#undef CHECK_MIN_MAX
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page