Skip to content

WASM callback merge failure#1996

Draft
cursor[bot] wants to merge 1 commit intofeat/sync-wasm-merge-callbackfrom
cursor/wasm-callback-merge-failure-23ca
Draft

WASM callback merge failure#1996
cursor[bot] wants to merge 1 commit intofeat/sync-wasm-merge-callbackfrom
cursor/wasm-callback-merge-failure-23ca

Conversation

@cursor
Copy link
Contributor

@cursor cursor bot commented Feb 14, 2026

Fix: WASM callback trait always returns failure

Description

This PR fixes a bug where the WasmMergeCallback trait implementation for RuntimeMergeCallback always returned MergeError::WasmCallbackFailed, preventing custom CRDT merges from being correctly dispatched and executed via WASM.

The RuntimeMergeCallback's trait methods (merge_custom and merge_root_state) now correctly acquire mutable access to the WASM Store (by wrapping it in a Mutex) and invoke the underlying WASM merge functions, ensuring custom CRDT types are merged as intended instead of falling back to LWW behavior.

Test plan

The following commands were run to verify the changes:

  • cargo build
  • cargo clippy
  • cargo fmt
  • cargo test

All tests passed, and the code compiles without warnings. No new end-to-end test cases were added, as the fix addresses an internal logic error in the callback mechanism.

Documentation update

None


…hods

The WasmMergeCallback trait requires &self methods, but the runtime
implementation needed &mut self to access the WASM store. This caused
both trait methods (merge_custom and merge_root_state) to always return
MergeError::WasmCallbackFailed, making custom CRDT merges via the trait
dispatch path fail and fall back to LWW behavior.

Fix by wrapping the Store in a Mutex<Store> to enable interior mutability.
The trait methods now properly acquire the lock and invoke WASM merge
functions instead of returning immediate errors.

The _mut variants are kept for backwards compatibility but now delegate
to the same internal implementation.
@cursor
Copy link
Contributor Author

cursor bot commented Feb 14, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@github-actions
Copy link

Your PR title does not adhere to the Conventional Commits convention:

<type>(<scope>): <subject>

Common errors to avoid:

  1. The title must be in lower case.
  2. Allowed type values are: build, ci, docs, feat, fix, perf, refactor, test.

Copy link

@meroreviewer meroreviewer bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Code Reviewer

Reviewed by 3 agents | Quality score: 94% | Review time: 157.3s

🟡 2 warnings, 💡 3 suggestions, 📝 1 nitpicks. See inline comments.


🤖 Generated by AI Code Reviewer | Review ID: review-52ec967b

@@ -420,6 +357,64 @@ impl RuntimeMergeCallback {
Err(MergeError::WasmCallbackFailed { message: error_str })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Bug fix lacks regression test

This PR fixes a bug where callbacks always returned failure, but no test demonstrates the fix works; a regression could silently reintroduce the bug.

Suggested fix:

Add a unit test that creates a RuntimeMergeCallback with a mock WASM module and verifies merge_custom/merge_root_state return Ok() on valid input.

@@ -201,9 +205,9 @@ impl RuntimeMergeCallback {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Unbounded memory allocation from WASM-controlled length

The read_from_wasm method allocates vec![0u8; len as usize] where len comes from WASM memory (via WasmMergeResult); a malicious WASM module could return a huge length to cause memory exhaustion (DoS).

Suggested fix:

Add a maximum length check (e.g., `const MAX_MERGE_DATA_LEN: u64 = 16 * 1024 * 1024;`) and return an error if exceeded.

local: &[u8],
remote: &[u8],
) -> Result<Vec<u8>, MergeError> {
///
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Mutex held during WASM execution without timeout

The store mutex is held while calling into WASM; without the timeout (noted as TODO for Issue #1780), a malicious or buggy WASM module could block indefinitely, preventing other merge operations.

Suggested fix:

Prioritize implementing the timeout handling mentioned in the TODO to prevent deadlock/DoS scenarios.

remote: &[u8],
) -> Result<Vec<u8>, MergeError> {
///
/// Acquires the store mutex internally.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Poisoned lock error message lacks diagnostic detail

When a Mutex is poisoned (due to a prior panic while holding the lock), the error message says 'Failed to acquire store lock' but doesn't indicate the Store may be in an inconsistent state, making debugging harder.

Suggested fix:

Include 'lock poisoned - prior panic occurred' in the message to help diagnose state corruption issues.

local: &[u8],
remote: &[u8],
) -> Result<Vec<u8>, MergeError> {
///
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Consider extracting lock acquisition into a helper

The same lock-acquisition-with-error-mapping pattern is duplicated in call_merge_root_state and call_merge_custom; a small helper would reduce repetition.

Suggested fix:

Extract a method like `fn acquire_store(&self) -> Result<MutexGuard<Store>, MergeError>` to centralize the lock acquisition and error mapping.

remote: &[u8],
type_name: &str,
) -> Result<Vec<u8>, MergeError> {
self.call_merge_custom(local, remote, type_name)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Nit: _mut methods no longer require mutable self

The merge_root_state_mut and merge_custom_mut methods retain &mut self for API stability but internally use interior mutability; the doc comment could clarify that mutable self is no longer semantically required.

Suggested fix:

Update doc comment to: 'This method is provided for backwards compatibility. The `&mut self` signature is preserved but interior mutability is used internally.'

@github-actions
Copy link

This pull request has been automatically marked as stale. If this pull request is still relevant, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize reviewing it yet. Your contribution is very much appreciated.

@github-actions github-actions bot added the Stale label Feb 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant