← All issues

[1] RegExpStringIterator.next receiver type-check elision

The JIT proved a broad type and treated it as proof of the exact one.

Severity: High | Component: JSC DFG/FTL JIT | e6fd50a

High. JIT가 생략한 check는, "임의의 object"가 "이 iterator의 internal field"로 읽히는지를 결정하는 바로 그 check입니다. Renderer script에서 곧바로 도달 가능하지만, write-primitive 쪽 절반은 type confusion만으로는 주어지지 않는 heap-layout 제어를 추가로 필요로 합니다.

JSC의 optimizing JIT tier들은 value type을 speculate한 뒤, abstract interpreter가 이미 redundant하다고 증명한 runtime check를 생략함으로써 hot path에서 안전성을 속도와 맞바꿉니다. Mid-tier인 DFG와 top-tier인 FTL 컴파일러는 RegExpStringIteratorNext 노드를 receiver type check로 guard합니다. 이 노드는 String.prototype.matchAll이 반환하는 iterator의 next() 동작에 대응하며, native helper 호출 직전에 이 check가 수행됩니다. 이 helper는 receiver에서 고정 offset의 internal field를 읽어오기 때문에, receiver가 정확히 JSRegExpStringIterator이고 그 이상으로 넓지 않다는 것이 증명된 경우에만 안전합니다.

관전 포인트: 어떤 renderer script든 RegExpStringIterator.prototype.next에 Array iterator 같은 wrong-typed receiver를 넘길 수 있고, 그러면 operation이 이 foreign object의 slot을 RegExpStringIterator의 regexp/subject/state pointer로 읽어들이게 됩니다. Heap-layout을 제어할 수 있다면 이는 fake-object 구성의 출발점이 될 수 있습니다.

SpecObjectOther type filtering은 operationRegExpStringIteratorNext operation이 전제하는 조건에 비해 지나치게 넓습니다. DFG/FTL_TYPE_CHECK가 speculated type을 runtime check와 동등하게 취급하기 때문에, JSC는 type check를 생략하게 되고 결과적으로 임의의 object가 operationRegExpStringIteratorNext 호출로 흘러들어갈 수 있습니다.

Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp

void SpeculativeJIT::compileRegExpStringIteratorNext(Node* node)
...
- speculateCellType(node->child1(), iteratorGPR, SpecObjectOther, JSRegExpStringIteratorType);
+ speculateCellTypeWithoutTypeFiltering(node->child1(), iteratorGPR, JSRegExpStringIteratorType);
callOperation(operationRegExpStringIteratorNext, ...);

Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

LValue iterator = lowCell(m_node->child1());
- FTL_TYPE_CHECK(jsValueValue(iterator), m_node->child1(), SpecObjectOther, isNotType(iterator, JSRegExpStringIteratorType));
+ speculate(BadType, jsValueValue(iterator), m_node->child1().node(), isNotType(iterator, JSRegExpStringIteratorType));
LValue match = vmCall(Int64, operationRegExpStringIteratorNext, weakPointer(globalObject), iterator);

JSTests/stress/regexp-string-iterator-next-wrong-receiver.js

+function makeWrongIterator(i) {
+ const array = [i, i + 1, i + 2, i + 3];
+ return array[Symbol.iterator]();
+}
+function probe(i) {
+ ...
+ if (i & 1) iter = makeWrongIterator(i); else iter = makeRegExpIterator(i);
+ ...
+ return regExpIteratorNext.call(iter);
+}
+for (let i = 0; i < testLoopCount; ++i) probe(i);

이번 변경은 두 JIT backend 모두에서 RegExpStringIteratorNext 노드에 대한 receiver check를 다시 작성합니다. SpeculativeJIT::compileRegExpStringIteratorNext에서 receiver check가 변경되었습니다. 기존에는 speculateCellType(node->child1(), iteratorGPR, SpecObjectOther, JSRegExpStringIteratorType)를 호출했습니다. 이제는 speculateCellTypeWithoutTypeFiltering(node->child1(), iteratorGPR, JSRegExpStringIteratorType)를 호출합니다. FTL lowering에서도 마찬가지입니다. 기존에는 FTL_TYPE_CHECK(..., SpecObjectOther, isNotType(iterator, JSRegExpStringIteratorType))를 사용했습니다. 이제는 raw speculate(BadType, ..., isNotType(iterator, JSRegExpStringIteratorType)) 형태로 바뀌었습니다. 두 변경 모두 SpecObjectOther speculated-type 인자를 제거합니다. 그 결과 concrete JSRegExpStringIteratorType runtime guard는 abstract interpreter의 needsTypeCheck에 의해 걸러지지 않고 무조건 emit됩니다. 또한 genuine iterator와 foreign iterator를 공유된 next를 통해 번갈아 실행하는 regression test인 regexp-string-iterator-next-wrong-receiver.js가 추가되었습니다.

증명된 abstract-type superset을 필요한 concrete type의 증명으로 취급하여, 증명된 집합이 check가 강제하는 집합보다 넓은 경우에도 JIT runtime check가 생략되도록 허용하는 패턴.

이 코드가 있는 위치. DFG와 FTL은 JSC의 optimizing JIT tier입니다. 함수가 hot해지면 baseline JIT는 이를 mid-tier인 DFG로 넘기고, 이후 top-tier인 FTL(B3 backend를 거쳐 lowering)로 넘깁니다. 두 tier 모두, runtime에 관찰된 value type을 전제로 하는 profile-guided speculative machine code를 컴파일합니다. 그 전제가 깨지면 interpreter로 돌아가는 OSR exit가 수행됩니다.

Abstract interpreter와 SpeculatedType. 각 tier는 data-flow graph 전체에 type 정보를 전파하는 abstract interpreter(AI)를 실행합니다. 이 AI는 모든 edge에 대해 SpeculatedType — type category들의 bitmask lattice — 를 계산합니다. SpecObjectOther는 다양한 non-final object 종류를 포괄하는 넓은 category입니다. 반면 JSRegExpStringIteratorType과 같은 JSType 값은 정확히 하나의 runtime cell type을 가리킵니다. 이런 category가 존재하는 이유는, AI가 각 concrete type을 개별적으로 추적하는 대신 여러 concrete JSType을 하나의 집합으로 묶어 IR 수준에서 빠르게 추론할 수 있게 하기 위해서입니다.

Type-check macro와 elision. speculateCellTypeFTL_TYPE_CHECK는 speculated-type 인자를 받습니다. 그리고 runtime guard를 emit하기 전에 m_interpreter.needsTypeCheck(edge, typesPassedThrough)를 확인합니다. 이때 전달된 SpeculatedType을 runtime check와 동등한 것으로 취급합니다. 반면 speculateCellTypeWithoutTypeFiltering과 raw speculate(BadType, ...) 형태는 이 AI 확인 과정을 건너뛰고 concrete-type branch를 항상 emit합니다.

Internal field와 operation. RegExpStringIteratorNextString.prototype.matchAll이 반환하는 iterator의 next()에 대해 생성되는 노드입니다. operationRegExpStringIteratorNext는 iterator object의 고정 offset internal field(regexp, subject string, flags/state)를 읽습니다. 이 internal field는 C++ 전용 slot으로, JavaScript property가 아니기 때문에 script에서 읽거나 쓸 수 없습니다. 따라서 이 operation의 정확성은 receiver가 실제로 자신이 가정하는 메모리 레이아웃을 가진 C++ type인지에 전적으로 의존합니다.

Root cause는 type-check elision에 있습니다. Receiver guard는 typesPassedThrough 인자로 SpecObjectOther를 전달했습니다. 하지만 operation이 요구하는 조건은 훨씬 좁습니다. Receiver가 정확히 JSRegExpStringIterator여야 한다는 invariant입니다. SpecObjectOther는 이 concrete type의 strict superset에 해당합니다. Macro는 "AI가 값이 typesPassedThrough 안에 있음을 증명했다"는 사실을, "runtime check가 redundant하다"는 의미로 취급합니다.

  Receiver edge proven SpecObjectOther on the compiled path

  Before (buggy):                    After (fixed):
  needsTypeCheck(SpecObjectOther)    speculateCellType...WithoutTypeFiltering
    -> value already in category       -> no AI consultation
    -> guard ELIDED                    -> guard ALWAYS emitted
         │                                   │
         ▼                                   ▼
  any non-final object flows in      isNotType(iterator,
  to operationRegExpStringIterator     JSRegExpStringIteratorType)
  Next unchecked                       -> OSR exit on mismatch

AI가 receiver edge를 SpecObjectOther로 narrowing하는 경우를 생각해 볼 수 있습니다. 예를 들어 control-flow context가 해당 값이 non-final object임을 증명하는 경우가 여기에 해당합니다. 이때 needsTypeCheck는 false를 반환하고, concrete JSRegExpStringIteratorType guard는 전혀 emit되지 않습니다. 그러면 이 넓은 category를 만족하는 어떤 object든 operationRegExpStringIteratorNext에 도달할 수 있고, 이 operation은 해당 object의 메모리를 JSRegExpStringIterator로 해석하게 됩니다.

Regression test는 정확히 이 trigger를 재현합니다.

  1. 공유된 iterator method를 가져옵니다: Object.getPrototypeOf("aa".matchAll(/a/g)).next.
  2. 홀수 번째 iteration에서는 makeWrongIterator(array[Symbol.iterator]())를 통해 foreign receiver를 만들고, 짝수 번째 iteration에서는 genuine RegExp iterator를 만듭니다.
  3. Hot loop(testLoopCount) 안에서 regExpIteratorNext.call(iter)를 호출하여 DFG에 이어 FTL이 next body를 컴파일하도록 합니다.
  4. Compile된 경로에서 AI가 receiver edge를 SpecObjectOther로 narrowing하면 concrete guard가 생략되고, foreign Array iterator가 검증 없이 operationRegExpStringIteratorNext로 흘러들어갑니다.

Exploitability 측면에서 보면, 이는 일반 JavaScript에서 도달 가능한 JIT type confusion에 해당합니다. Operation은 iterator의 inline capacity인 2에 맞춰진 offset에서 confused object의 field를 읽어, 이를 regexp/string/state pointer로 해석합니다. Heap layout을 제어할 수 있는 조건에서는, confused object의 내용을 조작하여 이후의 match/string operation에 attacker-influenced pointer를 공급하는 형태로 만들 수 있습니다. 이는 fake object 구성이나 relative read에 적합한 controlled type-confusion primitive로 이어질 가능성이 있습니다. 다만 완전한 read/write primitive를 얻으려면 추가적인 grooming이 필요합니다. 이렇게 얻어지는 primitive는 WebContent renderer sandbox 내부로 국한되므로, system compromise로 이어지려면 별도의 sandbox escape가 여전히 필요합니다.

이 vulnerability는 WebContent renderer 내부에서 JIT type system이 보장하는 memory-type safety를 약화시킵니다. Security model은 operationRegExpStringIteratorNext에 전달되는 receiver가 실제로 JSRegExpStringIterator임이 증명되었다는 전제를 깔고 있습니다. 넓은 SpecObjectOther speculation은 이 보장을 무너뜨릴 가능성이 있습니다.

이는 "category가 concrete type을 증명한다"는 전형적인 JIT 버그 패턴에 해당합니다. SpeculatedType category가 type-check macro의 두 번째 인자로 사용되지만, 정작 runtime branch는 단일 concrete JSType을 검증합니다. Macro가 speculated type을 runtime check와 동등하게 취급하기 때문에, 어떤 superset 관계든 조용히 생략된 check로 이어질 수 있습니다. Operation으로 guard되는 receiver check의 안전한 idiom은, 이번 fix가 전환한 WithoutTypeFiltering / raw speculate(BadType, ...) 형태입니다. 이는 다른 internal-field-object iterator operation들을 이미 guard하고 있는 것과 동일한 패턴입니다.


IPC Connection SyncMessageState 관련 취약점 커밋을 한국어 리포트로 번역하겠습니다.