← All issues

[4] [JSC] Check initial object structure in tryEnsureAbsence in DFG

DFG's tryEnsureAbsence validated the prototype chain but skipped the head — the JIT baked in absence assumptions the receiver itself disproved.

Severity: High | Component: JSC DFG JIT | 78c04ea

DFG speculation의 soundness 위반을 제거하는 변경이므로 High로 평가되었습니다. property-absence assumption이 해당 property를 own property로 소유하는 receiver에도 설치될 수 있었으며, regression test는 표준 fakeobj/addrof 조합으로 직결되는 controlled JSObject/Double type confusion을 제시합니다.

Graph::tryEnsureAbsence는 prototype chain에 대한 cacheability 검증은 수행하지만, head receiver 자체에 대해서는 전혀 검증하지 않았습니다. head object가 해당 property를 own property로 소유하는 경우에도, DFG는 absence를 주장하는 ObjectPropertyConditionSet을 그대로 설치했습니다. 이 invariant는 컴파일 시점부터 거짓인 상태였습니다.

Source/JavaScriptCore/dfg/DFGGraph.cpp

+ auto isAbsenceCacheable = [&](Structure* structure) {
+ if (structure->typeInfo().overridesGetOwnPropertySlot())
+ return false;
+ if (!structure->propertyAccessesAreCacheable())
+ return false;
+ if (!structure->propertyAccessesAreCacheableForAbsence())
+ return false;
+ unsigned attributes;
+ if (isValidOffset(structure->getConcurrently(identifier.uid(), attributes)))
+ return false;
+ if (structure->hasPolyProto())
+ return false;
+ return true;
+ };
+
+ if (!isAbsenceCacheable(headStructure))
+ return ObjectPropertyConditionSet::invalid();

cacheability 및 absence 검증이 isAbsenceCacheable lambda로 분리되었습니다. 이 lambda는 generateConditionsForPropertyMissConcurrently 실행 전 headStructure에 먼저 적용되며, 이후 prototype 객체 각각에 대해서도 재사용됩니다. 추가된 주석에는 generateConditionsForPropertyMissConcurrently가 prototype chain만 순회한다는 점과, head 검증이 호출자의 책임임을 명시하고 있습니다.

DFG absence-condition builder에서 prototype chain만 검증하고 head receiver 자체에 대한 self-check가 누락되어, JIT가 receiver의 own property에 의해 모순되는 absence assumption을 설치할 수 있었던 패턴.

JSC의 optimizing tier는 런타임 property lookup을 회피하기 위해 structural invariant를 ObjectPropertyConditionSet에 기록합니다. 예를 들어 "object O에 property P가 없다"는 식의 조건을 저장하며, downstream code가 이를 machine code로 변환합니다. tryEnsureAbsence는 이 중 absence variant를 구성하는 함수입니다. generateConditionsForPropertyMissConcurrently는 prototype chain을 순회하며 각 prototype에 대한 Miss condition을 생성하지만, head structure는 검사하지 않습니다. PolyProto는 인스턴스별 prototype storage를 나타내며, 이는 prototype-chain caching을 무효화합니다. re-entrancy 지점(Promise thenable 감지, JSON.stringify toJSON, ToPrimitive)에서는 getter를 통해 사용자 코드가 호출될 수 있으며, 이 과정에서 인접한 typed storage가 변형될 수 있습니다.

패치 이전 코드는 headStructure가 null인 경우에만 중단하도록 구성되어 있었습니다. head에 대한 overridesGetOwnPropertySlot, propertyAccessesAreCacheable[ForAbsence], structure->getConcurrently(identifier.uid(), ...) 검사는 전혀 수행되지 않았습니다. Object.create를 통해 chain 상에 { toJSON: 1, a: 1 } head를 배치하면 이 조건을 통과할 수 있었습니다. DFG는 tmp.toJSON이 없다는 거짓 가정을 바탕으로 fast path를 JIT 컴파일하게 됩니다.

Regression test를 살펴보면, PoC는 먼저 object1 = { toJSON: 1, a: 1 }을 구성하고 container1 -> object1 chain을 형성합니다. 이후 DFG tier에서 충분히 실행된 함수 내부의 Promise.resolve thenable 감지 과정에서 +tmp.toJSON을 수행합니다. thenable에 설치된 then getter는 trigger flag가 설정되면 array[0] = {}를 실행합니다. 이 동작으로 array의 indexing type이 Double에서 Contiguous로 변경됩니다. 워밍업이 완료된 후 trigger = true로 재진입하면, DFG fast path가 이미 모순된 assumption 하에서 실행됩니다. 이 시점에 array[0] = 2.3023e-320이 실행되면, 런타임이 object-typed으로 간주하는 storage에 raw double pattern이 기록됩니다. array[0].x는 이 double 값을 JSObject 포인터로 역참조하게 되며, WebContent process 내에서 object-vs-double type confusion이 발생합니다. array indexing type 교체와 연계하면 fakeobj/addrof primitive 쌍을 얻을 수 있으며, 이는 JSC에서 임의 읽기/쓰기로 이어지는 표준적인 시작점입니다.

이 vulnerability는 DFG의 type-system soundness를 약화시킵니다. receiver의 own property를 검사하지 않았기 때문에, 컴파일 시점부터 거짓인 invariant가 machine code에 박혀 설치될 수 있었습니다.