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.
This commit is contained in:
Chris Tate
2026-10-19 19:51:44 -05:00
parent 79ecacfb5d
commit 950ec9ee85
4 changed files with 65 additions and 9 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/bound-constructor-inspect-names.cjs"
],
"diags": []
}
+22 -9
View File
@@ -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 <name>"), 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;
}
}
@@ -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);
@@ -0,0 +1 @@
{ "plain": 2400, "san": 2700 }