[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가 발생합니다.
Patch Details
runtime helper 시그니처가 9개의 개별 EncodedJSValue 인자에서 단일 EncodedJSValue* descriptorBuffer로 변경되었습니다. 이로 인해 호출이 4개의 GPR 인자로 줄었습니다. DFGSpeculativeJIT::compileObjectDefinePropertyFromFields와 FTL::LowerDFGToB3::compileObjectDefinePropertyFromFields는 Node::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과 겹치는 문제.
Background
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개의 정수 인자를 x0–x7에 전달하며, 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 상태로 표시합니다.
Analysis
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을 덮어쓰게 됩니다.
Aaa Aaa a Aaa Aaa Aaaaaa Aa Aa Aaaaaa Aa Aa Aaaaaaaaaaaa Aaaaaa Aa Aaaa Aaa Aaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaa Aaaaaa a Aaaaaaaa Aaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaaa Aa Aaa Aaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaa Aaaaaaaaaaaaaaaa Aa Aaa Aa Aa Aaaaaaaaaa Aaaa Aaa Aaaa Aaa Aaa Aaaa Aa Aaaaaa Aaaaaa Aaaa Aaa Aaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaaaa Aa Aaaaaaaaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaa
Aaaa Aaa Aaaa Aaaaaa Aaaaaaaaaaa Aaaaaaaa Aaaaaaaaaaa Aaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaaaaa Aaaaaaaa Aaaaa Aaaaaaaaaaaaaaaa Aaaaaaaaa a Aaaaa Aaaaaa Aa Aaa Aa Aaaaaaaaaaaaaaaa Aaaaaaaa Aaaaa Aaaaaaaa Aaaaaaaa Aaa Aaaa Aa Aaa Aaaaaaaaaa Aaa Aaaa Aaaaa Aa Aa Aaaaa Aaaaa Aa Aa Aaa Aaa Aaaa Aaa Aaa Aaaaaaaa Aaaaa Aaaa Aaaa Aaa Aaa a Aaa Aaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaa Aaa Aaa Aaaaa Aa Aaaaaaaa Aaaaaaaa Aaa Aaaa Aa Aaaa Aaaaaa Aaaaa Aaaaaaaaaa Aaaa Aaaaaaaaa Aaaaaaaaaaa Aaa Aaaa Aaaaa Aa Aaaaa Aaaaa Aaa Aaaaaaa Aaaaaaa Aaaaaa
a Aaaaaaaaaaaaaa Aaa Aaaa Aaa Aaaaaaaaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aa Aaaaaaa Aaaa Aaaaaa Aaaaaaaaaa Aaaaaaaa Aaa Aaaa Aaaaaaa Aaaaaaaa a Aaa Aaaaaaaaaa Aaaaa Aaaaa Aaaaaa Aaaaaaa Aaaaaaa a Aaaaa Aa Aaa Aaaa Aaa Aaaa Aaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaa Aa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aaaaaaaaaa Aa Aaa Aa Aaaa Aaa Aaaaaaaaaa Aaaaa Aaa Aaa Aaa Aa Aaaaaaa Aaaa Aaaa Aaaaa Aaaaaa Aaaa Aaaaa Aaaaa Aaaaaa Aa Aa Aaa Aa Aaaaaa Aaaa Aaaa Aaa Aaaaa Aaaaa Aaaaaaa
🔒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.
더 확인하려면 구독해 주세요
Audit directions
a Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa a Aaaaa Aaaaa Aaaaaaaa Aa Aaa Aaaa Aaa Aaaaaaaaa Aaaaaaa Aaa Aaaaa Aaaaa Aaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaaa Aaaaaaaaa Aaa Aaaaaaaa Aaa Aaaa Aaaaa Aa Aaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaa Aaaa Aaaaaaa Aaaa Aa Aa Aaa Aa Aaa Aa Aaa Aaaa Aa Aa Aaaaaaa Aaaaaaa Aaaaa Aaaa Aaaa
a Aaaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaaa Aaa Aaa Aaaaaaa Aaa Aaaaaaaa Aa Aaa Aaa a Aaaaa Aa Aaaa Aaaaaaaaa Aaaa Aaa Aaaaaaaa Aaaa Aa Aa Aaa Aaaaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaa Aaa Aaaa Aa Aaaaaaa Aaaa Aaaa
a Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaa Aaa Aaaaa Aaa Aaaa Aaaaaaa Aaaa Aaaa Aaaaa Aa a Aaa Aaaa Aaaa Aaa Aa Aa Aaa Aa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaa
a Aaaa Aaa Aaaa a Aaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaa Aaaa Aaa Aaaaaaaa Aaa Aaaa Aaa Aa Aaaa Aaaaaaa Aaa Aaaa Aaaa
🔒Four reusable audit patterns identified, with concrete starting files and a class-of-helpers worth re-counting against the per-architecture register budget.
더 확인하려면 구독해 주세요