Skip to content

use-after-free in DisposableStack disposal: GC in a disposer marks freed resources (js_dispose_resources) #1564

Description

@xmzyshypnc

js_dispose_resources (quickjs.c:54131) has a use-after-free. When DisposableStack.prototype[Symbol.dispose] runs, it frees each resource's value/method inside its LIFO loop but does not shrink resource_count or clear the freed slots until the loop finishes, while js_disposable_stack_mark (quickjs.c:54329) marks resources[0 .. resource_count). A disposer that triggers a GC therefore makes the marker traverse already-freed resources → reference-count underflow / use-after-free.

Reproduces on master d950d55, -O2 -g (and with -fsanitize=address).

PoC (deterministic)

const s = new DisposableStack();
s.adopt({}, () => { gc(); });        // disposed last (i=0); its disposer runs the GC
s.use({ [Symbol.dispose]() {} });    // disposed first (i=1); freed before the adopt disposer runs
s.dispose();

gc() is only used to make the timing deterministic — any disposer that allocates enough to trigger cycle collection hits the same UAF.

Backtrace

quickjs.c:7323: gc_decref_child: Assertion `JS_REF_COUNT(p) > 0' failed.
    #7  gc_decref_child          quickjs.c:7323
    #8  JS_MarkValue             quickjs.c:7198
    #9  js_disposable_stack_mark quickjs.c:54329    # marks resources[0 .. resource_count)
    #10 gc_decref                quickjs.c:7344
    #11 JS_RunGC                 quickjs.c:7445
    #12 js_gc                    qjs.c:209           # gc() invoked inside the disposer
    #16 JS_Call                  quickjs.c:20855     # js_dispose_resources calling the disposer
    #17 js_dispose_resources     quickjs.c:54131     # LIFO loop, i=0; the i=1 slot is already freed

In a release build (asserts off) this is a use-after-free / heap corruption rather than an abort.

Fix

The dispose loop frees res->value/res->method but leaves the slot dangling until the loop ends, so a re-entrant GC mark reads freed values. Clear each slot right after freeing it:

        JS_FreeValue(ctx, res->value);
        JS_FreeValue(ctx, res->method);
        res->value = JS_UNDEFINED;
        res->method = JS_UNDEFINED;

Verified: the PoC no longer crashes. js_disposable_stack_mark / js_disposable_stack_clear already handle JS_UNDEFINED.

Credit

xmzyshypnc(@xmzyshypnc) and Yanjie Zhao(@carol233) and Yiyang Liu(@lyyffee)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions