← All issues

[3] DFG MapIterationEntryKey Type Confusion

Severity: Critical | Component: JSC DFG JIT — DFGNodeType.h | 3f6f783

관찰 가능한 효과가 DFG type confusion으로, object key가 integer semantics로 잘못 표현됩니다. 첨부된 test case는 heap spray를 통한 GC-assisted object aliasing이라는 거의 완성된 exploitation sequence를 시연합니다. 단일 토큰 수준의 root cause와 PoC 수준의 regression test를 근거로 confidence 0.88로 Critical이 산출되었습니다.

MapIterationEntryKey는 임의의 JSValue를 반환합니다. Map의 key는 어떤 타입도 될 수 있기 때문입니다. 따라서 MapIterationEntryValue와 맞추어 NodeResultJS로 선언되어야 합니다.

Source/JavaScriptCore/dfg/DFGNodeType.h

macro(MapIterationEntry, NodeResultJS) \
- macro(MapIterationEntryKey, NodeResultInt32) \
+ macro(MapIterationEntryKey, NodeResultJS) \
macro(MapIterationEntryValue, NodeResultJS) \

JSTests/stress/map-forEach.js

+ function inlinee(value, key) {
+ object.key = key;
+ }
+
+ for (let i = 0; i < 200; i++) {
+ map.forEach(inlinee);
+ }
+
+ for (let i = 0; i < 100; i++) {
+ startScan();
+ (new Map([[{0: 1.1}, 1]])).forEach(inlinee);
+ const spray = [];
+ for (let j = 0; j < 1000; j++) {
+ spray.push({0: 2.2});
+ }
+ if (object.key[0] === 2.2) {
+ throw new Error("bad");
+ }
+ }

수정 내용은 DFGNodeType.hFOR_EACH_DFG_OP 매크로 테이블에서 단일 토큰을 변경한 것입니다. MapIterationEntryKeyNodeResultInt32에서 NodeResultJS로 변경되었습니다. 함께 추가된 regression test는 object key를 이용한 Map.forEach로 버그를 재현합니다. hot loop를 통해 DFG 컴파일을 유발한 뒤, heap spray로 type confusion이 실제 object aliasing을 만들어냄을 확인합니다.

JSC의 DFG JIT는 FOR_EACH_DFG_OP 매크로로 구성된 node 타입 테이블을 사용합니다. 각 IR node는 NodeResultInt32, NodeResultJS, NodeResultDouble 같은 플래그로 결과 타입을 선언합니다. 이 플래그들은 DFG 파이프라인 전반에 걸쳐 prediction 전파, 레지스터 할당, representation 선택(boxed vs. unboxed), write barrier 생성을 결정합니다. NodeResultInt32는 해당 node가 항상 unboxed 32-bit integer를 생성한다는 것을 컴파일러에 알립니다. 이 선언에 의해 downstream node들은 tag를 제거하고 raw 32-bit payload를 직접 처리합니다.

JSC는 64-bit 플랫폼에서 JSValue를 인코딩하기 위해 NaN-boxing을 사용합니다. object pointer, double, integer 모두 tag bit로 구분되는 64-bit 표현을 공유합니다. DFG가 int32 speculation 하에 코드를 컴파일할 때는 tag를 제거하고 raw 32-bit payload를 처리하며, downstream에서 boxed JSValue가 필요하면 다시 tagging합니다.

JavaScript의 Map은 object, string, number, symbol 등 모든 타입을 key로 가질 수 있습니다. Map.prototype.forEach는 각 entry를 순회하며 (value, key, map)을 callback에 전달합니다. DFG가 이 callback을 inline 처리할 때는 MapIterationEntryKeyMapIterationEntryValue node를 생성하여 각 entry에서 key와 value를 추출합니다. MapIterationEntryValue는 이미 NodeResultJS로 올바르게 선언되어 있었습니다.

root cause는 DFG node 타입 테이블의 플래그 하나가 잘못된 것입니다. MapIterationEntryKeyNodeResultInt32로 선언되어 결과가 항상 unboxed 32-bit integer라고 명시되어 있었습니다. 실제로 Map의 key는 임의의 JSValue입니다.

DFG 컴파일러가 Map.forEach callback을 처리하면서 iteration key를 만나면, NodeResultInt32 선언을 바탕으로 prediction 전파와 코드 생성을 진행합니다. 해당 key가 실제로는 object pointer(NaN-boxing에서의 64-bit tagged JSValue)임에도, DFG는 이를 integer representation semantics로 처리하는 코드를 생성합니다.

이 잘못된 표현은 DFG 파이프라인 전반에서 object pointer가 잘못된 타입 가정으로 처리되는 결과를 낳습니다. 값이 잘려나가거나 잘못 tagging되거나, GC write barrier가 누락될 가능성이 있습니다. 다만 잘못된 representation이 collector로부터 pointer를 숨겨 GC가 해당 reference를 제대로 추적하지 못하는 경우, 원래의 key object가 수집되는 동안 stale pointer가 남아 use-after-free로 이어질 가능성이 있습니다.

MapIterationEntryKey(Int32)와 MapIterationEntryValue(JS) 사이의 비대칭은 copy-paste 실수이거나, map key를 임의의 값이 아닌 integer 인덱스로 가정한 결과일 가능성이 높습니다. FOR_EACH_DFG_OP 테이블은 DFG 파이프라인 전체에서 node 결과 타입의 단일 진실 공급원입니다. 이 테이블의 오류는 prediction, speculation, 레지스터 할당, write barrier 생성 전반에 자동으로 전파됩니다.

🔒

상세 취약점 분석, 공격 가능성 평가, 보안 영향 분석이 포함되어 있습니다

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

🔒

이 취약점 패턴의 변종을 찾기 위한 구체적인 탐색 방향이 포함되어 있습니다

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