Skip to content

Conversation

@SeanHeelan
Copy link

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
 Build: make CONFIG_ASAN=1 qjs
 Run: ./qjs poc.js

 =================================================================
 ==65==ERROR: AddressSanitizer: heap-use-after-free on address 0x5190000012a0 at pc 0x556cb34550db bp 0x7ffdd8583c90 sp 0x7ffdd8583c88
 WRITE of size 4 at 0x5190000012a0 thread T0
     #0 0x556cb34550da in js_atomics_op /tmp/quickjs/quickjs.c:58798
     #1 0x556cb3482a97 in js_call_c_function /tmp/quickjs/quickjs.c:17247
     #2 0x556cb33b0b82 in JS_CallInternal /tmp/quickjs/quickjs.c:17429
     #3 0x556cb33b19f8 in JS_CallInternal /tmp/quickjs/quickjs.c:17833
     #4 0x556cb33d217f in JS_CallFree /tmp/quickjs/quickjs.c:20139
     #5 0x556cb3562929 in JS_EvalFunctionInternal /tmp/quickjs/quickjs.c:36559
     #6 0x556cb3566808 in __JS_EvalInternal /tmp/quickjs/quickjs.c:36692
     #7 0x556cb3557497 in JS_EvalInternal /tmp/quickjs/quickjs.c:36718
     #8 0x556cb3557497 in JS_EvalThis /tmp/quickjs/quickjs.c:36752
     #9 0x556cb3557497 in JS_Eval /tmp/quickjs/quickjs.c:36760
     #10 0x556cb337a3e6 in eval_buf /tmp/quickjs/qjs.c:66
     #11 0x556cb337a5ca in eval_file /tmp/quickjs/qjs.c:101
     #12 0x556cb33794d9 in main /tmp/quickjs/qjs.c:519
     #13 0x7f6a7806bca7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
     #14 0x7f6a7806bd64 in __libc_start_main_impl ../csu/libc-start.c:360
     #15 0x556cb3379970 in _start (/tmp/quickjs/qjs+0x36970)

 0x5190000012a0 is located 800 bytes inside of 1024-byte region [0x519000000f80,0x519000001380)
 freed by thread T0 here:
     #0 0x7f6a7841bb58 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85
     #1 0x556cb3385a8a in js_def_realloc /tmp/quickjs/quickjs.c:1782
     #2 0x556cb33eaa90 in js_realloc_rt /tmp/quickjs/quickjs.c:1391
     #3 0x556cb33eaa90 in js_realloc /tmp/quickjs/quickjs.c:1441
     #4 0x556cb344002b in js_array_buffer_resize /tmp/quickjs/quickjs.c:56203
     #5 0x556cb3482a97 in js_call_c_function /tmp/quickjs/quickjs.c:17247
     #6 0x556cb33b0b82 in JS_CallInternal /tmp/quickjs/quickjs.c:17429
     #7 0x556cb33b19f8 in JS_CallInternal /tmp/quickjs/quickjs.c:17833
     #8 0x556cb33d217f in JS_CallFree /tmp/quickjs/quickjs.c:20139
     #9 0x556cb33f4465 in JS_ToPrimitiveFree /tmp/quickjs/quickjs.c:10698
     #10 0x556cb342da50 in JS_ToNumberHintFree /tmp/quickjs/quickjs.c:12565
     #11 0x556cb342e267 in JS_ToNumberFree /tmp/quickjs/quickjs.c:12611
     #12 0x556cb342e267 in JS_ToInt32Free /tmp/quickjs/quickjs.c:12949
     #13 0x556cb3453ed2 in JS_ToInt32 /tmp/quickjs/quickjs.c:12962
     #14 0x556cb3453ed2 in JS_ToUint32 /tmp/quickjs/quickjs.h:724
     #15 0x556cb3453ed2 in js_atomics_op /tmp/quickjs/quickjs.c:58769
     #16 0x556cb3482a97 in js_call_c_function /tmp/quickjs/quickjs.c:17247
     #17 0x556cb33b0b82 in JS_CallInternal /tmp/quickjs/quickjs.c:17429
     #18 0x556cb33b19f8 in JS_CallInternal /tmp/quickjs/quickjs.c:17833
     #19 0x556cb33d217f in JS_CallFree /tmp/quickjs/quickjs.c:20139
     #20 0x556cb3562929 in JS_EvalFunctionInternal /tmp/quickjs/quickjs.c:36559
     #21 0x556cb3566808 in __JS_EvalInternal /tmp/quickjs/quickjs.c:36692
     #22 0x556cb3557497 in JS_EvalInternal /tmp/quickjs/quickjs.c:36718
     #23 0x556cb3557497 in JS_EvalThis /tmp/quickjs/quickjs.c:36752
     #24 0x556cb3557497 in JS_Eval /tmp/quickjs/quickjs.c:36760
     #25 0x556cb337a3e6 in eval_buf /tmp/quickjs/qjs.c:66
     #26 0x556cb337a5ca in eval_file /tmp/quickjs/qjs.c:101
     #27 0x556cb33794d9 in main /tmp/quickjs/qjs.c:519
     #28 0x7f6a7806bca7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

 previously allocated by thread T0 here:
     #0 0x7f6a7841cc57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
     #1 0x556cb3385988 in js_def_malloc /tmp/quickjs/quickjs.c:1744
     #2 0x556cb33ee2e8 in js_malloc_rt /tmp/quickjs/quickjs.c:1381
     #3 0x556cb33ee2e8 in js_mallocz_rt /tmp/quickjs/quickjs.c:1402
     #4 0x556cb33ee2e8 in js_mallocz /tmp/quickjs/quickjs.c:1424
     #5 0x556cb34e330e in js_array_buffer_constructor3 /tmp/quickjs/quickjs.c:55756
     #6 0x556cb34f6726 in js_array_buffer_constructor2 /tmp/quickjs/quickjs.c:55792
     #7 0x556cb34f6726 in js_array_buffer_constructor0 /tmp/quickjs/quickjs.c:55856
     #8 0x556cb3482ba9 in js_call_c_function /tmp/quickjs/quickjs.c:17234
     #9 0x556cb350b8f1 in JS_CallConstructorInternal /tmp/quickjs/quickjs.c:20244
     #10 0x556cb33b72d7 in JS_CallInternal /tmp/quickjs/quickjs.c:17815
     #11 0x556cb33d217f in JS_CallFree /tmp/quickjs/quickjs.c:20139
     #12 0x556cb3562929 in JS_EvalFunctionInternal /tmp/quickjs/quickjs.c:36559
     #13 0x556cb3566808 in __JS_EvalInternal /tmp/quickjs/quickjs.c:36692
     #14 0x556cb3557497 in JS_EvalInternal /tmp/quickjs/quickjs.c:36718
     #15 0x556cb3557497 in JS_EvalThis /tmp/quickjs/quickjs.c:36752
     #16 0x556cb3557497 in JS_Eval /tmp/quickjs/quickjs.c:36760
     #17 0x556cb337a3e6 in eval_buf /tmp/quickjs/qjs.c:66
     #18 0x556cb337a5ca in eval_file /tmp/quickjs/qjs.c:101
     #19 0x556cb33794d9 in main /tmp/quickjs/qjs.c:519
     #20 0x7f6a7806bca7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

 SUMMARY: AddressSanitizer: heap-use-after-free /tmp/quickjs/quickjs.c:58798 in js_atomics_op

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant