Skip to content

fix(sona): stop JS input from aborting Node or poisoning learned state (@ruvector/sona 0.1.9, ruvector-sona 0.2.2) - #1322

Merged
ruvnet merged 3 commits into
mainfrom
fix/sona-review-2026-11-04
Nov 4, 2026
Merged

ruvnet merged 3 commits into
mainfrom
fix/sona-review-2026-11-04

Conversation

@ruvnet

@ruvnet ruvnet commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

A review of @ruvector/sona 0.1.8 (306k downloads a month) found inputs from JavaScript that abort the Node process. A Rust panic cannot unwind through NAPI, so the process gets SIGABRT ("fatal runtime error: failed to initiate panic, error 5, aborting") and the caller cannot catch it. The review also found that a single NaN can silently ruin the learned state.

Input 0.1.8 / HEAD This PR
new SonaEngine(64).applyMicroLora([1,2,3]) SIGABRT throws input length 3 does not match hiddenDim 64
new SonaEngine(8).applyBaseLora(0, Array(20)) SIGABRT (index out of bounds) throws
SonaEngine.withConfig({hiddenDim:64, microLoraRank:3 or 0}) SIGABRT throws microLoraRank must be 1 or 2
withConfig({baseLoraRank:0}).applyBaseLora(...) all NaN (scale 0/0) identity
one trajectory with NaN quality, reward, embedding or activations instant-loop EWC++ Fisher becomes permanently NaN (micro-LoRA still learns, but the Fisher state is corrupt), and after forceLearn every base-LoRA output is NaN the trajectory is dropped
a.beginTrajectory(), then b.endTrajectory(id) lands in engine b, because the builder map was process-global and leaked any trajectory that was never ended ids are per engine

On microLoraRank, I chose to throw an error rather than clamp, to match the (1-2) contract in index.d.ts. The return types are now napi::Result<T>, which leaves the generated TS signatures unchanged.

Tests (red first, then green)

  • crates/sona/tests/robustness.rs: 4 new tests. All 4 failed on HEAD and all 4 pass now.
  • npm/packages/sona/test/basic.test.js: 5 new tests. On HEAD the whole file died with signal: 'SIGABRT'. Against a locally built binary it now passes 19/19.
  • cargo test -p ruvector-sona: 114 + 4 pass. cargo clippy -p ruvector-sona --all-features --all-targets -D warnings and the same with --features napi are both clean.

Versions and release notes

Not changed (reported only)

  • The AVX2 forward_simd path is never compiled in: the published x64 binary contains 0 vfmadd instructions, so enableSimd does nothing.
  • beginTrajectory with an embedding whose length differs from hiddenDim makes micro-LoRA skip the update silently.
  • Very large hiddenDim, trajectoryCapacity or patternClusters values make the allocation abort.
  • ReasoningBank patterns grow without bound across background cycles.
  • src/napi.rs is dead code: it is not in the module tree.
  • wasm.rs and ruvector-py call the same asserting core apply_micro_lora without checking the length.

🤖 Generated with claude-flow

@ruvector/sona 0.1.8 aborts the whole Node process (SIGABRT, uncatchable)
on ordinary bad input, because a Rust panic cannot unwind through NAPI:

- applyMicroLora(x) with x.length != hiddenDim (assert_eq in MicroLoRA)
- applyBaseLora(i, x) with x.length > hiddenDim (index out of bounds)
- withConfig({ microLoraRank: 0 | 3 }) (assert in MicroLoRA::new)

These now throw JS errors. Also fixed in the core crate:

- One trajectory with NaN/Inf quality, reward, embedding or activations
  left the instant-loop EWC++ Fisher permanently NaN and, after
  forceLearn, made every base-LoRA output NaN.
  Non-finite trajectories are now dropped at InstantLoop::on_trajectory.
- BaseLoRA::forward_layer is a no-op for rank 0 (scale was 0/0 = NaN)
  and for input/output not hiddenDim long (was an OOB panic).
- Trajectory builders were one process-global map: any engine could end
  another engine's trajectory, and unended ones leaked for the process
  lifetime. They are now per engine.

Bumps ruvector-sona 0.2.1 -> 0.2.2 and @ruvector/sona 0.1.8 -> 0.1.9,
and realigns the committed optionalDependencies / npm/* versions (0.1.5)
with the published ones.

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet
ruvnet force-pushed the fix/sona-review-2026-11-04 branch from b689dfc to b9d62a5 Compare November 4, 2026 06:57
The unpinned install now resolves 3.10.x, whose --use-napi-cross setup
fails on aarch64-unknown-linux-gnu with 'Cannot find native binding'
(reproduced twice on this PR). 3.7.3 is what the last green run
(2026-07-17) used; pinning keeps the old-glibc napi-cross toolchain.

Co-Authored-By: claude-flow <ruv@ruv.net>
…-napi-cross

--use-napi-cross still fails with @napi-rs/cli pinned to 3.7.3 (same
'Cannot find native binding' while setting up the toolchain), so the
CLI version was not the cause. Use the gcc-aarch64-linux-gnu linker,
as build-diskann.yml does, on ubuntu-22.04.

Verified locally: produces an aarch64 ELF sona.linux-arm64-gnu.node,
highest symbol GLIBC_2.34 (Ubuntu 22.04+, Debian 12, RHEL 9).

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet
ruvnet merged commit 8d39dee into main Nov 4, 2026
92 of 93 checks passed
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.

1 participant