← 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);

The DFG compiles bytecode into a CPS-form graph where local variables and internal state live in numbered tmp slots, historically all allocated from the CodeBlock's own tmp count. Array.prototype.sort inlining is a newer DFG optimization that generates its own internal control flow (handleArraySort) and needs scratch tmps beyond what the bytecode's CodeBlock accounts for. This commit adds a per-InlineStackEntry private tmp allocator — m_numPrivateTmps, allocatePrivateTmps, tmpOffsetForInlineeOf — so each inlined frame gets a non-overlapping tmp range.

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

This is a memory-aliasing bug in a tier that emits machine code directly, so nested inlined sort comparators could read and write each other's temporary storage. The affected path — Array.prototype.sort with a comparator that sorts — is reachable from ordinary script.

Forward-facing, the question is which other DFG-internal tmp consumers exist or will exist. Narrow: confirm allocatePrivateTmps() and tmpOffsetForInlineeOf() are used by every DFG-internal tmp consumer, not just sort, and that OSR exit and bytecode liveness analysis correctly exclude these private tmp ranges — a private tmp that liveness treats as a bytecode local is the failure mode to look for. Wider: any compiler that allocates scratch storage out of a frame-relative index space shared with a caller has this shape; audit deeply nested or mutually recursive inline stacks (sort calling sort calling sort) for offset overflow or miscomputation as m_numPrivateTmps accumulates per frame. Match tell in review: an Operand::tmp(base + k) computed from anything other than a range object returned by the allocator.