Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in V8
DescriptionOut of bounds read in V8
ComponentV8
Bug ClassOOB
Tracker398999390
Fix commit812f91c58212 (v8/v8) +112/-70
CISA KEVNot listed
Creditedzeroxiaobai@
Disclosed2025-03-10

Changed Functions

FunctionChangeNotes
if
src/extensions/externalize-string-extension.cc
modified

Files Changed

  • src/extensions/externalize-string-extension.cc
From 812f91c5821259311a8d321b23654e7f74284a52 Mon Sep 17 00:00:00 2001
From: pthier <pthier@chromium.org>
Date: Thu, 27 Feb 2025 12:40:24 +0100
Subject: [PATCH] [test] Add createExternalizableTwoByteString to extension

Allow to explicitly create an external 2-byte string for 1-byte only
contents via the externalize-string extension.

Bug: 398999390
Change-Id: I1fe11186c686a31e1d68d662af156709967f499f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6304942
Commit-Queue: Patrick Thier <pthier@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Patrick Thier <pthier@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#98969}
---

diff --git a/src/extensions/externalize-string-extension.cc b/src/extensions/externalize-string-extension.cc
index a5b450a..bfbd58f 100644
--- a/src/extensions/externalize-string-extension.cc
+++ b/src/extensions/externalize-string-extension.cc
@@ -54,6 +54,7 @@
   base::SNPrintF(base::VectorOf(buf, size),
                  "native function externalizeString();"
                  "native function createExternalizableString();"
+                 "native function createExternalizableTwoByteString();"
                  "native function isOneByteString();"
                  "let kExternalStringMinOneByteLength = %d;"
                  "let kExternalStringMinTwoByteLength = %d;"
@@ -73,6 +74,10 @@
                     "createExternalizableString") == 0) {
     return v8::FunctionTemplate::New(
         isolate, ExternalizeStringExtension::CreateExternalizableString);
+  } else if (strcmp(*v8::String::Utf8Value(isolate, str),
+                    "createExternalizableTwoByteString") == 0) {
+    return v8::FunctionTemplate::New(
+        isolate, ExternalizeStringExtension::CreateExternalizableTwoByteString);
   } else {
     DCHECK_EQ(strcmp(*v8::String::Utf8Value(isolate, str), "isOneByteString"),
               0);
@@ -151,6 +156,80 @@
                                            AllocationType::kOld);
 }
 
+MaybeDirectHandle<String> CreateExternalizableString(
+    v8::Isolate* isolate, DirectHandle<String> string,
+    v8::String::Encoding encoding) {
+  Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate);
+  DCHECK_IMPLIES(encoding == v8::String::Encoding::ONE_BYTE_ENCODING,
+                 string->IsOneByteRepresentation());
+  if (string->SupportsExternalization(encoding)) {
+    return string;
+  }
+  // Return the string if it is already externalized.
+  if (StringShape(*string).IsExternal()) {
+    return string;
+  }
+
+  // Read-only strings are never externalizable. Don't try to copy them as
+  // some parts of the code might rely on some strings being in RO space (i.e.
+  // empty string).
+  if (HeapLayout::InReadOnlySpace(*string)) {
+    isolate->ThrowError("Read-only strings cannot be externalized.");
+    return kNullMaybeHandle;
+  }
+#ifdef V8_COMPRESS_POINTERS
+  // Small strings may not be in-place externalizable.
+  if (string->Size() < static_cast<int>(sizeof(UncachedExternalString))) {
+    isolate->ThrowError("String is too short to be externalized.");
+    return kNullMaybeHandle;
+  }
+#endif
+
+  // Special handling for ConsStrings, as the ConsString -> ExternalString
+  // migration is special for GC (Tagged pointers to Untagged pointers).
+  // Skip if the ConsString is flat, as we won't be guaranteed a string in old
+  // space in that case. Note that this is also true for non-canonicalized
+  // ConsStrings that TurboFan might create (the first part is empty), so we
+  // explicitly check for that case as well.
+  if (IsConsString(*string, i_isolate) && !string->IsFlat() &&
+      Cast<ConsString>(string)->first()->length() != 0) {
+    DirectHandle<String> result;
+    if (CopyConsStringToOld(i_isolate, Cast<ConsString>(string))
+            .ToHandle(&result)) {
+      DCHECK(result->SupportsExternalization(encoding));
+      return result;
+    }
+  }
+  // All other strings can be implicitly flattened.
+  if (encoding == v8::String::ONE_BYTE_ENCODING) {
+    MaybeDirectHandle<SeqOneByteString> maybe_result =
+        i_isolate->factory()->NewRawOneByteString(string->length(),
+                                                  AllocationType::kOld);
+    DirectHandle<SeqOneByteString> result;
+    if (maybe_result.ToHandle(&result)) {
+      DisallowGarbageCollection no_gc;
+      String::WriteToFlat(*string, result->GetChars(no_gc), 0,
+                          string->length());
+      DCHECK(result->SupportsExternalization(encoding));
+      return result;
+    }
+  } else {
+    MaybeDirectHandle<SeqTwoByteString> maybe_result =
+        i_isolate->factory()->NewRawTwoByteString(string->length(),
+                                                  AllocationType::kOld);
+    DirectHandle<SeqTwoByteString> result;
+    if (maybe_result.ToHandle(&result)) {
+      DisallowGarbageCollection no_gc;
+      String::WriteToFlat(*string, result->GetChars(no_gc), 0,
+                          string->length());
+      DCHECK(result->SupportsExternalization(encoding));
+      return result;
+    }
+  }
+  isolate->ThrowError("Unable to create string");
+  return kNullMaybeHandle;
+}
+
 }  // namespace
 
 void ExternalizeStringExtension::CreateExternalizableString(
@@ -161,82 +240,43 @@
         "First parameter to createExternalizableString() must be a string.");
     return;
   }
+  v8::Isolate* isolate = info.GetIsolate();
   DirectHandle<String> string =
       Utils::OpenDirectHandle(*info[0].As<v8::String>());
-  Isolate* isolate = reinterpret_cast<Isolate*>(info.GetIsolate());
   v8::String::Encoding encoding = string->IsOneByteRepresentation()
                                       ? v8::String::Encoding::ONE_BYTE_ENCODING
                                       : v8::String::Encoding::TWO_BYTE_ENCODING;
-  if (string->SupportsExternalization(encoding)) {
-    info.GetReturnValue().Set(Utils::ToLocal(string));
-    return;
-  }
-  // Return the string if it is already externalized.
-  if (StringShape(*string).IsExternal()) {
-    info.GetReturnValue().Set(Utils::ToLocal(string));
-    return;
-  }
-
-  // Read-only strings are never externalizable. Don't try to copy them as
-  // some parts of the code might rely on some strings being in RO space (i.e.
-  // empty string).
-  if (HeapLayout::InReadOnlySpace(*string)) {
-    info.GetIsolate()->ThrowError("Read-only strings cannot be externalized.");
-    return;
-  }
-#ifdef V8_COMPRESS_POINTERS
-  // Small strings may not be in-place externalizable.
-  if (string->Size() < static_cast<int>(sizeof(UncachedExternalString))) {
-    info.GetIsolate()->ThrowError("String is too short to be externalized.");
-    return;
-  }
-#endif
-
-  // Special handling for ConsStrings, as the ConsString -> ExternalString
-  // migration is special for GC (Tagged pointers to Untagged pointers).
-  // Skip if the ConsString is flat, as we won't be guaranteed a string in old
-  // space in that case. Note that this is also true for non-canonicalized
-  // ConsStrings that TurboFan might create (the first part is empty), so we
-  // explicitly check for that case as well.
-  if (IsConsString(*string, isolate) && !string->IsFlat() &&
-      Cast<ConsString>(string)->first()->length() != 0) {
-    DirectHandle<String> result;
-    if (CopyConsStringToOld(isolate, Cast<ConsString>(string))
-            .ToHandle(&result)) {
-      DCHECK(result->SupportsExternalization(encoding));
-      info.GetReturnValue().Set(Utils::ToLocal(result));
-      return;
-    }
-  }
-  // All other strings can be implicitly flattened.
-  if (encoding == v8::String::ONE_BYTE_ENCODING) {
-    MaybeDirectHandle<SeqOneByteString> maybe_result =
-        isolate->factory()->NewRawOneByteString(string->length(),
-                                                AllocationType::kOld);
-    DirectHandle<SeqOneByteString> result;
-    if (maybe_result.ToHandle(&result)) {
-      DisallowGarbageCollection no_gc;
-      String::WriteToFlat(*string, result->GetChars(no_gc), 0,
-                          string->length());
-      DCHECK(result->SupportsExternalization(encoding));
-      info.GetReturnValue().Set(Utils::ToLocal(Cast<String>(result)));
-      return;
-    }
+  MaybeDirectHandle<String> maybe_result =
+      i::CreateExternalizableString(isolate, string, encoding);
+  DirectHandle<String> result;
+  if (maybe_result.ToHandle(&result)) {
+    DCHECK(!isolate->HasPendingException());
+    info.GetReturnValue().Set(Utils::ToLocal(result));
   } else {
-    MaybeDirectHandle<SeqTwoByteString> maybe_result =
-        isolate->factory()->NewRawTwoByteString(string->length(),
-                                                AllocationType::kOld);
-    DirectHandle<SeqTwoByteString> result;
-    if (maybe_result.ToHandle(&result)) {
Loading diff…

Original Bug Report

reported by ze...@gmail.com

OOB read in JsonStringifier::SerializeString

VULNERABILITY DETAILS

After updating the parent of a SlicedString, its map object may become outdated, leading to Out-of-Band memory reads during JSON serialization.

VERSION

V8 commit: be6d95e1118064c533c56e0547627374bd2bd434

REPRODUCTION CASE

Build args:

is_component_build = false
is_debug = false
target_cpu = "arm64"
v8_target_cpu = "arm64"
v8_enable_sandbox = true
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
dcheck_always_on = false

Shell args:

./out/arm64.release/d8 --allow-natives-syntax bug.js

Shell output:

expect:  "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
found:  "扢扢扢扢扢扢扢扢扢扢扢扢扢扢啢\u0001切Β\u0000猀牴唀\u0001昀Ἤߺ\u0000猀"

CREDIT INFORMATION Reporter credit: zeroxiaobai@gmail.com

View on issue tracker