Skip to content

Reproducible list_empty assertion failure #235

@grassick

Description

@grassick

I'm using QuickJS to sandbox code on the server. When dealing with large objects being passed back from a function (if there are both a promise-based functions and a normal ones) at a certain size of object, I start getting list_empty assertions. With a smaller number of objects, let's say 6,500 instead of 7,000, the debug version reports no problems whatsoever and it works perfectly.

quickjs-emscripten 0.31.0.

Source that fails (I apologize for it not being shorter, but this is a very fiddly thing to reproduce):

const { newQuickJSWASMModule, DEBUG_SYNC, Scope } = require("quickjs-emscripten")

async function main() {

  const memoryLimit = 256 * 1024 * 1024
  const maxStackSize = 1024 * 1024

  // Create QuickJS module and context
  // const QuickJS = await newQuickJSWASMModule()
  const QuickJS = await newQuickJSWASMModule(DEBUG_SYNC)
  const runtime = QuickJS.newRuntime()

  runtime.setMemoryLimit(memoryLimit)
  runtime.setMaxStackSize(maxStackSize)

  const scope = new Scope()
  const context = scope.manage(runtime.newContext())

  // Function that simulates reading data asynchronously
  const readFileHandle = context.newFunction("readFile", () => {
    const promise = context.newPromise()
    setTimeout(() => {
      const content = "Sdfsdfsdfsdf"
      promise.resolve(scope.manage(context.newString(content || "")))
    }, 100)
    return scope.manage(promise.handle)
  })
  readFileHandle.consume(handle => context.setProp(context.global, "readFile", handle))

  // ========================================================================
  // Expose parseit (sync function)
  // ========================================================================
  const parseitHandle = context.newFunction("parseit", () => {
    const data2 = []
    // CHANGING THIS TO 6500 CAUSES THE TEST TO PASS!!
    for (let i = 0; i < 7000; i++) {
      data2.push({
        field1: 'IEQ 08-DH5NBT',
        field2: 'SAHI - WP Inventory',
        field3: 'IEQ 08',
        field4: 'M/B_MORONDAVA_CU MORONDAVA_FKT TSIMAHAVAOKELY_Puits non protégé Secteur I 6',
        field5: 'Final',
        field6: '847347003',
        field7: 'Secteur I 6',
        field8: '847346923',
        field12: 'Menabe',
        field13: 'Morondava',
        field14: 'Morondava',
        field15: '2025-09-11',
        field16: 'Periodic / sectoral monitoring',
      })
    }
    const evalResult = context.evalCode(`(${JSON.stringify(data2)})`)
    if (evalResult.error) {
      evalResult.error.dispose()
      throw new Error("Failed to create data")
    }
    return evalResult.value
  })
  context.setProp(context.global, "parseit", parseitHandle)
  parseitHandle.dispose()

  // ========================================================================
  // Execute the code
  // ========================================================================
  const code = `
  await readFile()
  parseit()
  `
  const wrappedCode = `(async () => { ${code} })()`

  const evalResult = context.evalCode(wrappedCode)
  if (evalResult.error) {
    console.error("Eval error:", context.dump(evalResult.error))
    evalResult.error.dispose()
    context.dispose()
    runtime.dispose()
    return
  }
  const promiseHandle = evalResult.value

  // Wait for the promise to settle
  let promiseState = context.getPromiseState(promiseHandle)
  while (promiseState.type === "pending") {
    const result = runtime.executePendingJobs()
    result.dispose()
    await new Promise(r => setTimeout(r, 1))
    promiseState = context.getPromiseState(promiseHandle)
  }

  // Drain any remaining pending jobs
  while (runtime.hasPendingJob()) {
    const result = runtime.executePendingJobs()
    result.dispose()
  }

  // Get the result
  if (promiseState.type === "fulfilled") {
    const result = context.dump(promiseState.value)
    promiseState.value.dispose()
  } else {
    console.error("Promise rejected:", context.dump(promiseState.error))
    promiseState.error.dispose()
  }

  // Dispose the main promise
  promiseHandle.dispose()

  scope.dispose()
  runtime.dispose()
}

main().catch(console.error)

If you change it to 6500 items instead of 7000, it works fine and shows no memory leak. However, at 7000, it shows the following in debug mode:

Object leaks:
       ADDRESS REFS SHRF          PROTO      CLASS PROPS
     0x2b03d40    1 [js_context]
Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at: ../../vendor/quickjs/quickjs.c,1998,JS_FreeRuntime)
RuntimeError: Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at: ../../vendor/quickjs/quickjs.c,1998,JS_FreeRuntime)
    at abort (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:693:11)
    at ___assert_fail (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:1485:7)
    at wasm://wasm/01944492:wasm-function[164]:0xc05b
    at wasm://wasm/01944492:wasm-function[60]:0x46f8
    at file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:728:12
    at ccall (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:4843:17)
    at QuickJSFFI.QTS_FreeRuntime (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:4859:27)
    at _Lifetime.disposer (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/quickjs-emscripten-core@0.31.0/node_modules/quickjs-emscripten-core/dist/index.js:6:949)
    at _Lifetime.dispose (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/quickjs-emscripten-core@0.31.0/node_modules/quickjs-emscripten-core/dist/index.js:2:2198)
    at _Scope.dispose (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/quickjs-emscripten-core@0.31.0/node_modules/quickjs-emscripten-core/dist/index.js:4:1321)

The code I'm writing is to help with importing large datasets, and so the fact that this dies unexpectedly with bigger data structures is a blocker. So if anyone has an idea on how to get around this, it would be much appreciated!

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