From patchwork Tue Nov 3 22:10:33 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Antonio Quartulli X-Patchwork-Id: 10648 Return-Path: Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1 raw public key) server-digest SHA384 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mail01.haj.ipfire.org", Issuer "YR1" (not verified)) by web04.haj.ipfire.org (Postfix) with ESMTPS id 4jK2pH6WxSz3x5B for ; Tue, 03 Nov 2026 22:11:03 +0000 (UTC) Received: from mail02.haj.ipfire.org (mail02.haj.ipfire.org [172.28.1.201]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519) (Client CN "mail02.haj.ipfire.org", Issuer "YE2" (not verified)) by mail01.ipfire.org (Postfix) with ESMTPS id 4jK2pH3sb7z836 for ; Tue, 03 Nov 2026 22:11:03 +0000 (UTC) Received: from mail02.haj.ipfire.org (localhost [IPv6:::1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4jK2p10mXDz36gv for ; Tue, 03 Nov 2026 22:10:49 +0000 (UTC) X-Original-To: development@lists.ipfire.org 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 server-signature ECDSA (secp384r1 raw public key) server-digest SHA384 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mail01.haj.ipfire.org", Issuer "YR1" (not verified)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4jK2ny2wF4z2xKZ for ; Tue, 03 Nov 2026 22:10:46 +0000 (UTC) Received: from mout-b-203.mailbox.org (mout-b-203.mailbox.org [195.10.208.52]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bit raw public key) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "*.mailbox.org", Issuer "Thawte TLS RSA CA G1" (verified OK)) by mail01.ipfire.org (Postfix) with ESMTPS id 4jK2np0tx3z6NH for ; Tue, 03 Nov 2026 22:10:38 +0000 (UTC) Authentication-Results: mail01.ipfire.org; dkim=pass header.d=mandelbit.com header.s=MBO0001 header.b=xPQJrjgs; spf=pass (mail01.ipfire.org: domain of antonio@mandelbit.com designates 195.10.208.52 as permitted sender) smtp.mailfrom=antonio@mandelbit.com; dmarc=none ARC-Seal: i=1; a=rsa-sha256; d=lists.ipfire.org; s=202003rsa; cv=none; t=1793743838; b=RMCyu9/YdYfWzFg593cHV/pj07qumlrBIDqdH3T8FsJ4qT8vbqfN0pgBRqgfhAL2t6eWNq RWteGmtkxnfQbIWx7BfEPHsHS4+GENheKzH/KjCPnTDyWptXKjVArUKOafNRL/NQZsuFED zeHdWvH3/Wp2OrInvPqDt+2QmV4E8nzDVRwP3a3keUXmwZrsAKQgOgauQ83QXW1c9ycznH BjU6hlpPh7noIcUJkuwtCGLuiDsGtIZViMPMPVtvSDoDvvpmUoHHfIsIhySXp54rU6MTDP I08iOZneiw65nGh5D/hwnZaNL4y1Wfx+2JlM69axJX1cmlTF2lDHB4ocvq/V6w== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=lists.ipfire.org; s=202003rsa; t=1793743838; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding:dkim-signature; bh=aewzG5Y5pvn0e5BupyEGc5hXu7ooxB0PKHEsHpzTvhQ=; b=YjV65Eci5Map2+nVyI44DdnMZxl9gOa9oG3yrJ5SI9EBMmiNYjkCazgpg2ob2b8fKqHHFR VLj/8xV+PjkTziSUSLD/RkhzUSPgbN6rp4oCe8lg+dta1PGZYGTnUIaG29YTWFbBlgg3Xl IlJcjrboFvX72l5chpAy053bPa3/wILgskKHMUjIQcFN0dGxDqlqpMxITPKGNrTE4afKvC qLQMUpBpZVaUFh7jYABR15K4ohlOCDlQI1XaUxSelmGyhWRjhWYU1mM1sNWkMldpPr11Sy izRd8zrAPY+kV3U43tAuSG6gIp+XXXhhaSFkSKisrqTNvgj6r8O6DpmCC6KAsQ== ARC-Authentication-Results: i=1; mail01.ipfire.org; dkim=pass header.d=mandelbit.com header.s=MBO0001 header.b=xPQJrjgs; spf=pass (mail01.ipfire.org: domain of antonio@mandelbit.com designates 195.10.208.52 as permitted sender) smtp.mailfrom=antonio@mandelbit.com; dmarc=none Received: from smtp102.mailbox.org (smtp102.mailbox.org [IPv6:2001:67c:2050:b231:465::102]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519MLKEM768 server-signature RSA-PSS (4096 bits) server-digest SHA512) (No client certificate requested) by mout-b-203.mailbox.org (Postfix) with ESMTPS id 4jK2nm2TGCzLm8t; Tue, 03 Nov 2026 23:10:36 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mandelbit.com; s=MBO0001; t=1793743836; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding; bh=aewzG5Y5pvn0e5BupyEGc5hXu7ooxB0PKHEsHpzTvhQ=; b=xPQJrjgsb0RoyUejCjQbSwR1GT44tgHWWQ4fH42poIAE0fBeRjlvFaiohd5PXkCCzQeA0A 9bhwMPnEhJLDeRNxFxKdmkcA2d3WwgPCyDXizwIGIvS2Byy7ENV72ePKGExWlzcaj/3m7J 8Crweu8jTPFIvNxjqlAC3yHeO7EOUcBb5viTyKMZgKfdcZu6zMcWVnQ35NIhQXmVorar8o oII2MVNx5jzg9LrI1wQrASKPQcZG6Och/IB4hYQb+iJp5uW4N5dR4JxVt5fzMst8/w47it hb+XOvBaCQ3tS/ifCnVHVR4YSgVn4/y7M6zBzSGJmqdPVHpRXbPE4T1F/9O3Bg== From: Antonio Quartulli To: development@lists.ipfire.org Cc: Antonio Quartulli Subject: [PATCH] linux: layer7: Don't linearize the skb in the match Date: Tue, 3 Nov 2026 23:10:33 +0100 Message-ID: <20261103221033.38011-1-antonio@mandelbit.com> X-Mailer: git-send-email 2.56.0 Precedence: list List-Id: List-Subscribe: , List-Unsubscribe: , List-Post: List-Help: Sender: Mail-Followup-To: MIME-Version: 1.0 X-Rspamd-Action: no action X-Spamd-Result: default: False [-8.29 / 11.00]; NEURAL_HAM(-3.00)[-1.000]; BAYES_HAM(-3.00)[100.00%]; R_DKIM_ALLOW(-1.45)[mandelbit.com:s=MBO0001]; MID_CONTAINS_FROM(1.00)[]; SPF_REPUTATION_HAM(-0.82)[-0.81898743000276]; DKIM_REPUTATION(-0.82)[-0.81898743000276]; R_MISSING_CHARSET(0.50)[]; RCVD_IN_DNSWL_LOW(-0.20)[2001:67c:2050:b231:465::102:received,195.10.208.52:from]; R_SPF_ALLOW(-0.20)[+ip4:195.10.208.0/24]; R_DKIM_ALIGNED(-0.10)[strict]; MX_GOOD(-0.10)[mxext4.mailbox.org]; MIME_GOOD(-0.10)[text/plain]; ARC_NA(0.00)[]; TO_DN_SOME(0.00)[]; MIME_TRACE(0.00)[0:+]; RCPT_COUNT_TWO(0.00)[2]; DMARC_NA(0.00)[mandelbit.com]; IP_REPUTATION_HAM(0.00)[asn: 199118(-0.33), country: DE(-0.01), ip: 195.10.208.52(0.00)]; DKIM_TRACE(0.00)[mandelbit.com:+]; ARC_SIGNED(0.00)[lists.ipfire.org:s=202003rsa:i=1]; FROM_EQ_ENVFROM(0.00)[]; RCVD_TLS_ALL(0.00)[]; TO_MATCH_ENVRCPT_SOME(0.00)[]; RCVD_COUNT_ONE(0.00)[1]; ASN(0.00)[asn:199118, ipnet:195.10.208.0/24, country:DE]; FROM_HAS_DN(0.00)[] X-Rspamd-Server: mail01.haj.ipfire.org X-Rspamd-Queue-Id: 4jK2np0tx3z6NH 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. Fixes: #13953 Signed-off-by: Antonio Quartulli --- How to reproduce (no QoS configuration needed): 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: 2. Add the same rules QoS creates (a layer7 match that is false for unclassified connections, followed by another rule): 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 3. Make KFENCE sample often: echo 1 > /sys/module/kfence/parameters/sample_interval 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). 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. src/patches/linux/linux-6.18-layer7.patch | 80 ++++++++++++++++------- 1 file changed, 55 insertions(+), 25 deletions(-) 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 == 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 = 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 = 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 == IPPROTO_UDP ) { + return ip_hl + 8; /* UDP header is always 8 bytes */ + } else if( ip_hdr(skb)->protocol == 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) != 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 = skb->data + app_data_offset(skb); -+ appdatalen = skb_tail_pointer(skb) - app_data; -+ + /* the return value gets checked later, when we're ready to use it */ + comppattern = 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 = app_data_offset(skb); ++ if(appdataoff < 0 || appdataoff > skb->len){ ++ app_data = NULL; ++ appdatalen = 0; ++ } else { ++ appdatalen = min_t(unsigned int, skb->len - appdataoff, ++ maxdatalen); ++ app_data = skb_header_pointer(skb, appdataoff, ++ appdatalen, l7_scratch); ++ if(!app_data) ++ appdatalen = 0; ++ } ++ + newbytes = add_data(master_conntrack, app_data, appdatalen); + if(newbytes == 0) { /* didn't add any data */ + skb->layer7_flags[0] = 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 = 65536; + } -+ return xt_register_matches(xt_layer7_match, -+ ARRAY_SIZE(xt_layer7_match)); ++ ++ l7_scratch = 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 = 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);