Skip to content

JSON.parse of an object with more than 10,000 keys: computed-key reads return undefined and Object.entries values are null #10175

Description

@proggeramlug

JSON.parse of an object with more than 10,000 keys produces an object whose values can't be read. Object.keys, in, hasOwnProperty and literal-key reads (v["k5"]) work. Computed-key reads (v["k" + i]) return undefined, and Object.entries(v) gives null for every value. Objects built by assignment (o["k" + i] = i) are unaffected.

Found on main (9b911855f8) during a merge-train audit; it is unrelated to the PR under audit (#10168), which behaves identically.

Reproduction

function viaJson(n: number) {
  const p: string[] = [];
  for (let i = 0; i < n; i++) p.push('"k' + i + '":' + i);
  return JSON.parse("{" + p.join(",") + "}");
}
for (const n of [10000, 10001]) {
  const v = viaJson(n);
  const key = "k" + 5;
  console.log(n, v[key], v["k5"], JSON.stringify(Object.entries(v)[5]));
}

Node 26.5.1:

10000 5 5 ["k5",5]
10001 5 5 ["k5",5]

Perry main (--no-auto-optimize, macOS arm64):

10000 5 5 ["k5",5]
10001 undefined 5 ["k5",null]

Bisected in-process: 10,000 keys are correct, 10,001 keys are broken (127,796 bytes of source). Values are irrelevant: the same happens with nested record values, e.g. a 60,000-key object of {"id","name","tags","score","active"} records reads v["k" + r] as undefined.

Likely cause

crates/perry-runtime/src/object/field_get_set/accessors.rs, object_field_at_with_live:

// Guard: corrupted objects with unreasonably large field_count
if live > 10000 {
    return JSValue::undefined();
}

The JSON object builder keeps every field inline, so a parsed object with more than 10,000 fields has live > 10000. Every indexed read of it then returns undefined, whatever the field index. Assignment-built objects spill fields past their inline capacity into the overflow map (the field_index >= live branch above the guard), so they never reach it. Literal-key reads take a different lookup path, which is why v["k5"] still works.

The guard dates from a corruption-detection era. Either it should bound only the index actually being read against the object's allocated slot capacity, or the parse builders should respect the same inline cap that assignment does.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions