Skip to content

incorrect reference handling in reset_weak_ref can lead to potential use-after-free #1352

@KpwnZ

Description

@KpwnZ

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():

  1. First pass unlinks map records and finalization entries from their containers.
  2. Second pass processes weak-ref records in list order.
  3. 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).
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions