[JSC] Use FrameTracer in wasm ref_func, table_get, and array_init_elem operations
Component: JSC WebAssembly | f771c50
JSC의 Wasm 구현에서는 funcref 값이 JS에서 처음 관찰되거나 table과 array 사이에서 복사될 때 (ensureFunctionWrapper 등을 통해) lazy하게 완전한 JS function wrapper 객체로 변환됩니다. 이 wrapper allocation 과정에서 GC가 유발될 수 있습니다. 문제는 Wasm tier들이 VM의 topCallFrame pointer를 just-in-time 방식으로만 갱신한다는 점입니다. 그래서 JS import call이 반환된 이후에는 topCallFrame이 이미 죽은 native state를 가리키고 있을 수 있습니다. FrameTracer는 바로 이런 GC 구간에서 collector와 ShadowChicken(profiler·debugger를 위해 native call stack을 재구성하는 메커니즘)을 위해 frame state를 일관되게 유지하는 역할을 합니다.
Source/JavaScriptCore/wasm/WasmOperations.cpp
Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp
이번 commit은 ref.func, table.get, array.init_elem 세 operation에 frame tracer를 추가합니다. JIT operation 경로와 IPInt slow path 양쪽 모두에 적용되었는데, 이 세 operation 모두 funcref에 대한 JS function wrapper를 생성할 수 있고, 그 결과 GC를 유발할 수 있기 때문입니다.
Significance
FrameTracer가 없으면, funcref wrapper를 생성하는 도중 발생하는 GC가 이전 JS import call이 남긴 stale topCallFrame을 건드리게 되고, 이로 인해 ShadowChicken이 이미 죽은 native state를 살아있는 JS CallFrame처럼 읽어버릴 수 있습니다. 이것은 frame-tracing의 일관성이 깨지는 버그로, table이나 typed function reference를 사용하는 평범한 Wasm 코드에서도 도달 가능합니다. 특이한 모듈 구조가 필요하지 않습니다.
Audit directions
앞으로 눈여겨봐야 할 패턴은, topCallFrame을 just-in-time으로만 갱신하는 tier에서 frame state를 세우지 않은 채 allocation을 수행할 수 있는 — 즉 GC를 유발할 수 있는 — slow path입니다. array-init-elem 테스트에 남아 있는 코멘트를 보면, 이전에 Wasm GC slow path를 점검했을 때도 바로 이 opcode를 놓쳤다고 되어 있습니다. 이유는 이 opcode 자체가 array를 직접 allocate하지 않기 때문인데, 이 지점이 바로 탐색의 기준점이 됩니다. 즉 opcode 자체의 최상위 레벨이 아니라 callee 내부에서 발생하는 allocation을 찾아야 한다는 뜻입니다. 좁게 보면, WasmIPIntSlowPaths.cpp와 WasmOperations.cpp를 훑어 ensureFunctionWrapper, copyElementSegment처럼 wrapper를 생성하는 루틴을 호출하면서도 여전히 tracer가 없는 다른 operation을 찾아야 합니다. 조금 더 넓혀 보면, 함께 추가된 stress test의 --slowPathAllocsBetweenGCs=1 --forceGCSlowPaths=true 옵션 조합을 템플릿으로 활용할 수 있습니다. 이 옵션 조합은 tracer 누락 조건을 타이밍에 의존하는 우연한 현상에서 항상 동일하게 재현되는 트리거로 바꿔주기 때문에, 이 방식이 있어야 전수 점검이 현실적으로 가능해집니다. 가장 넓게 보면, lazy하게 유지되는 stack-walk anchor를 가진 runtime이라면 helper가 transitively allocation을 수행하는 모든 지점에서 동일한 구조적 위험이 존재합니다. 점검할 때 눈여겨봐야 할 신호는, 함수 본문 전체가 helper 호출 하나뿐이면서 그 위에 tracer 선언이 없는 slow-path 함수입니다.