Skip to content

Fix changeset_apply crash: use proper WASM function pointer for conflict handler#1021

Open
acusti wants to merge 1 commit intolivestorejs:devfrom
acusti:fix/wa-sqlite-changeset-apply-function-pointer
Open

Fix changeset_apply crash: use proper WASM function pointer for conflict handler#1021
acusti wants to merge 1 commit intolivestorejs:devfrom
acusti:fix/wa-sqlite-changeset-apply-function-pointer

Conversation

@acusti
Copy link
Contributor

@acusti acusti commented Feb 13, 2026

Summary

Fixes #998

sqlite3.changeset_apply passes a conflict handler callback to the C function sqlite3changeset_apply. The current code passes a plain JS function, but cwrap declares all parameters as 'number', so the function is silently coerced to 0 (null pointer). This causes a RuntimeError: function signature mismatch whenever SQLite invokes the conflict callback during rebase rollback.

Changes

packages/@livestore/wa-sqlite/src/sqlite-api.js

  • Use Module.addFunction() to register the conflict handler as a proper WASM function pointer
  • The pointer is created lazily on the first changeset_apply call and cached for reuse, so environments that disallow dynamic WebAssembly compilation (e.g. Cloudflare Workers) don't crash during Factory() init if changeset_apply is never called

packages/@livestore/wa-sqlite/Makefile

  • Add -sALLOW_TABLE_GROWTH=1 to EMFLAGS_COMMON so the WASM function table can accommodate the addFunction call (the current binaries ship with table max == table min, leaving zero room for runtime function registration)

Notes

  • The WASM binaries will need to be rebuilt with the updated Makefile for the fix to work end-to-end. Without ALLOW_TABLE_GROWTH, addFunction fails because the function table can't grow.
  • The lazy initialization is important: addFunction calls convertJsFunctionToWasm, which dynamically compiles a small WASM module. This is fine in browsers and Node.js, but Cloudflare Workers disallow dynamic WebAssembly.Module() construction. By deferring to first use, environments that import but never call changeset_apply (e.g. Durable Objects using native SQLite) aren't affected.

Test plan

  • Verified fix resolves the RuntimeError: function signature mismatch crash in browser
  • Verified fix works in Cloudflare Durable Objects (no CompileError: Wasm code generation disallowed by embedder)
  • Verified all existing tests pass with the patched source + WASM table growth enabled

Responsible Disclosure

The code changes and description in this PR were created by Claude Code Opus 4.6. I reviewed the changes but I don’t have any experience with WASM, so I’m way out of my depth here. The first version of the fix for this created the onConflictPtr (const onConflictPtr = Module.addFunction(...)) directly in the IIFE (above the returned function), like:

    const onConflictPtr = Module.addFunction(
      (_pCtx, _eConflict, _pIter) => SQLITE_CHANGESET_REPLACE,
      'iiii'
    );

    return function(db, changesetData, options) {

but that triggered errors in my DO client: Error: (FiberFailure) CompileError: WebAssembly.Module(): Wasm code generation disallowed by embedder

Hence the lazy initialization approach taken here, which keeps the DO from crashing as long as it doesn’t hit the changeset_apply codepath.

The conflict handler passed to sqlite3changeset_apply was a plain JS
function, but cwrap declares all parameters as 'number'. The function
was silently coerced to 0 (null pointer), causing a "function signature
mismatch" RuntimeError whenever SQLite invoked the conflict callback
during rebase rollback.

Fix: use Module.addFunction() to register the conflict handler as a
proper WASM function pointer. The pointer is created lazily on first
changeset_apply call and cached for reuse, so environments that
disallow dynamic WebAssembly compilation (e.g. Cloudflare Workers)
don't crash during Factory() init if changeset_apply is never called.

Also add -sALLOW_TABLE_GROWTH to the Emscripten build flags so the
WASM function table can accommodate the new addFunction call (the
current binaries ship with table max == table min, leaving zero room).

Fixes livestorejs#998

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: wa-sqlite changeset_apply throws "function signature mismatch" during rebase rollback

1 participant