From 5541926e6a164275455e6013d903af975fa6e0cc Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 1 Mar 2026 21:41:00 -0500 Subject: [PATCH] fix(security): harden RuvSense pipeline against overflow and numerical instability - tomography.rs: use checked_mul for nx*ny*nz to prevent integer overflow on adversarial grid configurations - phase_align.rs: add defensive bounds check in mean_phase_on_indices to prevent panic on out-of-range subcarrier indices - multistatic.rs: stabilize softmax in attention_weighted_fusion with max-subtraction to prevent exp() overflow on extreme similarity values Co-Authored-By: claude-flow --- .../src/ruvsense/longitudinal.rs | 22 +++++++++++++------ .../src/ruvsense/multistatic.rs | 11 +++++++--- .../src/ruvsense/phase_align.rs | 7 ++++-- .../src/ruvsense/tomography.rs | 7 +++++- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/longitudinal.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/longitudinal.rs index 727623f..0b42b23 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/longitudinal.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/longitudinal.rs @@ -252,15 +252,23 @@ impl PersonalBaseline { let mut reports = Vec::new(); - for &(metric, value) in &summary.metrics { - let stats = self.stats_for_mut(metric); - stats.update(value); + let observation_days = self.observation_days; - if !self.is_ready_at(self.observation_days) { + for &(metric, value) in &summary.metrics { + // Update stats and extract values before releasing the mutable borrow + let (z, baseline_mean, baseline_std) = { + let stats = self.stats_for_mut(metric); + stats.update(value); + let z = stats.z_score(value); + let mean = stats.mean; + let std = stats.std_dev(); + (z, mean, std) + }; + + if !self.is_ready_at(observation_days) { continue; } - let z = stats.z_score(value); let idx = Self::metric_index(metric); if z.abs() > 2.0 { @@ -288,8 +296,8 @@ impl PersonalBaseline { direction, z_score: z, current_value: value, - baseline_mean: stats.mean, - baseline_std: stats.std_dev(), + baseline_mean, + baseline_std, sustained_days: self.drift_counters[idx], level, timestamp_us, diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs index 11c57e4..598a508 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs @@ -257,7 +257,7 @@ fn attention_weighted_fusion( } // Compute attention weights based on similarity to consensus - let mut weights = vec![0.0_f32; n_nodes]; + let mut logits = vec![0.0_f32; n_nodes]; for (n, amp) in amplitudes.iter().enumerate() { let mut dot = 0.0_f32; let mut norm_a = 0.0_f32; @@ -269,10 +269,15 @@ fn attention_weighted_fusion( } let denom = (norm_a * norm_b).sqrt().max(1e-12); let similarity = dot / denom; - weights[n] = (similarity / temperature).exp(); + logits[n] = similarity / temperature; } - // Normalize weights (softmax-style) + // Numerically stable softmax: subtract max to prevent exp() overflow + let max_logit = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max); + let mut weights = vec![0.0_f32; n_nodes]; + for (n, &logit) in logits.iter().enumerate() { + weights[n] = (logit - max_logit).exp(); + } let weight_sum: f32 = weights.iter().sum::().max(1e-12); for w in &mut weights { *w /= weight_sum; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/phase_align.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/phase_align.rs index 0b8be40..82dbce6 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/phase_align.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/phase_align.rs @@ -281,8 +281,11 @@ fn mean_phase_on_indices(phase: &[f32], indices: &[usize]) -> f32 { let mut sin_sum = 0.0_f32; let mut cos_sum = 0.0_f32; for &i in indices { - sin_sum += phase[i].sin(); - cos_sum += phase[i].cos(); + // Defensive bounds check: skip out-of-range indices rather than panic + if let Some(&p) = phase.get(i) { + sin_sum += p.sin(); + cos_sum += p.cos(); + } } sin_sum.atan2(cos_sum) diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/tomography.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/tomography.rs index a1f0f20..19614eb 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/tomography.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/tomography.rs @@ -199,7 +199,12 @@ impl RfTomographer { )); } - let n_voxels = config.nx * config.ny * config.nz; + let n_voxels = config.nx + .checked_mul(config.ny) + .and_then(|v| v.checked_mul(config.nz)) + .ok_or_else(|| TomographyError::InvalidGrid( + format!("Grid dimensions overflow: {}x{}x{}", config.nx, config.ny, config.nz), + ))?; // Precompute weight matrix let weight_matrix: Vec> = links