← All issues

[JSC] `isDefinitelyNonThenable` Structure cache can go stale when the prototype belongs to another realm

8d6b112

Source/JavaScriptCore/runtime/JSPromise.cpp

- if (!proto.isObject() || asObject(proto) == globalObject->objectPrototype())
+ if (!proto.isObject() || asObject(proto) == structure->realm()->objectPrototype())
structure->setDefinitelyNonThenableState(Structure::DefinitelyNonThenableState::NonThenable);
else
structure->setDefinitelyNonThenableState(Structure::DefinitelyNonThenableState::Uncacheable);

JSC's Promise resolution path calls isDefinitelyNonThenable to skip the expensive .then property lookup via a per-structure cache: if a structure has been seen and its prototype had no .then, the result is cached as NonThenable. This cache is guarded by promiseThenWatchpointSet — a watchpoint that fires when .then is added to a realm's Object.prototype, invalidating the cached result. The subtle invariant is that the guard belongs to structure->realm(), not to the caller's realm.

This commit fixes a stale cache that arises when an object's prototype belongs to a different realm than the object's own structure. Pre-fix the cacheability guard compared the prototype against the caller's globalObject->objectPrototype(), but the invalidating watchpoint fires on the structure's realm. The fix replaces globalObject->objectPrototype() with structure->realm()->objectPrototype(), making mixed-realm prototype chains uncacheable.

Before fix:
  Object: realm-A structure, prototype = realm-B Object.prototype

  Cache-populating resolve called from realm B:
    globalObject->objectPrototype() = realm-B Object.prototype  ← matches proto
    → NonThenable cached on realm-A structure
      (guarded by realm-A's promiseThenWatchpointSet)

  later: realm-B Object.prototype.then = fn
    → fires realm-B watchpoint only
    → realm-A structure cache untouched → stale NonThenable ❌

After fix:
  proto == structure->realm()->objectPrototype()?  → realm-A ≠ realm-B → Uncacheable ✓

A genuine thenable whose then is added post-cache-population gets silently treated as a plain value by the Promise resolution algorithm, breaking the ECMAScript spec invariant and allowing cross-realm prototype manipulation to bypass thenable detection. This is a class of bug worth hunting elsewhere — anywhere JSC caches a result guarded by realm-X's watchpoint but gates cacheability using a different realm's object.

🔒

The slow-path fallback and other realm-keyed caches in JSC share this pattern — audit directions included.

Subscribe to read more