← All issues

[Wasm] Use a lazy restore frame when returning from tail calls

5223ee5

JSTests/wasm/stress/tail-call-cross-instance-gc.js

+// cross-instance tail call chain 실행 중 GC가 발생해도
+// RestoreInstanceCallee thunk frame이 손상되지 않는지 검증한다.
+// thunk의 Callee slot은 RestoreInstanceCallee singleton을 보유하고,
+// CodeBlock은 wasmInstance pointer를 보유한다.
+// GC scanning 이후에도 두 값 모두 유효해야 한다.
+ function triggerGC() { $vm.gc(); }
+ assert.eq(instPing.exports.start(1), 255);
+ assert.eq(instPing.exports.start(11), 255);
+ for (let i = 0; i < wasmTestLoopCount; i++) {
+ assert.eq(instPing.exports.start(1), 255);
+ assert.eq(instPing.exports.start(11), 255);
+ }

각 WebAssembly instance는 memory base, memory bounds, instance pointer 등 instance별 데이터를 전용 레지스터에 고정합니다. tail call이 instance 경계를 넘을 때(return_call_indirect로 다른 module을 호출하는 경우 등), callee의 instance 정보가 해당 레지스터를 덮어씁니다. 이후 호출 chain이 원래의 non-tail caller로 복귀하는 경우, 실행을 재개하기 전에 그 레지스터를 반드시 복원해야 합니다.

이 commit은 기존의 compile-time transitive tail call clobbering 분석을 runtime "restore frame" 메커니즘으로 대체했습니다. Wasm tail call이 처음으로 instance 경계를 넘는 시점에, caller의 argument area 바로 위에 32바이트 frame이 지연 삽입됩니다. 이 frame에는 caller의 instance pointer와 원래 return address가 담기고, 나머지 frame 영역은 32바이트 아래로 이동됩니다. 복귀 시에는 전용 thunk(_wasm_restore_frame_return)가 instance 및 memory 레지스터를 재로드합니다. 아울러 callCanClobberInstancecomputeTransitiveTailCalls, 이에 따른 inlining 제한도 함께 제거되었습니다. 대신 ARM64E PAC re-signing 처리와 JIT cage gate thunk가 추가되었습니다. 한편 현재 return PC와 thunk 주소를 비교하는 재사용 확인 로직도 도입되어, 반복적인 cross-instance hop 시에도 restore frame이 중복 삽입되지 않도록 합니다. spec이 명시적으로 요구하는 동작입니다.

After (runtime restore frame, lazy insertion):
  Caller frame (inst A)
    arg area
    [32-byte restore frame inserted above args, first cross-inst call only]
      slot 0: original CallerFrameAndPC
      slot 1: saved instance A
      slot 2: RestoreFrameCallee
    callee-saves  <- entire frame shifted down 32 bytes

  return -> new cfr -> _wasm_restore_frame_return -> reload inst A regs -> jump to original retPC

  Reuse check: if retPC == thunk addr, skip insertion (no accumulation)

Cross-instance tail call chain이 이제 자유롭게 인라인될 수 있게 되었습니다. 다만 ABI 정확성은 새로운 runtime frame type에 의존하게 되었습니다. GC, stack unwinder, sampling profiler, 세 가지 Wasm backend(BBQ, OMG, IPInt) 모두가 이 frame type을 일관되게 처리해야 합니다. ARM64E PAC re-signing 처리도 이 범위에 포함됩니다. 전용 GC stress test를 함께 추가했다는 사실 자체가, 개발자들도 이 부분의 정확성 확보가 쉽지 않다는 점을 인식하고 있었음을 나타냅니다.

🔒

New runtime Wasm stack frame mechanism with cross-backend ABI and pointer-auth implications — several security audit directions included.

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