Skip to content

Heap Buffer Overflow (Due to Integer Overflow) in js_alloc_string_rt #399

@AlonZa

Description

@AlonZa

The Bug

There is a Heap Buffer Overflow in the js_alloc_string_rt function. The issue occurs due to an integer overflow in the LEB128 parsing, which leads to allocating a smaller buffer than needed. This results in zeroing four bytes after the end of the buffer allocation.

ASAN Report

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3587198609
INFO: Loaded 1 modules   (72672 inline 8-bit counters): 72672 [0x5567c9ff6fb0, 0x5567ca008b90),
INFO: Loaded 1 PC tables (72672 PCs): 72672 [0x5567ca008b90,0x5567ca124990),
./eval-fuzzer: Running 1 inputs 1 time(s) each.
Running: minimal_crash.bin
=================================================================
==2901354==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000cfc at pc 0x5567c9be2cee bp 0x7fff90bebc00 sp 0x7fff90bebbf8
WRITE of size 4 at 0x602000000cfc thread T0
    #0 0x5567c9be2ced in js_alloc_string_rt /home/quickjs/quickjs.c:1903:20
    #1 0x5567c9c582d7 in js_alloc_string /home/quickjs/quickjs.c:1913:9
    #2 0x5567c9c582d7 in JS_ReadString /home/quickjs/quickjs.c:35850:9
    #3 0x5567c9b8d328 in JS_ReadObjectAtoms /home/quickjs/quickjs.c:36767:13
    #4 0x5567c9b8d328 in JS_ReadObject /home/quickjs/quickjs.c:36815:9
    #5 0x5567c9abb6dd in test_array_buffer eval-fuzzer.c
    #6 0x5567c9ab8c26 in LLVMFuzzerTestOneInput (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x3f8c26) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #7 0x5567c99e1683 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x321683) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #8 0x5567c99cb3ff in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x30b3ff) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #9 0x5567c99d1156 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x311156) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #10 0x5567c99faf72 in main (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x33af72) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #11 0x7f3d27b19d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #12 0x7f3d27b19e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #13 0x5567c99c5cc4 in _start (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x305cc4) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)

0x602000000cfe is located 0 bytes to the right of 14-byte region [0x602000000cf0,0x602000000cfe)
allocated by thread T0 here:
    #0 0x5567c9a7dcfe in malloc (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x3bdcfe) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #1 0x5567c9bddb17 in js_def_malloc /home/quickjs/quickjs.c:1729:11
    #2 0x5567c9be2bad in js_malloc_rt /home/quickjs/quickjs.c:1316:12
    #3 0x5567c9be2bad in js_alloc_string_rt /home/quickjs/quickjs.c:1895:11
    #4 0x5567c9c582d7 in js_alloc_string /home/quickjs/quickjs.c:1913:9
    #5 0x5567c9c582d7 in JS_ReadString /home/quickjs/quickjs.c:35850:9
    #6 0x5567c9b8d328 in JS_ReadObjectAtoms /home/quickjs/quickjs.c:36767:13
    #7 0x5567c9b8d328 in JS_ReadObject /home/quickjs/quickjs.c:36815:9
    #8 0x5567c9abb6dd in test_array_buffer eval-fuzzer.c
    #9 0x5567c9ab8c26 in LLVMFuzzerTestOneInput (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x3f8c26) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #10 0x5567c99e1683 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x321683) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #11 0x5567c99cb3ff in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x30b3ff) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #12 0x5567c99d1156 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x311156) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #13 0x5567c99faf72 in main (/home/fuzz-builds/eval-fuzzer/eval-fuzzer+0x33af72) (BuildId: 680de49fc62205ed12e9ba9059f0c8e3ae561472)
    #14 0x7f3d27b19d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/quickjs/quickjs.c:1903:20 in js_alloc_string_rt
Shadow bytes around the buggy address:
  0x0c047fff8140: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa
  0x0c047fff8150: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
  0x0c047fff8160: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fff8170: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fff8180: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fd
=>0x0c047fff8190: fa fa fd fa fa fa fd fa fa fa 04 fa fa fa 00[06]
  0x0c047fff81a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff81b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff81c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff81d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff81e0: 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
==2901354==ABORTING

Description and Root Cause

static int get_leb128(uint32_t *pval, const uint8_t *buf,
                      const uint8_t *buf_end) {
  const uint8_t *ptr = buf;
  uint32_t v, a, i;
  v = 0;
  for (i = 0; i < 5; i++) {
    if (unlikely(ptr >= buf_end)) break;
    a = *ptr++;
    v |= (a & 0x7f) << (i * 7);
    if (!(a & 0x80)) {
      *pval = v;  // #2 <-- v here is 0xffffffff, hence *pval
      return ptr - buf;
    }
  }
  *pval = 0;
  return -1;
}

static int bc_get_leb128(BCReaderState *s, uint32_t *pval) {
  int ret;
  ret = get_leb128(pval, s->ptr,
                   s->buf_end);  // #1 <-- pval is set here too 0xffffffff by get_leb128 (from #2)
  if (unlikely(ret < 0)) return bc_read_error_end(s);
  s->ptr += ret;
  return 0;
}

static JSString *JS_ReadString(BCReaderState *s) {
  uint32_t len;
  size_t size;
  BOOL is_wide_char;
  JSString *p;

  if (bc_get_leb128(s, &len))  // #3 <-- len is set here too 0xffffffff by bc_get_leb128 (from #1 and #2)
    return NULL;
  is_wide_char = len & 1;
  len >>= 1;
  p = js_alloc_string(s->ctx, len,
                      is_wide_char);  // #4 <-- allocating memory using len

  ...
}

static JSString *js_alloc_string_rt(JSRuntime *rt, int max_len,
                                    int is_wide_char) {
  JSString *str;
  str = js_malloc_rt(
      rt, sizeof(JSString) + (max_len << is_wide_char) + 1 -
              is_wide_char);  // #5 <-- This will result in allocating 14 bytes
  if (unlikely(!str)) return NULL;
  str->header.ref_count = 1;
  str->is_wide_char = is_wide_char;
  str->len = max_len;
  str->atom_type = 0;
  str->hash = 0;                      /* optional but costless */
  str->hash_next = 0; /* optional */  // #6 <-- This will write 4 bytes after the allocation as 0
#ifdef DUMP_LEAKS
  list_add_tail(&str->link, &rt->string_list);
#endif
  return str;
}

The flow is:

  1. get_leb128 decodes a value (0xFFFFFFFF) from the input
  2. This value goes through bc_get_leb128 and becomes len in JS_ReadString
  3. js_alloc_string is called with this large value
  4. In js_alloc_string_rt, the size calculation sizeof(JSString) + (max_len << is_wide_char) + 1 - is_wide_char overflows
  5. This results in allocating a very small buffer (14 bytes)
  6. The code then tries to initialize the header fields, writing beyond the allocated buffer
    The key issue occurs when setting str->hash_next = 0 which writes 4 bytes past the end of the allocated buffer.

Reproducer


// Compile with address sanitizer (ASAN)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "quickjs-libc.h"
#include "quickjs.h"

static void js_error_handler(JSContext *ctx, JSValue obj) {
  const char *str = JS_ToCString(ctx, obj);
  if (str) {
    fprintf(stderr, "JS Error: %s\n", str);
    JS_FreeCString(ctx, str);
  } else {
    fprintf(stderr, "JS Error: <unknown>\n");
  }
}

int main(int argc, char **argv) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
    return 1;
  }

  JSRuntime *rt = JS_NewRuntime();
  JSContext *ctx = JS_NewContext(rt);

  JS_SetMemoryLimit(rt, 0x4000000);
  JS_SetMaxStackSize(rt, 0x10000);

  FILE *f = fopen(argv[1], "rb");
  if (!f) {
    fprintf(stderr, "Cannot open input file: %s\n", argv[1]);
    return 1;
  }

  fseek(f, 0, SEEK_END);
  size_t size = ftell(f);
  fseek(f, 0, SEEK_SET);

  uint8_t *data = malloc(size);
  if (!data) {
    fprintf(stderr, "Cannot allocate memory for input file\n");
    fclose(f);
    return 1;
  }

  if (fread(data, 1, size, f) != size) {
    fprintf(stderr, "Failed to read input file\n");
    free(data);
    fclose(f);
    return 1;
  }
  fclose(f);

  printf("Reading object from %zu bytes of data...\n", size);

  // Call the vulnerable JS_ReadObject function
  // This will trigger the heap buffer overflow with crafted input
  JSValue obj = JS_ReadObject(ctx, data, size, 0);

  if (JS_IsException(obj)) {
    JSValue exc = JS_GetException(ctx);
    js_error_handler(ctx, exc);
    JS_FreeValue(ctx, exc);
  } else {
    printf(
        "Successfully parsed object (this should not happen with crash "
        "input)\n");
    JS_FreeValue(ctx, obj);
  }

  free(data);
  JS_FreeContext(ctx);
  JS_FreeRuntime(rt);
  return 0;
}

To create the input use the following command:


echo -ne "\x04\x07\x01\xFF\xFF\xFF\xFF\x7F" > crashing_input.bin

Potential Fix

I suggest that in js_alloc_string_rt there will be a check for a potential overflow. For example:

static JSString *js_alloc_string_rt(JSRuntime *rt, int max_len,
                                    int is_wide_char) {
  JSString *str;
  size_t alloc_size;

  /* Check for overflow */
  if (max_len > (INT_MAX - sizeof(JSString) - 1) >> is_wide_char) {
    /* Handle error */
    return NULL;
  }

  alloc_size = sizeof(JSString) + (max_len << is_wide_char) + 1 - is_wide_char;
  str = js_malloc_rt(rt, alloc_size);
  ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions