JSC: cache isDefinitelyNonThenable on Structure
Source/JavaScriptCore/runtime/Structure.h
ECMAScript requires Promise.resolve and await to detect thenables by walking the prototype chain for a callable .then. JSC's isDefinitelyNonThenable() did this walk on every call. This commit caches the result on each Structure using a 2-bit lazy state (Unknown, NonThenable, MaybeThenable, Uncacheable). The NonThenable state is trusted only while the realm's promiseThenWatchpointSet is intact — now extended to cover then-absence on Object.prototype as well. Chains deeper than [self, Object.prototype] or [self] (null proto) are marked Uncacheable; dictionary structures are excluded.
Significance
This change sits directly on the thenable-detection path, where a misclassification turns a thenable into a plain value — a potential capability bypass if promise-guarded objects can be smuggled through as non-thenables.
Audit directions
-
Watchpoint invalidation timing. The
NonThenablecache is trusted whenpromiseThenWatchpointSet().isStillValid()is true. The read ofnonThenableStatus()and the watchpoint check are two separate loads with no lock between them. Investigate whether a concurrent watchpoint invalidation can race with the cache read. -
Cross-realm trust. The realm check compares
structure->globalObject() == globalObject. A structure created in realm A resolving in realm B's promise pipeline must fail this check. Confirm that structures of objects created bycreateGlobalObject()or cross-realmevalalways carry the originating realm's global. -
Dictionary transition after caching. A structure caches
NonThenable, thenObject.assign()or large property additions trigger dictionary inflation. Dictionary structures share the sameStructure*mutated in place — the window between the property mutation (addingthen) and the watchpoint fire, with the cachedNonThenablebit still readable, is worth verifying. -
Null-proto with deferred
thenaddition.Object.create(null)chains qualify forNonThenablecaching. ConfirmObject.defineProperty(obj, 'then', {get: ...})always produces a new Structure rather than mutating in place, which could leave a staleNonThenablebit on a now-thenable object.