diff --git a/CHANGELOG.md b/CHANGELOG.md index db09f3d5e..3d4b4c71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - tcp: add `set_min_rto`/`set_initial_rto` to tune the retransmission timeout bounds per socket. +- iface: fix the ICMP and TCP checksums of fragmented IPv4 packets, which covered stale bytes left in the fragmentation buffer by an earlier, larger packet. ## [0.14.0] - 2026-08-07 diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index b17571e85..787329f69 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -1345,8 +1345,10 @@ impl InterfaceInner { // Save the number of bytes we will send now. frag.sent_bytes = first_frag_ip_len; - // Emit the IP header to the buffer. - emit_ip(&ip_repr, &mut frag.buffer); + // Emit the IP header and the whole payload, but only within the packet's + // own length: ICMP and TCP checksum the slice they are given, and beyond + // the packet the buffer still holds bytes from an earlier, larger packet. + emit_ip(&ip_repr, &mut frag.buffer[..total_ip_len]); let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut frag.buffer[..]); frag.ipv4.ident = ipv4_id; diff --git a/src/iface/interface/tests/ipv4.rs b/src/iface/interface/tests/ipv4.rs index 75895ffe0..47263fd7a 100644 --- a/src/iface/interface/tests/ipv4.rs +++ b/src/iface/interface/tests/ipv4.rs @@ -879,6 +879,91 @@ fn test_packet_len(#[case] medium: Medium) { } } +/// A fragmented packet's checksum must cover that packet only, not what an +/// earlier, larger packet left behind in the fragmentation buffer. +#[rstest] +#[cfg_attr(feature = "medium-ip", case(Medium::Ip))] +#[cfg_attr(feature = "medium-ethernet", case(Medium::Ethernet))] +#[cfg(all( + feature = "proto-ipv4-fragmentation", + any(feature = "medium-ip", feature = "medium-ethernet") +))] +fn test_fragmented_checksum_ignores_stale_buffer(#[case] medium: Medium) { + use crate::phy::Device; + use crate::wire::{Icmpv4Packet, Icmpv4Repr}; + + let (mut iface, _, mut device) = setup(medium); + let src_addr = Ipv4Address::new(127, 0, 0, 1); + let dst_addr = Ipv4Address::new(127, 0, 0, 2); + #[cfg(feature = "medium-ethernet")] + iface.inner.neighbor_cache.fill( + dst_addr.into(), + EthernetAddress([2; 6]).into(), + Instant::ZERO, + ); + + let data = vec![0x5a; iface.inner.ip_mtu()]; + + // Dispatch an oversized packet into a buffer an earlier packet left junk + // in, and return its IP payload as the fragmenter holds it. + let mut emit = |next_header, payload_len, payload| { + iface.fragmenter.buffer.fill(0xa5); + let repr = Ipv4Repr { + src_addr, + dst_addr, + next_header, + payload_len, + hop_limit: 64, + }; + let tx = device.transmit(Instant::ZERO).unwrap(); + let packet = Packet::new_ipv4(repr, payload); + let frag = &mut iface.fragmenter; + iface + .inner + .dispatch_ip(tx, PacketMeta::default(), packet, frag) + .unwrap(); + assert!(!frag.is_empty(), "the packet was not fragmented"); + let emitted = frag.buffer[repr.buffer_len()..frag.packet_len].to_vec(); + frag.reset(); + emitted + }; + + let icmp = Icmpv4Repr::EchoReply { + ident: 1, + seq_no: 1, + data: &data, + }; + let emitted = emit(IpProtocol::Icmp, icmp.buffer_len(), IpPayload::Icmpv4(icmp)); + assert!( + Icmpv4Packet::new_checked(&emitted[..]) + .unwrap() + .verify_checksum() + ); + + #[cfg(feature = "socket-tcp")] + { + use crate::wire::{TcpControl, TcpPacket, TcpRepr, TcpSeqNumber}; + + let tcp = TcpRepr { + src_port: 1, + dst_port: 2, + control: TcpControl::None, + seq_number: TcpSeqNumber(1), + ack_number: Some(TcpSeqNumber(1)), + window_len: 1024, + window_scale: None, + max_seg_size: None, + sack_permitted: false, + sack_ranges: [None; 3], + timestamp: None, + payload: &data, + }; + let emitted = emit(IpProtocol::Tcp, tcp.buffer_len(), IpPayload::Tcp(tcp)); + let packet = TcpPacket::new_checked(&emitted[..]).unwrap(); + assert!(packet.verify_checksum(&src_addr.into(), &dst_addr.into())); + } +} + /// Check no reply is emitted when using a raw socket #[cfg(feature = "socket-raw")] fn check_no_reply_raw_socket(medium: Medium, frame: &crate::wire::ipv4::Packet<&[u8]>) {