[JSC] SerializedScriptValue is not carrying memory64 flag
Component: JSC WebAssembly | 3e351c7
Source/WebCore/bindings/js/SerializedScriptValue.cpp
WebAssembly.Memory 객체는 shared로 표시되어 browsing context와 worker 사이에서 postMessage를 통해 전달될 수 있습니다. 이때 실제 buffer contents는 진짜로 공유되지만, 양쪽은 각자 structured cloning을 거쳐 자신만의 WebAssembly.Memory wrapper를 다시 만들게 됩니다. 이 과정을 담당하는 것이 SerializedScriptValue.cpp의 CloneSerializer/CloneDeserializer 쌍입니다. 한편 memory64 proposal은 memory가 기본값인 32비트 대신 64비트(i64) addressing을 사용할 수 있게 하는데, 이 address type이 bounds, grow() 인자, 그리고 공유 buffer에 대한 pointer arithmetic이 해석되는 방식을 결정합니다. 따라서 clone 과정에서 이 값이 그대로 보존되어야 합니다.
이번 commit은 serializer가 write(memory->memory().addressType().is64Bit())를 통해 이 flag를 기록하도록 하고, deserializer는 이를 다시 읽어 JSC::Wasm::AddressType으로 만든 뒤 Wasm::Memory::create에 직접 전달하도록 변경합니다. 이전에는 serializer가 이 flag를 아예 기록하지 않았습니다. 게다가 deserializer(그리고 jsc.cpp의 유사한 receiveBroadcast 경로)는 adopt()가 호출되기 전, 즉 실제로 보고할 address type이 아직 없는 상태의 갓 생성된 JSWebAssemblyMemory에서 addressType()을 읽고 있었습니다. 이와 함께 memory-cost accounting loop에도 wasmMemoryHandlesArray의 각 entry에 대한 null check가 추가되었습니다.
Significance
memory64 기반의 shared WebAssembly.Memory가 수신 측에서 i32-addressed memory로 deserialize될 수 있었고, 그 결과 두 agent가 하나의 shared buffer를 서로 다른 방식으로 addressing하는 상황이 벌어질 수 있었습니다. Agent 경계를 넘나드는 address-type confusion은 bounds-check divergence로 이어지는 전형적인 형태에 해당합니다. 한쪽은 32비트 가정 하에 offset과 limit을 계산하는 반면, 다른 한쪽은 동일한 backing store를 64비트로 다루게 되기 때문입니다. 이번 fix는 두 agent가 wire를 통해 전달받은 값으로부터 동일한 address type을 재구성한다는 invariant를 복원합니다.
Audit directions
여기서 발견되는 전방위적 패턴은 type metadata가 payload 안이 아니라 그 옆에 별도로 존재하며, 그렇기 때문에 수신 측에서 반드시 다시 도출되어야 한다는 구조입니다. 좁게 보면, SerializedScriptValue.cpp의 dumpDerivedTerminal, readDerivedTerminal, computeMemoryCost를 다시 살펴보고 adopt()가 실행되기 전에 addressType()이나 다른 Wasm 객체의 accessor를 호출하는 지점이 남아 있지 않은지 확인할 필요가 있습니다. 그 시점에는 객체가 아직 부분적으로만 생성된 상태이기 때문입니다. 단서가 되는 패턴은 같은 함수 안에서 생성된 JS* wrapper에 대해, 아직 backing object가 전달되지 않은 상태에서 accessor를 호출하는 코드입니다. 조금 더 넓게 보면, 자신의 backing storage와 분리된 형태로 type metadata를 갖는 다른 Wasm 타입들, 즉 table(element type, limits), global(value type과 mutability), tag, shared struct에 대해서도 structured clone 경로가 수신 측이 동일한 type의 객체를 재구성하는 데 필요한 모든 field를 serialize하는지, 아니면 일부를 기본값으로 채우고 있는지 점검할 필요가 있습니다. 찾아야 할 형태는 serializer의 write(...) 시퀀스가 deserializer 쪽 create(...) signature보다 적은 수의 field만 기록하는 경우입니다. 가장 넓게 보면, 이 문제군은 한쪽이 field의 일부만으로 typed object를 재구성하고 나머지는 기본값으로 채우는 모든 cross-agent 또는 cross-process serializer에 적용됩니다. 감사할 때 항상 던져야 할 질문은 다음과 같습니다. "이 field가 wire에서 누락되면 수신 측은 어떤 값을 가정하는가, 그리고 송신 측이 그 가정과 다른 값을 가질 가능성은 없는가?"