Description
JSFinRecEntry::obj (fre->obj) is a raw JSValueConst reference to the FinalizationRegistry object created at registration time; it is not js_dup()'d in js_finrec_register().
Inside reset_weak_ref():
- First pass unlinks map records and finalization entries from their containers.
- Second pass processes weak-ref records in list order.
- If a
JS_WEAK_REF_KIND_MAP node is processed before a JS_WEAK_REF_KIND_FINALIZATION_REGISTRY_ENTRY node, JS_FreeValueRT(rt, mr->value) may free the FinalizationRegistry object (when that map value is its last strong ref).
- Later in the same second pass, code dereferences
fre->obj via JS_GetOpaque(fre->obj, ...), but fre->obj now points to freed object memory.
This is a UAF caused by incorrect lifetime/state assumptions across re-entrant frees in reset_weak_ref().
Reproduce
let key = Symbol('uaf-key');
let wm = new WeakMap();
let fr = new FinalizationRegistry(() => {});
fr.register(key, 1); // insert FR entry first
wm.set(key, fr); // insert WeakMap entry second (processed first)
fr = null; // only WeakMap value keeps registry alive
key = null; // triggers symbol free -> reset_weak_ref
Please test with ASAN build of quickjs-ng.
ASAN output
=================================================================
==1618980==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bac699e61b6 at pc 0x558c980f7271 bp 0x7fff6f040e80 sp 0x7fff6f040e70
READ of size 2 at 0x7bac699e61b6 thread T0
#0 0x558c980f7270 in JS_GetOpaque /root/xia0/project/quickjs-ng_src/quickjs.c:11438
#1 0x558c98260996 in reset_weak_ref /root/xia0/project/quickjs-ng_src/quickjs.c:59536
#2 0x558c980b83bb in JS_FreeAtomStruct /root/xia0/project/quickjs-ng_src/quickjs.c:3265
#3 0x558c980d26e1 in js_free_value_rt /root/xia0/project/quickjs-ng_src/quickjs.c:6656
#4 0x558c980d28f4 in JS_FreeValueRT /root/xia0/project/quickjs-ng_src/quickjs.c:6670
#5 0x558c980d29af in JS_FreeValue /root/xia0/project/quickjs-ng_src/quickjs.c:6677
#6 0x558c980b2d31 in set_value /root/xia0/project/quickjs-ng_src/quickjs.c:2520
#7 0x558c9812e993 in JS_CallInternal /root/xia0/project/quickjs-ng_src/quickjs.c:18245
#8 0x558c9814541c in async_func_resume /root/xia0/project/quickjs-ng_src/quickjs.c:20225
#9 0x558c98146bef in js_async_function_resume /root/xia0/project/quickjs-ng_src/quickjs.c:20480
#10 0x558c98147d4a in js_async_function_call /root/xia0/project/quickjs-ng_src/quickjs.c:20599
#11 0x558c98186c33 in js_execute_sync_module /root/xia0/project/quickjs-ng_src/quickjs.c:30572
#12 0x558c9818802f in js_inner_module_evaluation /root/xia0/project/quickjs-ng_src/quickjs.c:30684
#13 0x558c981887b4 in js_evaluate_module /root/xia0/project/quickjs-ng_src/quickjs.c:30731
#14 0x558c981b3701 in JS_EvalFunctionInternal /root/xia0/project/quickjs-ng_src/quickjs.c:36313
#15 0x558c981b3881 in JS_EvalFunction /root/xia0/project/quickjs-ng_src/quickjs.c:36327
#16 0x558c98084531 in eval_buf /root/xia0/project/quickjs-ng_src/qjs.c:128
#17 0x558c980847e9 in eval_file /root/xia0/project/quickjs-ng_src/qjs.c:165
#18 0x558c9808738e in main /root/xia0/project/quickjs-ng_src/qjs.c:686
#19 0x7f3c6ac36634 (/usr/lib/libc.so.6+0x27634) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#20 0x7f3c6ac366e8 in __libc_start_main (/usr/lib/libc.so.6+0x276e8) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#21 0x558c98082c44 in _start (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan/qjs+0x40c44) (BuildId: fc421c6389edb2882ba64463d1609b4b997304e2)
0x7bac699e61b6 is located 6 bytes inside of 72-byte region [0x7bac699e61b0,0x7bac699e61f8)
freed by thread T0 here:
#0 0x7f3c6af1f79d in free /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:51
#1 0x558c980b003c in js_def_free /root/xia0/project/quickjs-ng_src/quickjs.c:2029
#2 0x558c980ae564 in js_free_rt /root/xia0/project/quickjs-ng_src/quickjs.c:1639
#3 0x558c980d1f49 in free_object /root/xia0/project/quickjs-ng_src/quickjs.c:6563
#4 0x558c980d1fc9 in free_gc_object /root/xia0/project/quickjs-ng_src/quickjs.c:6571
#5 0x558c980d2105 in free_zero_refcount /root/xia0/project/quickjs-ng_src/quickjs.c:6593
#6 0x558c980d2646 in js_free_value_rt /root/xia0/project/quickjs-ng_src/quickjs.c:6639
#7 0x558c980d28f4 in JS_FreeValueRT /root/xia0/project/quickjs-ng_src/quickjs.c:6670
#8 0x558c98260854 in reset_weak_ref /root/xia0/project/quickjs-ng_src/quickjs.c:59526
#9 0x558c980b83bb in JS_FreeAtomStruct /root/xia0/project/quickjs-ng_src/quickjs.c:3265
#10 0x558c980d26e1 in js_free_value_rt /root/xia0/project/quickjs-ng_src/quickjs.c:6656
#11 0x558c980d28f4 in JS_FreeValueRT /root/xia0/project/quickjs-ng_src/quickjs.c:6670
#12 0x558c980d29af in JS_FreeValue /root/xia0/project/quickjs-ng_src/quickjs.c:6677
#13 0x558c980b2d31 in set_value /root/xia0/project/quickjs-ng_src/quickjs.c:2520
#14 0x558c9812e993 in JS_CallInternal /root/xia0/project/quickjs-ng_src/quickjs.c:18245
#15 0x558c9814541c in async_func_resume /root/xia0/project/quickjs-ng_src/quickjs.c:20225
#16 0x558c98146bef in js_async_function_resume /root/xia0/project/quickjs-ng_src/quickjs.c:20480
#17 0x558c98147d4a in js_async_function_call /root/xia0/project/quickjs-ng_src/quickjs.c:20599
#18 0x558c98186c33 in js_execute_sync_module /root/xia0/project/quickjs-ng_src/quickjs.c:30572
#19 0x558c9818802f in js_inner_module_evaluation /root/xia0/project/quickjs-ng_src/quickjs.c:30684
#20 0x558c981887b4 in js_evaluate_module /root/xia0/project/quickjs-ng_src/quickjs.c:30731
#21 0x558c981b3701 in JS_EvalFunctionInternal /root/xia0/project/quickjs-ng_src/quickjs.c:36313
#22 0x558c981b3881 in JS_EvalFunction /root/xia0/project/quickjs-ng_src/quickjs.c:36327
#23 0x558c98084531 in eval_buf /root/xia0/project/quickjs-ng_src/qjs.c:128
#24 0x558c980847e9 in eval_file /root/xia0/project/quickjs-ng_src/qjs.c:165
#25 0x558c9808738e in main /root/xia0/project/quickjs-ng_src/qjs.c:686
#26 0x7f3c6ac36634 (/usr/lib/libc.so.6+0x27634) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#27 0x7f3c6ac366e8 in __libc_start_main (/usr/lib/libc.so.6+0x276e8) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#28 0x558c98082c44 in _start (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan/qjs+0x40c44) (BuildId: fc421c6389edb2882ba64463d1609b4b997304e2)
previously allocated by thread T0 here:
#0 0x7f3c6af20cb5 in malloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x558c980b001e in js_def_malloc /root/xia0/project/quickjs-ng_src/quickjs.c:2024
#2 0x558c980ae36e in js_malloc_rt /root/xia0/project/quickjs-ng_src/quickjs.c:1620
#3 0x558c980ae8d0 in js_malloc /root/xia0/project/quickjs-ng_src/quickjs.c:1703
#4 0x558c980caa8a in JS_NewObjectFromShape /root/xia0/project/quickjs-ng_src/quickjs.c:5634
#5 0x558c980cb83c in JS_NewObjectProtoClass /root/xia0/project/quickjs-ng_src/quickjs.c:5769
#6 0x558c981439f2 in js_create_from_ctor /root/xia0/project/quickjs-ng_src/quickjs.c:20042
#7 0x558c9825f36b in js_finrec_constructor /root/xia0/project/quickjs-ng_src/quickjs.c:59360
#8 0x558c98119a88 in js_call_c_function /root/xia0/project/quickjs-ng_src/quickjs.c:17113
#9 0x558c98143e16 in JS_CallConstructorInternal /root/xia0/project/quickjs-ng_src/quickjs.c:20072
#10 0x558c98123bc9 in JS_CallInternal /root/xia0/project/quickjs-ng_src/quickjs.c:17769
#11 0x558c9814541c in async_func_resume /root/xia0/project/quickjs-ng_src/quickjs.c:20225
#12 0x558c98146bef in js_async_function_resume /root/xia0/project/quickjs-ng_src/quickjs.c:20480
#13 0x558c98147d4a in js_async_function_call /root/xia0/project/quickjs-ng_src/quickjs.c:20599
#14 0x558c98186c33 in js_execute_sync_module /root/xia0/project/quickjs-ng_src/quickjs.c:30572
#15 0x558c9818802f in js_inner_module_evaluation /root/xia0/project/quickjs-ng_src/quickjs.c:30684
#16 0x558c981887b4 in js_evaluate_module /root/xia0/project/quickjs-ng_src/quickjs.c:30731
#17 0x558c981b3701 in JS_EvalFunctionInternal /root/xia0/project/quickjs-ng_src/quickjs.c:36313
#18 0x558c981b3881 in JS_EvalFunction /root/xia0/project/quickjs-ng_src/quickjs.c:36327
#19 0x558c98084531 in eval_buf /root/xia0/project/quickjs-ng_src/qjs.c:128
#20 0x558c980847e9 in eval_file /root/xia0/project/quickjs-ng_src/qjs.c:165
#21 0x558c9808738e in main /root/xia0/project/quickjs-ng_src/qjs.c:686
#22 0x7f3c6ac36634 (/usr/lib/libc.so.6+0x27634) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#23 0x7f3c6ac366e8 in __libc_start_main (/usr/lib/libc.so.6+0x276e8) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#24 0x558c98082c44 in _start (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan/qjs+0x40c44) (BuildId: fc421c6389edb2882ba64463d1609b4b997304e2)
SUMMARY: AddressSanitizer: heap-use-after-free /root/xia0/project/quickjs-ng_src/quickjs.c:11438 in JS_GetOpaque
Shadow bytes around the buggy address:
0x7bac699e5f00: fa fa fd fd fd fd fd fd fd fd fd fa fa fa fa fa
0x7bac699e5f80: 00 00 00 00 00 00 00 00 00 fa fa fa fa fa 00 00
0x7bac699e6000: 00 00 00 00 00 00 00 fa fa fa fa fa 00 00 00 00
0x7bac699e6080: 00 00 00 00 00 fa fa fa fa fa 00 00 00 00 00 00
0x7bac699e6100: 00 00 00 fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x7bac699e6180: fd fa fa fa fa fa[fd]fd fd fd fd fd fd fd fd fa
0x7bac699e6200: fa fa fa fa 00 00 00 00 00 00 00 00 00 fa fa fa
0x7bac699e6280: fa fa 00 00 00 00 00 00 00 00 00 00 fa fa fa fa
0x7bac699e6300: 00 00 00 00 00 00 00 00 00 fa fa fa fa fa fd fd
0x7bac699e6380: fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa
0x7bac699e6400: 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
==1618980==ABORTING
Credit
Yuan (@Reset816) and xia0o0o0o (@KpwnZ).
Description
JSFinRecEntry::obj(fre->obj) is a rawJSValueConstreference to theFinalizationRegistryobject created at registration time; it is notjs_dup()'d injs_finrec_register().Inside
reset_weak_ref():JS_WEAK_REF_KIND_MAPnode is processed before aJS_WEAK_REF_KIND_FINALIZATION_REGISTRY_ENTRYnode,JS_FreeValueRT(rt, mr->value)may free theFinalizationRegistryobject (when that map value is its last strong ref).fre->objviaJS_GetOpaque(fre->obj, ...), butfre->objnow points to freed object memory.This is a UAF caused by incorrect lifetime/state assumptions across re-entrant frees in
reset_weak_ref().Reproduce
Please test with ASAN build of quickjs-ng.
ASAN output
Credit
Yuan (@Reset816) and xia0o0o0o (@KpwnZ).