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.
This commit is contained in:
Chris Tate
2026-10-24 05:57:26 -05:00
parent 7278c3c467
commit 8a02468073
2 changed files with 31 additions and 2 deletions
+6 -2
View File
@@ -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;
+25
View File
@@ -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();