JSON: parse counts a string it has read and a flattened text toward the heap limit

A string value the parser has read is held only by the parser until it is stored. The
property reservation can start a collection, which dropped the string from the heap
total. The value now stays rooted across that reservation, for array elements and for
object members. `JSON.parse` also charges the buffer it flattens a concatenated text
into, through the helper `parseInt` and `parseFloat` use.
This commit is contained in:
dotCooCoo
2026-11-08 15:54:03 -08:00
parent 4afee338cf
commit 5bd49c000e
3 changed files with 38 additions and 7 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ fn trim_start(s: &[u16]) -> &[u16] {
/// ToString of the first argument, as code units, with each one taking a unit of fuel. A
/// concatenation is flattened into a new buffer, which is charged to the heap.
fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult<std::rc::Rc<[u16]>> {
pub(super) fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult<std::rc::Rc<[u16]>> {
let s = interp.to_str(&arg(args, 0))?;
for _ in 0..s.len() {
interp.use_fuel()?;
+3 -6
View File
@@ -158,10 +158,7 @@ fn syntax(interp: &mut Interp, why: &str, at: usize) -> crate::interp::Abrupt {
/// `JSON.parse` (section 15.12.2).
fn parse(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
let text = interp.to_str(&arg(args, 0))?.flat();
for _ in 0..text.len() {
interp.use_fuel()?;
}
let text = super::global::units_of(interp, args)?;
let mark = interp.temp_roots.len();
let r = parse_text(interp, &text).and_then(|v| {
let reviver = arg(args, 1);
@@ -273,7 +270,7 @@ fn parse_text(interp: &mut Interp, text: &[u16]) -> JsResult<Value> {
Open::Array { obj, next } => {
let (obj, i) = (*obj, *next);
*next = next.saturating_add(1);
interp.reserve_heap(ELEMENT_BYTES)?;
interp.rooted(std::slice::from_ref(&v), |i| i.reserve_heap(ELEMENT_BYTES))?;
interp.define(obj, key_of(f64::from(i)), Prop::Data { value: v, flags: OPEN })?;
p.skip_space();
if p.eat(b',') {
@@ -288,7 +285,7 @@ fn parse_text(interp: &mut Interp, text: &[u16]) -> JsResult<Value> {
Open::Object { obj, name } => {
let obj = *obj;
let Some(k) = name.take() else { return Err(syntax(interp, "a member has no name", p.pos)) };
interp.reserve_heap(ELEMENT_BYTES)?;
interp.rooted(std::slice::from_ref(&v), |i| i.reserve_heap(ELEMENT_BYTES))?;
interp.define(obj, k, Prop::Data { value: v, flags: OPEN })?;
p.skip_space();
if p.eat(b',') {
+34
View File
@@ -659,6 +659,40 @@ fn json_stringify_unwraps_number_string_and_boolean_objects() {
assert_eq!(string("Object.getPrototypeOf(Object('')).toString = function () { return 'q'; }; JSON.stringify(Object('abc'))"), "\"q\"");
}
#[test]
fn json_parse_counts_a_string_while_it_reserves_the_property_for_it() {
// The limits around the parsed value's size include one where the reservation for the string's
// property is what starts a collection. The text is a concatenation, so the parse also flattens
// it. Whenever the parse fits, the heap total counts everything that is live.
let setup = |text: &str| format!("var s = new Array(4097).join('x'); var text = {text}; 0");
for text in ["'[\"' + s + '\"]'", "'{\"k\":\"' + s + '\"}'"] {
let setup = setup(text);
let parse = "var a = JSON.parse(text); 0";
let mut measure = small(Limits::default());
assert_eq!(measure.run(&setup), Ok(JsValue::Number(0.0)));
measure.interp().collect_garbage();
assert_eq!(measure.run(parse), Ok(JsValue::Number(0.0)));
let full = measure.interp().heap.bytes;
let (mut fits, mut over) = (0, 0);
for heap_bytes in (full - 8192..=full + 256).step_by(4) {
let mut realm = small(Limits { heap_bytes, ..Limits::default() });
assert_eq!(realm.run(&setup), Ok(JsValue::Number(0.0)));
realm.interp().collect_garbage();
match realm.run(parse) {
Ok(_) => {
let counted = realm.interp().heap.bytes;
let live = realm.collect_garbage();
assert!(counted >= live, "{text}: {counted} bytes counted with {live} live under a limit of {heap_bytes}");
fits += 1;
}
Err(RunError { kind: RunErrorKind::Limit(LimitKind::Heap), .. }) => over += 1,
Err(e) => panic!("{text}: {e:?}"),
}
}
assert!(fits > 0 && over > 0, "{text}: {fits} limits fit and {over} did not");
}
}
#[test]
fn deep_json_nesting_needs_no_native_stack() {
let handle = std::thread::Builder::new()