← All reports

[JSC] Private tmp mechanism in the DFG ByteCodeParser

Component: JSC DFG and FTL JIT | c415e39

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

+ unsigned ByteCodeParser::tmpOffsetForInlineeOf(InlineStackEntry* caller)
+ {
+ unsigned callerOffset = caller->m_inlineCallFrame ? caller->m_inlineCallFrame->tmpOffset : 0;
+ return callerOffset + caller->m_codeBlock->numTmps() + caller->m_numPrivateTmps;
+ }
+
+ auto ByteCodeParser::allocatePrivateTmps(unsigned slotCount) -> PrivateTmpRange
+ {
+ InlineStackEntry* top = m_inlineStackTop;
+ unsigned currentTmpOffset = top->m_inlineCallFrame ? top->m_inlineCallFrame->tmpOffset : 0;
+ unsigned relativeBase = top->m_codeBlock->numTmps() + top->m_numPrivateTmps;
+
+ ensureTmps(currentTmpOffset + relativeBase + slotCount);
+ top->m_numPrivateTmps += slotCount;
+
+ return { relativeBase, slotCount };
+ }
...
- unsigned tmpBase = m_inlineStackTop->m_codeBlock->numTmps() + maxNumCheckpointTmps;
- unsigned currentTmpOffset = m_inlineStackTop->m_inlineCallFrame ? m_inlineStackTop->m_inlineCallFrame->tmpOffset : 0;
- ensureTmps(currentTmpOffset + tmpBase + 9);
- Operand tmpI = Operand::tmp(tmpBase + 0);
...
+ constexpr unsigned numArraySortTmps = 9;
+ auto sortTmps = allocatePrivateTmps(numArraySortTmps);
+ Operand tmpI = sortTmps.operandAt(0);

DFG는 bytecode를 CPS 형태의 그래프로 컴파일하며, local 변수와 내부 상태는 번호가 매겨진 tmp slot에 저장됩니다. 지금까지는 이 slot이 모두 CodeBlock 자체의 tmp count에서 할당되어 왔습니다. Array.prototype.sort inlining은 비교적 최근에 추가된 DFG 최적화로, 자체 내부 control flow(handleArraySort)를 생성하며 bytecode의 CodeBlock이 관리하는 범위를 넘어서는 scratch tmp를 필요로 합니다. 이번 commit에서는 InlineStackEntry별 private tmp allocator인 m_numPrivateTmps, allocatePrivateTmps, tmpOffsetForInlineeOf가 추가되었습니다. 이를 통해 inline된 각 frame이 서로 겹치지 않는 tmp 범위를 갖도록 보장합니다.

Before:
Outer frame:  CodeBlock tmps [0..N) | checkpoint pad | outer's private sort tmps
Inlinee tmpOffset = N  (ignores the caller's own private sort tmps)
  => an inlined comparator that itself sorts allocates 9 private tmps that
     overlap the outer sort's scratch (tmpI, tmpArray, tmpLength, ...)

After:
Outer frame:  CodeBlock tmps [0..N) | outer's private sort tmps [N..N+9), tracked
Inlinee tmpOffset = tmpOffsetForInlineeOf(caller) = N + 9
  => the inner frame's tmps start fully above the outer's private range

기계어를 직접 생성하는 tier에서 발생하는 memory-aliasing 버그로, 중첩된 inline sort comparator들이 서로의 임시 저장공간을 읽고 쓸 수 있는 상황이 가능했습니다. 영향을 받는 경로는 Array.prototype.sort에 sort를 수행하는 comparator를 넘기는 경우이며, 일반적인 스크립트에서 충분히 도달 가능한 지점입니다.

앞으로 살펴봐야 할 지점은 DFG 내부의 tmp를 사용하는 다른 소비자가 또 있는지, 혹은 앞으로 생길 수 있는지입니다. 좁게 보면, allocatePrivateTmps()tmpOffsetForInlineeOf()가 sort뿐 아니라 DFG 내부에서 tmp를 사용하는 모든 지점에서 일관되게 쓰이는지 확인해야 합니다. 또한 OSR exit와 bytecode liveness 분석이 이 private tmp 범위를 올바르게 제외하는지도 점검해야 합니다. Liveness 분석이 private tmp를 bytecode local로 취급하는 경우가 바로 찾아야 할 실패 패턴입니다. 넓게 보면, caller와 공유하는 frame 상대 index 공간에서 scratch storage를 할당하는 컴파일러라면 어디든 이와 동일한 구조적 위험을 가질 수 있습니다. 깊게 중첩되거나 상호 재귀적인 inline stack(sort가 sort를 부르고 그 안에서 다시 sort를 부르는 경우)에서 m_numPrivateTmps가 frame마다 누적되면서 offset overflow나 계산 오류가 생기지 않는지도 점검이 필요합니다. 리뷰 시 눈여겨봐야 할 신호는 allocator가 반환한 range 객체가 아닌 다른 값으로부터 계산된 Operand::tmp(base + k) 형태의 코드입니다.