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.
Description
In
js_iterator_helper_next(), the MAP case (lines 43956-43983) never frees theitemJSValue returned byJS_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
f113949How to Reproduce
POC (minimal)
DUMP_LEAKS Output
All 5 yielded objects are leaked with
REFS=1— the reference fromitemis never released. The assertionlist_empty(&rt->gc_obj_list)atJS_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, functionjs_iterator_helper_next(), MAP case:Compare with the FILTER case at lines 43859/43868 which correctly frees
item: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.