Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Bug ClassUAF
Tracker513298483
Fix commite11fdcc64813 (v8/v8) +82/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/regexp/arm/regexp-macro-assembler-arm.cc
  • src/regexp/arm64/regexp-macro-assembler-arm64.cc
  • src/regexp/ia32/regexp-macro-assembler-ia32.cc
  • src/regexp/loong64/regexp-macro-assembler-loong64.cc
  • src/regexp/mips64/regexp-macro-assembler-mips64.cc
  • src/regexp/ppc/regexp-macro-assembler-ppc.cc
  • src/regexp/regexp-stack.h
  • src/regexp/riscv/regexp-macro-assembler-riscv.cc
  • src/regexp/s390/regexp-macro-assembler-s390.cc
  • src/regexp/x64/regexp-macro-assembler-x64.cc
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.