[JSC] `isDefinitelyNonThenable` Structure cache can go stale when the prototype belongs to another realm
Source/JavaScriptCore/runtime/JSPromise.cpp
JSC의 Promise resolution 경로는 per-structure cache를 기반으로 isDefinitelyNonThenable을 호출하여 비용이 큰 .then property 조회를 생략합니다. 이미 확인된 structure의 prototype에 .then이 없는 경우, 결과는 NonThenable로 캐싱됩니다. 이 cache를 보호하는 것이 promiseThenWatchpointSet인데, realm의 Object.prototype에 .then이 추가될 때 발동하여 기존 캐싱 결과를 무효화합니다. 다만 미묘한 invariant가 하나 있습니다. 이 guard는 caller의 realm이 아닌 structure->realm()에 귀속됩니다.
이 commit은 객체의 prototype이 해당 객체의 structure와 다른 realm에 속할 때 stale cache가 발생하는 문제를 수정합니다. 수정 전에는 cacheability guard가 prototype을 caller의 globalObject->objectPrototype()과 비교했습니다. 그러나 무효화 watchpoint는 structure의 realm에서 발동합니다. 두 realm이 서로 다른 경우에도 cache가 잘못 유지될 수 있는 이유입니다. 이번 수정에서는 globalObject->objectPrototype()을 structure->realm()->objectPrototype()으로 변경하여, mixed-realm prototype chain을 가진 객체를 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
cache 생성 이후에 .then이 추가된 진짜 thenable이 있다면, Promise resolution 알고리즘은 해당 객체를 평범한 값으로 조용히 처리합니다. 결과적으로 ECMAScript spec invariant가 깨지고, cross-realm prototype 조작을 통해 thenable 감지 자체를 우회하는 상황이 가능해집니다. realm-X의 watchpoint로 보호되는 결과를 캐싱하면서 cacheability 판단에는 다른 realm의 객체를 기준으로 삼는 코드가 JSC 안에 있다면, 동일한 문제가 발생합니다. 이런 버그 유형은 JSC 전반에서 추가로 점검할 가치가 있습니다.