From 4aa7c632ac0efe320a709cef33fcdccf961e448a Mon Sep 17 00:00:00 2001 From: Sean Heelan Date: Sat, 10 Jan 2026 11:55:08 +0000 Subject: [PATCH] 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 --- quickjs.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/quickjs.c b/quickjs.c index e30d393..4a2beac 100644 --- a/quickjs.c +++ b/quickjs.c @@ -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);