← All reports

[JSC] Emit mutatorFence in BBQ JIT for WasmGC

Component: JSC WebAssembly | ce60d5d

Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp

@@ emitAllocateGCStructUninitialized (default-init path)
- bool needsMutatorFence = false;
+ bool needsMutatorFence = structType.hasRefFieldTypes();
for (StructFieldCount i = 0; i < structType.fieldCount(); ++i) {
if (Wasm::isRefType(structType.field(i).type))
needsMutatorFence |= emitStructSet(resultGPR, structType, i, Value::fromRef(TypeKind::RefNull, JSValue::encode(jsNull())));
...
- // No write barrier needed here as all fields are set to constants.
- ASSERT_UNUSED(needsMutatorFence, !needsMutatorFence);
+ if (needsMutatorFence)
+ emitMutatorFence();
 
@@ emitAllocateGCStructUninitialized (args-init path)
- bool needsMutatorFence = false;
+ bool needsMutatorFence = structType.hasRefFieldTypes();
for (uint32_t i = 0; i < args.size(); ++i)
needsMutatorFence |= emitStructSet(resultGPR, structType, i, args[i]);

WasmGC struct는 heap에 할당되는 객체로, 필드에는 다른 GC 객체에 대한 참조가 들어갈 수도 있고 단순 scalar 값이 들어갈 수도 있습니다. JSC의 collector는 mutator 실행과 동시에 동작합니다. 그래서 JIT 코드가 객체를 할당하고 초기화할 때는 반드시 mutator fence를 걸어야 합니다. 이 fence는 객체가 GC에 의해 발견되고 추적되기 전에, 모든 필드 write가 정상적으로 보이고 순서대로 반영되도록 보장하는 barrier입니다. 참고로 BBQ는 JSC의 baseline Wasm tier로, 최적화보다 컴파일 속도를 우선하는 티어입니다.

기존에는 fence가 필요한지 여부를 저장되는 이 literal 상수인지만 보고 계산했습니다. 이번 패치에서는 이를 structType.hasRefFieldTypes()로 판단하도록 바꾸었습니다. 그 결과, ref 타입 필드를 가진 struct라면 모든 필드가 null 같은 상수로 초기화되더라도 fence가 걸리게 됩니다. 기존 코드는 잘못된 전제를 assertion에 그대로 담고 있었습니다. ASSERT_UNUSED(needsMutatorFence, !needsMutatorFence)는 default-init 경로에서 fence가 필요한 경우가 절대 없다고 단정하는 코드였는데, 문제를 잡아내기는커녕 그대로 통과되고 있었습니다.

fence가 실행되기 전, 동시 실행 중인 collector가 부분적으로만 초기화된 struct를 관찰할 수 있었고, 이로 인해 stale하거나 타입이 잘못된 메모리를 방문하게 될 가능성이 있었습니다. Collector 입장에서 중요한 것은 저장되는 값이 아니라 선언된 필드 타입입니다. Tracing 로직은 필드를 타입 기준으로 스캔하기 때문에, ref 타입 필드라면 null로 초기화되었더라도 barrier가 필요합니다. 다른 tier에서는 이미 fence를 올바르게 걸고 있었으므로, 이 문제는 BBQ의 할당 경로에 국한된 사안이었습니다.

여기서 반복 가능한 패턴은, barrier나 fence 조건을 write되는 슬롯의 타입이 아니라 으로부터 도출하는 방식입니다. 좁게 보면, WasmBBQJIT64.cpp에 남아 있는 나머지 struct 및 array 할당 지점들을 같은 관점에서 점검할 필요가 있습니다. 이때 매치되는 징후는, needsBarrier/needsFence 같은 boolean이 선언된 필드나 element 타입이 아니라 isConstant, isNull, fromRef 같은 값 단위 predicate로부터 누적되는 형태입니다. 조금 더 넓혀 보면, "이 값은 pointer가 아니라는 걸 알고 있다"는 근거로 GC write barrier를 생략하는 자리라면 어디서든 같은 패턴이 재현될 수 있습니다. JSC의 다른 JIT tier에 있는 emitPutById/storeValue의 barrier 판단 로직이나, DFG/FTL에서 WriteBarrier 노드를 방출하는 조건이 슬롯의 선언된 타입이 아니라 저장되는 값의 검증된 타입에 매여 있지는 않은지 확인할 만합니다. 가장 넓게 보면, 동시 실행 GC를 갖춘 런타임이라면 barrier의 필요 여부를 write되는 값으로 결정하는 방식 자체가 취약합니다. Collector는 슬롯을 선언된 타입 기준으로 읽기 때문입니다. 이런 취약점의 징후는 특정 경로에서 barrier가 절대 필요 없다고 단정하는 assertion입니다. 이런 assertion은 코드가 갖고 있는 것과 동일한 가정을 그대로 담고 있을 뿐, 그 가정을 반증할 수는 없습니다.