Re: [PATCH net 08/10] ipvs: avoid out-of-bounds write in ip_vs_nat_icmp

Jakub Kicinski <[email protected]> Tue, 4 Aug 2026 17:15:34 -0700
Newsgroups gmane.comp.security.firewalls.netfilter.devel,gmane.linux.network
Message-ID <[email protected]>
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
ipvs: avoid out-of-bounds write in ip_vs_nat_icmp

This passes the already-parsed ip_vs_iphdr down into ip_vs_nat_icmp() so
the embedded transport header offset comes from ciph->len instead of a
second read of the inner IPv4 ihl field.  The IPv4 header sanity checks in
ip_vs_out_icmp() and ip_vs_in_icmp() are also reworked to read ihl only
once.

Link: https://sashiko.dev/#/patchset/20260722101517.36313-1-ja%40ssi.bg

> diff --git a/net/netfilter/ipvs/ip_vs_core.c b/net/netfilter/ipvs/ip_vs_core.c
> index 6b79e0c4d9e28..0bdaeb4ed61e4 100644
> --- a/net/netfilter/ipvs/ip_vs_core.c
> +++ b/net/netfilter/ipvs/ip_vs_core.c
> @@ -925,28 +925,27 @@ static int ip_vs_route_me_harder(struct netns_ipvs *ipvs, int af,
>   */
>  void ip_vs_nat_icmp(struct sk_buff *skb, struct ip_vs_protocol *pp,
>  		    struct ip_vs_conn *cp, int inout, unsigned int toff,
> -		    bool has_ports)
> +		    bool has_ports, struct ip_vs_iphdr *ciph)
>  {
>  	struct iphdr *iph	 = ip_hdr(skb);
>  	struct icmphdr *icmph	 = (struct icmphdr *)(skb->data + toff);
> -	struct iphdr *ciph	 = (struct iphdr *)(icmph + 1);
> -	unsigned int coff __maybe_unused = toff + sizeof(struct icmphdr);
> +	struct iphdr *cih	 = (struct iphdr *)(icmph + 1);
>  
>  	if (inout) {
>  		iph->saddr = cp->vaddr.ip;
>  		ip_send_check(iph);
> -		ciph->daddr = cp->vaddr.ip;
> -		ip_send_check(ciph);
> +		cih->daddr = cp->vaddr.ip;
> +		ip_send_check(cih);
>  	} else {
>  		iph->daddr = cp->daddr.ip;
>  		ip_send_check(iph);
> -		ciph->saddr = cp->daddr.ip;
> -		ip_send_check(ciph);
> +		cih->saddr = cp->daddr.ip;
> +		ip_send_check(cih);
>  	}

[Severity: High]
This isn't a bug introduced by this patch, but the two ip_send_check(cih)
calls here still read the inner ihl field a second time, which looks like
the same multi-read pattern the commit message describes fixing in this
function.

ip_send_check() takes its own fresh read of ihl and checksums 4 * ihl
bytes:

net/ipv4/ip_output.c:ip_send_check() {
	iph->check = 0;
	iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
}

The only length the callers guarantee is the snapshot value ciph->len (plus
4 when ports are mangled):

net/netfilter/ipvs/ip_vs_core.c:handle_response_icmp() {
	unsigned int ctoff = ciph->len;
	...
		ctoff += 2 * sizeof(__u16);
	if (skb_ensure_writable(skb, ctoff))
		goto out;
	...
		ip_vs_nat_icmp(skb, pp, cp, 1, toff, has_ports, ciph);
}

ciph->len was computed earlier in ip_vs_fill_iph_skb_off() as
offset + iph->ihl * 4 from a skb_header_pointer() snapshot, and
skb_ensure_writable() -> pskb_may_pull() re-copies the bytes out of the
fragments, so the ihl byte ip_send_check() then reads can differ from the
one that sized the validated window.

If the fresh ihl is larger than the snapshot value, can ip_fast_csum() read
up to 40 bytes past what skb_ensure_writable() guaranteed, and fold
adjacent skb memory into the inner header checksum that is emitted?

If the fresh ihl ends up below 5 (for example the version/ihl byte going
from 0x45 to 0x40, compensated in tot_len so both the inner IP and ICMP
checksums stay valid), some architectures do not tolerate it:

arch/mips/include/asm/checksum.h:ip_fast_csum() {
	word += 4;
	do {
		csum += *word;
		carry = (csum < *word);
		csum += carry;
		word++;
	} while (word != stop);
}

with stop = iph + ihl, so for ihl <= 4 the loop termination is never hit.

arch/s390/include/asm/checksum.h:ip_fast_csum() {
	ihl -= 4;
	while (ihl--)
		csum += *ptr++;
}

where ihl == 0 underflows into roughly four billion forward word reads.

Would it make sense to checksum over the validated length
(ciph->len - ciph->off), or to re-validate version/ihl after
skb_ensure_writable(), matching what was done for the port offset?

The same sequence exists on the other call site:

net/netfilter/ipvs/ip_vs_xmit.c:ip_vs_icmp_xmit() {
	wlen = ciph->len;
	...
		wlen += 2 * sizeof(__u16); /* Also mangle ports */
	if (skb_ensure_writable(skb, wlen))
		goto tx_error;
	...
	ip_vs_nat_icmp(skb, pp, cp, 0, toff, has_ports, ciph);
}

>  
>  	/* the TCP/UDP/SCTP port */
>  	if (has_ports) {
> -		__be16 *ports = (void *)ciph + ciph->ihl*4;
> +		__be16 *ports = (void *)(skb->data + ciph->len);
>  
>  		if (inout)
>  			ports[1] = cp->vport;

[ ... ]

> @@ -1131,11 +1129,15 @@ static int ip_vs_out_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb,
>  
>  	/* Now find the contained IP header */
>  	offset += sizeof(_icmph);
> +	if (!ip_vs_fill_iph_skb_icmp(AF_INET, skb, offset, true, &ciph))
> +		return NF_ACCEPT; /* The packet looks wrong, ignore */
> +
>  	cih = skb_header_pointer(skb, offset, sizeof(_ciph), &_ciph);
> -	if (!(cih && cih->version == 4 && cih->ihl >= 5))
> +	if (!(cih && cih->version == 4 &&
> +	      ciph.len - ciph.off >= sizeof(struct iphdr)))
>  		return NF_ACCEPT; /* The packet looks wrong, ignore */

This check validates the snapshot length only, so nothing revalidates
version or ihl after the data is pulled into the linear area and handed to
ip_send_check().  Was that intentional?

[ ... ]