High watch CVSS 8.8 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Sub-componentJSON
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker452296415
Fix commit50eba5e6c269 (v8/v8) +50/-29
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
CreditedGoogle Big Sleep
Reported2025-10-15 (per vendor advisory)
Fix landed2025-10-16 (public git)
Disclosed2025-10-28
Patch gap12 days — fix public in source before the advisory

Background

`Expect`/`ExpectNext`
JsonParser helpers that check whether the next token matches an expected JsonToken and, on mismatch, report a parse error.
`ReportUnexpectedToken`
the error-reporting routine invoked on a failed expectation, which allocates and throws a JavaScript exception (an allocation that can trigger a GC).
`property_stack_` / `element_stack_`
parser-owned worklist vectors holding pending object properties and array elements while a JSON value is being built.
`V8_WARN_UNUSED_RESULT`
an attribute that forces callers to check a function’s return value at compile time.

Root Cause Analysis

The removed code shows the old Expect/ExpectNext implementation: on a token mismatch it called ReportUnexpectedToken and then simply set the cursor to the end of input and fell through with no signal to the caller. Because ReportUnexpectedToken allocates the exception object, it can trigger a GC, yet the caller continued executing the surrounding parse logic (e.g. ParseJsonPropertyValue, the object/array finalization paths, and the recursive parse loop) as if the expectation had succeeded, operating on parser state after an exception was already pending and a GC may have moved objects. The invariant violated is that once an exception is thrown parsing must unwind immediately rather than keep scanning and mutating property_stack_/element_stack_ and building objects.

The fix makes Expect/ExpectNext return a bool and marks them V8_WARN_UNUSED_RESULT, and replaces every call site with the EXPECT_RETURN_ON_ERROR/EXPECT_NEXT_RETURN_ON_ERROR macros that return early (with false or {}) the instant an expectation fails. This guarantees the parser stops touching heap state and stack vectors as soon as the exception is raised.

Key insight
The single mistake was that a failed token expectation reported/allocated an exception but did not abort parsing, letting the parser continue over possibly-relocated heap state with a pending exception; the fix converts the expectation into a checked bool and forces an immediate early return at every call site.

Attack Path

  1. Craft malformed JSON Supply JSON to JSON.parse that is well-formed enough to reach a point requiring a specific token (a : after a property name, a closing }/], or a quoted property name) but then presents the wrong token.
  2. Trigger the failed expectation The mismatch drives Expect/ExpectNext into ReportUnexpectedToken, which allocates the exception object and can provoke a GC. (inferred: the allocation/GC timing is the attacker-influenced trigger.)
  3. Continue parsing past the exception In the unpatched code the helper returns void, so the caller keeps running its finalization/loop logic and continues to read and mutate property_stack_/element_stack_ and build objects while an exception is pending and objects may have moved.
  4. Operate on inconsistent state Subsequent parser steps act on stale or relocated state, the logic error the advisory categorizes as an inappropriate implementation. (inferred) Any concrete memory-safety sink is not shown in the diff.

Impact Assessment

An attacker who controls a string passed to JSON.parse in the renderer can drive the JSON parser to continue executing after an exception allocation/GC, producing incorrect and potentially unsafe parser behavior. The advisory classifies this as a LogicError / inappropriate implementation in V8 and does not report exploitation in the wild; any escalation to a concrete memory-corruption or read/write primitive is (inferred) and not demonstrated by the patch. The only precondition is that untrusted input reaches JSON.parse.

Files Changed

  • src/json/json-parser.cc
  • src/json/json-parser.h

Audit Directions

  • Report-and-continue error helpers
    Look for parser/validator helpers that report or throw (and thus allocate) an error but return void, letting callers proceed; enforce a checked return value.
  • Allocation under a pending exception
    Audit code paths that keep reading or mutating heap-referencing state (worklists, handles, raw pointers) after a call that can allocate/GC or set a pending exception without unwinding.
  • Unchecked return values
    Search for expectation/consume-style APIs lacking V8_WARN_UNUSED_RESULT, where a silently ignored failure result changes control flow.
From 50eba5e6c269c71d6e0e758b84fafe8d5c37d210 Mon Sep 17 00:00:00 2001
From: pthier <pthier@chromium.org>
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 <ishell@chromium.org>
Commit-Queue: Patrick Thier <pthier@chromium.org>
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 bi...@google.com

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