← All reports

Wasm `table.get` inlined into BBQ and OMG

Component: JSC WebAssembly JIT tiers | 798b6ba

externref/anyref를 담는 Wasm table은 JS 값을 WriteBarrier<Unknown> 배열, 즉 ExternOrAnyRefTable::m_jsValues에 보관합니다. 그래서 table.get은 실제 인덱싱에 앞서 index가 table의 현재 length를 넘지 않는지 확인해야 합니다. 이전까지 BBQ와 OMG는 이 작업을 operationGetWasmTableElement에 전부 맡겨 두었는데, C++ runtime 함수 하나였던 만큼 bounds check도 한 곳에서만 맞으면 충분했습니다.

이제는 각 tier가 자체 inline 구현을 갖게 되었습니다. 먼저 BBQ는 Table*과 length를 scratch register에 로드한 뒤, index가 컴파일 타임 상수인지 register에 들어 있는지에 따라 서로 다르게 분기합니다. 그다음 m_jsValues에서 BaseIndex(table, index, ×8) 형태의 로드를 수행합니다. 반면 OMG는 B3 노드 Check(AboveEqual(index, ZExt32(length))) 하나만 세우고, 로드 주소는 jsValues + (index << 3)로 계산합니다. 이쪽에서 상수냐 register냐의 구분은 alias analysis가 참조할 m_jsValues abstract heap의 범위를 좁히는 데만 쓰이며, bounds check 로직 자체와는 무관합니다. 한편 funcref 경로는 여전히 runtime 호출을 거칩니다.

보안과 직결되는 bounds check가 충분히 검증된 C++ helper 한 곳에서 빠져나와, 두 개의 서로 다른 JIT tier가 각각 독립적으로 생성하는 수작업 기계어 코드로 옮겨갔습니다. 게다가 funcref용 호출 기반 경로도 그대로 남아 있습니다. 결과적으로 이전에는 하나였던 동일한 check가 이제 코드베이스 안에 세 벌 존재하게 되었습니다.

앞으로 눈여겨볼 패턴은 runtime helper의 안전성 검사가 JIT tier 안으로 inline되는 경우입니다. 한 곳에서만 맞으면 되던 정확성이, 서로 다른 속도로 변해가는 여러 generator에 복제되기 때문입니다. operation* helper에서 빠져나와 BBQ와 OMG emission으로 옮겨간 다른 연산들도 점검해 볼 필요가 있습니다. 확인할 지점은 inline check가 helper의 semantics를 정확히 재현하는지 여부입니다. 특히 index의 폭을 주의 깊게 봐야 합니다. helper는 call ABI를 거치며 이미 정규화된 값을 받지만, inline 코드는 raw register를 직접 다뤄야 하기 때문입니다. 코드 리뷰에서의 단서는 같은 opcode를 다루면서 작성 시점이 서로 다른 BBQ의 상수 index fast path와 OMG의 Check(AboveEqual(...)) 조합입니다. 둘 사이에 차이가 있다면, 둘 중 하나는 버그입니다.