You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
JSON.stringify(v, null, 2) returns nondeterministic garbage for arrays grown past inline capacity — pretty/replacer emitters miss clean_arr_ptr #7269
Found while verifying #7264 (PR #7265). Unrelated to that bug, and worse: the output is nondeterministic garbage, not merely wrong.
JSON.stringify(value, null, 2) on an array that has grown past its initial inline capacity returns a string of a different, wrong length on every run of the same binary. The non-pretty JSON.stringify(value) on the same array is correct and stable.
node: pretty 1575 plain 806
perry: pretty 2587 plain 806
perry: pretty 6146 plain 806 <- same binary, second run
perry: pretty 6533 plain 806 <- same binary, third run
plain is correct every time. Only the indented form is wrong, and it is wrong differently each run — it is reading uninitialized/foreign memory. Slicing the result (.slice(0, 200)) yields an empty string, and some variants of this shape SIGSEGV.
Threshold
It is the array's growth past its initial inline capacity (16), not the element count as such:
element count: 4 8 16 32
node pretty: 190 378 775 1575
perry pretty: 190 378 775 4097 <- and 6655 on the next run
Correct through 16, garbage at 32. Building the same array at top level instead of inside a function is correct and stable at 32 — consistent with the array being reallocated and the caller holding the pre-growth pointer.
Suspected cause
This is the #2021 forwarding-stub class. json/stringify.rs::stringify_array_depth documents it and guards against it:
an array that has grown past its initial inline capacity (16) was reallocated to a new block, leaving a GC_FLAG_FORWARDED stub at the old address … reading its first 8 bytes as (length, capacity) yields the forwarding pointer reinterpreted as a huge length and walks off into garbage
and calls crate::array::clean_arr_ptr to follow the chain. The pretty/replacer emitters in json/replacer.rs never got that guard. Unguarded as *const crate::ArrayHeader casts followed by a (*arr).length read:
line
function
544
stringify_array_with_replacer_pretty
868
stringify_value_pretty
1040
stringify_array_pretty
1297
stringify_array_with_array_replacer
(Lines 296 and 1282 in the same file do call clean_arr_ptr, so the fix is a one-line-per-site addition matching the existing pattern, not new machinery.)
A huge bogus length also explains the SIGSEGVs: with a replacer or an indent argument, JSON.stringify(JSON.parse(blob), null, 2) over a 32-element array segfaults outright.
Scope
Reproduces on origin/main (d4ac65f21) and with auto-optimize both on and off. Not introduced by #7265 — that PR touches only the non-pretty array shape template, and replacer.rs does not use it. Verified by A/B against a build with #7265's runtime changes reverted: same garbage, same nondeterminism.
Impact: any code that pretty-prints JSON — config writers, log formatters, API responses built with JSON.stringify(x, null, 2) — silently emits corrupt output once the array crosses 16 elements. That is a wider blast radius than #7264, since it needs no JSON.parse and no property read.
Activity
fix(json): resolve GC_FLAG_FORWARDED array stubs in pretty/replacer s…