← All issues

[6] LLInt stack overflow guard bypass in arity fixup

Severity: High | Component: JSC LLInt | 2a07f26

임의의 웹 페이지에서 조작된 JavaScript를 통해 soft stack limit을 초과하는 stack write를 유발할 수 있다는 점에서 High로 평가됩니다. 그 메커니즘(sp 대신 cfr을 기준으로 overflow 경계를 계산하여 local frame 전체 크기를 누락)은 세 파일에 걸친 diff를 통해 confidence 0.92로 확인됩니다. JIT 경로가 이미 올바르게 구현되어 있었다는 점은 이것이 실제 safety gap임을 뒷받침합니다.

LLInt arity fixup의 stack overflow check가 sp(stack pointer) 대신 cfr(call frame register)을 기준으로 계산되었습니다. Arity fixup은 callee의 local frame이 설정된 이후에 실행되므로, 이 시점에서 sp는 이미 cfr보다 훨씬 아래에 위치합니다. 이번 fix는 C++ slow path와 assembly fast path 양쪽에서 기준값을 sp로 변경했습니다.

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.`);

세 파일이 변경되었습니다. LLIntSlowPaths.cpp에서 arityCheckFor()는 이제 계산된 새 stack pointer를 vm.ensureJSStackCapacityFor()에 전달하기 전에 newCodeBlock->numCalleeLocals()maxFrameExtentForSlowPathCallInRegisters를 차감합니다. LowLevelInterpreter64.asmLowLevelInterpreter32_64.asm에서는 inline arity check 매크로의 subp cfr, t3, t5subp sp, t3, t5로 변경되어, overflow 비교에 frame pointer 대신 실제 stack 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)

JavaScript 함수가 N개의 매개변수를 선언했지만 N개보다 적은 인수로 호출될 경우, JSC는 callee가 완전한 인수 벡터를 볼 수 있도록 call frame을 undefined 슬롯으로 채워야 합니다. 이 과정을 "arity fixup"이라고 합니다. LLInt에서는 functionArityCheck 매크로(assembly)와 arityCheckFor slow path(C++)에서 이 작업이 수행됩니다.

JSC의 calling convention에서 cfr(call frame register)은 논리적 call frame 헤더의 상단을 가리키고, sp(stack pointer)는 모든 callee local과 scratch space 아래, 즉 할당된 frame의 실제 하단을 가리킵니다. Frame 설정 후에는 sp = cfr - numCalleeLocals - maxFrameExtent 관계가 성립합니다. JSC는 VM 단위로 soft stack limit을 유지하며, stack 확장 전에 예상 stack pointer가 이 limit을 초과하는지 검사합니다. 초과할 경우 RangeError(stack overflow)가 발생하게 됩니다.

LLInt arity fixup 과정에서 stack pointer 대신 frame pointer를 기준으로 stack overflow guard를 계산하여, 이미 할당된 local frame 공간이 누락된 패턴.

cfr을 기준으로 overflow 경계를 계산했기 때문에, check는 남은 stack 용량을 local frame 크기(numCalleeLocals + maxFrameExtentForSlowPathCallInRegisters)만큼 과대평가했습니다. 여기서 두 조건이 겹치는 함수를 고려할 수 있습니다. 매개변수가 많으면 caller가 더 적은 인수를 전달하여 arity fixup이 유발되고, 지역 변수가 많으면 spcfr보다 훨씬 아래에 위치하게 됩니다. 이 두 조건이 동시에 성립할 경우, arity fixup 이후 실제 stack pointer가 soft stack limit을 초과함에도 check가 통과하는 상황이 발생했습니다. Commit 메시지는 JIT 경로가 이미 올바르게 구현되어 있었음을 확인해 주므로, 이는 LLInt에만 존재하던 버그에 해당합니다.

🔒

Detailed analysis of how the cfr/sp divergence creates a precise stack overwrite window, and what memory regions fall within the corruption range

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

🔒

Multiple audit patterns identified for LLInt/JIT safety-check parity, with specific search targets across stack management code

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