Files
ruvnet--RuView/v2/crates/ruview-unified/tests/security_boundaries.rs
T
Claude 42485495ed feat(ruview-unified): complete Gaussian update loop, separable delay-Doppler, property-tested boundary hardening
Third increment: closes the remaining implementable ADR-275 update-loop
steps, optimizes the delay-Doppler transform, and hardens every boundary
surface with property testing that found and fixed three real
input-controlled defects.

- ADR-275 update loop: GaussianMap::merge_overlapping (step 5 — mutual
  Mahalanobis + semantic-compatibility dedup catching drift the
  insert-time ±1-cell gate misses; orthogonal semantics stay separate)
  and lifetime-aware decay (step 7 — tau_eff = tau*(1+ln(1+lifetime/tau))
  so confirmed structures outlive transients at equal nominal tau).
- Separable delay-Doppler (ADR-281): O(B^2*S + S^2*B) instead of
  O(B^2*S^2), proven equivalent to the direct reference to <1e-10 and
  measured 8.3x faster (520us vs 4.34ms at 56x8). Direct form kept as
  the benchmark baseline + equivalence oracle.
- Security property tests (tests/security_boundaries.rs, 8 proptest
  properties over arbitrary values incl. NaN/inf via f64::from_bits).
  Found and fixed:
  * ble_cs_range unwrap infinite loop on non-finite phase (+inf) and
    ~1e299-iteration loop on finite-huge phase -> O(1) modular unwrap +
    plausibility bound (|phase| <= 1e6 rad);
  * subnormal Gaussian scale (5e-324) overflowing 1/sigma^2 to NaN
    density -> physical bounds (sigma in [1e-6, 1e4] m, occupancy in
    [0, 1e6] nepers/m).
  Properties proven: tensor/Gaussian/BoundedEvent constructors never
  panic; policy engine fail-closed for every (purpose,grants,zone);
  raw export structurally unreachable; coherent fusion rejects every
  non-finite/out-of-bounds sync state; occupancy reps never retain
  identity.
- New criterion benches for all increment-2/3 hot paths (to_canonical
  38us, ble_cs_range 481ns, AoI planner 647ns/200 regions, coherent
  fusion 1.5us/32 members, factorized pose 521ns, delay-Doppler
  separable vs direct).

Validation: ruview-unified 98 tests (87 lib + 3 acceptance + 8
security), 0 failed, clippy-clean; Python proof VERDICT PASS. Witness
bundle regeneration still blocked on the desktop/Tauri crate's GTK dev
headers (unavailable in this container) — pre-existing environment
limitation, flagged for the release owner.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Q1R5zhz6sSfXGRXpgBwpFX
2026-07-26 20:29:58 +00:00

269 lines
10 KiB
Rust

//! Security property tests (ADR-273 pre-merge item 12): the crate's
//! system boundaries must never panic on hostile input and must stay
//! fail-closed under arbitrary authorization states.
//!
//! Strategy: proptest drives the validated constructors and policy gates
//! with arbitrary values (including NaN/±inf smuggled through
//! `f64::from_bits`) and asserts the *contract*, not specific values:
//! every input either yields a valid object or a typed error — never a
//! panic, and never a permissive default.
use proptest::prelude::*;
use ruview_unified::adapters::{ble_cs_range, BleCsFrame};
use ruview_unified::control::{
admit_task, validate_representation, CoherentSensorGroup, MemberSyncState, PrivacyClass,
SensingTask, SpatialZone, TaskSufficientRepresentation,
};
use ruview_unified::gaussian::primitive::{Provenance, RfGaussian};
use ruview_unified::policy::{
BoundedEvent, EventValue, PolicyEngine, PrivacyZone, SensingPurpose,
};
use ruview_unified::tensor::{CalibrationMeta, LinkGeometry, RfModality, RfTensor};
/// Arbitrary f64 including NaN, ±inf, subnormals — the values an attacker
/// or a broken driver would deliver.
fn any_f64() -> impl Strategy<Value = f64> {
any::<u64>().prop_map(f64::from_bits)
}
fn any_purpose() -> impl Strategy<Value = SensingPurpose> {
prop_oneof![
Just(SensingPurpose::Presence),
Just(SensingPurpose::Activity),
Just(SensingPurpose::Vitals),
Just(SensingPurpose::Localization),
Just(SensingPurpose::PoseTracking),
Just(SensingPurpose::IdentityRecognition),
Just(SensingPurpose::ChannelDiagnostics),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
/// RfTensor::new never panics; invalid numeric fields are typed errors.
#[test]
fn rf_tensor_constructor_never_panics(
re in any_f64(),
im in any_f64(),
freq in any_f64(),
bw in any_f64(),
age in any_f64(),
clock in any_f64(),
unc in any_f64(),
tx in prop::array::uniform3(any_f64()),
) {
let data = ndarray::Array3::from_elem((1, 4, 2), num_complex::Complex64::new(re, im));
let links = vec![LinkGeometry { tx_pos: tx, rx_pos: [1.0, 0.0, 1.0] }];
let result = RfTensor::new(
RfModality::WifiCsi, freq, bw, data, links, age, 0, "prop".into(),
clock, unc, CalibrationMeta::default(),
);
// Contract: Ok only when every validated field is actually valid.
if let Ok(t) = result {
prop_assert!(t.center_freq_hz.is_finite() && t.center_freq_hz > 0.0);
prop_assert!((0.0..=1.0).contains(&t.clock_quality));
prop_assert!((0.0..=1.0).contains(&t.uncertainty));
prop_assert!(t.sample_age_s.is_finite() && t.sample_age_s >= 0.0);
prop_assert!(t.data.iter().all(|z| z.re.is_finite() && z.im.is_finite()));
}
}
/// RfGaussian::new never panics; accepted Gaussians have a normalized
/// quaternion and in-range trust fields.
#[test]
fn rf_gaussian_constructor_never_panics(
pos in prop::array::uniform3(any_f64()),
scale in prop::array::uniform3(any_f64()),
quat in prop::array::uniform4(any_f64()),
occ in any_f64(),
conf in any_f64(),
tau in any_f64(),
) {
let result = RfGaussian::new(
pos, scale, quat, occ, conf, 0, tau,
Provenance { device_id: "prop".into(), model_version: 1, synthetic: true },
);
if let Ok(g) = result {
let qn: f64 = g.orientation.iter().map(|q| q * q).sum::<f64>().sqrt();
prop_assert!((qn - 1.0).abs() < 1e-9, "quaternion must be normalized");
prop_assert!(g.scale.iter().all(|s| *s > 0.0));
prop_assert!(g.occupancy >= 0.0);
prop_assert!((0.0..=1.0).contains(&g.confidence));
// Density at the centre of a valid Gaussian is exactly 1.
prop_assert!((g.density_at(g.position) - 1.0).abs() < 1e-9);
}
}
/// BoundedEvent::new never panics; exported accountability fields are
/// always in range.
#[test]
fn bounded_event_constructor_never_panics(
uncertainty in any_f64(),
model_version in any::<u32>(),
value in any_f64(),
) {
let result = BoundedEvent::new(
SensingPurpose::Presence,
EventValue::RespirationBpm(value),
uncertainty,
Provenance { device_id: "prop".into(), model_version: 1, synthetic: false },
model_version,
0,
"zone",
);
if let Ok(e) = result {
prop_assert!((0.0..=1.0).contains(&e.uncertainty));
prop_assert!(e.model_version != 0, "unassigned model version must never export");
}
}
/// ble_cs_range never panics on arbitrary phases/frequencies/RTT, and
/// any Ok evidence has a finite, non-negative distance.
#[test]
fn ble_cs_range_never_panics(
phases in prop::collection::vec(any_f64(), 0..24),
f0 in any_f64(),
df in any_f64(),
rtt in prop::option::of(any_f64()),
) {
let n = phases.len();
let frame = BleCsFrame {
frequency_steps_hz: (0..n).map(|k| f0 + df * k as f64).collect(),
phase_samples_rad: phases,
round_trip_time_ns: rtt,
};
if let Ok(ev) = ble_cs_range(&frame) {
// Non-finite inputs are rejected at the boundary (the unwrap
// loop would otherwise spin forever on +inf — the DoS this
// suite originally caught), so Ok evidence is fully finite.
prop_assert!(ev.phase_distance_m.is_finite() && ev.phase_distance_m >= 0.0);
prop_assert!((0.0..=1.0).contains(&ev.confidence));
if let Some(d) = ev.rtt_distance_m {
prop_assert!(d.is_finite());
}
}
}
/// The policy engine is fail-closed for every purpose against every
/// zone configuration that does not explicitly grant it.
#[test]
fn policy_engine_is_fail_closed_under_arbitrary_grants(
purpose in any_purpose(),
granted in prop::collection::btree_set(any_purpose(), 0..7),
identity_enabled in any::<bool>(),
query_unknown_zone in any::<bool>(),
) {
let mut engine = PolicyEngine::new();
engine.upsert_zone(PrivacyZone {
id: "z".into(),
allowed_purposes: granted.clone(),
retention_s: 60,
identity_explicitly_enabled: identity_enabled,
});
let zone_id = if query_unknown_zone { "nope" } else { "z" };
let verdict = engine.authorize(zone_id, purpose);
if query_unknown_zone {
prop_assert!(verdict.is_err(), "unknown zone must always deny");
} else if !granted.contains(&purpose) {
prop_assert!(verdict.is_err(), "ungranted purpose must deny");
} else if purpose == SensingPurpose::IdentityRecognition && !identity_enabled {
prop_assert!(verdict.is_err(), "identity single-gate must deny");
} else {
prop_assert!(verdict.is_ok());
}
}
/// Task admission can never approve raw export, whatever else is true.
#[test]
fn raw_export_is_unreachable(
purpose in any_purpose(),
raw in any::<bool>(),
confidence in any_f64(),
) {
let mut engine = PolicyEngine::new();
engine.upsert_zone(PrivacyZone {
id: "z".into(),
allowed_purposes: [purpose].into_iter().collect(),
retention_s: 60,
identity_explicitly_enabled: true,
});
let task = SensingTask {
task_id: 1,
purpose,
target_area: SpatialZone { id: "z".into(), min_m: [0.0; 3], max_m: [1.0; 3] },
modalities: vec![RfModality::WifiCsi],
requested_resolution_m: 0.5,
maximum_latency_ms: 100,
minimum_confidence: confidence,
raw_retention_seconds: 60,
result_retention_seconds: 60,
authorized_consumers: vec![],
consent_reference: Some("consent-1".into()),
raw_export_allowed: raw,
};
let verdict = admit_task(&engine, &task);
if raw {
prop_assert!(verdict.is_err(), "raw export must be structurally unreachable");
}
}
/// Coherent fusion denies whenever any reported error is non-finite or
/// out of bounds — NaN cannot sneak past the gate.
#[test]
fn coherent_fusion_rejects_non_finite_sync_state(
time_err in any_f64(),
phase_err in any_f64(),
hash in any::<u64>(),
) {
let group = CoherentSensorGroup {
group_id: "g".into(),
members: vec!["m".into()],
maximum_time_error_ns: 50.0,
maximum_phase_error_rad: 0.2,
baseline_geometry_hash: 7,
};
let verdict = group.can_fuse(&[MemberSyncState {
member_id: "m".into(),
time_error_ns: time_err,
phase_error_rad: phase_err,
geometry_hash: hash,
}]);
let in_bounds = time_err.is_finite()
&& time_err.abs() <= 50.0
&& phase_err.is_finite()
&& phase_err.abs() <= 0.2
&& hash == 7;
prop_assert_eq!(verdict.is_ok(), in_bounds, "NaN/inf must deny, bounds must bind");
}
/// Representation validation never approves an identity-retaining
/// occupancy representation, whatever the other fields say.
#[test]
fn occupancy_representations_must_exclude_identity(
class in prop_oneof![
Just(PrivacyClass::P0), Just(PrivacyClass::P1), Just(PrivacyClass::P2),
Just(PrivacyClass::P3), Just(PrivacyClass::P4), Just(PrivacyClass::P5)
],
excluded in prop::collection::vec("[a-z]{1,10}", 0..4),
bits in any_f64(),
) {
let rep = TaskSufficientRepresentation {
task_id: 1,
source_receipts: vec![1],
semantic_state: vec![0.5],
information_bound_bits: bits,
excluded_information: excluded.clone(),
privacy_class: class,
};
let verdict = validate_representation(&rep, SensingPurpose::Presence);
let excludes_required = excluded.iter().any(|e| e == "identity")
&& excluded.iter().any(|e| e == "vitals");
if verdict.is_ok() {
prop_assert!(excludes_required, "approved rep must exclude identity+vitals");
prop_assert!(class <= PrivacyClass::P2, "approved rep must respect the ceiling");
}
}
}