From 950ec9ee8522e034df2956fec31eb9a4647e8c1c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 19:51:44 -0500 Subject: [PATCH] Give prototype name stamps their own copy of the name A construction through a plain function stamps the prototype with the name of the function in its `constructor` property, for util.inspect. The stamp pointed at the function's name. A bound function's name ("bound " and the target's name) is a string the bound function owns, so after the `constructor` property was replaced and the bound function released, inspecting an instance read freed memory. The stamp now holds a copy of the name, freed with the stamp at exit. The corpus program constructs through functions whose prototype's `constructor` is a bound function (and a bound function of one), then replaces the property and inspects the instances. --- .../bound-constructor-inspect-names.cjs.json | 6 ++++ packages/runtime/src/scr_json.c | 31 +++++++++++----- .../bound-constructor-inspect-names.cjs | 36 +++++++++++++++++++ .../bound-constructor-inspect-names.cjs.json | 1 + 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json create mode 100644 tests/corpus/bound-constructor-inspect-names.cjs create mode 100644 tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..b6046aead --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/bound-constructor-inspect-names.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0840b483e..83b56bea4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4360,16 +4360,25 @@ bool scr_dyn_data_view_is(const ScrDyn *value) { * `constructor` data property on its prototype chain (Node's * getConstructorName). The dyn printer reads runtime class names * (v.obj.class_name, set only on prototypes that stay alive), so a - * construction through a plain function stamps that prototype with the - * function's static name and keeps it alive; the stamps are cleared before - * release at exit, so freelist reuse never sees them. */ -static SCR_TL ScrDyn **scr_dyn_named_prototypes; + * construction through a plain function stamps that prototype with a copy + * of the function's name and keeps it alive. The copy is the stamp's own: a + * function's name can live in storage the function owns (a bound + * function's "bound "), and the function is released when the + * writable `constructor` property is replaced. The stamps are cleared and + * their copies freed before release at exit, so freelist reuse never sees + * them. */ +typedef struct { + ScrDyn *prototype; + char *name; +} ScrDynNamedPrototype; +static SCR_TL ScrDynNamedPrototype *scr_dyn_named_prototypes; static SCR_TL size_t scr_dyn_named_count, scr_dyn_named_cap; static void scr_dyn_named_prototypes_cleanup(void) { for (size_t i = 0; i < scr_dyn_named_count; i++) { - scr_dyn_named_prototypes[i]->v.obj.class_name = NULL; - scr_dyn_release(scr_dyn_named_prototypes[i]); + scr_dyn_named_prototypes[i].prototype->v.obj.class_name = NULL; + scr_dyn_release(scr_dyn_named_prototypes[i].prototype); + free(scr_dyn_named_prototypes[i].name); } free(scr_dyn_named_prototypes); scr_dyn_named_prototypes = NULL; @@ -4386,13 +4395,17 @@ static void scr_dyn_name_prototype_chain(const ScrDyn *object) { if (scr_dyn_named_count == scr_dyn_named_cap) { if (scr_dyn_named_cap == 0) scr_atexit(scr_dyn_named_prototypes_cleanup); size_t cap = scr_dyn_named_cap ? scr_dyn_named_cap * 2 : 8; - ScrDyn **grown = realloc(scr_dyn_named_prototypes, cap * sizeof *grown); + ScrDynNamedPrototype *grown = realloc(scr_dyn_named_prototypes, cap * sizeof *grown); if (!grown) scr_json_oom(); scr_dyn_named_prototypes = grown; scr_dyn_named_cap = cap; } - p->v.obj.class_name = name; - scr_dyn_named_prototypes[scr_dyn_named_count++] = scr_dyn_retain(p); + size_t len = strlen(name); + char *copy = malloc(len + 1); + if (!copy) scr_json_oom(); + memcpy(copy, name, len + 1); + p->v.obj.class_name = copy; + scr_dyn_named_prototypes[scr_dyn_named_count++] = (ScrDynNamedPrototype){ scr_dyn_retain(p), copy }; return; } } diff --git a/tests/corpus/bound-constructor-inspect-names.cjs b/tests/corpus/bound-constructor-inspect-names.cjs new file mode 100644 index 000000000..52faa689c --- /dev/null +++ b/tests/corpus/bound-constructor-inspect-names.cjs @@ -0,0 +1,36 @@ +'use strict'; +// util.inspect names an object after the function in the first own +// `constructor` property on its prototype chain. Here that function is a +// bound function, whose name ("bound " and the target's name) is built at +// bind time, and the property is replaced after construction, which +// releases the first bound function. The instance keeps the name. The +// targets are anonymous, so the bound names are "bound " and "bound bound ". +const util = require('util'); + +// Array elements give the functions no name. +const [Point, Pair] = [ + function (x) { + this.x = x; + }, + function (a, b) { + this.a = a; + this.b = b; + }, +]; +Point.prototype.constructor = Point.bind(null); +const p = new Point(1); +console.log(util.inspect(p)); + +Point.prototype.constructor = Point.bind(null); +const filler = []; +for (let i = 0; i < 200; i++) filler.push('bound '.repeat(i % 5) + i); +console.log(util.inspect(p)); +console.log(p); +console.log(filler.length); + +// A bound function of a bound function, replaced the same way. +Pair.prototype.constructor = Pair.bind(null).bind(null); +const q = new Pair(1, 2); +Pair.prototype.constructor = Pair.bind(null).bind(null); +console.log(util.inspect(q)); +console.log(q); diff --git a/tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json b/tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..ab2bc177b --- /dev/null +++ b/tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 2700 }