← All issues

[5] [JSC] Defer GC across direct eval cache key construction

Severity: High | Component: JSC interpreter | 32f1bfb

DirectEvalCodeCache::CacheLookupKey가 rope fiber에서 추출한 raw StringImpl*를 보유하는 구간에 DeferGC를 삽입한 fix입니다. 이 조치 없이 parser/profiler 할당 중 GC가 발생하면, 참조되지 않은 fiber가 sweep됩니다. 결과적으로 cache key와 이후의 삽입 과정이 해제된 메모리를 역참조하는 상황이 생기는데, 이것이 High로 평가된 이유입니다.

JSC::eval()은 cache-miss 컴파일 경로 전체를 DeferGC로 감쌉니다. key는 기존과 동일하게 programStr.data.impl()로 구성되며, deferral이 key 구성부터 cache 삽입까지의 모든 GC-safepoint 구간에서 rope fiber가 해제되지 않도록 보호합니다.

Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp

+ DeferGC deferGC(vm);
CacheLookupKey cacheKey { programStr.data.impl(), callerBytecodeIndex };
...
// profiler hook, LiteralParser, collectClosureVariablesUnderTDZ, parser, 삽입

Raw StringImpl* lifetime escape: eval cache key가 참조하는 rope fiber의 유일한 root는 스택 위의 rope이며, GC-safepoint parser 할당이 이 구간에 걸쳐 있습니다.

DeferGC는 key 구성부터 cache 삽입까지의 cache-miss 분기 전체를 감쌉니다. StringImpl에 대한 reference는 직접 획득하지 않으며, deferral이 참조되지 않은 fiber가 남아있는 동안 sweep을 억제하는 방식입니다.

DirectEvalCodeCache(StringImpl*, BytecodeIndex) 쌍을 key로 컴파일된 DirectEvalExecutable을 캐싱합니다. 같은 호출 지점에서 eval(sameString)이 반복될 때 비용을 줄이기 위한 구조입니다.

JSString::value()StringViewWithUnderlyingString을 반환하는데, rope인 경우 이 과정에서 rope가 resolve됩니다. 반환된 view는 독립적인 RefPtr<StringImpl>을 갖지 않고, rope의 fiber JSString 중 하나의 내용을 직접 가리키는 형태로 존재합니다.

패치 이전에는 cacheKey.implprogramStr.data.impl()에서 가져온 raw StringImpl*였습니다. resolve 이후에는 rope의 내부 layout이 변경될 수 있는데, rope flattening이나 substring sharing이 발생하면 key에 StringImpl을 보유하는 fiber가 스택 위의 rope에서 도달 불가능해집니다.

cache-miss 분기에서는 이후 여러 단계가 순서대로 이어집니다. source-profiler hook, sloppy-JSON eval fast path를 위한 LiteralParser 할당, collectClosureVariablesUnderTDZ, 전체 parser 할당, 그리고 cache 삽입이 차례로 실행되며, 이 모든 단계가 잠재적인 GC safepoint에 해당합니다.

어느 safepoint에서든 GC가 발생하면, 도달 불가능해진 fiber가 sweep됩니다. StringImpl의 reference count는 이 시점에 0이 되고 기반 버퍼가 해제되어, cacheKey.impl은 dangling pointer 상태에 놓이게 됩니다.

이후 cache 삽입이 이 pointer를 hash하고 저장합니다. dangling pointer는 기존 key와의 hashing 및 equality 비교에 사용된 뒤, 새 항목의 key로 cache에 저장됩니다. 이로 인해 이후의 lookup이 해제된 메모리를 역참조하게 됩니다.

이 문제는 JS-visible string 콘텐츠에서 파생된 cache key의 lifetime invariant를 훼손합니다. exploit primitive는 WTF::StringImpl에 대한 heap UAF에 해당하며, 즉각적인 read(hashing)와 cache 내 dangling pointer의 장기 보존이 모두 포함됩니다.