[JSC] Redesign WeakGCSet to match WeakGCMap
Component: JavaScriptCore Heap | 7692eec
Source/JavaScriptCore/runtime/WeakGCSet.h
Source/JavaScriptCore/runtime/WeakGCSetInlines.h
WeakGCSet은 JS 객체를 memoize하면서도 GC 너머로 생존시키지는 않는 weak hash-set입니다. JSGlobalObject가 이를 사용해, RegExp의 $&, $_, input 같은 accessor용으로 만들어진 JSCustomGetterFunction/JSCustomSetterFunction 객체를 캐싱합니다. 그래서 Object.getOwnPropertyDescriptor()를 반복 호출해도 동일한 함수 객체가 반환됩니다. 이번 commit은 WeakGCMap에 적용되었던 재설계(320942@main)를 이쪽에도 그대로 이식한 것입니다. 먼저 bucket이 Weak<T> 핸들 대신 raw ValueArg* 포인터를 직접 보관하도록 바뀌었습니다. 죽은 entry는 reconcileWeakReferencesAtGCEnd에서 vm.heap.isMarked() 한 번의 sweep으로 제거됩니다. 또한 add()/ensure()가 markDirty(m_vm)를 호출하므로, GC 종료 시점에 해당 set이 반드시 다시 방문되는 것이 보장됩니다. 호출 측인 custom getter/setter 캐시, WasmTable::grow, ScriptExecutionContext의 microtask global object 캐시도 Weak<T>를 null 검사하는 대신 raw 포인터를 곧바로 역참조하도록 수정되었습니다.
Before: After:
bucket ── Weak<T> handle ──► T cell bucket ── raw T* (unguarded)
hash()/equal()/isWeakNullValue() hash()/equal() dereference directly
null-check the handle before use add()/ensure() call markDirty(m_vm)
pruneStaleEntries() only on Full GC: reconcileWeakReferencesAtGCEnd() runs
removeIf(!entry) after EVERY collection:
(eden-only deaths linger to next full GC) removeIf(!vm.heap.isMarked(T*))
Significance
이제 pruning이 full GC로 미뤄지지 않고 모든 collection 직후에 수행되므로, 죽은 bucket이 관찰되는 상태로 남는 일이 없습니다. 바로 이 점이 hash()와 equal()에서 null 검사를 걷어낼 수 있게 한 근거입니다. 이번 변경은 이미 갱신된 WeakGCMap과 설계를 맞추기 위한 이식이며, 발견된 vulnerability에 대한 대응은 아닙니다. 부수적으로 entry마다 붙던 Weak<> 핸들 오버헤드가 사라지고, 호출 지점마다 반복되던 null 검사 boilerplate도 함께 제거되었습니다.
Audit directions
hash()/equal()가 더 이상 bucket 포인터를 방어하지 않는 만큼, correctness는 전적으로 타이밍에 의존하게 됩니다. 즉 lookup이나 순회, hash 계산이 marking되지 않은 cell을 관찰하기 전에 reconcileWeakReferencesAtGCEnd가 실행되고 완료되어야 합니다. Narrow: set을 변경하는 모든 경로가 markDirty(m_vm)를 안정적으로 호출하는지 점검해야 합니다. markDirty 하나가 누락되면 GC 종료 단계에서 해당 set이 방문되지 않은 채 남을 수 있습니다. 그러면 죽은 raw 포인터가 cell의 회수 이후까지 살아남아, 아무 보호 없는 역참조로 들어갈 가능성이 있습니다. 여기에 더해 JSGlobalObject.h의 WeakCustomGetterOrSetterHash에 걸린 safeToCompareToEmptyOrDeleted=false 계약도 확인 대상입니다. 새로 달린 주석이 지적하듯 WeakCustomGetterOrSetterHashTranslator::equal()은 건네받은 bucket을 조건 없이 역참조하므로, 해당 플래그가 안전성을 떠받치는 역할을 합니다. Wider: 이번 commit이 대체한 구식 deferred-Weak<> 패턴을 여전히 쓰고 있는 다른 JSC weak collection 호출 지점들을 열거해 볼 만합니다. 각각은 migration되는 순간 동일한 reconciliation 타이밍 의존성을 떠안게 되므로, migration 순서 자체가 risk입니다. Widest: entry 단위 유효성 검사를 버리고 "아무도 보기 전에 sweep이 먼저 돈다"는 전역 invariant로 갈아탄 자료구조는 전부 같은 형태를 갖습니다. 이때 들고 다닐 점검 질문은 하나입니다 — collection과 sweep 사이에 어떤 코드가 실행될 수 있는가. Code-review tell: hash나 equality functor에서 null 검사가 사라지고, 대신 mutator 쪽에 markDirty 계열의 통지가 새로 추가된 조합입니다.