[JSC] DFG SwitchChar releases its operand before speculating that it is a string
Component: JSC DFG JIT | 3ddc704
Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
WebKit의 DFG JIT는 hot function을 speculative하게 컴파일하면서, variable event stream을 통해 operand의 liveness를 추적합니다. 이렇게 해두면 type check가 실패해서 OSR exit가 발생했을 때, baseline이 재개하는 데 필요한 정확한 interpreter 상태를 재구성할 수 있습니다. use()는 이 stream에서 operand의 마지막 사용, 즉 death 시점을 표시하는 역할을 하며, speculateString()은 exit를 유발할 수 있는 guard를 내보냅니다.
문제는 SwitchChar의 string case에서 이 둘의 순서가 뒤바뀌어 있었다는 점입니다. Guard보다 death 이벤트가 먼저 기록되고 있었습니다.
Before: After:
op1.use() <- operand dead speculateString() <- exit sees it live
speculateString() <- may exit here op1.use() <- dead after exit point
-> exit reconstructs scrutinee local -> exit reconstructs with correct value
as dead-value recovery
Significance
DFG가 MovHint를 통해 해당 node와 연결시킨 local — 여기서는 SetLocal이 제거되었기 때문에 발생하는 경우 — 은 guard 실패 시 실제 값이 아니라 dead placeholder로 잘못 재구성됩니다. 이후 baseline은 잘못 재구성된 operand를 기준으로 switch를 다시 실행하게 됩니다. SwitchString은 이미 올바른 순서를 사용하고 있었습니다.
Audit directions
앞으로 살펴봐야 할 패턴은, exit가 가능한 guard보다 먼저 liveness-death 이벤트가 기록되는 경우입니다. 이 JIT의 OSR 재구성 로직이 바로 그 stream을 읽기 때문입니다. 좁게 보면, DFGSpeculativeJIT.cpp 안의 다른 emit* 및 speculate* 호출 지점들을 grep해서 동일한 use()-before-speculate*() 순서가 있는지 확인할 필요가 있습니다. 판별 기준은, 같은 operand를 다루는 speculate/spill/type-check emitter보다 텍스트상 위쪽에 .use() 호출이 위치하는지 여부입니다. 올바른 형태는 항상 guard가 먼저이고 release가 그다음이기 때문입니다. 조금 더 넓게 보면, 이 버그 class는 정적 분석이 아니라 poisonDeadOSRExitVariables debug mode에 의해 발견되었습니다. 따라서 코드를 읽기보다는 이 mode를 폭넓게 돌려보는 쪽이 더 생산적인 접근이며, FTL의 동등한 liveness bookkeeping 로직과 여러 operand 중 일부만 speculate되는 다른 node type들로 범위를 넓혀볼 필요가 있습니다. 가장 넓게 보면, live한 객체가 dead로 잘못 재구성되는 상황은 단순한 정확성 crash가 아니라 state-confusion primitive에 해당합니다. 발견된 각 지점에 대해, 잘못된 값을 기준으로 재개된 baseline이 그 값을 pointer로 읽어들이는 경로에 도달하는지를 확인해야 합니다. 그 여부가 단순한 오답 버그와 type confusion을 가르는 기준이 됩니다.