From d3fb6b707163105a8613fba365b06b2b84b95c39 Mon Sep 17 00:00:00 2001 From: Sudheer Obbu Date: Tue, 28 Apr 2026 20:01:44 -0400 Subject: [PATCH] fix(agent): prevent shift-left overflow in EpcNetIpKey::clone_by_masklen Fixes #8700 When a CIDR with prefix length 0 (e.g. ::/0) is registered and an IPv6 lookup is performed, clone_by_masklen(0, false) computes: u128::MAX << IPV6_BITS.saturating_sub(0) = u128::MAX << 128 Shifting a u128 by its own bit-width (128) is undefined behaviour and panics in Rust debug builds with: panicked at src/policy/labeler.rs:72:27: attempt to shift left with overflow The fix replaces the bare shift with checked_shl, which returns None when the shift amount equals or exceeds the bit-width, and falls back to 0. A mask of 0 is semantically correct for a /0 prefix (no network bits to preserve). Before: self.ip & (u128::MAX << max_prefix.saturating_sub(masklen)) After: self.ip & u128::MAX .checked_shl(max_prefix.saturating_sub(masklen) as u32) .unwrap_or(0) A regression test test_ipv6_zero_masklen_no_panic is added to cover the ::/0 case end-to-end through the labeler API. Signed-off-by: Sudheer Obbu --- agent/src/policy/labeler.rs | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/agent/src/policy/labeler.rs b/agent/src/policy/labeler.rs index 62484e75bb5..c19435bf7eb 100644 --- a/agent/src/policy/labeler.rs +++ b/agent/src/policy/labeler.rs @@ -70,7 +70,7 @@ impl EpcNetIpKey { fn clone_by_masklen(&self, masklen: usize, is_ipv4: bool) -> Self { let max_prefix = if is_ipv4 { IPV4_BITS } else { IPV6_BITS }; Self { - ip: self.ip & (u128::MAX << max_prefix.saturating_sub(masklen)), + ip: self.ip & u128::MAX.checked_shl(max_prefix.saturating_sub(masklen) as u32).unwrap_or(0), epc_id: self.epc_id, masklen: masklen as u8, } @@ -1368,4 +1368,37 @@ mod tests { assert_eq!(endpoints.src_info.l2_epc_id, 10); assert_eq!(endpoints.src_info.l3_epc_id, 10); } + /// Regression test for https://github.com/deepflowio/deepflow/issues/8700 + /// + /// When a CIDR with masklen=0 (i.e. ::/0) is registered and an IPv6 lookup + /// is performed, `clone_by_masklen(0, false)` used to compute + /// `u128::MAX << 128` which panics in debug builds (and is UB in release). + /// The fix uses `checked_shl` so the shift is saturated to 0 instead. + #[test] + fn test_ipv6_zero_masklen_no_panic() { + use std::str::FromStr; + use ipnet::IpNet; + use crate::common::policy::{Cidr, CidrType}; + + let mut labeler: Labeler = Default::default(); + + // Register a catch-all IPv6 CIDR (::/0) — masklen 0 triggers the shift. + let cidr = Cidr { + ip: IpNet::from_str("::/0").unwrap(), + epc_id: 42, + cidr_type: CidrType::Lan, + ..Default::default() + }; + labeler.update_cidr_table(&vec![Arc::new(cidr)], false, &mut false); + + // A lookup for any IPv6 address must not panic. + let mut endpoint: EndpointInfo = Default::default(); + labeler.set_epc_by_cidr( + "2001:db8::1".parse().unwrap(), + 42, + &mut endpoint, + ); + assert_eq!(endpoint.l3_epc_id, 42); + } + }