Medium watch ~ plausible CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in V8
DescriptionOut of bounds read in V8
ComponentV8
Sub-componentTest
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: The core mechanism matches the diff and the reporter's output ('bb'→U+6262, 29 chars read). The pre-fix stringifier and SerializeString_ bodies are absent, so the uc16-over-one-byte read is inferred. The 'removed selector' line is actually context, and the map-inl.h move rationale is unsupported. The reporter's repro uses --allow-natives-syntax.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker398999390
Fix commit8db16e651ff4 (v8/v8) +123/-56
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedzeroxiaobai@
Reported2025-02-25 · bounty $2000 (per vendor advisory)
Fix landed2025-02-26 (public git)
Disclosed2025-03-10
Patch gap12 days — fix public in source before the advisory

Background

`SlicedString`
a V8 string that references a range of a parent string instead of owning its own character storage.
`IsOneByteRepresentation()`
a String accessor that reports one- vs two-byte encoding from the string’s own map/instance type alone.
`IsOneByteRepresentationUnderneath()`
a static helper that walks an indirect string (cons/sliced/thin) down to its underlying sequential string to determine the true encoding.
`JsonStringifier`
V8’s JSON.stringify implementation that copies string characters into an output buffer.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: The core mechanism matches the diff and the reporter's output ('bb'→U+6262, 29 chars read). The pre-fix stringifier and SerializeString_ bodies are absent, so the uc16-over-one-byte read is inferred. The 'removed selector' line is actually context, and the map-inl.h move rationale is unsupported. The reporter's repro uses --allow-natives-syntax.
Re-analyzed on 2026-10-03 against the actual fix commit 8db16e651ff4. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (812f91c58212).

The original CL being reverted (e3f33e99…) had removed IsOneByteRepresentationUnderneath and replaced its callers with IsOneByteRepresentation(), which reads encoding from the top-level string’s own map only (src/builtins/builtins-json.cc:26 string->IsOneByteRepresentation()). For an indirect string such as a SlicedString, the wrapper’s own encoding flag can disagree with the actual encoding of its parent/underlying string — the reporter notes the slice’s map can become outdated after its parent is updated. In src/json/json-stringifier.cc the serialize/append path decided the copy width from this top-level flag (removed diff lines string->IsOneByteRepresentation() and flat.IsOneByte()): when the wrapper’s flag claimed two-byte while the underlying storage was actually one-byte, the serializer took the two-byte (base::uc16) read path over a one-byte buffer (the SerializeString_<base::uc16, base::uc16, ...> instantiation shown as unchanged context in the json-stringifier.cc @@ -1690 hunk is the two-byte path; its body is not in the material), reading two bytes per character and running length bytes past the end of the stored data. The reporter’s output confirms this direction — "bbbb…" (bytes 0x62 0x62 …) came back as U+6262 (扢) code units, i.e. adjacent one-byte values read as single uint16 characters, which is only possible if one-byte storage was read as uc16; the opposite (reading two-byte data as one-byte for length chars) would stay in bounds and could not leak.

The fix (commit 8db16e65…, a partial revert) re-adds String::IsOneByteRepresentationUnderneath in src/objects/string-inl.h (added block following pre-fix line 368), a loop that follows string->GetUnderlying() while switching on kIsIndirectStringMask | kStringEncodingMask, and restores calls to it at the flattened-string representation decisions the original CL had replaced, including the stringifier copy loops (added diff lines if (String::IsOneByteRepresentationUnderneath(string)) paired with string->GetCharVector<uint8_t>() / <uint16_t>()). This restores the invariant that encoding decisions for flat indirect strings are taken from the underlying storage, so the chosen copy width always matches the real character width. The static-roots fast path introduced by the original CL is deliberately kept; only the function’s removal is reverted.

Key insight
The encoding of an indirect (e.g. sliced) string must be read from its underlying string, not from the wrapper’s own map flag; the fix reinstates IsOneByteRepresentationUnderneath so JSON serialization selects the correct one-/two-byte copy path.

Attack Path

  1. Build a one-byte-backed slice with a stale flag Create an indirect string (e.g. a SlicedString) whose own encoding flag indicates two-byte while its underlying storage is one-byte. (inferred)
  2. Reach JSON serialization Pass that string to JSON.stringify so JsonStringifier serializes it.
  3. Mismatched copy width The serializer reads the stale two-byte flag (string->IsOneByteRepresentation() returns false) and takes the uc16 copy path, reading the one-byte storage two bytes at a time (removed diff: flat.IsOneByte() / string->IsOneByteRepresentation()).
  4. Out-of-bounds read The uc16 read consumes length bytes past the one-byte buffer; those out-of-range heap bytes are emitted as uint16 code units, matching the reporter’s corrupted output ('bb' → U+6262).

Impact Assessment

Attacker-controlled script in the renderer can make V8 read out-of-bounds heap memory during JSON.stringify and surface those bytes in the resulting string, an information disclosure within the renderer process. The advisory rates this medium severity and records no in-the-wild exploitation. Escalation beyond an OOB read is not shown by the material.

Changed Functions

FunctionChangeNotes
if
src/json/json-stringifier.cc
modified
while
src/objects/string-inl.h
modified
if
src/objects/string-inl.h
modified
while
src/objects/string.tq
modified
typeswitch
src/objects/string.tq
modified

Files Changed

  • src/builtins/builtins-json.cc
  • src/builtins/builtins-string.cc
  • src/builtins/string-iswellformed.tq
  • src/builtins/string-towellformed.tq
  • src/json/json-stringifier.cc
  • src/numbers/conversions.cc
  • src/objects/instance-type-inl.h
  • src/objects/js-raw-json.cc
  • src/objects/js-regexp.cc
  • src/objects/map-inl.h
  • src/objects/map.h
  • src/objects/string-inl.h
  • src/objects/string.h
  • src/objects/string.tq
  • src/objects/templates-inl.h
  • src/regexp/experimental/experimental-interpreter.cc
  • src/regexp/regexp-interpreter.cc
  • src/regexp/regexp-macro-assembler.cc
  • src/regexp/regexp.cc
  • src/runtime/runtime-strings.cc
  • src/strings/string-builder.cc
  • src/strings/uri.cc

Audit Directions

  • Indirect-string encoding
    Audit any code deciding character width from IsOneByteRepresentation() on a possibly-indirect (cons/sliced/thin) string; after flattening it should use IsOneByteRepresentationUnderneath.
  • Representation caching
    Flag code that caches a string’s encoding across operations able to change the underlying string (externalization, parent update) and later copies using the stale value.
  • Static-roots fast paths
    Review map-word / static-roots shortcuts that infer encoding without dereferencing the underlying string, confirming they agree with the real representation.
diff --git a/src/builtins/builtins-json.cc b/src/builtins/builtins-json.cc
index 6773cc1..3cee2e7 100644
--- a/src/builtins/builtins-json.cc
+++ b/src/builtins/builtins-json.cc
@@ -23,7 +23,7 @@
                                      Object::ToString(isolate, source));
   string = String::Flatten(isolate, string);
   RETURN_RESULT_OR_FAILURE(
-      isolate, string->IsOneByteRepresentation()
+      isolate, String::IsOneByteRepresentationUnderneath(*string)
                    ? JsonParser<uint8_t>::Parse(isolate, string, reviver)
                    : JsonParser<uint16_t>::Parse(isolate, string, reviver));
 }
diff --git a/src/builtins/builtins-string.cc b/src/builtins/builtins-string.cc
index 0eb19b3..6c6fa83 100644
--- a/src/builtins/builtins-string.cc
+++ b/src/builtins/builtins-string.cc
@@ -344,7 +344,7 @@
   // character is also ASCII.  This is currently the case, but it
   // might break in the future if we implement more context and locale
   // dependent upper/lower conversions.
-  if (s->IsOneByteRepresentation()) {
+  if (String::IsOneByteRepresentationUnderneath(*s)) {
     // Same length as input.
     DirectHandle<SeqOneByteString> result =
         isolate->factory()->NewRawOneByteString(length).ToHandleChecked();
diff --git a/src/builtins/string-iswellformed.tq b/src/builtins/string-iswellformed.tq
index 2041efb..d0ff406 100644
--- a/src/builtins/string-iswellformed.tq
+++ b/src/builtins/string-iswellformed.tq
@@ -35,7 +35,7 @@
   // InstanceType. See
   // https://docs.google.com/document/d/15f-1c_Ysw3lvjy_Gx0SmmD9qeO8UuXuAbWIpWCnTDO8/
   const flat = Flatten(s);
-  if (flat.IsOneByteRepresentation()) return True;
+  if (flat.IsOneByteRepresentationUnderneath()) return True;
   try {
     const illFormed = HasUnpairedSurrogate(flat) otherwise Indirect;
     return illFormed ? False : True;
diff --git a/src/builtins/string-towellformed.tq b/src/builtins/string-towellformed.tq
index e00abc4d..e47876e 100644
--- a/src/builtins/string-towellformed.tq
+++ b/src/builtins/string-towellformed.tq
@@ -33,7 +33,7 @@
   // 4. Let k be 0.
   // 5. Let result be the empty String.
   const flat = Flatten(s);
-  if (flat.IsOneByteRepresentation()) return flat;
+  if (flat.IsOneByteRepresentationUnderneath()) return flat;
   let result = flat;
 
   // 6. Repeat, while k < strLen,
diff --git a/src/json/json-stringifier.cc b/src/json/json-stringifier.cc
index b1c0bfc..099981e 100644
--- a/src/json/json-stringifier.cc
+++ b/src/json/json-stringifier.cc
@@ -155,25 +155,29 @@
                           const DisallowGarbageCollection& no_gc) {
     DCHECK_EQ(length, string->length());
     DCHECK(encoding_ == String::TWO_BYTE_ENCODING ||
-           (string->IsFlat() && string->IsOneByteRepresentation()));
+           (string->IsFlat() &&
+            String::IsOneByteRepresentationUnderneath(string)));
     DCHECK(CurrentPartCanFit(length + 1));
-    String::FlatContent flat = string->GetFlatContent(no_gc);
     if (encoding_ == String::ONE_BYTE_ENCODING) {
-      if (flat.IsOneByte()) {
-        CopyChars<uint8_t, uint8_t>(one_byte_ptr_ + current_index_,
-                                    flat.ToOneByteVector().begin(), length);
+      if (String::IsOneByteRepresentationUnderneath(string)) {
+        CopyChars<uint8_t, uint8_t>(
+            one_byte_ptr_ + current_index_,
+            string->GetCharVector<uint8_t>(no_gc).begin(), length);
       } else {
         ChangeEncoding();
-        CopyChars<uint16_t, uint16_t>(two_byte_ptr_ + current_index_,
-                                      flat.ToUC16Vector().begin(), length);
+        CopyChars<uint16_t, uint16_t>(
+            two_byte_ptr_ + current_index_,
+            string->GetCharVector<uint16_t>(no_gc).begin(), length);
       }
     } else {
-      if (flat.IsOneByte()) {
-        CopyChars<uint8_t, uint16_t>(two_byte_ptr_ + current_index_,
-                                     flat.ToOneByteVector().begin(), length);
+      if (String::IsOneByteRepresentationUnderneath(string)) {
+        CopyChars<uint8_t, uint16_t>(
+            two_byte_ptr_ + current_index_,
+            string->GetCharVector<uint8_t>(no_gc).begin(), length);
       } else {
-        CopyChars<uint16_t, uint16_t>(two_byte_ptr_ + current_index_,
-                                      flat.ToUC16Vector().begin(), length);
+        CopyChars<uint16_t, uint16_t>(
+            two_byte_ptr_ + current_index_,
+            string->GetCharVector<uint16_t>(no_gc).begin(), length);
       }
     }
     current_index_ += length;
@@ -186,7 +190,8 @@
       Tagged<String> string = *string_handle;
       const bool representation_ok =
           encoding_ == String::TWO_BYTE_ENCODING ||
-          (string->IsFlat() && string->IsOneByteRepresentation());
+          (string->IsFlat() &&
+           String::IsOneByteRepresentationUnderneath(string));
       if (representation_ok) {
         size_t length = string->length();
         while (!CurrentPartCanFit(length + 1)) {
@@ -1529,7 +1534,8 @@
         vector, &no_extend);
   } else {
     DCHECK(encoding_ == String::TWO_BYTE_ENCODING ||
-           (string->IsFlat() && string->IsOneByteRepresentation()));
+           (string->IsFlat() &&
+            String::IsOneByteRepresentationUnderneath(string)));
     size_t uncopied_src_index = 0;  // Index of first char not copied yet.
     for (size_t i = 0; i < vector.size(); i++) {
       SrcChar c = vector.at(i);
@@ -1690,14 +1696,14 @@
   DisallowGarbageCollection no_gc;
   auto string = *object;
   if (encoding_ == String::ONE_BYTE_ENCODING) {
-    if (string->IsOneByteRepresentation()) {
+    if (String::IsOneByteRepresentationUnderneath(string)) {
       return SerializeString_<uint8_t, uint8_t, raw_json>(string, no_gc);
     } else {
       ChangeEncoding();
     }
   }
   DCHECK_EQ(encoding_, String::TWO_BYTE_ENCODING);
-  if (string->IsOneByteRepresentation()) {
+  if (String::IsOneByteRepresentationUnderneath(string)) {
     return SerializeString_<uint8_t, base::uc16, raw_json>(string, no_gc);
   } else {
     return SerializeString_<base::uc16, base::uc16, raw_json>(string, no_gc);
diff --git a/src/numbers/conversions.cc b/src/numbers/conversions.cc
index 0e9646a..3a9a1ef 100644
--- a/src/numbers/conversions.cc
+++ b/src/numbers/conversions.cc
@@ -343,7 +343,7 @@
   bool IsOneByte() const {
     if (raw_two_byte_subject_ != nullptr) return false;
     return raw_one_byte_subject_ != nullptr ||
-           subject_->IsOneByteRepresentation();
+           String::IsOneByteRepresentationUnderneath(*subject_);
   }
 
   base::Vector<const uint8_t> GetOneByteVector(
@@ -1365,7 +1365,7 @@
     return std::nullopt;
   }
 
-  if (object->IsOneByteRepresentation()) {
+  if (String::IsOneByteRepresentationUnderneath(*object)) {
     uint8_t buffer[kMaxLengthForConversion];
     SharedStringAccessGuardIfNeeded access_guard(isolate);
     String::WriteToFlat(*object, buffer, 0, length, access_guard);
diff --git a/src/objects/instance-type-inl.h b/src/objects/instance-type-inl.h
index 50e60eb..7eb127b 100644
--- a/src/objects/instance-type-inl.h
+++ b/src/objects/instance-type-inl.h
@@ -13,6 +13,7 @@
 #include "src/base/bounds.h"
 #include "src/execution/isolate-utils-inl.h"
 #include "src/objects/instance-type-checker.h"
+#include "src/objects/map-inl.h"
 
 // Has to be the last include (doesn't have include guards):
 #include "src/objects/object-macros.h"
diff --git a/src/objects/js-raw-json.cc b/src/objects/js-raw-json.cc
index 810dbc7..f7623f7 100644
--- a/src/objects/js-raw-json.cc
+++ b/src/objects/js-raw-json.cc
@@ -20,7 +20,7 @@
   ASSIGN_RETURN_ON_EXCEPTION(isolate, json_string,
                              Object::ToString(isolate, text));
   Handle<String> flat = String::Flatten(isolate, json_string);
-  if (flat->IsOneByteRepresentation()) {
+  if (String::IsOneByteRepresentationUnderneath(*flat)) {
     if (!JsonParser<uint8_t>::CheckRawJson(isolate, flat)) {
       DCHECK(isolate->has_exception());
       return MaybeDirectHandle<JSRawJson>();
diff --git a/src/objects/js-regexp.cc b/src/objects/js-regexp.cc
index ed46821..624d2ad 100644
--- a/src/objects/js-regexp.cc
+++ b/src/objects/js-regexp.cc
@@ -302,7 +302,7 @@
                                              DirectHandle<String> source) {
   DCHECK(source->IsFlat());
   if (source->length() == 0) return isolate->factory()->query_colon_string();
-  bool one_byte = source->IsOneByteRepresentation();
+  bool one_byte = String::IsOneByteRepresentationUnderneath(*source);
   bool needs_escapes = false;
   int additional_escape_chars =
       one_byte ? CountAdditionalEscapeChars<uint8_t>(source, &needs_escapes)
diff --git a/src/objects/map-inl.h b/src/objects/map-inl.h
index 1a0f6f6..3689a1d 100644
--- a/src/objects/map-inl.h
+++ b/src/objects/map-inl.h
@@ -363,6 +363,17 @@
   return AddMissingTransitions(isolate, split_map, descriptors);
 }
 
+InstanceType Map::instance_type() const {
+  // TODO(solanes, v8:7790, v8:11353, v8:11945): Make this and the setter
+  // non-atomic when TSAN sees the map's store synchronization.
+  return static_cast<InstanceType>(
+      RELAXED_READ_UINT16_FIELD(*this, kInstanceTypeOffset));
+}
+
+void Map::set_instance_type(InstanceType value) {
+  RELAXED_WRITE_UINT16_FIELD(*this, kInstanceTypeOffset, value);
+}
+
 int Map::UnusedPropertyFields() const {
 #if V8_ENABLE_WEBASSEMBLY
   DCHECK(!IsWasmObjectMap(*this));
diff --git a/src/objects/map.h b/src/objects/map.h
index a36cf90..bd88404 100644
--- a/src/objects/map.h
+++ b/src/objects/map.h
@@ -1109,20 +1109,6 @@
 inline bool IsSpecialReceiverMap(Tagged<Map> map);
 inline bool IsCustomElementsReceiverMap(Tagged<Map> map);
 
-// Define the instance type accessors in the `.h` instead of `-inl.h` to avoid
-// a circular dependency with the instance-type-inl.h header.
-
-InstanceType Map::instance_type() const {
-  // TODO(solanes, v8:7790, v8:11353, v8:11945): Make this and the setter
-  // non-atomic when TSAN sees the map's store synchronization.
-  return static_cast<InstanceType>(
-      RELAXED_READ_UINT16_FIELD(*this, kInstanceTypeOffset));
-}
-
-void Map::set_instance_type(InstanceType value) {
-  RELAXED_WRITE_UINT16_FIELD(*this, kInstanceTypeOffset, value);
-}
-
 }  // namespace v8::internal
 
 #include "src/objects/object-macros-undef.h"
diff --git a/src/objects/string-inl.h b/src/objects/string-inl.h
index e484c1a..7ef6664 100644
--- a/src/objects/string-inl.h
+++ b/src/objects/string-inl.h
@@ -368,6 +368,24 @@
   return InstanceTypeChecker::IsTwoByteString(map());
 }
 
+// static
+bool String::IsOneByteRepresentationUnderneath(Tagged<String> string) {
+  while (true) {
+    uint32_t type = string->map()->instance_type();
+    static_assert(kIsIndirectStringTag != 0);
+    static_assert((kIsIndirectStringMask & kStringEncodingMask) == 0);
+    DCHECK(string->IsFlat());
+    switch (type & (kIsIndirectStringMask | kStringEncodingMask)) {
+      case kOneByteStringTag:
+        return true;
+      case kTwoByteStringTag:
+        return false;
+      default:  // Cons, sliced, thin, strings need to go deeper.
+        string = string->GetUnderlying();
+    }
+  }
+}
+
 base::uc32 FlatStringReader::Get(uint32_t index) const {
   if (is_one_byte_) {
     return Get<uint8_t>(index);
@@ -1102,7 +1120,7 @@
   // InstanceType. See
   // https://docs.google.com/document/d/15f-1c_Ysw3lvjy_Gx0SmmD9qeO8UuXuAbWIpWCnTDO8/
   string = Flatten(isolate, string);
-  if (string->IsOneByteRepresentation()) return true;
+  if (String::IsOneByteRepresentationUnderneath(*string)) return true;
   DisallowGarbageCollection no_gc;
   String::FlatContent flat = string->GetFlatContent(no_gc);
   DCHECK(flat.IsFlat());
diff --git a/src/objects/string.h b/src/objects/string.h
index 31fc114..3a4d4510 100644
--- a/src/objects/string.h
+++ b/src/objects/string.h
@@ -234,10 +234,19 @@
   inline void set_length(uint32_t hash);
   inline void set_length(uint32_t hash, ReleaseStoreTag);
 
-  // Returns whether this string is stored with one-byte chars.
+  // Returns whether this string has only one-byte chars, i.e. all of them can
+  // be one-byte encoded.  This might be the case even if the string is
+  // two-byte.  Such strings may appear when the embedder prefers
+  // two-byte external representations even for one-byte data.
   inline bool IsOneByteRepresentation() const;
   inline bool IsTwoByteRepresentation() const;
 
+  // Cons and slices have an encoding flag that may not represent the actual
+  // encoding of the underlying string.  This is taken into account here.
+  // This function is static because that helps it get inlined.
+  // Requires: string.IsFlat()
+  static inline bool IsOneByteRepresentationUnderneath(Tagged<String> string);
+
   // Get and set individual two byte chars in the string.
   inline void Set(uint32_t index, uint16_t value);
   // Get individual two byte char in the string.  Repeated calls
diff --git a/src/objects/string.tq b/src/objects/string.tq
index 7b4dcab..0165da4 100644
--- a/src/objects/string.tq
+++ b/src/objects/string.tq
@@ -21,6 +21,33 @@
     return IsOneByteStringMap(this.map);
   }
 
+  // Keep this in sync with the C++ String::IsOneByteRepresentationUnderneath.
+  macro IsOneByteRepresentationUnderneath(): bool {
+    let string = this;
+    while (true) {
+      typeswitch (string) {
+        case (cons: ConsString): {
+          dcheck(cons.IsFlat());
+          string = cons.first;
+        }
+        case (thin: ThinString): {
+          // Internalized strings can't change representation.
+          dcheck(
+              thin.IsOneByteRepresentation() ==
+              thin.actual.IsOneByteRepresentation());
+          return thin.IsOneByteRepresentation();
+        }
+        case (slice: SlicedString): {
+          string = slice.parent;
+        }
+        case (str: String): {
+          return str.IsOneByteRepresentation();
+        }
+      }
+    }
+    VerifiedUnreachable();
+  }
+
   const length: int32;
 }
 
diff --git a/src/objects/templates-inl.h b/src/objects/templates-inl.h
index dfbe43e..b111c75 100644
--- a/src/objects/templates-inl.h
+++ b/src/objects/templates-inl.h
@@ -6,6 +6,7 @@
 #define V8_OBJECTS_TEMPLATES_INL_H_
 
 #include "src/heap/heap-write-barrier-inl.h"
+#include "src/objects/dictionary.h"
 #include "src/objects/objects-inl.h"
 #include "src/objects/oddball.h"
 #include "src/objects/shared-function-info.h"
diff --git a/src/regexp/experimental/experimental-interpreter.cc b/src/regexp/experimental/experimental-interpreter.cc
index fa6a7ef..375f94a 100644
--- a/src/regexp/experimental/experimental-interpreter.cc
+++ b/src/regexp/experimental/experimental-interpreter.cc
@@ -760,7 +760,12 @@
         isolate_->StackOverflow();
         return RegExp::kInternalRegExpException;
       } else if (check.InterruptRequested()) {
-        const bool was_one_byte = input_object_->IsOneByteRepresentation();
+        // TODO(mbid): Is this really equivalent to whether the string is
+        // one-byte or two-byte? A comment at the declaration of
+        // IsOneByteRepresentationUnderneath says that this might fail for
+        // external strings.
+        const bool was_one_byte =
+            String::IsOneByteRepresentationUnderneath(input_object_);
 
         Tagged<Object> result;
         {
@@ -774,7 +779,8 @@
         // If we changed between a LATIN1 and a UC16 string, we need to restart
         // regexp matching with the appropriate template instantiation of
         // RawMatch.
-        if (input_handle->IsOneByteRepresentation() != was_one_byte) {
+        if (String::IsOneByteRepresentationUnderneath(*input_handle) !=
+            was_one_byte) {
           return RegExp::kInternalRegExpRetry;
         }
 
diff --git a/src/regexp/regexp-interpreter.cc b/src/regexp/regexp-interpreter.cc
index e1c1363..655f0cf 100644
--- a/src/regexp/regexp-interpreter.cc
+++ b/src/regexp/regexp-interpreter.cc
@@ -294,7 +294,7 @@
       return ThrowStackOverflow(isolate, call_origin);
     } else if (check.InterruptRequested()) {
       const bool was_one_byte =
-          (*subject_string_out)->IsOneByteRepresentation();
+          String::IsOneByteRepresentationUnderneath(*subject_string_out);
       Tagged<Object> result;
       {
         AllowGarbageCollection yes_gc;
@@ -307,7 +307,8 @@
       // If we changed between a LATIN1 and a UC16 string, we need to
       // restart regexp matching with the appropriate template instantiation of
       // RawMatch.
... (file diff truncated)
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
Uploaded PoC files