Skip to content

correctness: reading an inherited Object.prototype member as a value returns undefined until the realm is populated (calls work; silent and order-dependent) #10689

Description

@proggeramlug

Summary

Reading an inherited Object.prototype member as a value from a plain object returns undefined until something else has forced the globalThis realm to populate. Calling the same member works. There is no error — the program gets a wrong answer and continues.

const o = { a: 1 };
console.log(typeof o.constructor);     // perry: undefined   node: function
console.log(typeof o.toString);        // perry: undefined   node: function
console.log(typeof o.hasOwnProperty);  // perry: undefined   node: function

But every call through the same chain is correct:

const o = { a: 1 };
console.log(o.toString());             // perry: [object Object]   node: [object Object]  ✓
console.log(o.hasOwnProperty("a"));    // perry: true              node: true             ✓
console.log("" + o);                   // perry: [object Object]   node: [object Object]  ✓
console.log(`${o}`);                   // perry: [object Object]   node: [object Object]  ✓

And the prototype link itself is intact:

Object.getPrototypeOf(o) === Object.prototype                      // true  ✓
"constructor" in o                                                  // true  ✓
Object.prototype.hasOwnProperty.call(Object.prototype, "constructor") // true ✓

So: the chain is correct, membership tests are correct, calls are correct. Only the value-read path is wrong.

It is ordering-dependent, which is what makes it dangerous

const o = { a: 1 };
JSON.stringify(o);                     // forces realm population
console.log(typeof o.toString, typeof o.constructor);  // perry: function function  ✓

Anything that forces populate_global_this_builtins repairs it for the rest of the process. JSON.stringify does. Merely evaluating Object or Object.prototype does not:

const x = Object.prototype;
const o = { a: 1 };
console.log(typeof o.toString, typeof o.constructor);  // perry: undefined undefined   node: function function

That means the same expression yields different results depending on what ran earlier in the process — including code in an unrelated module. A library that feature-detects at import time gets one answer; the same check after the first JSON.stringify anywhere gets another.

Class instances and arrays are unaffected — new C().constructor === C and [].constructor === Array are both correct.

Why this matters more than a typical divergence

Value-reads of constructor and toString are how a great deal of library code decides what something is — plain-object checks, clone/merge helpers, serializer dispatch, duck-typing. Those checks do not throw when they get undefined; they take the other branch. The failure is silent and order-dependent, which is the hardest combination to attribute from a bug report downstream.

Relationship to #10686, and a warning about fixing them together

Both are consequences of the same lazy populate_global_this_builtins. They pull in opposite directions and must not be fixed with one reflex.

A change that makes the population check lazier in general would make this bug worse, and a change that makes everything force would undo #10686. Whoever takes either should state which reads force and which do not, and this pair should be a test.

Environment

perry built from main 68a545439 + #10611, on Linux x86-64. Compared against node v26.8.1. Every case above was run in both runtimes.

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