[2] DFG/FTL stack corruption from 9-argument ObjectDefinePropertyFromFields helper
Severity: High | Component: JSC DFG/FTL JITs | 59a20dd
Rated High because the diff repairs JIT-emitted code that pokes the 9th C-argument into a stack slot aliasing the lowest DFG spill slot; the corrupting bits are attacker-chosen EncodedJSValue descriptor encodings and the spilled value is later re-read by the same JIT code, yielding a controllable spill-slot clobber primitive inside WebContent.
ObjectDefinePropertyFromFields was calling an operation with 9 parameters, but ARM64 / x86_64 only support up to 8 / 6 register parameters respectively. The DFG JIT assumes runtime helpers never need stack arguments. The fix uses a scratch buffer for the descriptor fields.
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
+// The 9th argument was poked to [sp + 0], but maxFrameExtentForSlowPathCall is 0 on
+// those targets, so [sp + 0] aliased the lowest spill slot and corrupted whatever was
+// spilled there. Here, the spilled value happens to be the function argument that
+// later feeds a ValueAdd; reading the corrupted slot crashed in operationValueAddNotNumber.
Patch Details
The runtime helper signature changes from 9 individual EncodedJSValue arguments to a single EncodedJSValue* descriptorBuffer, reducing the call to 4 GPR arguments. DFGSpeculativeJIT::compileObjectDefinePropertyFromFields and FTL::LowerDFGToB3::compileObjectDefinePropertyFromFields are reworked to acquire a ScratchBuffer of Node::numberOfDescriptorSlots entries, store each descriptor field into the buffer at its Node::DescriptorSlot index, then pass the buffer pointer as the fourth argument. The runtime helper decodes the slots back inside an ActiveScratchBufferScope (so the GC scans the buffer).
Violation of the JIT slow-path calling-convention invariant that runtime helpers must fit in argument registers, causing a stack-passed argument to alias a JIT spill slot.
Background
In JSC's optimising JITs, runtime helpers (operationXxx) are called as ordinary C functions from JIT-emitted code. The JIT relies on the constant maxFrameExtentForSlowPathCall to reserve outgoing-argument stack space when entering an OSR-able region; on ARM64 and x86_64 it is 0, encoding the invariant that no helper passes arguments on the stack. Spill slots are where the DFG places JSValues that did not fit in physical registers; they live near the current stack pointer.
ARM64 passes the first 8 integer arguments in x0–x7; x86_64 SysV passes the first 6 in rdi/rsi/rdx/rcx/r8/r9. Further arguments are placed on the stack starting at [sp + 0]. A ScratchBuffer is a VM-owned, GC-scanned scratch region used to hand arbitrary data to runtime helpers without consuming arg registers, and ActiveScratchBufferScope marks the buffer as live for conservative scanning during the helper's execution.
Analysis
operationObjectDefinePropertyFromFields was declared with 9 pointer-sized arguments. On x86_64 arguments 7–9 spilled to the stack at [sp + 0], [sp + 8], [sp + 16]. Because maxFrameExtentForSlowPathCall is 0 on these targets, the compiler does not reserve any space below the stack pointer for outgoing C-call stack arguments — the DFG instead uses [sp + 0..] (and the words just below the current SP) as the lowest spill slots for live JIT values. So when the JIT generated the call, the poked stack arguments overwrote one or more spill slots that held in-flight JSValues.
Aaa Aaaa Aaaaaaaaaaaa Aaa Aaaaaaaaaaa a Aaaaa Aaaaaaa Aaaaaa Aaa Aaaa Aa Aaaaa Aaaaaaaa Aa Aaaaaaaaaaa Aaa Aaa Aaaaaaaa Aaaa Aaa Aaaaaaaaa Aaaa Aaa Aaaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaa Aaa Aaaaaaa Aaaaaaaaaaaaaa Aaaa Aaaaaaaaaa a Aaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaa Aaaa Aaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaa Aaaaaaaa a Aaa Aaaaaaaa Aaaa Aaaaa Aaaaaaa Aaaaaaaaaa Aaaa Aaaaaa Aaa Aaaaaaaaaaaaaaaa Aaaa Aaaaaaaa Aaaaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaa a Aaaaaaaaaa Aaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaa Aa Aaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaaaaa
Aaa Aaaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaa a Aaaa Aaa Aaa Aaaaaaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaaaaaaaaa Aaaaaaaa Aaaaaaaaaaa Aaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaaaaa Aaaaaa Aa Aaa Aaaaaaa Aaaaa Aaaaaaa Aa Aa a Aaaaaaaaaaaaa Aaaaaaaaa Aaaaaaa Aaaaaaaa Aaaaa Aaaaaaaa Aaaaaaaa Aaa Aaaaaaaaaa Aaaaa Aaaaaaa Aa Aaaa a Aaaaaa Aaaaaaaaaa Aaaaaaaaaa Aaa Aaaa Aaaa Aaaaaaaa Aaa Aaaaa Aaaa Aaaaa Aaaaaaaaaaa Aaa Aaaaaaaaaaaaaaa Aaa Aaaaaaaa Aaaaa Aaaaaaaaaaaaa Aa Aaaaaaaaa Aaa Aaaa Aaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaaaaaaa Aaaaaaaaa a Aaaa Aaaaaaaa Aaaaaaaa Aaaaaa Aaa Aaa Aa Aaaaaaaaaaa Aa Aa Aaa Aaaaaaaa Aaaaa Aaa Aaaa Aa Aaaaaaaaa Aaaaaaaaaa Aaaaaa Aaaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaaaaa a Aaaaaaaa Aaaaaaa Aaaaaa Aa Aaaaa Aaa Aaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaaa Aaaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaaa Aaaaa Aaa Aaaaaaa Aaaa Aaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaa a Aaaaaaaaaa Aaaaaaaaaa Aaaaaaa Aa Aaa Aaaaa Aaa Aaaaaaa Aa Aaaaaaaaa Aa Aaaaaaaaaa Aaaaa a Aaaaaaa Aaaaaaaa a Aaaaaaaaa Aaaaa Aa Aaa Aaaaaaaa Aaaa Aaaaa Aaa Aaa Aaaaa Aaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaa Aaa Aaaaaaaaaaa Aaa Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aa a Aaaaaa Aaaaaaaa Aa Aaa Aaaaaa Aaa a Aaaaaaaa Aaaaaaaaa a Aa Aaaaaa a Aaaaaaaaaaaa Aaaaaa Aaaaaaaa Aaaaaaaa Aaaa Aaaaaaa Aa Aaaa Aaaaaaa Aaa Aaaaaaaa Aaaaaaaa Aaaaa Aaaaa Aaaa Aaaa Aaa Aaaaaaaaaaa Aaaa Aaaaaaa Aa Aaaaaa
🔒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.
Subscribe to read more
Audit directions
a Aaaaa Aaaaaaaaa Aaaaaaa Aaaaa a Aaaaaaaaaa Aaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaa Aaaaa Aaaaaaaa Aaaaaaa Aaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaaa Aaaa Aaaa Aaaa a Aaaaaaaaaa Aa Aaaaaa Aa Aaaa Aaaa a Aa Aaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaaaaaaa Aaaaaaa Aaaaaa Aaa Aaaa Aaaa Aaaa a Aaaaaaa Aaaaaa Aaaaaa Aaaa Aaaaaa Aaaaaaaaaaaaa Aa Aaa Aaaaaaa
a Aaaaaaaaaaaaaaaa Aaaaaaa Aaaaa Aaaaaaaaaaaaaaaa a Aaaaaa Aaaa Aaaaa Aaaa a Aa Aaa Aaaaaaaaa Aaaa Aaaa Aaa Aaaaaaa Aaaaa Aaa Aaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaaaaaa Aaa Aaa Aaaaaaa Aa Aaaaaaaaa Aaaaaaaaaa Aaaa Aaa Aaaa Aaa Aaaaa Aaa Aaaaaaaa Aaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaa
a Aaaaaaaaaaaaaaa Aaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaaaa a Aaaaaaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaa Aaaaa Aa Aaaaa Aa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaaaa Aa Aaaa Aaaaaa Aa Aaaaaaaaaaa Aaaaaa Aaaaa Aa Aaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaa Aaaaaaaaaaa Aaaaaa
a Aaaaaaaaaaaaaa Aaaaa Aaa Aaaaaaaaaaa Aaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaa Aaaaa Aaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaa Aaaaaa Aaaa Aaaa Aaaaaaaaa Aaaa Aaa Aaaaaaaa Aaaaaaaa Aaaaaaa
🔒Four reusable audit patterns identified, with concrete starting files and a class-of-helpers worth re-counting against the per-architecture register budget.
Subscribe to read more