[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가 무제한으로 커질 가능성이 있습니다.
Audit directions
- 314730@main(이 commit이 수정한 "GC 사이 clearing" 기능)을 점검합니다. marker thread가 refcount 없이 접근하는 data structure 중, clearing path에
mutatorShouldBeFenced()확인이 없는 것이 있는지 살펴봐야 합니다. 이어서Source/JavaScriptCore/heap/내 다른 곳에 추가된 유사한 GC 사이 cleanup도 확인해야 합니다. concurrent marker가 아직 순회 중일 수 있는 데이터를 대상으로 동작하는 경우가 없는지가 핵심입니다. JSString::fiberConcurrently()는 refcount 없이 rawStringImpl포인터를 marker에 전달하는 메커니즘입니다. 이 함수의 모든 호출 지점과, 각 호출이 impl lifetime에 대해 전제하는 조건을 점검해야 합니다.- 기존 guard 조건(JS 미실행,
GCOwnedDataScope없음, JIT 진행 없음)은 유용한 점검 목록입니다.mutatorShouldBeFenced()외에 누락된 조건이 있는지 확인해야 합니다. 아울러 유예 window 동안 공격자가 retained list를 충분히 크게 유지하여 memory pressure를 유발하거나, 보안에 영향을 미치는 방식으로 GC 타이밍을 조작할 수 있는지도 살펴봐야 합니다.