ip_do_fragment() completes a CHECKSUM_PARTIAL skb before reading the IPv4 header length. A virtualization interface can supply a checksum start that still points inside the IPv4 header after link-layer removal. This does not require a virtual-machine guest. A TUN device with virtio-net header support is sufficient to reach this path. skb_checksum_help() can then change iph->ihl after the packet was parsed and routed. Fragmentation trusts the changed IHL and can copy beyond the skb's logical linear head into transmitted IPv4 options. Read and validate IHL before checksum completion, reject a checksum start inside that header, retain the validated length, and reacquire iph after skb_checksum_help(). Fixes: dbd3393c56a8 ("ipv4: add defensive check for CHECKSUM_PARTIAL skbs in ip_fragment") Reported-by: Paulos Yibelo Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Paulos Yibelo Acked-by: Michael S. Tsirkin --- Changes in v4: - State explicitly that a TUN device is sufficient and no guest is required, as noted by Michael S. Tsirkin. No code changes. Changes in v3: - No code changes. Changes in v2: - No code changes. net/ipv4/ip_output.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c index a24cc8e..ff902a2 100644 --- a/net/ipv4/ip_output.c +++ b/net/ipv4/ip_output.c @@ -770,17 +770,29 @@ int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb, struct ip_frag_state state; int err = 0; - /* for offloaded checksums cleanup checksum before fragmentation */ - if (skb->ip_summed == CHECKSUM_PARTIAL && - (err = skb_checksum_help(skb))) - goto fail; - /* * Point into the IP datagram header. */ iph = ip_hdr(skb); + hlen = iph->ihl * 4; + if (unlikely(hlen < sizeof(*iph) || hlen > skb_headlen(skb))) { + err = -EINVAL; + goto fail; + } + /* Complete offloaded checksums only after the validated IP header. */ + if (skb->ip_summed == CHECKSUM_PARTIAL) { + if (unlikely(skb_checksum_start_offset(skb) < hlen)) { + err = -EINVAL; + goto fail; + } + err = skb_checksum_help(skb); + if (err) + goto fail; + iph = ip_hdr(skb); + } + mtu = ip_skb_dst_mtu(sk, skb); if (IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size < mtu) mtu = IPCB(skb)->frag_max_size; @@ -789,7 +801,6 @@ int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb, * Setup starting values. */ - hlen = iph->ihl * 4; if (mtu < hlen + 8) { err = -EMSGSIZE; goto fail;