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

Aperta
#604 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 3 giorni

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
55/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
c
Ambito
backend

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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;
}
Lingua principale
C
Stelle
11.1k
Fork
1.2k
Merge medio
11h 20m
PR unite (30g)
1

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di bellard/quickjs

Tutte le issue di bellard/quickjs

Issue simili

Altre issue su C

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.