CVE-2026-3926
Overview
Files Changed
src/builtins/builtins-regexp.cc
Patch
From 134c3696cd53593d7a668bf3cc88a06a1750c67d Mon Sep 17 00:00:00 2001
From: pthier <pthier@chromium.org>
Date: Tue, 27 Jan 2026 16:58:47 +0100
Subject: [PATCH] Fix RegExp.escape byte check
RegExp.escape wrongly used IsOneByteRepresentation() instead of
IsOneByteRepresentationUnderneath().
Fixed: 478659010
Bug: 353856236
Change-Id: Icbee422d6e76f423c447877956554156287b72d8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7520539
Commit-Queue: Jakob Linke <jgruber@chromium.org>
Auto-Submit: Patrick Thier <pthier@chromium.org>
Reviewed-by: Jakob Linke <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/main@{#104948}
---
diff --git a/src/builtins/builtins-regexp.cc b/src/builtins/builtins-regexp.cc
index fc2ddc1..8566a84 100644
--- a/src/builtins/builtins-regexp.cc
+++ b/src/builtins/builtins-regexp.cc
@@ -370,7 +370,7 @@
// A copy of the input characters is needed because RegExpEscapeImpl builds up
// the escaped string using IncrementalStringBuilder, which may allocate.
str = String::Flatten(isolate, str);
- if (str->IsOneByteRepresentation()) {
+ if (String::IsOneByteRepresentationUnderneath(*str)) {
base::OwnedVector<const uint8_t> copy;
{
DisallowGarbageCollection no_gc;
Original Bug Report
RegExp.escape byteness mismatch OOB read
#VULNERABILITY DETAILS RegExp.escape determines whether to copy the input as one-byte or two-byte based on the string’s map (String::IsOneByteRepresentation). A crafted SlicedString can keep a two-byte map while its underlying storage becomes one-byte after internalization/thinning, causing RegExp.escape to read the string as UC16 from one-byte backing storage. This results in an out-of-bounds read.
#VERSION d8 Version: [14.6.0] + [candidate] Operating System: [Ubuntu 24.04 x64]
#Root cause RegExp.escape flattens the input, then branches on IsOneByteRepresentation():
- builtins-regexp.cc:352-388
- String::Flatten(isolate, str)
- if (str->IsOneByteRepresentation())
copy = OwnedCopyOf(str->GetFlatContent().ToOneByteVector())
else
copy = OwnedCopyOf(str->GetFlatContent().ToUC16Vector())
IsOneByteRepresentation() is map-based (string shape), not content-based:
- string-inl.h:521-523
String::IsOneByteRepresentation() -> InstanceTypeChecker::IsOneByteString(map)
When a non-internalized string is internalized, it can become a ThinString that points to a canonical internalized string whose map encoding is one-byte:
- string.cc:155-200
String::MakeThin uses internalized->IsOneByteRepresentation() to select the
thin map and rewrites the original to ThinString(actual = internalized)
For sliced strings, the slice keeps its own map (two-byte) even if the parent string becomes thin to a one-byte internalized canonical. This creates a byteness mismatch: map says two-byte, backing storage is one-byte.
#Crash State
=================================================================
==125374==ERROR: AddressSanitizer: use-after-poison on address 0x6e12c0005028 at pc 0x57ee1e60cffb bp 0x7ffcef6cb890 sp 0x7ffcef6cb050
READ of size 40 at 0x6e12c0005028 thread T0
#0 0x57ee1e60cffa in __asan_memcpy (/home/qy/new2/v8/out/x64.asan/d8+0x13b8ffa) (BuildId: e276665bb2c58cdf)
#1 0x57ee1eb00b1f in MemCopy src/base/memcopy.h
#2 0x57ee1eb00b1f in Copy<const unsigned short *, unsigned short *> src/base/algorithm.h:38:3
#3 0x57ee1eb00b1f in NewByCopying<unsigned short> src/base/vector.h:304:5
#4 0x57ee1eb00b1f in OwnedCopyOf<unsigned short> src/base/vector.h:386:10
#5 0x57ee1eb00b1f in OwnedCopyOf<v8::base::Vector<const unsigned short> > src/base/vector.h:395:10
#6 0x57ee1eb00b1f in v8::internal::Builtin_Impl_RegExpEscape(v8::internal::BuiltinArguments, v8::internal::Isolate*) src/builtins/builtins-regexp.cc:385:14
#7 0x57ee23415235 in Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit setup-isolate-deserialize.cc
#8 0x57ee23364829 in Builtins_InterpreterEntryTrampoline setup-isolate-deserialize.cc
#9 0x57ee233615db in Builtins_JSEntryTrampoline setup-isolate-deserialize.cc
#10 0x57ee2336132a in Builtins_JSEntry setup-isolate-deserialize.cc
#11 0x57ee1ed1a126 in Call src/execution/simulator.h:216:12
#12 0x57ee1ed1a126 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/execution.cc:442:22
#13 0x57ee1ed1b5a8 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSFunction>, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>) src/execution/execution.cc:542:10
#14 0x57ee1e936b4b in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) src/api/api.cc:2015:7
#15 0x57ee1e6780e7 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local<v8::String>, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*) src/d8/d8.cc:1037:44
#16 0x57ee1e6b0549 in v8::SourceGroup::Execute(v8::Isolate*) src/d8/d8.cc:5591:10
#17 0x57ee1e6bc84d in v8::Shell::RunMainIsolate(v8::Isolate*, bool) src/d8/d8.cc:6599:37
#18 0x57ee1e6bbc85 in v8::Shell::RunMain(v8::Isolate*, bool) src/d8/d8.cc:6507:18
#19 0x57ee1e6bf327 in v8::Shell::Main(int, char**) src/d8/d8.cc:7404:18
#20 0x736220e2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#21 0x736220e2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#22 0x57ee1e56d029 in _start (/home/qy/new2/v8/out/x64.asan/d8+0x1319029) (BuildId: e276665bb2c58cdf)
Address 0x6e12c0005028 is a wild pointer inside of access range of size 0x000000000028.
SUMMARY: AddressSanitizer: use-after-poison (/home/qy/new2/v8/out/x64.asan/d8+0x13b8ffa) (BuildId: e276665bb2c58cdf) in __asan_memcpy
Shadow bytes around the buggy address:
0x6e12c0004d80: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0004e00: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0004e80: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0004f00: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0004f80: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
=>0x6e12c0005000: 00 00 00 00 00[f7]f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0005080: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0005100: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0005180: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0005200: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x6e12c0005280: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
NOTE: the stack trace above identifies the code that *accessed* the poisoned memory.
To identify the code that *poisoned* the memory, try the experimental setting ASAN_OPTIONS=poison_history_size=<size>.
==125374==ABORTING
Received signal 6
==== C stack trace ===============================
out/x64.asan/d8(__interceptor_backtrace+0x46)[0x57ee1e5b4b36]
out/x64.asan/d8(+0x63976c0)[0x57ee235eb6c0]
/lib/x86_64-linux-gnu/libc.so.6(+0x45330)[0x736220e45330]
/lib/x86_64-linux-gnu/libc.so.6(pthread_kill+0x11c)[0x736220e9eb2c]
/lib/x86_64-linux-gnu/libc.so.6(gsignal+0x1e)[0x736220e4527e]
/lib/x86_64-linux-gnu/libc.so.6(abort+0xdf)[0x736220e288ff]
out/x64.asan/d8(+0x13dbc5c)[0x57ee1e62fc5c]
out/x64.asan/d8(+0x13da44e)[0x57ee1e62e44e]
out/x64.asan/d8(+0x13c188b)[0x57ee1e61588b]
out/x64.asan/d8(+0x13c366d)[0x57ee1e61766d]
out/x64.asan/d8(__asan_memcpy+0x3ab)[0x57ee1e60d02b]
out/x64.asan/d8(+0x18acb20)[0x57ee1eb00b20]
out/x64.asan/d8(+0x61c1236)[0x57ee23415236]
[end of stack trace]
Aborted