Skip to content

Memory leak in Iterator.prototype.mapitem JSValue never freed #493

@hkbinbin

Description

@hkbinbin

Description

In js_iterator_helper_next(), the MAP case (lines 43956-43983) never frees the item JSValue returned by JS_IteratorNext. Both the success and error paths leak it. Every call to .next() on a mapped iterator leaks the original item's JSValue (16 bytes + referenced object). This causes unbounded memory growth and triggers a hard assertion failure on shutdown in debug builds.

Environment

  • QuickJS version: 2025-09-13
  • Commit: f113949
  • OS: macOS 15.4 arm64 (Darwin 25.2.0), reproducible on all platforms
  • Compiler: Apple clang 17.0.0 (clang-1700.6.4.2)
  • Build: All build configurations affected (release, debug, ASAN)

How to Reproduce

git clone https://github.com/bellard/quickjs.git && cd quickjs
git checkout f113949

# Build with DUMP_LEAKS to see leaked objects
make DEFINES='-D_GNU_SOURCE -DCONFIG_VERSION=\"2025-09-13\" -DDUMP_LEAKS=1' qjs

# Run POC — dumps leaked objects and triggers assertion failure
./qjs poc_iterator_map_leak.js

POC (minimal)

function* gen() {
    for (let i = 0; i < 5; i++) {
        yield { index: i };
    }
}

const mapped = gen().map(x => x.index * 2);

for (const val of mapped) {}

DUMP_LEAKS Output

Object leaks:
       ADDRESS REFS SHRF          PROTO CONTENT
   0x84b00f390    1   0*    0x1017d9f90 { index: 0 }
   0x84b00f3e0    1   0*    0x1017d9f90 { index: 1 }
   0x84b00f430    1   0*    0x1017d9f90 { index: 2 }
   0x84b00f480    1   0*    0x1017d9f90 { index: 3 }
   0x84b00f4d0    1   0*    0x1017d9f90 { index: 4 }
Assertion failed: (list_empty(&rt->gc_obj_list)), function JS_FreeRuntime, file quickjs.c, line 2036.

All 5 yielded objects are leaked with REFS=1 — the reference from item is never released. The assertion list_empty(&rt->gc_obj_list) at JS_FreeRuntime (line 2036) fires because these leaked objects remain on the GC object list at shutdown. With 100,000 iterations the leak scales linearly (100,000 leaked objects).

Root Cause

In quickjs.c, function js_iterator_helper_next(), MAP case:

// Line 43967:
item = JS_IteratorNext(ctx, it->obj, it->next, 0, NULL, &done);  // item allocated

// Line 43978:
ret = JS_Call(ctx, it->func, JS_UNDEFINED, 1, &item);  // item passed to mapper

// Line 43981-43982:
if (JS_IsException(ret))
    goto fail;    // item NOT freed
goto done;        // item NOT freed

Compare with the FILTER case at lines 43859/43868 which correctly frees item:

JS_FreeValue(ctx, item);  // FILTER does this, MAP does not

Impact

Memory leak of 16 bytes + referenced object per .next() call on any mapped iterator. In long-running programs this causes unbounded memory growth and eventual OOM. In debug builds it triggers a hard assertion failure at runtime shutdown.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions