From 30dd689741f40a82ead094d1f01c3b74d6a436dc Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 11 Apr 2026 10:49:17 +0200 Subject: [PATCH 1/2] Fix stale CASE exchange blocking new session handshake When a Matter client restarts and sends a new CASESigma1 reusing the same exchange ID as an in-progress handshake, the old exchange is still awaiting CASESigma3. The new Sigma1 gets routed to the stale exchange, producing "Invalid opcode: CASESigma1, expected: CASESigma3" and the handshake fails. The client must then wait through its retry/backoff cycle (typically 30-60s) before succeeding on a subsequent attempt with a different exchange ID. Fix: in Session::post_recv(), when an existing exchange receives a new session request (CASESigma1 or PBKDFParamRequest), remove the stale exchange via remove_exch() and fall through to create a fresh one. remove_exch() either clears the slot immediately (allowing the new exchange to reuse it) or marks it as Dropped if MRP operations are pending. --- rs-matter/src/transport/session.rs | 47 +++++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/rs-matter/src/transport/session.rs b/rs-matter/src/transport/session.rs index 0b4f0d630..5ecd285c0 100644 --- a/rs-matter/src/transport/session.rs +++ b/rs-matter/src/transport/session.rs @@ -312,33 +312,38 @@ impl Session { let exch_index = self.get_exch_for_rx(&rx_header.proto); if let Some(exch_index) = exch_index { - let exch = unwrap!(self.exchanges[exch_index].as_mut()); + // If we receive a new session request (CASESigma1/PBKDFParamRequest) on an + // existing exchange, the peer has restarted the handshake. Remove the stale + // exchange (or mark as dropped if MRP is pending) and create a fresh one. + if MessageMeta::from(&rx_header.proto).is_new_session() { + warn!("New session request on existing exchange — peer restarted handshake, evicting stale exchange"); + self.remove_exch(exch_index); + // Fall through to create a new exchange below + } else { + let exch = unwrap!(self.exchanges[exch_index].as_mut()); - exch.post_recv(&rx_header.plain, &rx_header.proto, epoch)?; + exch.post_recv(&rx_header.plain, &rx_header.proto, epoch)?; - Ok(false) - } else { - if !rx_header.proto.is_initiator() - || !MessageMeta::from(&rx_header.proto).is_new_exchange() - { - // Do not create a new exchange if the peer is not an initiator, or if - // the packet is NOT a candidate for a new exchange - // (i.e. it is a standalone ACK or a SC status response) - Err(ErrorCode::NoExchange)?; + return Ok(false); } + } - if let Some(exch_index) = - self.add_exch(rx_header.proto.exch_id, Role::Responder(Default::default())) - { - // unwrap is safe as we just created the exchange - let exch = unwrap!(self.exchanges[exch_index].as_mut()); + // Create a new exchange for this message (new session request, or no existing exchange) + if !rx_header.proto.is_initiator() || !MessageMeta::from(&rx_header.proto).is_new_exchange() + { + Err(ErrorCode::NoExchange)?; + } - exch.post_recv(&rx_header.plain, &rx_header.proto, epoch)?; + if let Some(exch_index) = + self.add_exch(rx_header.proto.exch_id, Role::Responder(Default::default())) + { + let exch = unwrap!(self.exchanges[exch_index].as_mut()); - Ok(true) - } else { - Err(ErrorCode::NoSpaceExchanges)? - } + exch.post_recv(&rx_header.plain, &rx_header.proto, epoch)?; + + Ok(true) + } else { + Err(ErrorCode::NoSpaceExchanges)? } } From 4d7e717df3add57ae044a94710c5779ec009d41b Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sun, 12 Apr 2026 10:54:46 +0200 Subject: [PATCH 2/2] Fix panic in remove_exch when exchange already evicted When a stale exchange is evicted by post_recv (new CASESigma1 on an existing exchange), the slot is set to None. The old Exchange object still holds a reference to that slot index. When it is eventually dropped, Drop calls remove_exch on the already-cleared slot, causing an unwrap panic. Handle the None case gracefully by returning true (already cleaned up) instead of panicking. --- rs-matter/src/transport/session.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rs-matter/src/transport/session.rs b/rs-matter/src/transport/session.rs index 5ecd285c0..d57c05c7a 100644 --- a/rs-matter/src/transport/session.rs +++ b/rs-matter/src/transport/session.rs @@ -474,7 +474,10 @@ impl Session { } pub(crate) fn remove_exch(&mut self, index: usize) -> bool { - let exchange = unwrap!(self.exchanges[index].as_mut()); + let Some(exchange) = self.exchanges[index].as_mut() else { + // Already removed (e.g. evicted by stale-exchange handling before Drop ran) + return true; + }; let exchange_id = ExchangeId::new(self.id, index); if exchange.mrp.is_retrans_pending() {