Skip to content

Commit 7b5b5f2

Browse files
author
Mohamed Sayed
committed
deps: backport V8 escaped JSON key transition fix
Backport V8 commit 93cd21e8254b65c15ff131b2dcadf19beadb559d. Refs: https://chromium.googlesource.com/v8/v8/+/93cd21e8254b65c15ff131b2dcadf19beadb559d Signed-off-by: Mohamed Sayed <k@3zrv.com>
1 parent 019e869 commit 7b5b5f2

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

‎deps/v8/src/json/json-parser.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,12 @@ class JsonParser final {
383383
JsonString ScanJsonPropertyKey(JsonContinuation* cont);
384384
base::uc32 ScanUnicodeCharacter();
385385
base::Vector<const Char> GetKeyChars(JsonString key) {
386+
// For escaped keys the source range starting at `key.start()` holds the raw
387+
// (undecoded) characters while `key.length()` is the decoded length, so the
388+
// bytes here do not represent the actual decoded key. Return an empty
389+
// vector to signal that the byte-compare transition fast path must be
390+
// skipped (see JSDataObjectBuilder::TryFastTransitionToPropertyKey).
391+
if (key.has_escape()) return base::Vector<const Char>();
386392
return base::Vector<const Char>(chars_ + key.start(), key.length());
387393
}
388394
Handle<String> MakeString(const JsonString& string,

‎deps/v8/src/objects/js-data-object-builder-inl.h‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,12 @@ bool JSDataObjectBuilder::TryFastTransitionToPropertyKey(
417417
descriptor_index)),
418418
isolate_);
419419
target_map = expected_final_map_;
420-
} else {
420+
} else if (key_chars.data() != nullptr) {
421+
// The byte-compare transition fast path is only valid when `key_chars`
422+
// faithfully represent the decoded key. Callers that cannot provide valid
423+
// chars (the JSON parser passes an empty vector for escaped keys, whose raw
424+
// source bytes differ from the decoded key) fall through to the
425+
// decode-then-FindTransitionToField path below.
421426
TransitionsAccessor transitions(isolate_, *map_);
422427
auto expected_transition = transitions.ExpectedTransition(key_chars);
423428
if (!expected_transition.first.is_null()) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2026 the V8 project authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Regression test: JSON.parse must not match a hidden-class transition for an
6+
// escaped property key using the raw (undecoded) source bytes. The builder's
7+
// fast transition path compared `chars_ + key.start()` over `key.length()`
8+
// (the *decoded* length), so escaped keys (e.g. "\t", "\uXXXX") collapsed to
9+
// the byte 0x5C and matched a previously-seen "\\" key, making JSON.parse adopt
10+
// the wrong key. Transitions are isolate-global, so a prior parse corrupted a
11+
// later, unrelated parse.
12+
13+
// A single-char escaped key must keep its decoded value even after a sibling
14+
// "\\" (backslash) key has been parsed on the same prefix map.
15+
(function TestEscapedKeyNotRenamedByPriorBackslashKey() {
16+
function canaryKeys() {
17+
return Object.keys(JSON.parse('{"\\r":1,"\\f":2}'));
18+
}
19+
assertArrayEquals(['\r', '\f'], canaryKeys());
20+
// Plants a {<prefix>} -> "\" transition in the global transition tree.
21+
JSON.parse('{"\\r":3,"\\\\":4}');
22+
// Must be unaffected: "\f" stays U+000C, not U+005C.
23+
assertArrayEquals(['\r', '\f'], canaryKeys());
24+
})();
25+
26+
// Multi-character escaped key: decoded length 2 must not be compared against
27+
// the first two raw bytes ("\" + "r").
28+
(function TestMultiCharEscapedKey() {
29+
function keys() {
30+
return Object.keys(JSON.parse('{"\\t":1,"\\r\\r":2}'));
31+
}
32+
assertArrayEquals(['\t', '\r\r'], keys());
33+
JSON.parse('{"\\t":1,"\\\\r":2}'); // plants the literal "\r" (0x5C 0x72) key
34+
assertArrayEquals(['\t', '\r\r'], keys());
35+
})();
36+
37+
// \uXXXX-escaped single-character key (here decodes to plain 'A').
38+
(function TestUnicodeEscapedKey() {
39+
function keys() {
40+
return Object.keys(JSON.parse('{"x":1,"\\u0041":2}'));
41+
}
42+
assertArrayEquals(['x', 'A'], keys());
43+
JSON.parse('{"x":1,"\\\\":2}');
44+
assertArrayEquals(['x', 'A'], keys());
45+
})();
46+
47+
// Override/collide: an escaped key must not be renamed onto a real "\" sibling
48+
// and silently destroy a property (key count must be preserved).
49+
(function TestNoPropertyDestruction() {
50+
// Prime the transition state that previously triggered the collapse.
51+
JSON.parse('{"p":1,"\\\\":0}');
52+
JSON.parse('{"p":1,"\\\\":0,"\\t":0}');
53+
const o = JSON.parse('{"p":1,"\\t":"a","\\\\":"b"}');
54+
assertEquals(3, Object.keys(o).length);
55+
assertEquals('a', o['\t']);
56+
assertEquals('b', o['\\']);
57+
})();
58+
59+
// Self-contained single document: a later sibling object value must not be
60+
// corrupted by an earlier one within the same parse.
61+
(function TestNestedSiblingNotCorrupted() {
62+
const o = JSON.parse(
63+
'{"a":{"p":1,"\\\\":9},"b":{"p":1,"\\\\":"A","\\t":7},' +
64+
'"c":{"p":1,"\\t":7,"\\\\":"B"}}');
65+
assertEquals(3, Object.keys(o.c).length);
66+
assertEquals(7, o.c['\t']);
67+
assertEquals('B', o.c['\\']);
68+
})();
69+
70+
// A plain (unescaped) empty key must still work (regression guard for the
71+
// empty-key_chars path the fix relies on).
72+
(function TestEmptyKeyStillWorks() {
73+
const o = JSON.parse('{"a":1,"":2,"b":3}');
74+
assertArrayEquals(['a', '', 'b'], Object.keys(o));
75+
assertEquals(2, o['']);
76+
})();

0 commit comments

Comments
 (0)