From 8a02468073255db4bad38f098f7206052d99a408 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 05:57:26 -0500 Subject: [PATCH] Take number and string boxes out of the candidate buffers before pooling them scr_dyn_release returned the last reference of a number or string box to the free list without scr_cyc_dead, because the release paths never buffer those boxes. A collector pass can: capture boxes trace their payload, and scr_xg_release, which settles the cross-generation edges a restricted pass skipped, calls scr_cyc_on_release on the target whatever its kind. The pooled block then stayed in its candidate buffer, and the next box that reused it carried the stale buffer position. Its death swapped another object into that slot, which corrupted the buffers and crashed a later scr_cyc_on_dead. The fast path now takes a buffered box out of its buffer first. The sanitized runtime does not pool boxes, so only plain builds were affected. --- packages/runtime/src/scr_json.c | 8 ++++++-- packages/runtime/src/scr_json.test.c | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index c2396212d..eb03ab6cc 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1286,10 +1286,14 @@ void scr_dyn_release(ScrDyn *d) { return; } #ifndef SCR_RC_AUDIT - /* Number and string boxes own at most one string, are never cycle - * candidates and cannot be weak keys: return them to the free list. */ + /* Number and string boxes own at most one string and cannot be weak + * keys: return them to the free list. Their releases never buffer them, + * but a collector pass can (a box that traces its payload reaches them, + * and scr_xg_release buffers what it settles), so a buffered box leaves + * its candidate buffer first. */ if ((d->kind == SCR_DYN_NUM || d->kind == SCR_DYN_STR) && !d->prototype && !d->symbol_properties && !d->symbol_keys && scr_dyn_free_count < SCR_DYN_FREE_MAX) { + scr_cyc_dead(d); if (d->kind == SCR_DYN_STR) scr_str_release(d->v.str); d->v.str = (ScrStr *)scr_dyn_free_misc; scr_dyn_free_misc = d; diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index a28759876..29ba4bfe3 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -391,6 +391,30 @@ static bool buffered_after_read(ScrDyn *d) { return buffered(d); } +/* A collector pass can buffer a number or string box that it reached + * through an owner that traces it: scr_xg_release, settling an edge a + * restricted pass skipped, calls scr_cyc_on_release on the target. The + * box's last release must take it out of its candidate buffer before its + * block returns to the free list, which hands it to the next box. */ +static void buffered_leaf_release(void) { + for (uint16_t gen = SCR_CYC_NURSERY; gen < SCR_CYC_NGENS; gen++) { + ScrStr *text = scr_str_new("buffered", 8); + ScrDyn *leaves[2] = {scr_dyn_new_str(text), scr_dyn_new_num(0.5)}; + scr_str_release(text); + for (int i = 0; i < 2; i++) { + scr_cyc_set_gen(scr_cyc_hdr(leaves[i]), gen); + scr_cyc_on_release(leaves[i]); + scr_dyn_release(leaves[i]); + } + ScrDyn *fresh[2] = {scr_dyn_new_num(1.5), scr_dyn_new_num(2.5)}; + assert(!buffered(fresh[0]) && !buffered(fresh[1])); + scr_collect_cycles(); + assert(fresh[0]->v.num == 1.5 && fresh[1]->v.num == 2.5); + scr_dyn_release(fresh[0]); + scr_dyn_release(fresh[1]); + } +} + static ScrDyn *parse_text(const char *text) { ScrStr *input = scr_str_new(text, strlen(text)); ScrDyn *parsed = scr_json_parse(input); @@ -823,6 +847,7 @@ int main(void) { shared_property_keys(); checked_leaf_cycles(); checked_number_storage(); + buffered_leaf_release(); json_intact_regions(); json_schema_parse(); json_schema_special_names();