← All reports

[Wasm] OMG tail call patchpoint needs to clobber late pinned registers

JSC WebAssemblyOther

Component: JSC WebAssembly | c18d1e3

Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp

// (OMGIRGenerator::emitIndirectCall, root-tail path)
// restored the late-clobber declaration on the pinned registers used for
// cross-instance memory reload in return_call_ref/return_call_indirect,
// so B3 keeps live late inputs (boxed callee, shuffled args) out of
// wasmBaseMemoryPointer/wasmBoundsCheckingSizeRegister.

JSTests/wasm/stress/omg-indirect-tail-call-late-input-clobber.js

// builds a relay module that return_call_ref's into a 24-arg target
// through a 12-arg caller, forcing register pressure so B3 would pick
// pinned registers for late inputs if not marked as late-clobbered.

WebKit의 OMG tier는 WebAssembly를 B3를 거쳐 JIT-compile하는데, 이때 pinned register는 현재 instance의 linear memory base pointer와 bounds-check size를 담아 메모리 접근을 빠르고 branch가 적게 처리할 수 있도록 합니다. Tail call(return_call_ref / return_call_indirect)은 다른 Wasm instance로 전환해야 하므로, jump하기 전에 이 pinned register들을 다시 로드해야 합니다. Callee가 자신만의 별도 메모리를 갖고 있기 때문입니다. B3의 register allocator는 patchpoint가 register 값을 파괴하는 시점이 정상적으로 예정된 사용이 끝난 이후라는 사실을, late clobber annotation을 통해 인식합니다. 이 annotation이 없으면 allocator는 해당 register를 같은 시점에 필요한 다른 live value, 예를 들어 tail-call argument를 담는 데도 안전하게 재사용할 수 있다고 가정하게 됩니다. 이번 commit은 OMG tail-call patchpoint에서 누락되어 있던 late-clobber 선언을 복원하고, register pressure가 높은 상황을 재현하는 regression test를 추가했습니다.

Tail-call patchpoint execution order:
  1. reload wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister from callee instance
  2. run parallel-move shuffle (places boxed callee + args into registers)
  3. jump to callee

Bug: without late-clobber flag, B3 may allocate a live late input
(callee ref / arg) into the pinned register BEFORE step 1 overwrites it
  → shuffle in step 2 reads a corrupted value
  → callee runs with wrong bounds-check register

Bounds-checking register가 메모리 접근을 검증하는 데 쓰이기 직전에 오염될 수 있었고, 이로 인해 register-allocation 버그가 JIT-compile된 Wasm에서 out-of-bounds access primitive로 이어질 가능성이 있었습니다. 이는 memory-safety에 직접 영향을 주는 register-allocator correctness 버그의 전형적인 형태에 해당합니다. Callee가 linear-memory 접근을 제한하는 데 신뢰하는 바로 그 값이, allocator가 자유롭게 재사용할 수 있었던 값이었던 셈입니다.

좁게 보면, pinned register를 reload하거나 clobber하는 다른 B3 patchpoint들, 즉 일반 call, exception handling, stack switching 등에서 같은 late-clobber annotation 누락이 있는지 검토할 필요가 있습니다. 코드 리뷰에서 눈여겨볼 신호는 generator lambda 안에서 wasmBaseMemoryPointerwasmBoundsCheckingSizeRegister에 값을 쓰면서도 patchpoint object에 대응하는 late-clobber 선언이 없는 경우입니다. 넓게 보면, 이 범주는 pinned register 쌍에 국한되지 않고, 생성된 코드가 주변 IR이 여전히 live로 간주하는 register를 변경하는 모든 patchpoint에 해당합니다. Callee-saved state를 spill하거나 scratch, reload하는 patchpoint들을 함께 검토하고, register pressure가 높은 call 형태(많은 argument, 중첩된 tail call)를 OMG tier-up 근처에서 fuzzing하는 방식을 병행할 필요가 있습니다. Allocator가 실제로 여유 register를 모두 소진했을 때만 이 버그가 드러나기 때문입니다. 가장 넓게 보면, 핵심 invariant는 optimizing compiler 안에 내장된 손으로 작성한 machine-code 영역이라면 자신의 전체 clobber set을 반드시 선언해야 한다는 것입니다. Allocator의 correctness에 대한 근거 전체가 바로 이 선언들로부터 나오기 때문이며, 이는 LLVM inline-asm clobber list, V8 Turbofan의 CallDescriptor register constraint, 그리고 patchpoint나 stub-call 추상화를 갖춘 모든 JIT에 그대로 적용됩니다.