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

  1. proggeramlug commented on Sep 1, 2026

    @proggeramlug
    ContributorAuthor

    Minimised further — no function call and no parameter needed. Three lines:

    const o = {};
    o["x" + 1] = 1;            // a computed-key write
    o.prototype = { m: 7 };    // silently dropped
    console.log(Object.prototype.hasOwnProperty.call(o, "prototype"));
    // node: true    perry: false

    Replacing the computed key with a static one makes it work:

    const o = {};
    o.x1 = 1;                  // static key
    o.prototype = { m: 7 };
    console.log(Object.prototype.hasOwnProperty.call(o, "prototype"));   // both: true

    So the trigger is precisely: one computed-key write ahead of it is enough to make a later o.prototype = v store go nowhere. That subsumes the parameter case in the issue body (a parameter is just another way for the receiver's static type to stop being a known plain object).

    The read side is fine, for what it is worth — I checked .prototype reads through a function parameter, at a polymorphic call site, and inside a static method after a computed-key write, a symbol-keyed write, a chained symbol write and a plain static-property write on this; all eight are node-identical. It is specifically the store that is discarded.

  2. proggeramlug commented on Sep 1, 2026

    @proggeramlug
    ContributorAuthor

    Scoped precisely. After one computed-key write on a plain object, I stored under seven names, each both ways (statically named o.k = v and computed o[k] = v) — 14 stores. Exactly one is dropped:

    store perry
    o.prototype = v (static name) DROPPED
    o["prototype"] = v (computed) kept
    o.foo / o.constructor / o.name / o.length / o.caller / o.arguments (static) kept
    the same six, computed kept

    So it is not "dynamic-shaped receivers lose stores" and not "special names are mishandled". It is specifically the statically-named .prototype store, which codegen presumably routes to the function/class prototype slot; when the receiver's shape is no longer statically a plain object, that store lands nowhere and the ordinary own-property path is never taken. The computed form escapes because it cannot take that specialised route.

    That also explains why the parameter case in the issue body fails: a parameter is simply another receiver whose static type is not a known plain object.

  3. proggeramlug commented on Sep 5, 2026

    @proggeramlug
    ContributorAuthor

    Fixed by #9757, which landed on main via merge train #9798. The train was merged as its own branch, so the Fixes #9365 keyword in #9757 never fired — closing manually. Verified the fix is on main by patch-id (git cherry), not just by the PR being closed.

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