[6] LLInt stack overflow guard bypass in arity fixup
Severity: High | Component: JSC LLInt | 2a07f26
Rated High because the observable effect is a stack write past the soft stack limit reachable from any web page via crafted JavaScript, and the mechanism (computing the overflow bound from cfr instead of sp, missing the entire local frame extent) is confirmed at confidence 0.92 by the diff across three files — the JIT path was already correct, confirming this is a real safety gap.
The stack overflow check in LLInt arity fixup was computed against cfr (the call frame register) instead of sp (the stack pointer). Since arity fixup occurs after the callee's local frame is set up, sp is already well below cfr. The fix changes the base to sp in both the C++ slow path and the assembly fast path.
Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
- int padding = numberOfStackPaddingSlotsWithExtraSlots(newCodeBlock, callFrame->argumentCountIncludingThis());
- Register* newStack = callFrame->registers() - WTF::roundUpToMultipleOf(stackAlignmentRegisters(), padding);
- if (!vm.ensureJSStackCapacityFor(newStack)) [[unlikely]]
+ int slotsToAdd = numberOfStackPaddingSlotsWithExtraSlots(newCodeBlock, callFrame->argumentCountIncludingThis());
+ Register* newStackPointer = callFrame->registers() - WTF::roundUpToMultipleOf(stackAlignmentRegisters(), slotsToAdd) - newCodeBlock->numCalleeLocals() - maxFrameExtentForSlowPathCallInRegisters;
+ if (!vm.ensureJSStackCapacityFor(newStackPointer)) [[unlikely]]
return -1;
- return padding;
+ return slotsToAdd;
Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
- subp cfr, t3, t5
+ subp sp, t3, t5
JSTests/stress/stack-overflow-llint-large-params-and-large-locals.js
+ var args = [];
+ for (var i = 0; i < 20; i++) {
+ args[i] = args.toLocaleString() + "x" + i;
+ }
+ var min = new Function(args, "return Math.min(" + args.join(",") + ");");
+ shouldThrow(() => {
+ min(0);
+ }, `RangeError: Maximum call stack size exceeded.`);
Patch Details
Three files are changed. In LLIntSlowPaths.cpp, arityCheckFor() now subtracts newCodeBlock->numCalleeLocals() and maxFrameExtentForSlowPathCallInRegisters from the computed new stack pointer before passing it to vm.ensureJSStackCapacityFor(). In LowLevelInterpreter64.asm and LowLevelInterpreter32_64.asm, the inline arity check macro changes subp cfr, t3, t5 to subp sp, t3, t5, so the overflow comparison uses the actual stack pointer rather than the frame pointer.
Before: After:
cfr ──────────── (frame pointer) cfr ──────────── (frame pointer)
│ │
│ numCalleeLocals │ numCalleeLocals
│ + maxFrameExtent │ + maxFrameExtent
│ │
sp ──────────── (stack pointer) sp ──────────── (stack pointer)
│ │
│ arity padding (slotsToAdd) │ arity padding (slotsToAdd)
│ │
└── old check: cfr - slotsToAdd └── new check: sp - slotsToAdd
(WRONG: missed frame gap) (CORRECT: from actual sp)
Background
When a JavaScript function declares N parameters but is called with fewer than N arguments, JSC must pad the call frame with undefined slots so the callee sees a full argument vector. This is called "arity fixup." In the LLInt, this happens in the functionArityCheck macro (assembly) and the arityCheckFor slow path (C++).
In JSC's calling convention, cfr (call frame register) points to the top of the logical call frame header, while sp (stack pointer) points to the actual bottom of the allocated frame — below all callee locals and scratch space. After frame setup, sp = cfr - numCalleeLocals - maxFrameExtent. JSC maintains a per-VM soft stack limit; before growing the stack, the engine checks that the projected new stack pointer does not exceed this limit. If it does, a RangeError (stack overflow) is thrown.
Analysis
Stack overflow guard computed against the frame pointer instead of the stack pointer, failing to account for already-allocated local frame space during LLInt arity fixup.
By computing the overflow bound from cfr, the check overestimated the remaining stack capacity by exactly the size of the local frame (numCalleeLocals + maxFrameExtentForSlowPathCallInRegisters). When a function had both many parameters (triggering arity fixup because the caller passed fewer arguments) and many local variables (pushing sp far below cfr), the check would pass even though the actual stack pointer after arity fixup would extend past the soft stack limit. The commit message confirms the JIT path was already correct — this was an LLInt-only bug.
Aaa Aaaa Aaaa Aaaaaaaaaa a Aaaaaaaa Aaa Aaaa Aaaaaaaaaaaaaa Aaaaaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaa Aaaaaaaaaa Aaaa Aaaaaaaa Aaaaaaaaaaa Aaa Aaaaaaaa Aaaaa Aaaaa Aa Aaaaaaa Aa Aa Aaaaaaaa Aaaa a Aaaaaa Aaaaaaaaa Aaaaaa Aaa Aaaa Aaaa Aaaaaa Aaaaaaaaaaaaa Aaaaa Aaaa Aaa Aaaa Aaaaa Aaaaaa Aaaa Aaa Aaaa Aa Aaaaaaaaa Aaaaaa Aaaaaaaaaaaa Aaaaaaa Aaaa Aaaaa Aaaa Aaaaaaaaaa
Aaa Aaaaa Aaaaa Aaaaaa Aaaaaaaaaaa Aaa Aaaaaaaa Aaaaa Aaaaaaaa Aaaa Aaaaa Aaa Aaaaaa Aa Aaaaa Aaaaaaa Aaaa Aaa Aaaaa Aaaaaa Aaaaaaaaaaaaa Aaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a Aaa Aaa Aaaaaaa Aaaaa Aaa Aaaa Aaaa Aaa Aaa Aaaaa Aaaaaa Aa Aaaaaaa Aaaa Aaaa Aaaaaaa Aaaaaa Aa Aaaaaaaaaa Aaa Aaaaaaa Aa Aaaaaaaa Aaaaa Aaaaaaa Aaa Aaa Aaaa Aaa Aaaaa Aaa Aaaaa Aaaaaaaa Aaa Aaaaaaa Aaaaaa Aaa Aaaaaaaaaaa Aaaaa Aa Aaaaa Aaaaaaa Aaa Aaaaaaaaa Aaaaaaaa Aaaaaa Aaaa Aaaaaaaa Aaa Aaa Aaaaaaaaa Aaaaa Aaaaaaa Aaaaa Aaaaa Aaaaaaaaa Aaaaaa Aaaaaaaaaa Aa Aaaaaaaaaaaa Aaaaaaaaa Aa Aaaaaaaa Aaaaaa a Aaaaaaaaa a Aaaaaaaaa Aaaaaaaaaaaa Aaaaaa Aaaaaaaaa Aaaaaa Aaa Aaaaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aa Aaa Aaaaa Aaaaaaaa Aa Aaa Aaaaaaaa Aaaaaaaa Aaa Aaaa Aaaaa Aaaaa Aa Aaa Aaaaaaa Aaaaaaa Aaaaaaa Aaaaaaaaaaaaaa Aaa Aaaa Aa Aaaa Aaaa Aaaaaaa Aaaaaa Aaa Aaaa Aa Aaaaaaaa Aaaaa Aaaaa Aaaaaaaaaa Aaaa Aaaaaaaa Aaa Aaaaaaa Aaaa Aaaaaaaa Aaaa Aaaaaaaa
Aaa Aaaaa Aaa Aaa Aaaaa Aaaaaaa Aa Aaaaa Aaaaaa Aaaaaaaaaa Aaaaaaa Aaaa Aaa Aaaaaaaaaa Aaaaaaaaaaa Aaa Aaaaa Aaaaaaaa Aaa Aaa Aaaa Aaaa Aaaa Aaaaaaaaaaaaa Aaaaaaaa Aaa Aaaa Aaaaa Aaaaaa Aaaaaaaaaa Aa Aaa Aaaa Aaa Aaaa Aaaa Aaa Aaaaa Aaaaaaaaaa Aa Aaaaa Aaaa Aaaaaa Aaa Aa Aaaaaaaaaaaa
🔒Detailed analysis of how the cfr/sp divergence creates a precise stack overwrite window, and what memory regions fall within the corruption range
Subscribe to read more
Audit directions
a Aaaaaaaa Aaaaa Aaaaaaaa Aaaaaa Aaaa Aaa Aaaaa Aaaaaaa Aa Aaaa Aa Aaa Aaaaa Aaaaaaa Aa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaa Aaaaa Aaaaaa Aaaaa Aaa Aaaaa Aaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaaa a Aaaaaa Aaaa Aaa Aaaaaaaa Aaa Aaaaaaaaa Aaaaa Aaaaaa Aaaa Aaaa Aaa Aaaaaaaaaaaa Aaaa Aaaaa Aaaaa Aaa Aaaa Aaaaa Aaaaa Aaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaa Aaaaaa
a Aaaaaaaa Aaaaaaaaa Aaaaaa Aaaa Aa Aaaaaaaaaaaaaaa Aaaaaaa Aaa Aaaaaa Aaaaaaa Aaaaaaaaaa Aaaaaa Aaa Aaa Aaa Aaaaaaa Aaaaa Aaa Aaaaa Aaa Aaaa Aaaaa Aaaaa Aaaaa Aaaaa Aaa Aaaaa Aaaaaa Aaaaa Aaa Aaaaaaa Aaaaaaaaaa a Aaaaaaa Aaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaaaa Aaa Aaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaa Aaa Aaaaaa Aaaaa Aaaaaaa Aa Aaaaaaaaaaaaa Aaaa Aaa Aaaaaa Aa Aaaaaa Aa Aaa Aaaaa Aa a Aaaaaaaaa Aaaaaaaaaaaaaa
a Aaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaaaaaa Aaaaaaaaa Aaa Aaaaaa Aaa Aaaaa Aaaaaa Aaaaaa Aaaa Aaaaaaaa Aaaaaaaaaa Aaaaaaa Aaaaa Aaaaaa Aaa Aaaaaaaaa Aaa Aaaaa Aaa Aaaa Aa Aaaaaaaa Aa Aaa Aaaaa Aaaaaaaa Aaaaaaaaaaa Aaaaaa Aaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaa Aaaaa Aaa Aaa Aaaa Aaa Aaaaaa Aaaa Aaaaaa Aaaaaaaa Aa Aa Aaa Aaaaaaaa Aaaaaa
Aaaaaaaaa Aaaa Aaaaaaa a Aaaaaaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaa Aaaaaaa Aa Aaa Aaaaa Aaa Aaa Aaaaaa Aa Aaaaaaaa Aaaaaaa Aaaa Aaa Aaaaaaaaa a Aaa Aaaaaaaa Aaaa Aaa Aaaaa Aaaaaa Aaaaa Aaaaaa Aaaa Aaaaaaaa Aaaaa Aa Aaa Aaaaa Aaa Aaaa Aaaaa Aaaaaa Aaaa Aaaaaaaa Aaa Aaa Aaaaaaaa Aaaaaa Aa Aaaaaaaaa Aaaaaa Aaa Aaaaa Aaaaaaa Aaaaaa
🔒Multiple audit patterns identified for LLInt/JIT safety-check parity, with specific search targets across stack management code
Subscribe to read more