Skip to content

Own property named prototype is silently dropped when written through a function parameter (and after a dynamic-key write) #9365

Description

@proggeramlug

Repro (4 lines)

const V = { marker: 7 };
const o = {};
(function (t) { t.prototype = V; })(o);
console.log(Object.prototype.hasOwnProperty.call(o, "prototype"), o.prototype);
node : true  { marker: 7 }
perry: false undefined

The write is silently dropped — no throw, no warning. o simply never gains the property: hasOwnProperty is false, Object.keys(o) does not list it, and Object.getOwnPropertyDescriptor(o, "prototype") returns undefined.

What distinguishes the failing shape

Eleven variants, all writing an own prototype onto a plain object. Only the parameter write fails:

shape perry
function f(){const o={};o.prototype=V;return o} (local) ok
const o={};o.prototype=V (same scope) ok
{prototype:V} object literal ok
o["prototype"]=V ok
Object.defineProperty(o,"prototype",…) ok
{...{prototype:V}} / Object.assign({},{prototype:V}) ok
(function(t){t.prototype=V})(o) — write through a parameter DROPPED

A second failing shape, same family, where the receiver is a local whose shape has been made dynamic first:

function wide(n) {
  const o = {};
  for (let k = 0; k < n; k++) o["p" + k] = k;   // dynamic keys
  o.prototype = { marker: 7 };
  return o;
}
console.log(wide(0).prototype);   // perry: undefined   node: { marker: 7 }

This one fails for every n, including n = 0 (the loop never runs). So it is not about property count or overflow slots — the mere presence of the dynamic-key write is enough to change how the later o.prototype = … is compiled.

The unifying pattern in both: when the receiver's static type is not known to be a plain object, x.prototype = v appears to be routed to the function/class prototype slot rather than to an ordinary own property, and for a plain object that store goes nowhere.

Why this matters beyond the fixture

This is the mechanism family behind #9341 (cc --help down on main). The diagnostic there shows the failing call is Object.defineProperty(undefined, "getContentType", …), where the undefined is z in axios's

static accessor(q){  let z = this.prototype;  xp5(z, A)  }

i.e. a .prototype access on a receiver of imprecise static type yielding undefined instead of the prototype. The read side and the write side above are the same fragility. #9341's trigger is a regression inside 83754818e..main (this write-drop reproduces identically on the green 83754818e build, so it is not itself the regression) — but a .prototype access that silently answers undefined on an imprecisely-typed receiver is the machinery that regression is landing on, and hardening it would make that whole class of failure loud instead of silent.

At minimum, a store that cannot be honoured should not be discarded in silence.

Found while bisecting #9341; verified pre-existing on 83754818e (#9242) and a03be729c (#9336) — byte-identical failure on both.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions