[JSC] `isDefinitelyNonThenable` Structure cache can go stale when the prototype belongs to another realm
Source/JavaScriptCore/runtime/JSPromise.cpp
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 ✓
Significance
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.