git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
34 KiB
BMSSP WASM Integration Security Review
Date: 2026-01-25 Auditor: Security Architecture Agent Scope: Comprehensive security review of BMSSP WASM integration for j-tree operations Version: ADR-002-addendum-bmssp-integration (Proposed) Classification: Internal Security Document
Executive Summary
This security review examines the proposed integration of @ruvnet/bmssp (Bounded Multi-Source Shortest Path) WASM module with the ruvector-mincut j-tree hierarchy. The review covers WASM sandbox security, FFI boundary safety, input validation, resource exhaustion vectors, supply chain risks, error handling, and cryptographic considerations.
Risk Summary Matrix
| Category | Critical | High | Medium | Low | Info |
|---|---|---|---|---|---|
| WASM Sandbox Security | 0 | 1 | 2 | 1 | 2 |
| FFI Boundary Safety | 0 | 2 | 1 | 2 | 1 |
| Input Validation | 0 | 1 | 3 | 2 | 1 |
| Resource Exhaustion | 0 | 1 | 2 | 1 | 2 |
| Supply Chain | 0 | 1 | 1 | 2 | 2 |
| Error Handling | 0 | 0 | 2 | 2 | 1 |
| Cryptographic | 0 | 0 | 1 | 1 | 2 |
| Total | 0 | 6 | 12 | 11 | 11 |
Overall Risk Rating: MEDIUM-HIGH
The integration introduces significant FFI boundary complexity and external dependency risks that require careful mitigation before production deployment.
1. WASM Sandbox Security
1.1 Memory Isolation Analysis
Current Implementation (ruvector-mincut/src/wasm/agentic.rs):
// FINDING: Static mutable global state pattern
#[cfg(target_arch = "wasm32")]
pub mod ffi {
static mut INSTANCE: Option<AgenticMinCut> = None;
#[no_mangle]
pub extern "C" fn mincut_init(num_vertices: u16, num_edges: u16, strategy: u8) {
unsafe {
// Direct mutation of global state
INSTANCE = Some(instance);
}
}
}
Identified Issues:
| ID | Severity | Issue | Location | CVSS 3.1 |
|---|---|---|---|---|
| WASM-SEC-001 | High | Static mutable state without synchronization | agentic.rs:90-106 |
6.5 |
| WASM-SEC-002 | Medium | No memory isolation between BMSSP instances | Proposed integration | 5.3 |
| WASM-SEC-003 | Medium | WASM linear memory shared across all graph operations | simd.rs:14-46 |
4.8 |
| WASM-SEC-004 | Low | No memory page limit enforcement | All WASM modules | 3.7 |
WASM-SEC-001 Analysis:
The current FFI implementation uses static mut INSTANCE which is not thread-safe. While WASM itself is single-threaded, the proposed BMSSP integration adds complexity:
Risk Scenario:
1. JavaScript calls mincut_init() with graph A
2. Before completion, another call modifies INSTANCE for graph B
3. Graph A computation uses corrupted state
Mitigation Required:
// RECOMMENDED: Use RefCell or OnceCell for safer state management
use core::cell::RefCell;
thread_local! {
static INSTANCE: RefCell<Option<AgenticMinCut>> = RefCell::new(None);
}
#[no_mangle]
pub extern "C" fn mincut_init(num_vertices: u16, num_edges: u16, strategy: u8) {
INSTANCE.with(|instance| {
let mut inst = instance.borrow_mut();
// Safe state mutation
*inst = Some(AgenticMinCut::new());
inst.as_mut().unwrap().init(num_vertices, num_edges, strategy.into());
})
}
1.2 BMSSP WASM Memory Model
Proposed BMSSP Integration (from ADR-002-addendum-bmssp-integration.md):
class WasmGraph {
constructor(vertices: number, directed: boolean);
add_edge(from: number, to: number, weight: number): boolean;
compute_shortest_paths(source: number): Float64Array;
free(): void;
}
Security Concerns:
-
Memory Ownership Transfer:
Float64Arrayreturned fromcompute_shortest_pathspoints to WASM linear memory. If the caller retains this reference afterfree(), use-after-free occurs. -
Double-Free Vulnerability: No mechanism to prevent multiple
free()calls on the same instance. -
Memory Leak Vector: JavaScript garbage collection does not automatically call
free()on WASM objects.
Recommended Pattern:
// SECURE: Wrap in managed object with destructor tracking
class SecureBmsspGraph implements Disposable {
private graph: WasmGraph | null;
private disposed = false;
constructor(vertices: number, directed: boolean) {
this.graph = new WasmGraph(vertices, directed);
}
[Symbol.dispose](): void {
if (!this.disposed && this.graph) {
this.graph.free();
this.graph = null;
this.disposed = true;
}
}
computeShortestPaths(source: number): Float64Array {
if (this.disposed) {
throw new Error('Graph already disposed');
}
// Copy data out of WASM memory to prevent use-after-free
const wasmResult = this.graph!.compute_shortest_paths(source);
return Float64Array.from(wasmResult);
}
}
1.3 Buffer Overflow in FFI Boundary
SIMD Operations (simd.rs:13-46):
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn simd_popcount(bits: &[u64; 4]) -> u32 {
unsafe {
// Load 128-bit chunks
let v0 = v128_load(bits.as_ptr() as *const v128);
let v1 = v128_load(bits.as_ptr().add(2) as *const v128);
// ...
}
}
Analysis:
- Fixed-size array
[u64; 4]ensures bounds are compile-time verified - No runtime validation needed for this pattern
- Status: SECURE
XOR Operation (simd.rs:56-75):
#[cfg(target_arch = "wasm32")]
pub fn simd_xor(a: &BitSet256, b: &BitSet256) -> BitSet256 {
unsafe {
let mut result = BitSet256::new();
let a0 = v128_load(a.bits.as_ptr() as *const v128);
// Fixed-size struct, bounds guaranteed
// ...
}
}
Analysis:
BitSet256has fixed[u64; 4]internal storage- Pointer arithmetic is bounded by struct layout
- Status: SECURE
2. Input Validation
2.1 Vertex ID Bounds Checking
Current State (compact/mod.rs):
impl BitSet256 {
#[inline(always)]
pub fn insert(&mut self, v: CompactVertexId) {
let idx = (v / 64) as usize;
let bit = v % 64;
if idx < 4 { // BOUNDS CHECK PRESENT
self.bits[idx] |= 1u64 << bit;
}
}
#[inline(always)]
pub fn contains(&self, v: CompactVertexId) -> bool {
let idx = (v / 64) as usize;
let bit = v % 64;
idx < 4 && (self.bits[idx] & (1u64 << bit)) != 0 // BOUNDS CHECK PRESENT
}
}
Analysis: BitSet256 properly validates vertex IDs against MAX_VERTICES_PER_CORE (256).
BMSSP Integration Gap:
| ID | Severity | Issue | Impact |
|---|---|---|---|
| INPUT-001 | High | No validation for BMSSP vertex IDs exceeding u32::MAX | Integer overflow |
| INPUT-002 | Medium | Missing validation in add_edge() for self-loops |
Algorithm correctness |
| INPUT-003 | Medium | No validation for duplicate edge insertion | Memory waste |
| INPUT-004 | Low | Vertex count mismatch between BMSSP and native | Incorrect results |
Required Validation for BMSSP Integration:
pub struct BmsspJTreeLevel {
wasm_graph: WasmGraph,
vertex_count: usize,
// Add validation bounds
max_vertex_id: u32,
}
impl BmsspJTreeLevel {
pub fn add_edge(&mut self, src: u32, tgt: u32, weight: f64) -> Result<(), MinCutError> {
// Vertex bounds validation
if src >= self.max_vertex_id || tgt >= self.max_vertex_id {
return Err(MinCutError::InvalidVertex(src.max(tgt) as u64));
}
// Self-loop validation
if src == tgt {
return Err(MinCutError::InvalidEdge(src as u64, tgt as u64));
}
// Weight validation (see 2.2)
Self::validate_weight(weight)?;
self.wasm_graph.add_edge(src, tgt, weight);
Ok(())
}
}
2.2 Edge Weight Validation
Critical Floating-Point Cases:
| Value | Risk | Impact |
|---|---|---|
NaN |
Algorithm produces undefined results | Incorrect cuts |
Infinity |
Path computation never terminates or overflows | DoS |
-Infinity |
Negative cycle detection fails | Incorrect results |
| Negative weights | Bellman-Ford required; Dijkstra incorrect | Algorithm mismatch |
| Subnormal values | Performance degradation | Timing side-channel |
| Zero | Division by zero in some algorithms | Crash |
Required Validation:
impl BmsspJTreeLevel {
fn validate_weight(weight: f64) -> Result<(), MinCutError> {
// Check for NaN
if weight.is_nan() {
return Err(MinCutError::InvalidParameter(
"Edge weight cannot be NaN".to_string()
));
}
// Check for infinity
if weight.is_infinite() {
return Err(MinCutError::InvalidParameter(
"Edge weight cannot be infinite".to_string()
));
}
// Check for negative weights (BMSSP assumes non-negative)
if weight < 0.0 {
return Err(MinCutError::InvalidParameter(
format!("Edge weight {} must be non-negative", weight)
));
}
// Check for subnormal (optional, for performance)
if weight != 0.0 && weight.abs() < f64::MIN_POSITIVE {
// Normalize to zero or reject
return Err(MinCutError::InvalidParameter(
"Subnormal edge weights not supported".to_string()
));
}
Ok(())
}
}
2.3 Graph Size Limits
Current Limits (compact/mod.rs):
pub const MAX_VERTICES_PER_CORE: usize = 256;
pub const MAX_EDGES_PER_CORE: usize = 384;
BMSSP Proposed Limits (ADR-002-addendum):
| Metric | Value | Memory Impact |
|---|---|---|
| Max vertices (browser) | 100K | ~4MB per graph |
| Max vertices (Node.js) | 1M | ~40MB per graph |
| Max edges | Unbounded | Risk: OOM |
Recommended Limits:
pub struct BmsspConfig {
/// Maximum vertices allowed (default: 1M)
pub max_vertices: u32,
/// Maximum edges allowed (default: 10M)
pub max_edges: u32,
/// Maximum memory allocation in bytes (default: 100MB)
pub max_memory_bytes: usize,
/// Maximum path cache entries (default: 10K)
pub max_cache_entries: usize,
}
impl Default for BmsspConfig {
fn default() -> Self {
Self {
max_vertices: 1_000_000,
max_edges: 10_000_000,
max_memory_bytes: 100 * 1024 * 1024, // 100MB
max_cache_entries: 10_000,
}
}
}
3. Resource Exhaustion
3.1 Memory Limits for Large Graphs
Attack Vector:
// Malicious input: Create graph with maximum vertices
const graph = new WasmGraph(0xFFFFFFFF, false);
// WASM memory allocation: 4GB * 8 bytes = 32GB
// Result: Browser/Node.js OOM crash
Mitigation:
impl BmsspJTreeLevel {
pub fn new(vertex_count: usize, config: &BmsspConfig) -> Result<Self, MinCutError> {
// Memory estimation: vertices * sizeof(f64) * expected_edges_per_vertex
let estimated_memory = vertex_count
.checked_mul(8) // sizeof(f64)
.and_then(|v| v.checked_mul(10)) // avg 10 edges/vertex
.ok_or_else(|| MinCutError::CapacityExceeded(
"Memory estimation overflow".to_string()
))?;
if estimated_memory > config.max_memory_bytes {
return Err(MinCutError::CapacityExceeded(
format!("Estimated memory {}B exceeds limit {}B",
estimated_memory, config.max_memory_bytes)
));
}
if vertex_count > config.max_vertices as usize {
return Err(MinCutError::CapacityExceeded(
format!("Vertex count {} exceeds limit {}",
vertex_count, config.max_vertices)
));
}
// Proceed with allocation
Ok(Self { /* ... */ })
}
}
3.2 CPU Time Limits for Pathological Inputs
Attack Vectors:
| Attack | Complexity | Example |
|---|---|---|
| Dense complete graph | O(n^2 log n) | K_n with n=10K |
| Long chain graph | O(n^2) worst case | Linear path graph |
| Repeated queries same source | Cache miss flood | Source cycling |
Pathological Graph Examples:
1. Complete Graph K_n:
- n=10,000 vertices
- 50M edges
- Single SSSP: ~500ms
- All-pairs: ~5000 seconds
2. Adversarial Sparse Graph:
- Carefully constructed to maximize relaxation steps
- Can cause O(V*E) behavior in Dijkstra variants
Mitigation - Timeout Mechanism:
use std::time::{Duration, Instant};
pub struct TimeLimitedBmssp {
inner: BmsspJTreeLevel,
timeout: Duration,
}
impl TimeLimitedBmssp {
pub fn compute_shortest_paths(&self, source: u32) -> Result<Vec<f64>, MinCutError> {
let start = Instant::now();
// For WASM, we cannot interrupt mid-computation
// Instead, validate complexity before execution
let estimated_ops = self.estimate_operations(source);
let estimated_time = Duration::from_nanos(estimated_ops * 10); // ~10ns per op
if estimated_time > self.timeout {
return Err(MinCutError::CapacityExceeded(
format!("Estimated time {:?} exceeds timeout {:?}",
estimated_time, self.timeout)
));
}
let result = self.inner.compute_shortest_paths(source);
if start.elapsed() > self.timeout {
// Log warning for monitoring
tracing::warn!(
source = source,
elapsed = ?start.elapsed(),
timeout = ?self.timeout,
"BMSSP computation exceeded timeout"
);
}
Ok(result)
}
fn estimate_operations(&self, _source: u32) -> u64 {
let n = self.inner.vertex_count as u64;
let m = self.inner.edge_count as u64;
// BMSSP complexity: O(m * log^(2/3) n)
let log_n = (n as f64).ln().max(1.0);
let log_factor = log_n.powf(2.0 / 3.0);
(m as f64 * log_factor) as u64
}
}
3.3 Cache Size Bounds
Current Cache (from ADR):
pub struct BmsspJTreeLevel {
path_cache: HashMap<(VertexId, VertexId), f64>,
// ...
}
Attack Vector:
1. Query all n*(n-1)/2 pairs
2. Cache grows to O(n^2) entries
3. For n=100K: 10B entries * 24 bytes = 240GB
Mitigation - LRU Cache with Bounded Size:
use lru::LruCache;
use std::num::NonZeroUsize;
pub struct BmsspJTreeLevel {
path_cache: LruCache<(VertexId, VertexId), f64>,
cache_hits: u64,
cache_misses: u64,
}
impl BmsspJTreeLevel {
pub fn new(config: &BmsspConfig) -> Self {
let cache_capacity = NonZeroUsize::new(config.max_cache_entries)
.unwrap_or(NonZeroUsize::new(10_000).unwrap());
Self {
path_cache: LruCache::new(cache_capacity),
cache_hits: 0,
cache_misses: 0,
}
}
pub fn min_cut(&mut self, s: VertexId, t: VertexId) -> f64 {
// Normalize key for undirected graphs
let key = if s <= t { (s, t) } else { (t, s) };
if let Some(&cached) = self.path_cache.get(&key) {
self.cache_hits += 1;
return cached;
}
self.cache_misses += 1;
// Compute and cache
let distances = self.wasm_graph.compute_shortest_paths(s as u32);
let cut_value = distances[t as usize];
self.path_cache.put(key, cut_value);
cut_value
}
}
4. Supply Chain Security
4.1 @ruvnet/bmssp Package Integrity
Package Analysis:
| Attribute | Value | Risk |
|---|---|---|
| npm package | @ruvnet/bmssp |
Scoped package (trusted author) |
| Source repository | https://github.com/ruvnet/bmssp | Verify ownership |
| WASM binary size | 27KB | Small attack surface |
| Dependencies | None (standalone WASM) | Low transitive risk |
Verification Steps Required:
# 1. Verify package signature (if using npm provenance)
npm audit signatures @ruvnet/bmssp
# 2. Verify WASM binary hash
sha256sum node_modules/@ruvnet/bmssp/bmssp.wasm
# Expected: [document expected hash in SECURITY.md]
# 3. Verify source matches binary
cd node_modules/@ruvnet/bmssp
wasm-decompile bmssp.wasm > decompiled.wat
# Compare with reference build
Package Lock Recommendation:
// package.json
{
"dependencies": {
"@ruvnet/bmssp": "1.0.0"
},
"overrides": {
"@ruvnet/bmssp": "$@ruvnet/bmssp"
}
}
// .npmrc
package-lock=true
save-exact=true
4.2 Known Vulnerabilities Check
| ID | Severity | Issue | Status |
|---|---|---|---|
| SUPPLY-001 | High | No SBOM (Software Bill of Materials) | Action Required |
| SUPPLY-002 | Medium | WASM binary not reproducibly built | Action Required |
| SUPPLY-003 | Low | No npm provenance attestation | Recommended |
| SUPPLY-004 | Info | No security.txt in package | Informational |
Required Actions:
- Generate SBOM:
# Using syft
syft dir:node_modules/@ruvnet/bmssp -o spdx-json > bmssp-sbom.json
- Verify Reproducible Build:
# Clone source
git clone https://github.com/ruvnet/bmssp.git
cd bmssp
# Build with deterministic settings
RUSTFLAGS="-C lto=thin" wasm-pack build --release
# Compare hash
sha256sum pkg/bmssp_bg.wasm
4.3 WASM Binary Verification
Runtime Verification:
import { createHash } from 'crypto';
const EXPECTED_WASM_HASH = 'sha256:abc123...'; // Document this
async function verifyBmsspWasm(): Promise<boolean> {
const wasmBytes = await fetch('/node_modules/@ruvnet/bmssp/bmssp.wasm')
.then(r => r.arrayBuffer());
const hash = createHash('sha256')
.update(new Uint8Array(wasmBytes))
.digest('hex');
const expected = EXPECTED_WASM_HASH.replace('sha256:', '');
if (hash !== expected) {
console.error(`BMSSP WASM hash mismatch!
Expected: ${expected}
Got: ${hash}`);
return false;
}
return true;
}
// Call before initializing BMSSP
if (!await verifyBmsspWasm()) {
throw new Error('BMSSP WASM integrity check failed');
}
5. Error Handling
5.1 Panic Safety Across FFI Boundary
Current Panic Handling (lib.rs:116):
#![cfg_attr(not(feature = "wasm"), deny(unsafe_code))]
Issue: Panics in WASM are not handled - they become WASM traps that JavaScript cannot catch gracefully.
Current FFI Error Handling (agentic.rs:148-149):
#[no_mangle]
pub extern "C" fn mincut_get_result() -> u16 {
unsafe { INSTANCE.as_ref().map(|i| i.min_cut()).unwrap_or(u16::MAX) }
}
Analysis:
- Uses
unwrap_orfor graceful degradation (good) - Returns sentinel value (u16::MAX) on error
- No panic path in this function
Identified Issues:
| ID | Severity | Issue | Location |
|---|---|---|---|
| ERR-001 | Medium | Panics in test code can propagate | paper_impl.rs:613, 654 |
| ERR-002 | Medium | No structured error return from FFI | All FFI functions |
| ERR-003 | Low | Error messages may leak internal state | Error formatting |
Recommended Error Handling Pattern:
/// Error codes for FFI boundary
#[repr(u8)]
pub enum BmsspErrorCode {
Success = 0,
InvalidVertex = 1,
InvalidWeight = 2,
OutOfMemory = 3,
Timeout = 4,
InternalError = 255,
}
/// Result structure for FFI
#[repr(C)]
pub struct BmsspResult {
pub error_code: u8,
pub result: u16,
}
#[no_mangle]
pub extern "C" fn mincut_compute(s: u16, t: u16) -> BmsspResult {
// Use catch_unwind to prevent panics crossing FFI
let result = std::panic::catch_unwind(|| {
unsafe {
INSTANCE.as_mut()
.map(|i| i.min_cut(s as usize, t as usize))
.unwrap_or(u16::MAX)
}
});
match result {
Ok(value) => BmsspResult {
error_code: BmsspErrorCode::Success as u8,
result: value,
},
Err(_) => BmsspResult {
error_code: BmsspErrorCode::InternalError as u8,
result: u16::MAX,
},
}
}
5.2 Graceful Degradation on WASM Failure
Fallback Strategy:
/// Hybrid cut computation with fallback
pub struct HybridMinCut {
/// Primary: BMSSP WASM acceleration
bmssp: Option<BmsspJTreeLevel>,
/// Fallback: Native Rust implementation
native: SubpolynomialMinCut,
/// Failure count for circuit breaker
wasm_failures: AtomicU32,
/// Circuit breaker threshold
failure_threshold: u32,
}
impl HybridMinCut {
pub fn min_cut(&mut self, s: VertexId, t: VertexId) -> CutResult {
// Check circuit breaker
if self.wasm_failures.load(Ordering::Relaxed) >= self.failure_threshold {
return self.native_fallback(s, t);
}
// Try WASM first
if let Some(ref mut bmssp) = self.bmssp {
match bmssp.try_min_cut(s, t) {
Ok(result) => {
// Reset failure count on success
self.wasm_failures.store(0, Ordering::Relaxed);
return result;
}
Err(e) => {
// Increment failure count
self.wasm_failures.fetch_add(1, Ordering::Relaxed);
tracing::warn!(error = ?e, "BMSSP failed, using fallback");
}
}
}
self.native_fallback(s, t)
}
fn native_fallback(&self, s: VertexId, t: VertexId) -> CutResult {
CutResult::exact(self.native.min_cut_between(s, t))
}
}
5.3 Information Leakage in Error Messages
Current Error Types (error.rs):
#[derive(Error, Debug)]
pub enum MinCutError {
#[error("Invalid vertex ID: {0}")]
InvalidVertex(u64),
// Exposes internal vertex ID representation
#[error("Internal algorithm error: {0}")]
InternalError(String),
// May expose internal state via string
}
Recommended Sanitization:
impl MinCutError {
/// Return user-safe error message without internal details
pub fn user_message(&self) -> &'static str {
match self {
MinCutError::EmptyGraph => "Graph is empty",
MinCutError::InvalidVertex(_) => "Invalid vertex identifier",
MinCutError::InvalidEdge(_, _) => "Invalid edge specification",
MinCutError::DisconnectedGraph => "Graph is not connected",
MinCutError::CutSizeExceeded(_, _) => "Result exceeds supported size",
MinCutError::InvalidEpsilon(_) => "Invalid approximation parameter",
MinCutError::InvalidParameter(_) => "Invalid parameter value",
MinCutError::CallbackError(_) => "Callback execution failed",
MinCutError::InternalError(_) => "Internal error occurred",
MinCutError::ConcurrentModification => "Concurrent modification detected",
MinCutError::CapacityExceeded(_) => "Capacity limit exceeded",
MinCutError::SerializationError(_) => "Data serialization failed",
}
}
}
// For FFI: return only opaque error codes
#[no_mangle]
pub extern "C" fn mincut_get_last_error() -> u8 {
// Return error code, not detailed message
thread_local! {
static LAST_ERROR: Cell<u8> = Cell::new(0);
}
LAST_ERROR.with(|e| e.get())
}
6. Cryptographic Considerations
6.1 Random Number Generation for Sampling
Current RNG Usage (snn/attractor.rs:440-442):
let mut rng_state = seed.wrapping_add(0x9e3779b97f4a7c15);
// ...
rng_state = rng_state.wrapping_mul(0x5851f42d4c957f2d).wrapping_add(1);
Analysis:
- Uses simple LCG (Linear Congruential Generator)
- Constants from SplitMix64
- Not cryptographically secure (by design - for performance)
BMSSP Sampling Requirements:
| Use Case | CSPRNG Required | Rationale |
|---|---|---|
| Vertex sampling for testing | No | Reproducibility more important |
| Random pivot selection | No | Any distribution works |
| Cryptographic commitments | Yes | Must be unpredictable |
| Audit trail generation | Yes | Prevent manipulation |
Recommendation:
/// RNG wrapper with appropriate strength for use case
pub enum BmsspRng {
/// Fast, reproducible (default for graph algorithms)
Fast(FastRng),
/// Cryptographically secure (for audit/security features)
Secure(SecureRng),
}
impl BmsspRng {
/// Use fast RNG for graph algorithm internals
pub fn for_algorithm() -> Self {
BmsspRng::Fast(FastRng::from_seed([0u8; 8]))
}
/// Use secure RNG for audit trail
pub fn for_audit() -> Self {
BmsspRng::Secure(SecureRng::new())
}
}
6.2 Determinism Requirements
J-Tree Algorithm Determinism:
| Operation | Must be Deterministic | Rationale |
|---|---|---|
| Shortest path computation | Yes | Reproducible results |
| Cache key generation | Yes | Consistent lookups |
| Witness generation | Yes | Verifiable proofs |
| Performance sampling | No | Statistical validity |
Ensuring Determinism:
impl BmsspJTreeLevel {
/// Compute shortest paths with deterministic tie-breaking
pub fn compute_shortest_paths_deterministic(&self, source: u32) -> Vec<f64> {
// BMSSP uses Dijkstra variant - inherently deterministic
// for same input graph and source
// Ensure vertex iteration order is deterministic
let result = self.wasm_graph.compute_shortest_paths(source);
// Verify determinism in debug builds
#[cfg(debug_assertions)]
{
let result2 = self.wasm_graph.compute_shortest_paths(source);
assert_eq!(result, result2, "Non-deterministic shortest path computation");
}
result
}
}
7. Recommended Mitigations Summary
7.1 Immediate Actions (P0 - Before Integration)
| ID | Action | Effort | Impact |
|---|---|---|---|
| P0-1 | Add vertex ID bounds validation | 2 hours | High |
| P0-2 | Add edge weight validation (NaN, Inf, negative) | 2 hours | High |
| P0-3 | Implement memory allocation limits | 4 hours | High |
| P0-4 | Document expected WASM binary hash | 1 hour | Medium |
| P0-5 | Add catch_unwind to FFI functions |
4 hours | Medium |
7.2 Short-Term Actions (P1 - First Release)
| ID | Action | Effort | Impact |
|---|---|---|---|
| P1-1 | Implement LRU cache with bounded size | 4 hours | Medium |
| P1-2 | Add timeout estimation for operations | 8 hours | Medium |
| P1-3 | Create SBOM for BMSSP package | 2 hours | Low |
| P1-4 | Implement circuit breaker for WASM failures | 4 hours | Medium |
| P1-5 | Add memory ownership wrapper for JavaScript | 4 hours | Medium |
7.3 Long-Term Actions (P2 - Future Releases)
| ID | Action | Effort | Impact |
|---|---|---|---|
| P2-1 | Implement reproducible WASM build verification | 1 week | Medium |
| P2-2 | Add fuzzing targets for BMSSP integration | 1 week | Medium |
| P2-3 | Consider WASM Component Model migration | 2 weeks | Low |
| P2-4 | Implement comprehensive audit logging | 1 week | Low |
8. Code Changes Required
8.1 New File: src/wasm/bmssp_security.rs
//! Security wrappers for BMSSP WASM integration
//!
//! Provides input validation, resource limits, and error handling
//! for safe BMSSP integration.
use crate::error::{MinCutError, Result};
use std::time::{Duration, Instant};
/// Security configuration for BMSSP integration
#[derive(Debug, Clone)]
pub struct BmsspSecurityConfig {
/// Maximum vertices allowed
pub max_vertices: u32,
/// Maximum edges allowed
pub max_edges: u32,
/// Maximum memory in bytes
pub max_memory_bytes: usize,
/// Maximum cache entries
pub max_cache_entries: usize,
/// Operation timeout
pub timeout: Duration,
/// Enable WASM binary verification
pub verify_wasm_hash: bool,
/// Expected WASM binary hash (SHA-256)
pub expected_wasm_hash: Option<String>,
}
impl Default for BmsspSecurityConfig {
fn default() -> Self {
Self {
max_vertices: 1_000_000,
max_edges: 10_000_000,
max_memory_bytes: 100 * 1024 * 1024,
max_cache_entries: 10_000,
timeout: Duration::from_secs(30),
verify_wasm_hash: true,
expected_wasm_hash: None,
}
}
}
/// Validate edge weight for BMSSP compatibility
pub fn validate_edge_weight(weight: f64) -> Result<()> {
if weight.is_nan() {
return Err(MinCutError::InvalidParameter(
"Edge weight cannot be NaN".into()
));
}
if weight.is_infinite() {
return Err(MinCutError::InvalidParameter(
"Edge weight cannot be infinite".into()
));
}
if weight < 0.0 {
return Err(MinCutError::InvalidParameter(
"Edge weight must be non-negative".into()
));
}
Ok(())
}
/// Validate vertex ID is within bounds
pub fn validate_vertex_id(vertex: u32, max_vertices: u32) -> Result<()> {
if vertex >= max_vertices {
return Err(MinCutError::InvalidVertex(vertex as u64));
}
Ok(())
}
/// Estimate memory usage for graph
pub fn estimate_memory_usage(vertices: usize, edges: usize) -> usize {
// Vertex array: vertices * sizeof(f64)
let vertex_memory = vertices.saturating_mul(8);
// Edge list: edges * (2 * sizeof(u32) + sizeof(f64))
let edge_memory = edges.saturating_mul(16);
// Cache overhead estimate
let cache_overhead = vertices.saturating_mul(24);
vertex_memory
.saturating_add(edge_memory)
.saturating_add(cache_overhead)
}
8.2 Updates to src/wasm/mod.rs
//! WASM bindings and optimizations for agentic chip
//!
//! Provides:
//! - SIMD-accelerated boundary computation
//! - Agentic chip interface
//! - Inter-core messaging
//! - BMSSP security wrappers (new)
pub mod agentic;
pub mod simd;
pub mod bmssp_security; // Add this line
pub use agentic::*;
pub use simd::*;
pub use bmssp_security::*; // Add this line
9. Testing Requirements
9.1 Security Test Cases
#[cfg(test)]
mod security_tests {
use super::*;
#[test]
fn test_nan_weight_rejected() {
let result = validate_edge_weight(f64::NAN);
assert!(result.is_err());
}
#[test]
fn test_infinity_weight_rejected() {
let result = validate_edge_weight(f64::INFINITY);
assert!(result.is_err());
}
#[test]
fn test_negative_weight_rejected() {
let result = validate_edge_weight(-1.0);
assert!(result.is_err());
}
#[test]
fn test_vertex_bounds_check() {
let result = validate_vertex_id(100, 50);
assert!(result.is_err());
}
#[test]
fn test_memory_estimation_overflow() {
let mem = estimate_memory_usage(usize::MAX, usize::MAX);
// Should not panic, should saturate
assert!(mem <= usize::MAX);
}
}
9.2 Fuzzing Targets
// fuzz/fuzz_targets/bmssp_input.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if data.len() < 16 { return; }
// Parse fuzzer input
let vertex_count = u32::from_le_bytes(data[0..4].try_into().unwrap());
let edge_count = u32::from_le_bytes(data[4..8].try_into().unwrap());
// Validate with security checks
let config = BmsspSecurityConfig::default();
let _ = validate_vertex_id(vertex_count, config.max_vertices);
let _ = estimate_memory_usage(vertex_count as usize, edge_count as usize);
});
10. Verification Checklist
Pre-Integration Checklist
- All P0 mitigations implemented
- WASM binary hash documented
- Input validation tests passing
- Memory limit tests passing
- Panic safety verified with
catch_unwind - No
unwrap()in FFI code - Error codes documented for JavaScript consumers
- SBOM generated for BMSSP package
Pre-Production Checklist
- P1 mitigations implemented
- Fuzzing targets created and run for 24+ hours
- Circuit breaker tested under failure conditions
- Memory leak tests passing (long-running)
- Timeout mechanism validated
- Security review by second party
- Penetration testing completed
11. Conclusion
The proposed BMSSP WASM integration offers significant performance benefits for j-tree operations but introduces several security considerations that require mitigation:
Primary Concerns:
- FFI boundary safety with static mutable state
- Input validation gaps for vertex IDs and edge weights
- Resource exhaustion vectors through unbounded allocation
- Supply chain risks from external WASM dependency
Recommended Approach:
- Implement all P0 mitigations before initial integration
- Use defense-in-depth with validation at multiple layers
- Maintain native Rust fallback for graceful degradation
- Establish ongoing monitoring and circuit breaker patterns
Overall Assessment: The integration is viable with the recommended security mitigations in place. The performance benefits (10-15x speedup) justify the additional security engineering investment.
Appendix A: Security Review Sign-Off
| Role | Name | Date | Signature |
|---|---|---|---|
| Security Architect | ___________________ | ________ | ________ |
| Lead Developer | ___________________ | ________ | ________ |
| QA Lead | ___________________ | ________ | ________ |
Appendix B: References
- ADR-002: Dynamic Hierarchical j-Tree Decomposition
- ADR-002-addendum-bmssp-integration: BMSSP WASM Integration Proposal
- BMSSP Paper: "Breaking the Sorting Barrier for SSSP" (arXiv:2501.00660)
- npm package: https://www.npmjs.com/package/@ruvnet/bmssp
- RuVector Security Audit Report (2026-01-18)
- OWASP WASM Security Guidelines
- Rust FFI Safety Guidelines
This security review was conducted as part of the ADR-002-addendum-bmssp-integration proposal review process.