From c59834737b7b693edb543dbe411245136632df71 Mon Sep 17 00:00:00 2001 From: "K. Isom" Date: Sun, 18 Oct 2026 02:03:52 -0700 Subject: [PATCH] feat(core): reject expired or malformed addressed messages --- tines-core/src/exchange.rs | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tines-core/src/exchange.rs b/tines-core/src/exchange.rs index 3317677..80e6c2f 100644 --- a/tines-core/src/exchange.rs +++ b/tines-core/src/exchange.rs @@ -205,6 +205,9 @@ impl Exchange { } } Frame::Obs { from: _, obs } => { + if reject(&obs) { + return None; + } let returned = obs.clone(); return match store.insert(obs, now) { Ok(true) => Some(returned), @@ -219,6 +222,9 @@ impl Exchange { data, } => { if let Some(obs) = peer.reassembler.feed(id, index, count, &data, now) { + if reject(&obs) { + return None; + } let returned = obs.clone(); return match store.insert(obs, now) { Ok(true) => Some(returned), @@ -240,6 +246,22 @@ fn summary_due(peer: &Peer, now: Tick) -> bool { } } +/// Returns `true` when a received [`Observation`] must be dropped before it is +/// stored: either its addressing is malformed, or it is an addressed message +/// whose age has passed its lifetime. +/// +/// An addressed message (`to = Some`) must carry a `lifetime_ms`; a mismatched +/// pair — `to = Some` with no lifetime, or a lifetime with no recipient — +/// violates the addressing invariant and is dropped in either direction. A +/// well-formed addressed message is rejected once its age exceeds its +/// lifetime. Broadcasts (`to = None`, `lifetime_ms = None`) are never rejected. +fn reject(obs: &Observation) -> bool { + if obs.to.is_some() != obs.lifetime_ms.is_some() { + return true; + } + obs.to.is_some() && obs.age_ms > obs.lifetime_ms.unwrap() +} + /// Emits at most one frame for `peer`: continuing an in-flight fragment if one /// is pending, otherwise draining the send queue (skipping ids no longer /// stored). @@ -744,6 +766,71 @@ mod tests { assert_eq!(store.len(), 1); } + #[test] + fn expired_addressed_obs_is_rejected() { + let mut ex = Exchange::new(ME); + let mut store = MemStore::new(); + ex.on_event(Event::Contact(1), &mut store, 0); + // Addressed to us, lifetime 100ms, but age 200ms -> expired. + let mut o = obs([2; 16], 7, 0, None, b"t", b"hi"); + o.to = Some(ME); + o.lifetime_ms = Some(100); + o.age_ms = 200; + let frame = Frame::Obs { from: [2; 16], obs: o.clone() }; + assert!( + ex.on_event(Event::Frame(1, frame_bytes(&frame)), &mut store, 0) + .is_none(), + "an expired addressed obs must not be delivered" + ); + assert_eq!(store.len(), 0, "an expired addressed obs must not be stored"); + } + + #[test] + fn expired_addressed_frag_is_rejected() { + let mut ex = Exchange::new(ME); + let mut store = MemStore::new(); + // A large addressed obs that must be fragmented; already expired. + let mut big = obs([2; 16], 7, 0, None, b"t", &[0u8; 1024]); + big.to = Some(ME); + big.lifetime_ms = Some(100); + big.age_ms = 200; + let mut frames: Vec> = Vec::new(); + crate::frag::fragment([2; 16], &big, 200, &mut |b| { + frames.push(heapless::Vec::from_slice(b).unwrap()); + }) + .expect("fragment the addressed obs into frags"); + for f in frames { + let _ = ex.on_event(Event::Frame(9, f), &mut store, 0); + } + assert_eq!( + store.len(), + 0, + "an expired addressed obs must not survive fragmentation" + ); + } + + #[test] + fn addressing_invariant_violation_is_dropped() { + let mut ex = Exchange::new(ME); + let mut store = MemStore::new(); + ex.on_event(Event::Contact(1), &mut store, 0); + // to = Some but lifetime_ms = None: the lifetime==to invariant is + // violated, so this frame is malformed and must be dropped, not stored. + let mut o = obs([2; 16], 7, 0, None, b"t", b"hi"); + o.to = Some(ME); + o.lifetime_ms = None; + let frame = Frame::Obs { from: [2; 16], obs: o.clone() }; + assert!( + ex.on_event(Event::Frame(1, frame_bytes(&frame)), &mut store, 0) + .is_none() + ); + assert_eq!( + store.len(), + 0, + "an invariant-violating frame must not be stored" + ); + } + #[test] fn addressed_obs_not_in_summary() { let mut ex = Exchange::new(ME);