From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mail02.haj.ipfire.org (localhost [IPv6:::1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4jKT5l4D7Nz2xnD for ; Wed, 04 Nov 2026 14:55:55 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [IPv6:2001:678:b28::25]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519) (Client CN "mail01.haj.ipfire.org", Issuer "YR1" (not verified)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4jKT5h05ZLz2xL0 for ; Wed, 04 Nov 2026 14:55:52 +0000 (UTC) Received: from [127.0.0.1] (localhost [127.0.0.1]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail01.ipfire.org (Postfix) with ESMTPSA id 4jKT5f0PzDz2WX; Wed, 04 Nov 2026 14:55:49 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ipfire.org; s=202003rsa; t=1793804150; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=o4g8odk32Bc8hXsgMqE2e4Rr8Pnyb3baG7Mt00IkgMg=; b=IYku4SfYc/SDXvgtiA+uE+yll2aM0c8uW6AsKWZsvl4YYJT8qxuHInt1ibBBi1hRhIG3gh KY1Lmw+Q9oHFx13biiaajq0kb/FiaUPHCIZNfRsHmfmWhaLrQPpUWJO0+G+x7obMxYIatG dEmSElhfpL7mv6sDIsi0ScGdf1wTl4SEgSGVN7/4tPFJY8PgtZheA1wY9zYRRGJ1hPEM4X 930re5xpzwrmv/4Ov5aQfJ7QwBQYx9f/H1RgmstEyVWCL8ZWZf47Fo74TP54nyH/uoU7Kg uox4VakshezVIuYK1C7UCeIwAaFOLvA++it9Sze7kCItzll1GCcnCQWUnKWOQA== DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=ipfire.org; s=202003ed25519; t=1793804150; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=o4g8odk32Bc8hXsgMqE2e4Rr8Pnyb3baG7Mt00IkgMg=; b=gAhBoeKpWiJD2DP7LoJxpKxjU30FYwRelP/3HAUXll8tn+VtQEYs5UNqMkyC/UmzYzRlRF lzZwf2blFCAaNzCQ== Content-Type: text/plain; charset=us-ascii Precedence: list List-Id: List-Subscribe: , List-Unsubscribe: , List-Post: List-Help: Sender: Mail-Followup-To: Mime-Version: 1.0 Subject: Re: [PATCH] linux: layer7: Don't linearize the skb in the match From: Michael Tremer In-Reply-To: <20261103221033.38011-1-antonio@mandelbit.com> Date: Wed, 4 Nov 2026 14:55:49 +0000 Cc: development@lists.ipfire.org Content-Transfer-Encoding: quoted-printable Message-Id: References: <20261103221033.38011-1-antonio@mandelbit.com> To: Antonio Quartulli Hello Antonio, Thank you very much for tracking this one down. This bug has been open = for a long time and nobody could reproduce it reliably, so the detailed = reproducer is very much appreciated. The analysis is spot on: when a match returns false, ipt_do_table() goes = straight to the next rule with the cached IP header pointer, and a match = should never modify the skb in the first place. Reading the data with = skb_header_pointer() is the right way to do this. I have merged the patch into next, so it will be part of Core Update = 207, which ships a new kernel anyway. I have also removed the old layer7 = patch for 6.12 which was not used any more. Best, -Michael > On 3 Nov 2026, at 22:10, Antonio Quartulli = wrote: >=20 > skb_linearize() can free the old skb head while ipt_do_table() keeps > using its cached IP header pointer for the next rule, which KFENCE > reports as a use-after-free when QoS is enabled. Read the application > data without modifying the skb instead. >=20 > Fixes: #13953 > Signed-off-by: Antonio Quartulli > --- > How to reproduce (no QoS configuration needed): >=20 > 1. The match must see received, non-cloned, non-linear skbs with a = small > kmalloc'ed head, e.g. on Intel I226-V (igc), or on a VM whose red = virtio > NIC has mergeable rx buffers disabled and receives GSO packets: > >=20 > 2. Add the same rules QoS creates (a layer7 match that is false for > unclassified connections, followed by another rule): >=20 > iptables -t mangle -N L7T > iptables -t mangle -A PREROUTING -i red0 -j L7T > iptables -t mangle -A L7T -m layer7 --l7dir = /etc/l7-protocols/protocols \ > ! --l7proto unset -j MARK --set-xmark 0x1/0xf > iptables -t mangle -A L7T -j CONNMARK --save-mark --mask 0xf >=20 > 3. Make KFENCE sample often: >=20 > echo 1 > /sys/module/kfence/parameters/sample_interval >=20 > 4. Open many short TCP connections from the firewall to a host on red = that > sends data back (e.g. 5000 connections downloading 256 KiB each). >=20 > Without this patch, "BUG: KFENCE: use-after-free read in ipt_do_table" > shows up within seconds (31 reports for 5000 connections on a Core 205 > VM). With it, there were none after 50000 connections, and layer7 = still > classified all connections correctly. >=20 > src/patches/linux/linux-6.18-layer7.patch | 80 ++++++++++++++++------- > 1 file changed, 55 insertions(+), 25 deletions(-) >=20 > diff --git a/src/patches/linux/linux-6.18-layer7.patch = b/src/patches/linux/linux-6.18-layer7.patch > index a99800d62..8ecf69b57 100644 > --- a/src/patches/linux/linux-6.18-layer7.patch > +++ b/src/patches/linux/linux-6.18-layer7.patch > @@ -1501,7 +1501,7 @@ new file mode 100644 > index 000000000000..a7eb760123bf > --- /dev/null > +++ b/net/netfilter/xt_layer7.c > -@@ -0,0 +1,666 @@ > +@@ -0,0 +1,696 @@ > +/* > + Kernel module to match application layer (OSI layer 7) data in = connections. > + > @@ -1572,6 +1572,10 @@ index 000000000000..a7eb760123bf > + > +DEFINE_SPINLOCK(l7_lock); > + > ++/* Copy of the application data of the packet being matched = (maxdatalen > ++bytes), protected by l7_lock */ > ++static char *l7_scratch; > ++ > +#ifdef CONFIG_IP_NF_MATCH_LAYER7_DEBUG > +/* Converts an unfriendly string into a friendly one by > +replacing unprintables with periods and all whitespace with " ". */ > @@ -1763,7 +1767,8 @@ index 000000000000..a7eb760123bf > + return 1; > +} > + > -+/* Returns offset the into the skb->data that the application data = starts */ > ++/* Returns offset the into the skb->data that the application data = starts, > ++or -1 if it can't be determined */ > +static int app_data_offset(const struct sk_buff *skb) > +{ > + /* In case we are ported somewhere (ebtables?) where ip_hdr(skb) > @@ -1774,10 +1779,15 @@ index 000000000000..a7eb760123bf > + /* 12 =3D=3D offset into TCP header for the header length field. > + Can't get this with skb->h.th->doff because the tcphdr > + struct doesn't get set when routing (this is confirmed to be > -+ true in Netfilter as well as QoS.) */ > -+ int tcp_hl =3D 4*(skb->data[ip_hl + 12] >> 4); > ++ true in Netfilter as well as QoS.) The TCP header might not be > ++ in the linear part, so don't read it from skb->data directly. */ > ++ u8 _doff, *doff; > ++ > ++ doff =3D skb_header_pointer(skb, ip_hl + 12, sizeof(_doff), = &_doff); > ++ if (!doff) > ++ return -1; > + > -+ return ip_hl + tcp_hl; > ++ return ip_hl + 4 * (*doff >> 4); > + } else if( ip_hdr(skb)->protocol =3D=3D IPPROTO_UDP ) { > + return ip_hl + 8; /* UDP header is always 8 bytes */ > + } else if( ip_hdr(skb)->protocol =3D=3D IPPROTO_ICMP ) { > @@ -1945,6 +1955,7 @@ index 000000000000..a7eb760123bf > + struct nf_conn *master_conntrack, *conntrack; > + unsigned char * app_data; > + unsigned int pattern_result, appdatalen; > ++ int appdataoff; > + regexp * comppattern; > + > + /* Be paranoid/incompetent - lock the entire match function. */ > @@ -1992,20 +2003,6 @@ index 000000000000..a7eb760123bf > + return (pattern_result ^ info->invert); > + } > + > -+ if(skb_is_nonlinear(skb)){ > -+ if(skb_linearize(skb) !=3D 0){ > -+ if (net_ratelimit()) > -+ printk(KERN_ERR "layer7: failed to linearize " > -+ "packet, bailing.\n"); > -+ spin_unlock_bh(&l7_lock); > -+ return info->invert; > -+ } > -+ } > -+ > -+ /* now that the skb is linearized, it's safe to set these. */ > -+ app_data =3D skb->data + app_data_offset(skb); > -+ appdatalen =3D skb_tail_pointer(skb) - app_data; > -+ > + /* the return value gets checked later, when we're ready to use it = */ > + comppattern =3D compile_and_cache(info->pattern, info->protocol); > + > @@ -2028,6 +2025,25 @@ index 000000000000..a7eb760123bf > + if(!skb->layer7_flags[0]){ > + int newbytes; > + master_conntrack->layer7.packets++; > ++ > ++ /* Don't linearize the skb to get at the application data: a > ++ match must not modify the skb, because ipt_do_table() keeps > ++ using pointers into its old head, which skb_linearize() might > ++ free. Copy the data into l7_scratch instead if it is not in > ++ the linear part. */ > ++ appdataoff =3D app_data_offset(skb); > ++ if(appdataoff < 0 || appdataoff > skb->len){ > ++ app_data =3D NULL; > ++ appdatalen =3D 0; > ++ } else { > ++ appdatalen =3D min_t(unsigned int, skb->len - appdataoff, > ++ maxdatalen); > ++ app_data =3D skb_header_pointer(skb, appdataoff, > ++ appdatalen, l7_scratch); > ++ if(!app_data) > ++ appdatalen =3D 0; > ++ } > ++ > + newbytes =3D add_data(master_conntrack, app_data, appdatalen); > + if(newbytes =3D=3D 0) { /* didn't add any data */ > + skb->layer7_flags[0] =3D 1; > @@ -2137,11 +2153,9 @@ index 000000000000..a7eb760123bf > + > +static int __init xt_layer7_init(void) > +{ > -+// need_conntrack(); > ++ int ret; > + > -+ // Register proc interface > -+ proc_create_data("layer7_numpackets", 0644, > -+ init_net.proc_net, &layer7_numpackets_proc_fops, NULL); > ++// need_conntrack(); > + > + if(maxdatalen < 1) { > + printk(KERN_WARNING "layer7: maxdatalen can't be < 1, " > @@ -2155,14 +2169,30 @@ index 000000000000..a7eb760123bf > + "using 65536\n"); > + maxdatalen =3D 65536; > + } > -+ return xt_register_matches(xt_layer7_match, > -+ ARRAY_SIZE(xt_layer7_match)); > ++ > ++ l7_scratch =3D kmalloc(maxdatalen, GFP_KERNEL); > ++ if(!l7_scratch) > ++ return -ENOMEM; > ++ > ++ // Register proc interface > ++ proc_create_data("layer7_numpackets", 0644, > ++ init_net.proc_net, &layer7_numpackets_proc_fops, NULL); > ++ > ++ ret =3D xt_register_matches(xt_layer7_match, > ++ ARRAY_SIZE(xt_layer7_match)); > ++ if(ret) { > ++ remove_proc_entry("layer7_numpackets", init_net.proc_net); > ++ kfree(l7_scratch); > ++ } > ++ > ++ return ret; > +} > + > +static void __exit xt_layer7_fini(void) > +{ > + remove_proc_entry("layer7_numpackets", init_net.proc_net); > + xt_unregister_matches(xt_layer7_match, ARRAY_SIZE(xt_layer7_match)); > ++ kfree(l7_scratch); > +} > + > +module_init(xt_layer7_init); > --=20 > 2.56.0 >=20 >=20