
CVE-2026-84118에 대한 개념 증명(PoC)으로, SpiderMonkey GC의 use-after-free 취약점으로 인해 경계를 벗어난 읽기/쓰기가 발생하고 잠재적으로 코드 실행이 가능합니다. 결정적 SEGV 트리거와 임의 읽기 및 ASLR 우회로의 권한 상승에 대한 연구를 포함합니다.
이것은 Mozilla가 Critical을 High로 분류한 사례 중 하나입니다. 그렇다면 누군가 Firefox에 가서 원래 버그 헌터들의 보상을 업데이트하도록 요청하고 싶을 수도 있습니다.
요약: 제어된 포인터에 대해 OOB R 프리미티브를 수행한 다음 해당 포인터를 기준으로 위아래로 이동할 수 있습니다. 제어된 포인터를 배열에 지정하면 재미있는 일을 할 수 있습니다.
PoC에는 SEGV 부분만 있으므로 나머지는 배열을 가리키고 원하는 WASM으로 점프하는 재미있는 모험이 됩니다.
Firefox/SpiderMonkey 154.0.1용 결정적 릴리스 빌드 SEGV 개념 증명. 155에서 수정됨(AtomMarking 재작업). Mozilla 심각도: HIGH.
/path/to/release/js poc.js
154.0.1에서 재현: 일반 최적화 셸에서 3/3(SIGSEGV, exit 139), 릴리스+gczeal 셸에서 5/5, 매 실행마다 동일한 오류:
SEGV on address 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, +0x14 offset
154는 영역별 원자 표시 비트맵과 참조 추적을 혼동합니다. 약하게만 유지되는 Symbol — 여기서는 Baseline CacheIR IC 스텁 내부의 약한 Value 필드로 캡처됨 — 이 수집되지 않은 영역에서 참조되는 경우, 영역 범위 GC에서 원자 영역이 수집될 때 다시 표시되지 않습니다. 결과적으로 스윕/재배치된 셀에 대한 오래된 엣지가 남습니다.
릴리스 빌드의 결정적 킬 샷은 두 번째 정적 버그입니다: MightBeForwarded<JS::Symbol>이 false로 하드코딩되어 있어 IsForwarded가 확인 없이 false를 반환하고(gc/Marking-inl.h:92-97), 이를 기반으로 하는 모든 포인터 수정 경로가 엣지를 조용히 건너뜁니다. 원자 영역이 압축되면 강한 엣지는 올바르게 업데이트되지만, 이 약한 CacheIR 엣지는 피해 Symbol의 이전 주소에 영구적으로 매달린 채 남습니다. (디버그 빌드는 Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94로 이를 포착합니다.)
PoC는 재사용 가능한 힙에 대한 오래된 포인터만 남기는 것이 아니라 백킹 페이지를 munmap하여 다음 역참조에서 바로 오류가 발생하게 합니다.
Zone::availableChunks/fullChunks), 원자 영역 청크에는 원자 영역 아레나만 포함됩니다. 청크의 운명은 순전히 원자/Symbol 수명으로 제어할 수 있습니다.c.eval(b)는 최상위 호출 IC가 약한 스텁 필드에 Symbol을 캡처하게 합니다; b = undefined는 마지막 강한 루트를 제거합니다; 스프레이는 제거됩니다; 두 번의 영역 범위 GC(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") — 빈 문자열은 원자 영역을 예약함)가 죽은 스프레이를 스윕하고, 피해자를 자석 아레나로 압축하며, 약한 엣지 수정을 건너뛰고, 피해자의 아레나를 해제합니다. 이제 해당 청크는 완전히 비어 있습니다.gcparam("minEmptyChunkCount", 0)은 모든 빈 청크를 만료 가능하게 만듭니다; sleep(0.5) 동안 BackgroundDecommitTask가 expireEmptyChunkPool -> FreeChunkPool -> 를 실행합니다 — 피해 청크의 진정한 munmap입니다. (아레나 수준의 decommit은 — 읽기 가능한 0 — 일 뿐이며, 전체 청크 해제만이 유일한 munmap 경로이므로 1단계의 청크 격리가 중요한 이유입니다.)이것은 단순한 DoS가 아닙니다. 입증된 에스컬레이션(GC-NDAY/ 및 CHAIN/ 연구 디렉토리의 PoC 및 로그):
segv9): munmap 후 minorgc() 세미스페이스 플립으로 nursery가 해제된 VA를 차지합니다; 원시 밀집 더블 스프레이가 오래된 아레나 헤더의 zone 필드 위에 공격자 qword를 씁니다. 그런 다음 스윕이 attacker_ptr + 0x14를 역참조합니다 — 0x424242424256에서 결정적 3/3.js::Zone*가 이 리필을 통해 심어졌습니다; 수집기는 오류 없이 위조된 아레나 메타데이터를 소비했습니다(1비트 교란된 제어 심기는 크래시 발생), ASLR 무력화 -> UAF 심기 구성을 입증합니다.UnmapPagesmadvise(MADV_DONTNEED)gc()가 JIT 데이터를 스윕합니다: sweepJitDataOnMainThread -> ICEntry::traceWeak -> TraceWeakCacheIRStub가 오래된 Symbol 포인터를 읽은 다음, SweepingTracer::onEdge(gc/Marking.cpp:3195)가 오래된 주소에 대해 청크 산술을 수행하여 아레나 헤더의 zone 필드를 로드합니다. 페이지가 사라졌거나 0으로 채워졌으므로 zone == nullptr이고, zone->isGCSweeping()이 nullptr + 0x14(shadow::Zone::gcState)를 읽습니다 -> SIGSEGV. 오류 시점에 rcx는 오래된 Symbol 포인터를, rdx는 munmap된 청크 베이스를 보유합니다.