← All issues

[JSC] `Heap::clearConcurrentRetainedDataIfPossible()` should not run while concurrent marking is active

c8e53c7

Source/JavaScriptCore/heap/Heap.cpp

@@ -1251,6 +1251,10 @@ void Heap::clearConcurrentRetainedDataIfPossible()
 
if (!m_possiblyAccessedStringsFromConcurrentThreadsOrGCOwnedDataScope.size())
return;
+
+ // marking 중에는 mutator에 fence가 필요하며, marker thread가 StringImpl::costDuringGC에 접근할 수 있으므로 Impl을 살려두어야 합니다.
+ if (mutatorShouldBeFenced())
+ return;
#if ENABLE(JIT)
auto* worklist = JITWorklist::existingGlobalWorklistOrNull();

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

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가 무제한으로 커질 가능성이 있습니다.

🔒

Related ref-free marker access patterns and list growth behavior under prolonged marking cycles are worth investigation.

더 확인하려면 구독해 주세요