Fix use-after-free in Atomics operations with resizable ArrayBuffers

The Atomics operations (add, sub, and, or, xor, exchange, compareExchange,
store) capture a pointer to the buffer element before calling JS_ToUint32()
or JS_ToBigInt64() on the value arguments. These conversion functions can
execute arbitrary JavaScript via valueOf() callbacks, which may resize the
underlying ArrayBuffer.

After the value conversion, only a detached check was performed, but not a
bounds re-validation. If the buffer was resized (not detached), the stale
pointer would still be used, leading to a heap-use-after-free.

The fix re-validates the atomic access by calling js_atomics_get_ptr() again
after the value conversions. This follows the RevalidateAtomicAccess()
pattern already present in js_atomics_get_ptr() itself.

Proof of Concept:
  let ab = new ArrayBuffer(1024, { maxByteLength: 2048 });
  let int32Array = new Int32Array(ab);
  let malicious = { valueOf: () => { ab.resize(8); return 1; } };
  Atomics.add(int32Array, 200, malicious);  // heap-use-after-free
This commit is contained in:
Sean Heelan 2026-01-10 11:55:08 +00:00
parent f1139494d1
commit 4aa7c632ac

@ -58775,8 +58775,11 @@ static JSValue js_atomics_op(JSContext *ctx,
rep_val = v32;
}
}
if (abuf->detached)
return JS_ThrowTypeErrorDetachedArrayBuffer(ctx);
/* the buffer may have been resized or detached during the
value conversion via valueOf(), so the pointer must be re-fetched */
if (js_atomics_get_ptr(ctx, &ptr, &abuf, &size_log2, &class_id,
argv[0], argv[1], 0))
return JS_EXCEPTION;
}
switch(op | (size_log2 << 3)) {
@ -58901,8 +58904,12 @@ static JSValue js_atomics_store(JSContext *ctx,
JS_FreeValue(ctx, ret);
return JS_EXCEPTION;
}
if (abuf->detached)
return JS_ThrowTypeErrorDetachedArrayBuffer(ctx);
/* buffer may have been resized or detached during value conversion */
if (js_atomics_get_ptr(ctx, &ptr, &abuf, &size_log2, NULL,
argv[0], argv[1], 0)) {
JS_FreeValue(ctx, ret);
return JS_EXCEPTION;
}
atomic_store((_Atomic(uint64_t) *)ptr, v64);
} else {
uint32_t v;
@ -58914,8 +58921,12 @@ static JSValue js_atomics_store(JSContext *ctx,
JS_FreeValue(ctx, ret);
return JS_EXCEPTION;
}
if (abuf->detached)
return JS_ThrowTypeErrorDetachedArrayBuffer(ctx);
/* buffer may have been resized or detached during value conversion */
if (js_atomics_get_ptr(ctx, &ptr, &abuf, &size_log2, NULL,
argv[0], argv[1], 0)) {
JS_FreeValue(ctx, ret);
return JS_EXCEPTION;
}
switch(size_log2) {
case 0:
atomic_store((_Atomic(uint8_t) *)ptr, v);