← All reports

[JSC] IPInt slow path for `memory.atomic.notify` truncates the Memory64 pointer and offset to 32 bits

A Memory64 address of 2^32 looked like address 0 to the bounds check.

Component: JSC | afc44e3

Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp

WASM_IPINT_EXTERN_CPP_DECL(memory_atomic_notify, IPIntStackEntry* args)
{
#if CPU(ARM64) || CPU(X86_64)
- unsigned offset = args[0].i32;
+ uint64_t offset = args[0].i64;
uint8_t memoryIndex = args[1].i32;
int32_t count = args[2].i32;
- unsigned base = args[3].i32;
+ uint64_t base = args[3].i64;
int32_t result = Wasm::memoryAtomicNotify(instance, base, offset, count, memoryIndex);
WASM_RETURN_TWO(std::bit_cast<void*>(static_cast<intptr_t>(result)), nullptr);

JSTests/wasm/stress/memory64-atomic-notify-out-of-bounds.js

// memory.atomic.notify with a Memory64 pointer just past the 32-bit boundary.
// The memory is 1 page (64 KiB), so pointer 2^32 is out of bounds and must trap.
assert.throws(() => exports.notify(0x1_0000_0000n), WebAssembly.RuntimeError, "Out of bounds memory access");
assert.throws(() => exports.notify(0xffff_ffff_ffff_ffffn), WebAssembly.RuntimeError, "Out of bounds memory access");
assert.eq(exports.notify(0n), 0);

IPInt는 assembly로 인라인 처리할 수 없는 instruction을 만나면 C++ slow path로 fallback합니다. 이때 인자는 IPIntStackEntry union을 통해 전달되는데, 이 union의 .i32.i64 멤버는 storage를 공유합니다. 잘못된 멤버로 읽으면 상위 비트가 런타임 오류 없이 조용히 사라집니다. memory.atomic.notify는 Wasm에서 futex wake에 해당하는 명령으로, 이 slow path는 64비트 주소 operand와 immediate offset을 모두 .i32로 읽고 있었습니다. 수정은 두 값을 모두 .i64로 바꾸는 방식으로 이루어졌습니다. Assembly fast path는 항상 완전한 64비트 값을 전달했기 때문에, 이 버그는 Memory32 모듈에서는 드러나지 않았고 Memory64 주소의 상위 32비트가 설정된 경우에만 나타났습니다.

IPInt assembly fast path
  └─► pushes args[0]=offset (i64), args[3]=base (i64) onto stack as full 64-bit values

BEFORE (buggy slow path):
  offset = args[0].i32  →  0x1_0000_0000 truncates to 0x00000000
  base   = args[3].i32  →  0x1_0000_0000 truncates to 0x00000000
                            bounds_check(0x00000000) → IN BOUNDS → no trap ✗

AFTER (fixed slow path):
  offset = args[0].i64  →  0x1_0000_0000 preserved
  base   = args[3].i64  →  0x1_0000_0000 preserved
                            bounds_check(0x1_0000_0000) → OUT OF BOUNDS → trap ✓

2^32에 해당하는 out-of-bounds 주소가 0으로 잘려나가면서 bounds check를 통과하는 것처럼 보였고, 그 결과 memory.atomic.notify는 trap을 발생시키는 대신 linear memory의 주소 0을 대상으로 동작했습니다. Memory64 slow path에서 확인된 bounds check bypass이며, Memory64 컨텍스트의 atomic instruction 안전성을 점검하는 입장에서는 이를 단발성 실수가 아니라 하나의 확인된 취약점 class로 다뤄야 합니다.

.i64 값을 저장하는 union에서 .i32로 읽는 패턴은 복사-붙여넣기로 쉽게 재생산되는 기계적인 실수이며, 이 문제가 memory_atomic_notify에만 국한된다고 가정할 근거는 없습니다. WasmIPIntSlowPaths.cpp에서 memory_atomic_wait32, memory_atomic_wait64, atomic.rmw.* 계열, atomic.store, atomic.load를 포함한 다른 모든 atomic instruction과, Memory64를 지원하는 그 밖의 모든 memory instruction slow path를 대상으로 동일한 args[N].i32 vs args[N].i64 불일치를 점검할 필요가 있습니다. 단 하나라도 놓치면 동일한 truncation이 재현됩니다. 리뷰 시에는 Memory64를 지원하는 slow path에서 주소나 offset을 받는 unsigned/uint32_t 지역 변수가 있는지 살펴보는 것이 단서입니다. Union 멤버를 확인하기 전에, 선언된 C++ 타입 자체가 먼저 신호를 줍니다. 별도로, fix에서 memoryIndexcount.i32로 읽는 처리가 맞는지도 확인할 필요가 있습니다. 두 값 모두 겉보기에는 순수하게 32비트로 보이지만, 이 가정은 assembly fast path가 실제로 무엇을 push하는지와 대조해 확인되어야 합니다.