[Vulnerability]Use-After-Free in JS_DefineProperty() Accessor Conversion: old value freed before JSPropertyGetSet allocation

Đang mở
#604 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 3 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
55/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
c
Lĩnh vực
backend

Hướng nghiên cứu

Read the data/VARREF-to-accessor conversion branch in JS_DefineProperty() in quickjs.c, especially the release and allocation steps around lines 10645–10660. Start by tracing what happens when allocating JSPropertyGetSet fails under the reported memory limit. Done looks like the property remains valid after allocation failure and the provided reproduction no longer reports a use-after-free.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

1. Summary

JS_DefineProperty() converts an existing data property (or variable reference) to an accessor in one branch that releases the old value first and allocates the new JSPropertyGetSet afterwards. If that allocation fails, the function returns -1 without updating prs->flags or pr->u, so the property keeps JS_PROP_NORMAL while its value has already been passed to JS_FreeValue() (or free_var_ref() for the VARREF case). The property stays on a live object; any later property store or GC walks the dangling JSValue, which is a heap-use-after-free.
Ordinary JavaScript triggers it: Object.defineProperty(o, k, { get: f }) on an existing configurable data property, while a host memory limit (qjs --memory-limit / JS_SetMemoryLimit) is being exhausted by the script. ASan reliably reports a heap-use-after-free READ of size 4 in JS_FreeValue() on a freed 72-byte JSObject; a plain release build SIGSEGVs in list_del() called from __JS_FreeValueRT().

2. version:

Git master branch
commit: https://github.com/bellard/quickjs/commit/092277ac1eb2997916a8ad0c4fbf961c91180498

3. OS and version

Ubuntu 22.04 LTS (x86_64)

4. Steps to Reproduce:

The proof-of-concept file for reproducing the vulnerability:

poc.zip

(1) debug + asan build: heap-use-after-free

command:

./qjs-debug --memory-limit 2M ./poc.js

Output: heap-use-after-free

=================================================================
==23413==ERROR: AddressSanitizer: heap-use-after-free on address 0x73e48b5e56c4 at pc 0x5b4a96f733d9 bp 0x7fffff9adc20 sp 0x7fffff9adc18
READ of size 4 at 0x73e48b5e56c4 thread T0
    #0 0x5b4a96f733d8 in JS_FreeValue /home/testuser/Desktop/js/quickjs/quickjs/./quickjs.h:719:13
    #1 0x5b4a96f7a94a in set_value /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:2729:5
    #2 0x5b4a96f96299 in JS_SetPropertyInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:9920:13
    #3 0x5b4a96f9a797 in JS_SetPropertyValue /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:10286:15
    #4 0x5b4a96fc02b0 in JS_CallInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:20191:27
    #5 0x5b4a96f8dbb4 in JS_CallFree /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:21331:19
    #6 0x5b4a96fcdc8b in JS_EvalFunctionInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38347:19
    #7 0x5b4a96fe1fb9 in __JS_EvalInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38480:19
    #8 0x5b4a96fce0a9 in JS_EvalInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38506:11
    #9 0x5b4a96fcddee in JS_EvalThis /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38539:11
    #10 0x5b4a96fce267 in JS_Eval /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38547:12
    #11 0x5b4a96f6d715 in eval_buf /home/testuser/Desktop/js/quickjs/quickjs/qjs.c:70:15
    #12 0x5b4a96f6d8e6 in eval_file /home/testuser/Desktop/js/quickjs/quickjs/qjs.c:105:11
    #13 0x5b4a96f6cdc7 in main /home/testuser/Desktop/js/quickjs/quickjs/qjs.c:538:17
    #14 0x77748c229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #15 0x77748c229e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #16 0x5b4a96e7b734 in _start (/home/testuser/Desktop/js/quickjs/build/debug-asan/qjs-debug+0x1fe734) (BuildId: b020112db5246ccb8b913d77e39fd7e7e6cf3359)

0x73e48b5e56c4 is located 4 bytes inside of 72-byte region [0x73e48b5e56c0,0x73e48b5e5708)
freed by thread T0 here:
    #0 0x5b4a96f299a6 in free (/home/testuser/Desktop/js/quickjs/build/debug-asan/qjs-debug+0x2ac9a6) (BuildId: b020112db5246ccb8b913d77e39fd7e7e6cf3359)
    #1 0x5b4a96fed131 in js_def_free /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:2214:5
    #2 0x5b4a96f6e671 in __js_free /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:1643:13
    #3 0x5b4a96f6e56c in js_free_rt /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:1839:5
    #4 0x5b4a96ff0eae in free_object /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:6510:13
    #5 0x5b4a96ff088c in free_gc_object /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:6521:9
    #6 0x5b4a96f84fa7 in free_zero_refcount /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:6549:9
    #7 0x5b4a96f84439 in __JS_FreeValueRT /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:6604:21
    #8 0x5b4a96f85041 in __JS_FreeValue /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:6628:5
    #9 0x5b4a96f733f5 in JS_FreeValue /home/testuser/Desktop/js/quickjs/quickjs/./quickjs.h:720:13
    #10 0x5b4a96f7a94a in set_value /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:2729:5
    #11 0x5b4a96f96299 in JS_SetPropertyInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:9920:13
    #12 0x5b4a96f9a797 in JS_SetPropertyValue /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:10286:15
    #13 0x5b4a96fc02b0 in JS_CallInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:20191:27
    #14 0x5b4a96f8dbb4 in JS_CallFree /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:21331:19
    #15 0x5b4a96fcdc8b in JS_EvalFunctionInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38347:19
    #16 0x5b4a96fe1fb9 in __JS_EvalInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38480:19
    #17 0x5b4a96fce0a9 in JS_EvalInternal /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38506:11
    #18 0x5b4a96fcddee in JS_EvalThis /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38539:11
    #19 0x5b4a96fce267 in JS_Eval /home/testuser/Desktop/js/quickjs/quickjs/quickjs.c:38547:12
    #20 0x5b4a96f6d715 in eval_buf /home/testuser/Desktop/js/quickjs/quickjs/qjs.c:70:15
    #21 0x5b4a96f6d8e6 in eval_file /home/testuser/Desktop/js/quickjs/quickjs/qjs.c:105:11
    #22 0x5b4a96f6cdc7 in main /home/testuser/Desktop/js/quickjs/quickjs/qjs.c:538:17
    #23 0x77748c229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
......
(2) release build: SIGSEGV

command:

./qjs --memory-limit 2M ./poc.js; echo "exit=$?"

Output:

Segmentation fault (core dumped)
exit=139

gdb output:

~/Desktop/quickjs/build/release$ gdb -nx -batch -q ./qjs -ex 'set pagination off' -ex 'run --memory-limit 2M ./poc.js' -ex 'bt 10'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
list_del (el=0x55555584bcb8) at ./list.h:74
74	    prev->next = next;
#0  list_del (el=0x55555584bcb8) at ./list.h:74
#1  __JS_FreeValueRT (rt=0x5555558242a0, v=<optimized out>) at quickjs.c:6600
#2  0x00005555555f4ed0 in __JS_FreeValue (v=93824995343544, ctx=<optimized out>) at quickjs.c:6628
#3  JS_FreeValue (v=93824995343544, ctx=<optimized out>) at ./quickjs.h:720
#4  set_value (pval=<optimized out>, ctx=<optimized out>, new_val=<optimized out>) at quickjs.c:2729
#5  JS_SetPropertyInternal (ctx=ctx@entry=0x55555582d1c0, obj=<optimized out>, prop=prop@entry=2147483787, val=<optimized out>, this_obj=<optimized out>, flags=flags@entry=32768) at quickjs.c:9920
#6  0x00005555555f5e87 in JS_SetPropertyValue (ctx=ctx@entry=0x55555582d1c0, this_obj=<optimized out>, prop=<optimized out>, val=<optimized out>, flags=flags@entry=32768) at quickjs.c:10286
#7  0x00005555556069d9 in JS_CallInternal (caller_ctx=caller_ctx@entry=0x55555582d1c0, func_obj=func_obj@entry=18446697161216539904, this_obj=this_obj@entry=18446697161216422720, new_target=new_target@entry=422212465065984, argc=argc@entry=0, argv=argv@entry=0x0, flags=2) at quickjs.c:20191
#8  0x000055555560c84d in JS_CallFree (ctx=0x55555582d1c0, func_obj=18446697161216539904, this_obj=18446697161216422720, argc=0, argv=0x0) at quickjs.c:21331
#9  JS_EvalFunctionInternal (ctx=ctx@entry=0x55555582d1c0, fun_obj=0, this_obj=this_obj@entry=18446697161216422720, var_refs=var_refs@entry=0x0, sf=sf@entry=0x0) at quickjs.c:38347
(3)The dangling property frees a live script object (release build)
./qjs --memory-limit 2M ./poc2.js; echo "exit=$?"

output:

Segmentation fault (core dumped)
exit=139

gdb output:

~/Desktop/quickjs/build/release$  gdb -nx -batch -q ./qjs -ex 'set pagination off' -ex 'set confirm off' -ex 'dprintf free_object, "FREE_OBJECT p=%p class=%d\n", p, p->class_id' -ex 'run --memory-limit 2M ./poc2.js' -ex 'bt 10'
Dprintf 1 at 0xcb6b9: file quickjs.c, line 6470.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
FREE_OBJECT p=0x555555a207c0 class=19
FREE_OBJECT p=0x55555584bcb8 class=1
FREE_OBJECT p=0x55555584bcb8 class=1

Program received signal SIGSEGV, Segmentation fault.
find_own_property (p=0x55555584bcb8, atom=665, ppr=<optimized out>) at quickjs.c:6266
6266	    h = (uintptr_t)atom & sh->prop_hash_mask;
#0  find_own_property (p=0x55555584bcb8, atom=665, ppr=<optimized out>) at quickjs.c:6266
#1  JS_CallInternal (caller_ctx=caller_ctx@entry=0x55555582d1c0, func_obj=func_obj@entry=18446697161216539904, this_obj=this_obj@entry=18446697161216422720, new_target=new_target@entry=422212465065984, argc=argc@entry=0, argv=argv@entry=0x0, flags=2) at quickjs.c:19796
#2  0x000055555560c84d in JS_CallFree (ctx=0x55555582d1c0, func_obj=18446697161216539904, this_obj=18446697161216422720, argc=0, argv=0x0) at quickjs.c:21331
#3  JS_EvalFunctionInternal (ctx=ctx@entry=0x55555582d1c0, fun_obj=140737488355327, this_obj=this_obj@entry=18446697161216422720, var_refs=var_refs@entry=0x0, sf=sf@entry=0x0) at quickjs.c:38347
#4  0x00005555556180c2 in __JS_EvalInternal (ctx=0x55555582d1c0, this_obj=18446697161216422720, input=<optimized out>, input_len=<optimized out>, filename=<optimized out>, flags=0, scope_idx=-1) at quickjs.c:38480
#5  0x000055555560cebb in JS_EvalInternal (ctx=0xfffbffffffffffff, this_obj=562949953421312, input=0x299 <error: Cannot access memory at address 0x299>, input_len=0, filename=0x7fffffffd298 "\270\274\204UU\325\377\377BB", flags=1434600096, scope_idx=-1) at quickjs.c:38506
#6  JS_EvalThis (ctx=0xfffbffffffffffff, this_obj=562949953421312, eval_flags=1434600096, input=<optimized out>, input_len=<optimized out>, filename=<optimized out>) at quickjs.c:38539
#7  JS_Eval (ctx=0xfffbffffffffffff, ctx@entry=0x55555582d1c0, input=0x299 <error: Cannot access memory at address 0x299>, input_len=0, filename=0x7fffffffd298 "\270\274\204UU\325\377\377BB", eval_flags=1434600096) at quickjs.c:38547
#8  0x00005555555e57ec in eval_buf (ctx=ctx@entry=0x55555582d1c0, buf=0x2000000000000, buf@entry=0x555555851578, buf_len=<optimized out>, filename=0x0, filename@entry=0x7fffffffdef3 "./poc2.js", eval_flags=-11624) at qjs.c:70
#9  0x00005555555e52ef in eval_file (ctx=0x55555582d1c0, filename=0x7fffffffdef3 "./poc2.js", module=0, strict=0) at qjs.c:105

The same 72-byte JSObject address is freed twice (class=1 is JS_CLASS_OBJECT): first the original value v, then the reused victim.

5. Root cause

JS_DefineProperty() releases the old value before allocating the new JSPropertyGetSet, and on failure returns without restoring anything:

/* quickjs.c:10645 — data/VARREF -> accessor conversion */
10645:  if ((prs->flags & JS_PROP_TMASK) != JS_PROP_GETSET) {
10646:      JSPropertyGetSet *getset;
10647:      if (js_shape_prepare_update(ctx, p, &prs))
10648:          return -1;
10649:      /* convert to getset */
10650:      if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) {
10651:          if (unlikely(p->class_id == JS_CLASS_GLOBAL_OBJECT)) {
10652:              if (remove_global_object_property(ctx, p, prs, pr))
10653:                  return -1;
10654:          }
10655:          free_var_ref(ctx->rt, pr->u.var_ref);       /* old value released first */
10656:      } else {
10657:          JS_FreeValue(ctx, pr->u.value);             /* old value released first */
10658:      }
10659:      getset = js_malloc(ctx, sizeof(JSPropertyGetSet));  /* allocation afterwards */
10660:      if (!getset)
10661:          return -1;                                  /* returns without restoring state */
10662:      prs->flags = (prs->flags &
10663:                    (JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE)) |
10664:          JS_PROP_GETSET;
10665:      pr->u.getset = getset;
10666:      getset->getter = NULL;
10667:      getset->setter = NULL;

6. Fix recommendations

Allocate the JSPropertyGetSet before releasing the old value; if the allocation fails, leave the property untouched. Every early failure after the allocation must free the new object and return before any release:

if ((prs->flags & JS_PROP_TMASK) != JS_PROP_GETSET) {
    JSPropertyGetSet *getset;

    /* 1. allocate first: on failure the property stays untouched */
    getset = js_malloc(ctx, sizeof(JSPropertyGetSet));
    if (!getset)
        return -1;

    /* 2. only now modify the property */
    if (js_shape_prepare_update(ctx, p, &prs)) {
        js_free(ctx, getset);
        return -1;                      /* old value is still owned */
    }
    if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) {
        if (unlikely(p->class_id == JS_CLASS_GLOBAL_OBJECT)) {
            if (remove_global_object_property(ctx, p, prs, pr)) {
                js_free(ctx, getset);
                return -1;
            }
        }
        free_var_ref(ctx->rt, pr->u.var_ref);
    } else {
        JS_FreeValue(ctx, pr->u.value);
    }

    /* 3. commit union member + flags together */
    prs->flags = (prs->flags &
                  (JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE)) |
        JS_PROP_GETSET;
    pr->u.getset = getset;
    getset->getter = NULL;
    getset->setter = NULL;
}
Ngôn ngữ chính
C
Star
11.1k
Fork
1.2k
Merge trung bình
11 giờ 20 phút
Pull request đã merge (30 ngày)
1

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của bellard/quickjs

Tất cả issue của bellard/quickjs

Issue tương tự

Thêm issue về C

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.