git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
1200 lines
34 KiB
Markdown
1200 lines
34 KiB
Markdown
# 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):**
|
|
|
|
```rust
|
|
// 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:**
|
|
```rust
|
|
// 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):**
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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):**
|
|
|
|
```rust
|
|
#[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):**
|
|
|
|
```rust
|
|
#[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):**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```rust
|
|
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):**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```javascript
|
|
// 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:**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```rust
|
|
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):**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```bash
|
|
# 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:**
|
|
|
|
```json
|
|
// 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:**
|
|
```bash
|
|
# Using syft
|
|
syft dir:node_modules/@ruvnet/bmssp -o spdx-json > bmssp-sbom.json
|
|
```
|
|
|
|
2. **Verify Reproducible Build:**
|
|
```bash
|
|
# 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:**
|
|
|
|
```typescript
|
|
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):**
|
|
|
|
```rust
|
|
#![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):**
|
|
|
|
```rust
|
|
#[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:**
|
|
|
|
```rust
|
|
/// 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:**
|
|
|
|
```rust
|
|
/// 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):**
|
|
|
|
```rust
|
|
#[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:**
|
|
|
|
```rust
|
|
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):**
|
|
|
|
```rust
|
|
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:**
|
|
|
|
```rust
|
|
/// 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:**
|
|
|
|
```rust
|
|
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`
|
|
|
|
```rust
|
|
//! 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`
|
|
|
|
```rust
|
|
//! 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
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
// 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.*
|