← All reports

JSC B3-to-Air lowering used a locked value as a scaled index

The wasm bounds check was fine — it just described a different address.

Component: JavaScriptCore B3/Air JIT | 10d4d20

B3는 JSC의 optimizing IR로, DFG IR과 Air 사이에 위치합니다. Air는 어셈블리 수준의 IR이며, B3는 JavaScript용 FTL top-tier JIT과 WebAssembly 컴파일을 모두 뒷받침합니다. 이 중 LowerToAir 패스는 블록을 pre-order로 순회하면서, 대상 ISA가 허용하는 한 여러 연산을 하나의 복잡한 명령으로 적극적으로 접어 넣습니다. 예를 들어 scale factor를 나타내는 left-shift는 별도의 shift 명령으로 내보내지 않고 base + index*scale + offset 형태의 addressing mode에 직접 녹여 넣습니다. 한편 ARM64에서는 같은 패스 안에서 어떤 값이 locked 상태가 될 수 있으며, 이 상태는 m_locked로 관리됩니다. BitAnd나 ZExt32(Trunc)가 UBFIZ로 접혀 들어가는 경우처럼, 값이 이미 fused instruction에 편입된 시점에 lock이 걸립니다. 이렇게 locked된 값에는 더 이상 해당 값을 위한 Tmp를 독립적으로 정의하는 명령이 존재하지 않습니다.

이번 commit은 WasmAddress 처리 경로를 수정했습니다. 기존에는 pointer 폭의 Shl을 scaled-index addressing mode로 접어 넣으면서, Shl의 자식 값이 이미 locked 상태인지 먼저 확인하지 않았습니다. 이미 lock이 걸려 있던 경우, fold 결과는 정의 명령이 없는 Tmp를 참조하게 되었습니다. 그 결과 메모리 접근이 정의되지 않은 index register를 사용하는 형태로 생성되었습니다.

  Before:                                  After:
  Shl(x, k)  ── already locked into UBFIZ   Shl(x, k)  ── already locked
    │                                          │
    └─► WasmAddress folds Shl into             └─► WasmAddress checks m_locked
        [base + Tmp(x)*scale]                        └─► falls back: emit the
              ▲ no defining instruction                   shift, use a real Tmp
        bounds check validated a               bounds check and access now
        different expression                  agree on the same address

wasm bounds check는 올바른 pointer 표현식을 검증했지만, 정작 load나 store는 정의되지 않은 register로 구성된 addressing mode를 사용했습니다. 검사한 주소와 실제로 접근한 주소가 서로 어긋나는 이런 형태는, JIT으로 컴파일된 sandbox 코드에서 out-of-bounds primitive로 이어지는 전형적인 패턴입니다. 이때 guard가 우회된다기보다는 무의미해진다고 보는 편이 정확합니다. 뒤따르는 접근을 더 이상 기술하지 못하기 때문입니다.

Narrow: LowerToAir에서 자식 값을 addressing mode나 복잡한 명령으로 접어 넣는 다른 모든 케이스에도 동일한 m_locked 확인이 필요합니다. 판별 단서는 lock 여부를 묻지 않은 채 자식의 Tmp를 읽는 fold 지점입니다. Wider: 더 넓게 찾아볼 형태는, 단일 패스 IR lowering에서 한 규칙이 값을 사용하기로 결정한 순간 그 값이 독립적으로 materialize되어 있다는 이후 규칙의 전제가 깨지는 구조입니다. locking은 JSC가 이 개념에 붙인 이름일 뿐이고, fusion을 수행하는 instruction-selection 패스라면 어떤 것이든 같은 역할을 하는 장치를 갖고 있습니다. 위험한 지점은 정확히 두 규칙이 같은 자식을 동시에 차지하려 드는 곳입니다. Widest: 특히 bounds check가 적용된 JIT 코드에서는, check가 이미 생성된 이후에 주소 표현식을 다시 쓰는 변환이 있다면 두 표현이 여전히 같은 값을 가리키는 이유를 명시적으로 설명할 수 있어야 합니다. 리뷰 단서는 check는 IR 노드를 기준으로, 접근은 lowering된 addressing mode를 기준으로 생성되고 그 사이에 fold가 끼어 있는 구조입니다.