← All issues

[2] DFG/FTL stack corruption from 9-argument ObjectDefinePropertyFromFields helper

Severity: High | Component: JSC DFG/FTL JITs | 59a20dd

diff가 9번째 C 인자를 가장 낮은 DFG spill slot과 겹치는 stack slot에 기록하는 JIT-emitted 코드를 수정하기 때문에 High로 평가됩니다. 손상되는 비트는 공격자가 선택한 EncodedJSValue descriptor encoding이며, spill된 값은 이후 동일한 JIT 코드에 의해 다시 읽힙니다. 결과적으로 WebContent 내부에서 제어 가능한 spill-slot clobber primitive가 만들어집니다.

ObjectDefinePropertyFromFields는 9개 매개변수로 연산을 호출하고 있었습니다. 그러나 ARM64는 최대 8개, x86_64는 최대 6개의 register 매개변수만 지원합니다. DFG JIT는 runtime helper가 stack 인자를 필요로 하지 않는다고 가정합니다. 이번 수정에서는 descriptor 필드에 scratch buffer를 사용하도록 변경되었습니다.

Source/JavaScriptCore/dfg/DFGOperations.h

-JSC_DECLARE_JIT_OPERATION(operationObjectDefinePropertyFromFields, void, (JSGlobalObject*, JSObject*, EncodedJSValue, EncodedJSValue, EncodedJSValue, EncodedJSValue, EncodedJSValue, EncodedJSValue, EncodedJSValue));
+JSC_DECLARE_JIT_OPERATION(operationObjectDefinePropertyFromFields, void, (JSGlobalObject*, JSObject*, EncodedJSValue, EncodedJSValue*));

Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp

- JSValueOperand enumerable(this, m_graph.varArgChild(node, 2));
- ...
- JSValueOperand setter(this, m_graph.varArgChild(node, 7));
+ GPRTemporary buffer(this);
+ constexpr size_t scratchSize = sizeof(EncodedJSValue) * Node::numberOfDescriptorSlots;
+ ScratchBuffer* scratchBuffer = vm().scratchBufferForSize(scratchSize);
+ EncodedJSValue* scratchData = static_cast<EncodedJSValue*>(scratchBuffer->dataBuffer());
+ move(TrustedImmPtr(scratchData), bufferGPR);
+ for (unsigned slot = 0; slot < Node::numberOfDescriptorSlots; ++slot) {
+ JSValueOperand operand(this, m_graph.varArgChild(node, slot + 2));
+ storeValue(operand.jsValueRegs(), Address(bufferGPR, sizeof(EncodedJSValue) * slot));
+ operand.use();
+ }
- callOperation(operationObjectDefinePropertyFromFields, ..., enumerableRegs, configurableRegs, valueRegs, writableRegs, getterRegs, setterRegs);
+ callOperation(operationObjectDefinePropertyFromFields, LinkableConstant::globalObject(*this, node), targetGPR, keyRegs, bufferGPR);

JSTests/stress/object-define-property-fields-spilled-arg.js

+// 9번째 인자가 [sp + 0]에 저장되었지만, 해당 대상에서 maxFrameExtentForSlowPathCall은 0이므로
+// [sp + 0]이 가장 낮은 spill slot과 겹쳐 spill되어 있던 값을 손상시킵니다.
+// 여기서 spill된 값은 이후 ValueAdd로 전달되는 함수 인자이며,
+// 손상된 slot을 읽어 operationValueAddNotNumber에서 crash가 발생합니다.

runtime helper 시그니처가 9개의 개별 EncodedJSValue 인자에서 단일 EncodedJSValue* descriptorBuffer로 변경되었습니다. 이로 인해 호출이 4개의 GPR 인자로 줄었습니다. DFGSpeculativeJIT::compileObjectDefinePropertyFromFieldsFTL::LowerDFGToB3::compileObjectDefinePropertyFromFieldsNode::numberOfDescriptorSlots 항목의 ScratchBuffer를 확보하고, 각 descriptor 필드를 Node::DescriptorSlot 인덱스 위치에 저장한 뒤 buffer pointer를 네 번째 인자로 전달하도록 재구성되었습니다. runtime helper는 ActiveScratchBufferScope 안에서 slot을 다시 디코딩합니다(GC가 buffer를 스캔할 수 있도록).

runtime helper가 argument register 안에 수용되어야 한다는 JIT slow-path calling-convention invariant를 위반하여, stack으로 전달된 인자가 JIT spill slot과 겹치는 문제.

JSC의 optimizing JIT에서 runtime helper(operationXxx)는 JIT-emitted 코드에서 일반 C 함수로 호출됩니다. JIT는 OSR 가능한 영역에 진입할 때 maxFrameExtentForSlowPathCall 상수에 의존하여 outgoing argument stack 공간을 예약합니다. ARM64와 x86_64에서 이 값은 0으로, helper가 stack에 인자를 전달하지 않는다는 invariant를 나타냅니다. Spill slot은 물리 register에 들어가지 못한 JSValue를 DFG가 배치하는 위치로, 현재 stack pointer 근처에 존재합니다.

ARM64에서는 처음 8개의 정수 인자를 x0x7에 전달하며, x86_64 SysV에서는 처음 6개를 rdi/rsi/rdx/rcx/r8/r9에 전달합니다. 이를 초과하는 인자는 [sp + 0]부터 stack에 배치됩니다. ScratchBuffer는 VM이 소유하며 GC가 스캔하는 scratch 영역으로, argument register를 소비하지 않고 임의 데이터를 runtime helper에 전달하기 위해 사용됩니다. ActiveScratchBufferScope는 helper 실행 중 보수적 스캔(conservative scanning)을 위해 buffer를 live 상태로 표시합니다.

operationObjectDefinePropertyFromFields는 포인터 크기의 인자 9개로 선언되어 있었습니다. x86_64에서는 7번째부터 9번째 인자가 [sp + 0], [sp + 8], [sp + 16]에 stack으로 spill됩니다. 해당 대상 아키텍처에서 maxFrameExtentForSlowPathCall이 0이므로, 컴파일러는 outgoing C-call stack 인자를 위한 stack pointer 아래 공간을 예약하지 않습니다. 대신 DFG는 [sp + 0..](및 현재 SP 바로 아래의 word들)를 실행 중인 JIT 값의 가장 낮은 spill slot으로 사용합니다. 따라서 JIT가 해당 호출 코드를 생성할 때, stack으로 전달된 인자들이 실행 중이던 JSValue를 보관한 spill slot을 덮어쓰게 됩니다.

🔒

The calling-convention assumption broken here is global to the JIT — explore what a stack-poked argument can do when the surrounding code spills, and what the attacker controls in the overwriting bits.

더 확인하려면 구독해 주세요

🔒

Four reusable audit patterns identified, with concrete starting files and a class-of-helpers worth re-counting against the per-architecture register budget.

더 확인하려면 구독해 주세요