[JSC] Fix wasm type parsing regression by making RTT formal canonicalized types
Component: JSC | 103a40a
JSTests/wasm/stress/cross-module-rtt-identity.js
WebAssembly GC는 heap에 할당되는 struct, array, 그리고 재귀 타입을 추가합니다. 이런 객체들의 타입 identity는 Runtime Type(RTT)이 담당하며, ref.cast와 subtype check를 실행할 때는 포인터 동일성으로 비교됩니다. 그러므로 정확성을 확보하려면 isorecursive canonicalization이 반드시 필요합니다. 즉, 서로 독립적으로 컴파일된 두 모듈이 구조적으로 동일한 재귀 타입을 선언하더라도, 둘은 동일한 canonical RTT 포인터를 받아야 합니다. 기존 설계에서는 파싱용 TypeDefinition과 런타임용 RTT라는 두 가지 표현을 병행 유지했는데, 이 때문에 lifetime 관리가 복잡해졌을 뿐 아니라 GC 구조체를 구성하는 과정에서 반복적인 순회가 발생해 성능 저하까지 초래했습니다. 이번 commit은 TypeDefinition을 완전히 제거합니다. 임시로 쓰이던 Subtype, Projection, RecursionGroup 객체는 이제 새로 도입된 TypeSectionState 내부의 SegmentedVector에 자리 잡으며, type section 파싱이 진행되는 동안에만 존재합니다. 파싱이 끝나면 이 정보들은 모두 미리 할당된 RTT로 구워지고, 중복이 제거된 뒤 프로세스 전역의 TypeInformation singleton에 등록됩니다. RTT는 형제 RTT에 대한 RefPtr 참조를 갖는데, 상호 재귀 타입에 대해서는 의도적으로 refcount cycle을 형성하며 이 cycle은 영구적으로 leak됩니다. V8의 현재 접근 방식과 동일한 처리이며, 추후 cycle collector가 도입되기 전까지는 이 상태로 유지됩니다.
Before: After:
Type section parse Type section parse
└─ TypeDefinition (heap, refcounted) └─ TypeSectionState
FunctionSignature RecursionGroup ┐ SegmentedVector,
StructType Projection │ freed after parse
ArrayType Subtype ┘
RecursionGroup │
Projection bake into pre-allocated RTT
Subtype │
↓ TypeInformation::canonicalize()
creatCanonicalRTTForType() ├─ already registered? → reuse canonical RTT
└─► RTT (canonical) └─ new? → register, expose process-wide
At runtime: TypeDefinition gone. Only RTT exists.
Mutually-recursive RTTs hold RefPtr<RTT> to siblings → intentional refcount cycle (leaked).
Significance
이번 commit은 struct 할당, array access, ref.cast, subtype check 등 WebAssembly GC의 모든 동작이 의존하는 타입 인프라를 밑바닥부터 다시 작성한 결과물입니다. 이 정도 규모의 mechanical refactor 과정에서 버그가 섞여 들어가면, 곧바로 JavaScript에서 도달 가능한 type confusion으로 이어집니다.
Audit directions
Canonicalization hash and equality over recursive types: WasmTypeDefinition.cpp의 canonicalizeRecursionGroupImpl과 canonicalizeSingletonImpl은 모든 cross-module ref.cast를 뒷받침하는 중복 제거 로직을 구현합니다. hashRTTForRecGroup과 equalRTTsForRecGroup은 구성 과정에서 placeholder 참조를 사용하는 재귀 구조를 다루는데, self-reference나 mutual reference 처리에서 미묘한 실수가 생기면 구조적으로 다른 타입이 동일하다고 비교될 수 있습니다. 이는 곧 type confusion입니다. 반대로 동등한 타입이 다르다고 비교되는 경우도 있을 수 있는데, 이 경우 정상적인 cast가 깨지며 fallback 경로를 통해 exploit 가능성으로 이어질 수도 있습니다.
TypeSectionState pointer escape: Subtype, Projection, RecursionGroup 객체는 TypeSectionState 내부의 SegmentedVector에 할당되며, type section 파싱이 끝나면 sweep됩니다. 약 백 개 파일에 걸친 이번 refactor에서는 이 객체들에 대한 포인터가 RTT나 ModuleInformation, 혹은 그 밖의 더 오래 사는 구조체로 흘러 들어가지 않도록 보장해야 합니다. Raw pointer 하나라도 빠뜨리면, type section 파싱이 끝나는 순간 UAF가 됩니다. 이 문제는 일반화될 수 있는데, 트리 안에서 arena scope로 파싱되는 구조체라면 어디든 동일한 escape 점검이 필요합니다.
RTT cycle anchoring completeness: 이번 commit은 raw TypeIndex 참조 대신 RefPtr<const RTT> anchor slot(TypeSlot::rttAnchor, StructFieldEntry::rttAnchor, RTTArrayPayload::m_elementTypeAnchor)을 도입해, 상호 재귀로 인한 cycle 안에서도 형제 RTT가 살아있도록 유지합니다. 다른 RTT를 참조하는 payload 필드가 대응하는 anchor RefPtr 없이 존재한다면, 이번 commit이 고치려던 바로 그 dangling pointer 문제가 다시 발생하는 셈입니다. Diff 규모가 크기 때문에 이런 누락은 놓치기 쉽습니다.
ref.cast and subtype check correctness: isSubRTT와 isStrictSubRTT는 canonical RTT 포인터의 동일성에 의존합니다. typeIndexFromFunctionIndexSpace를 rttFromFunctionIndexSpace로 교체하는 작업은 JIT 백엔드(BBQ, OMG, IPInt), B3 IR 노드, DFG/FTL 전반에 걸쳐 이루어지는데, 기계적이긴 하지만 범위가 매우 넓습니다. Cast나 call-ref 경로 안 어느 callsite에서든 off-by-one 오류나 잘못된 RTT 선택이 하나라도 있으면 type-confusion primitive가 됩니다.