quickjs-emscripten icon indicating copy to clipboard operation
quickjs-emscripten copied to clipboard

Reproducible list_empty assertion failure

Open grassick opened this issue 1 month ago • 2 comments

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/@[email protected]/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/@[email protected]/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/@[email protected]/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:728:12
    at ccall (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@[email protected]/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/@[email protected]/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:4859:27)
    at _Lifetime.disposer (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/[email protected]/node_modules/quickjs-emscripten-core/dist/index.js:6:949)
    at _Lifetime.dispose (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/[email protected]/node_modules/quickjs-emscripten-core/dist/index.js:2:2198)
    at _Scope.dispose (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/[email protected]/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!

grassick avatar Dec 08 '25 22:12 grassick