Files
wifi-densepose/crates/ruvector-mincut/docs/security/BMSSP-SECURITY-REVIEW.md
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

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:

  1. Memory Ownership Transfer: Float64Array returned from compute_shortest_paths points to WASM linear memory. If the caller retains this reference after free(), use-after-free occurs.

  2. Double-Free Vulnerability: No mechanism to prevent multiple free() calls on the same instance.

  3. 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:

  • BitSet256 has 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:

  1. Generate SBOM:
# Using syft
syft dir:node_modules/@ruvnet/bmssp -o spdx-json > bmssp-sbom.json
  1. 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_or for 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.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:

  1. FFI boundary safety with static mutable state
  2. Input validation gaps for vertex IDs and edge weights
  3. Resource exhaustion vectors through unbounded allocation
  4. Supply chain risks from external WASM dependency

Recommended Approach:

  1. Implement all P0 mitigations before initial integration
  2. Use defense-in-depth with validation at multiple layers
  3. Maintain native Rust fallback for graceful degradation
  4. 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

  1. ADR-002: Dynamic Hierarchical j-Tree Decomposition
  2. ADR-002-addendum-bmssp-integration: BMSSP WASM Integration Proposal
  3. BMSSP Paper: "Breaking the Sorting Barrier for SSSP" (arXiv:2501.00660)
  4. npm package: https://www.npmjs.com/package/@ruvnet/bmssp
  5. RuVector Security Audit Report (2026-01-18)
  6. OWASP WASM Security Guidelines
  7. Rust FFI Safety Guidelines

This security review was conducted as part of the ADR-002-addendum-bmssp-integration proposal review process.