[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.
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
Patch Details
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을 설치할 수 있었던 패턴.
Background
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가 변형될 수 있습니다.
Analysis
패치 이전 코드는 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에 박혀 설치될 수 있었습니다.
Audit directions
- Prototype chain을 순회하지만 head 검증을 호출자에게 위임하는 condition-building helper.
generateConditionsForPropertyMissConcurrently,generateConditionsForPropertySetterMiss*,generateConditionsForPropertyHit*,generateConditionsForInstanceOf*의 모든 호출 지점을 점검해야 합니다.Source/JavaScriptCore/bytecode/PolyProtoAccessChain.cpp,runtime/ObjectPropertyConditionSet.cpp, 그리고 모든Graph::tryEnsure*helper를 시작점으로 삼는 것이 좋습니다. - Callback 경계를 넘는 fast path를 제어하는 absence/presence condition. absence가 잘못된 상태에서 callback이 인접한 typed storage를 변형할 수 있으면, 테스트에서 확인된 array indexing type confusion이 발생합니다.
JSPromiseConstructor::resolvelowering,JSONStringifytoJSON inlining,op_to_primitivelowering에서의 thenable resolution을 점검할 필요가 있습니다. - 여러 호출 지점에 중복 존재하지만 일부 지점에서 묵시적으로 누락된 검증 lambda.
Source/JavaScriptCore/dfg/와ftl/에서propertyAccessesAreCacheable과propertyAccessesAreCacheableForAbsence를 검색하고, 비대칭 패턴을 확인하면 soundness 버그로 이어질 가능성이 높습니다. - Invalidation 시점. 설치 시점부터 잘못된 condition이 이후의 structure 전환 때만이 아닌 설치 시점에서 포착되는지 확인해야 합니다.
Watchpoint.cpp와ObjectPropertyCondition::isStillValid를 점검할 필요가 있습니다.