[JSC] `Heap::clearConcurrentRetainedDataIfPossible()` should not run while concurrent marking is active
Source/JavaScriptCore/heap/Heap.cpp
JSC는 mutator와 병렬로 object graph를 순회하는 concurrent marker thread를 실행합니다. marker가 JSString을 방문하면, fiberConcurrently()를 통해 내부 StringImpl 포인터를 로드합니다. 이때 refcount는 증가시키지 않은 채로 역참조가 이루어지는데, JSString::estimatedSize에서 호출하는 StringImpl::costDuringGC가 대표적인 예입니다. 이 window 동안 해당 impl을 살려두는 유일한 메커니즘이 m_possiblyAccessedStringsFromConcurrentThreadsOrGCOwnedDataScope 리스트입니다. 314730@main에서 도입된 GC 사이 cleanup path는 IncrementalSweeper 타이머로 이 리스트를 정리하도록 설계되었습니다. 다만 당시 구현은 JS 실행 중, 활성 GCOwnedDataScope 존재, 진행 중인 JIT 컴파일에 대해서만 보호했고—concurrent marking은 고려되지 않았습니다.
이 commit은 mutatorShouldBeFenced() early-return을 추가하여 이 공백을 메웠습니다. mutatorShouldBeFenced()는 write barrier에서 "marker가 실행 중일 수 있다"는 의미로 이미 사용되는 표준 플래그입니다. 여기에 적용함으로써, marker가 아직 retained impl 중 하나를 역참조하고 있을 가능성이 있는 경우 IncrementalSweeper 타이머가 clear를 건너뛰게 됩니다.
Mutator (IncrementalSweeper, ~100ms timer) Collector (marker)
clearConcurrentRetainedDataIfPossible() SlotVisitor::drain()
│ │
│ JSString::visitChildrenImpl()
│ │
▼ fiberConcurrently() → StringImpl* ← no ref held
retainedList.clear() ◄────── UAF ─────────────────────────┤
(StringImpl freed) StringImpl::costDuringGC(*impl) ← dangling read
After fix:
if (mutatorShouldBeFenced()) return; // markers active → defer clear to next timer fire
Significance
GC collector thread에서 ASAN으로 확인된 UAF로, memory safety bug 중에서도 exploit이 빈번하게 이루어지는 유형에 해당합니다. 수정 자체는 한 줄짜리 코드입니다. 다만 기저에 있는 패턴은 구조적으로 주목할 만합니다. marker thread가 refcount 없이 접근하는 GC-retained data에 나중에 별도의 clearing path가 추가된 이 구조는, heap subsystem 내 다른 곳에서도 유사한 누락이 발생할 수 있는 틀에 해당합니다. 한편 이 수정은 clear를 미룰 뿐입니다. 타이머는 100ms마다 재실행되므로, marking이 오래 이어지거나 빠르게 반복 실행되면 유예 window 동안 retained list가 무제한으로 커질 가능성이 있습니다.