[JSC] Incorrect side-effect modeling for Spread(SetObjectUse)
Set spread was proven side-effect-free by a comment, not by a check
Component: JSC DFG and FTL JIT | fb8bfea
Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
DFG abstract interpreter는 structure proof — 객체의 hidden class가 바뀌지 않았다는 컴파일 타임 보장 — 를 추적하여, 중복된 CheckStructure 노드를 제거하고 이후의 GetByOffset 같은 property access를 안전하게 fold할 수 있게 합니다. 다만 이 최적화가 안전하려면, proof 시점과 그 proof를 사용하는 시점 사이에 side-effecting 코드가 실행되지 않아야 한다는 전제가 필요합니다. Spread(SetObjectUse)는 side-effect가 없는 연산으로 가정되어 왔는데, JSC가 Set을 spread할 때 통상 JS iterator protocol을 우회하고 internal storage를 직접 순회하기 때문입니다. 이번 패치는 이 일괄적인 가정을 structure-subset 검사로 대체합니다. 이제 fold는 operand의 proven structure가 global object의 원본 Set structure의 subset일 때만 허용되며, 이 조건은 own Symbol.iterator가 없음을 보장하고 watchpoint로도 보호됩니다. 그렇지 않은 경우에는 world 전체를 clobber시켜 관련 check들이 다시 emit됩니다.
Significance
313031@main 이후로, fast internal-storage 경로를 사용할 수 없는 상황에서는 Set spread가 사용자 정의 Symbol.iterator를 호출할 수 있게 되었습니다. 즉 spread 도중에 임의의 JavaScript가 실행될 수 있으며, 이 JavaScript는 객체의 구조를 바꿀 수 있습니다. CheckStructure가 잘못 fold되어 사라진 상태에서 JIT-compiled 함수가 stale property slot을 읽을 수 있으며, 이는 일반 스크립트에서 도달 가능한 type-confusion 형태의 read에 해당합니다.
Audit directions
이 케이스는 stale-structure-proof 패턴의 전형적인 예시이며, 앞으로 살펴봐야 할 방향은 이와 유사한 다른 지점들입니다. 좁게 보면, executeEffects 안에 있는 다른 useKind 기반 "no side effects" 분기들을 모두 나열한 뒤, 각각을 게이팅하는 fast-path predicate(canDoFastSpread에 대응하는 것)가 user-observable JS로 fallback할 수 있는지 확인할 필요가 있습니다. 넓게 보면, fast-path bypass check의 실패 분기가 iterator, valueOf, toString, proxy 등으로 이어지는데도 abstract interpreter가 이를 반영하도록 업데이트되지 않은 경우가 일반적인 패턴이므로, internal-storage 경로와 generic-protocol 경로를 동시에 갖는 모든 DFG node의 lowering을 점검해볼 만합니다. 또한 새로 추가된 canFold 조건이 Set.prototype[Symbol.iterator]에 걸어둔 watchpoint 보호가, structure-subset check로는 커버되지 않는 방식으로 무효화될 수 있는지도 확인이 필요합니다. 예를 들어 realm/global-object mismatch나 OSR 도중 발생하는 concurrent structure transition 같은 경우입니다. Diff에서 이런 패턴을 찾는 실마리는, didFoldClobberWorld() 호출의 근거가 runtime이나 watchpoint 기반 predicate가 아니라 단순 주석으로만 되어 있는 지점입니다.