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 <jgruber@chromium.org>
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 <jgruber@chromium.org>
> Commit-Queue: Jakob Linke <jgruber@chromium.org>
> Reviewed-by: Patrick Thier <pthier@chromium.org>
> 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 <jgruber@chromium.org>
Reviewed-by: Patrick Thier <pthier@chromium.org>
Commit-Queue: Jakob Linke <jgruber@chromium.org>
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