From: Pablo Neira Ayuso <pablo@netfilter.org>
To: netfilter-devel@vger.kernel.org
Cc: davem@davemloft.net, netdev@vger.kernel.org, kuba@kernel.org,
pabeni@redhat.com, edumazet@google.com, horms@kernel.org,
fw@strlen.de, ja@ssi.bg
Subject: [PATCH net-next 1/8] netfilter: validate L4 headers after userspace packet writes
Date: Tue, 18 Aug 2026 01:29:50 +0200 [thread overview]
Message-ID: <20260817232957.1281637-2-pablo@netfilter.org> (raw)
In-Reply-To: <20260817232957.1281637-1-pablo@netfilter.org>
From: Zhiling Zou <zhilinz@nebusec.ai>
NFQUEUE and nft_payload can hand packet data modified by userspace back
to the stack. Recent restrictions keep link and network headers stable,
but transport header fields can still be changed.
A packet can therefore keep the same network header and conntrack entry
while changing the transport header layout. For TCP, increasing doff can
make later helper or NAT code use a different transport-header base than
the parser used, and can make offsets point past skb->tail.
Extend NFQUEUE payload validation to check the final L4 protocol and
known base headers after IPv4 options or IPv6 extension headers. Reject
packets whose L4 protocol no longer matches an attached non-template
conntrack entry, and reject IP fragments that already have such a
conntrack entry before trying to validate transport headers. Unknown L4
protocols are left to their normal protocol handlers.
For nft payload writes, reject transport-header stores that overlap TCP
doff. nft_nh_write_ok() already rejects network-header protocol changes,
so keeping doff stable prevents nft payload writes from changing the TCP
header length underneath conntrack and helper users.
This patch is a follow up to commit df07998dfd40 ("netfilter: nftables:
restrict linklayer and network header writes") and commit 54f34607d184
("netfilter: nfnetlink_queue: restrict writes to network header").
Reported-by: Vega <vega@nebusec.ai>
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/nfnetlink_queue.c | 72 +++++++++++++++++++++++++++++++--
net/netfilter/nft_payload.c | 13 ++++++
2 files changed, 82 insertions(+), 3 deletions(-)
diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
index b8aaf39cb4d8..c727668b0c5b 100644
--- a/net/netfilter/nfnetlink_queue.c
+++ b/net/netfilter/nfnetlink_queue.c
@@ -28,10 +28,17 @@
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_queue.h>
#include <linux/netfilter/nf_conntrack_common.h>
+#include <linux/icmp.h>
+#include <linux/icmpv6.h>
+#include <linux/ip.h>
#include <linux/list.h>
+#include <linux/sctp.h>
#include <linux/cgroup-defs.h>
#include <linux/rhashtable.h>
#include <linux/jhash.h>
+#include <linux/tcp.h>
+#include <linux/udp.h>
+#include <net/gre.h>
#include <net/gso.h>
#include <net/sock.h>
#include <net/tcp_states.h>
@@ -1206,10 +1213,62 @@ static bool nfqnl_validate_ipopts(const struct iphdr *iph_new,
return memcmp(iph_new + 1, ip_hdr(e->skb) + 1, ihl - sizeof(*iph_orig)) == 0;
}
+static bool nfqnl_validate_l4(const u8 *data, unsigned int data_len,
+ const struct nf_queue_entry *e, u8 proto,
+ bool fragment)
+{
+#if IS_ENABLED(CONFIG_NF_CONNTRACK)
+ enum ip_conntrack_info ctinfo;
+ const struct nf_conn *ct;
+
+ ct = nf_ct_get(e->skb, &ctinfo);
+ if (ct && !nf_ct_is_template(ct)) {
+ if (fragment || nf_ct_protonum(ct) != proto)
+ return false;
+ }
+#endif
+
+ if (fragment)
+ return true;
+
+ switch (proto) {
+ case IPPROTO_TCP: {
+ const struct tcphdr *th = (const struct tcphdr *)data;
+ unsigned int thlen;
+
+ if (data_len < sizeof(*th))
+ return false;
+
+ thlen = __tcp_hdrlen(th);
+ if (thlen < sizeof(*th) || data_len < thlen)
+ return false;
+
+ return true;
+ }
+ case IPPROTO_UDP:
+ return data_len >= sizeof(struct udphdr);
+ case IPPROTO_ICMP:
+ return data_len >= sizeof(struct icmphdr);
+ case IPPROTO_ICMPV6:
+ return data_len >= sizeof(struct icmp6hdr);
+ case IPPROTO_SCTP:
+ return data_len >= sizeof(struct sctphdr);
+ case IPPROTO_GRE:
+ return data_len >= sizeof(struct gre_base_hdr);
+ case IPPROTO_AH:
+ return data_len >= sizeof(struct ip_auth_hdr);
+ case IPPROTO_ESP:
+ return data_len >= sizeof(struct ip_esp_hdr);
+ }
+
+ return true;
+}
+
static bool nfqnl_validate_ip4(const struct iphdr *iph, unsigned int data_len,
const struct nf_queue_entry *e)
{
unsigned int ihl;
+ bool fragment;
if (data_len < sizeof(*iph))
return false;
@@ -1226,10 +1285,14 @@ static bool nfqnl_validate_ip4(const struct iphdr *iph, unsigned int data_len,
if (ntohs(iph->tot_len) != data_len)
return false;
+ fragment = iph->frag_off & htons(IP_MF | IP_OFFSET);
+
/* support for ipopts mangling would require
* recompile + skb transport header update.
*/
- return nfqnl_validate_ipopts(iph, e);
+ return nfqnl_validate_ipopts(iph, e) &&
+ nfqnl_validate_l4((const u8 *)iph + ihl, data_len - ihl, e,
+ iph->protocol, fragment);
}
static bool nfqnl_validate_one_exthdr(const u8 *data,
@@ -1273,6 +1336,7 @@ static bool nfqnl_validate_exthdr(const struct ipv6hdr *ip6_new,
const u8 *data = (const u8 *)ip6_new;
u8 orig_nexthdr = ip6_orig->nexthdr;
u8 new_nexthdr = ip6_new->nexthdr;
+ bool fragment = false;
if (new_nexthdr != orig_nexthdr)
return false;
@@ -1286,7 +1350,8 @@ static bool nfqnl_validate_exthdr(const struct ipv6hdr *ip6_new,
int hdrlen;
if (orig_nexthdr == NEXTHDR_NONE)
- return true;
+ return nfqnl_validate_l4(data, data_len, e,
+ new_nexthdr, fragment);
if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
return false;
@@ -1297,6 +1362,7 @@ static bool nfqnl_validate_exthdr(const struct ipv6hdr *ip6_new,
switch (orig_nexthdr) {
case NEXTHDR_FRAGMENT:
+ fragment = true;
hdrlen = sizeof(struct frag_hdr);
break;
case NEXTHDR_AUTH:
@@ -1323,7 +1389,7 @@ static bool nfqnl_validate_exthdr(const struct ipv6hdr *ip6_new,
data += hdrlen;
}
- return true;
+ return nfqnl_validate_l4(data, data_len, e, new_nexthdr, fragment);
}
static bool nfqnl_validate_ip6(const struct ipv6hdr *ip6, unsigned int data_len,
diff --git a/net/netfilter/nft_payload.c b/net/netfilter/nft_payload.c
index 8a4472fd77d9..e315d35f73d4 100644
--- a/net/netfilter/nft_payload.c
+++ b/net/netfilter/nft_payload.c
@@ -1067,6 +1067,17 @@ static bool nft_payload_csum_write_ok(const struct nft_pktinfo *pkt,
return false;
}
+static bool nft_th_write_ok(const struct nft_pktinfo *pkt,
+ const struct nft_payload_set *priv)
+{
+ unsigned int doff = offsetof(struct tcphdr, ack_seq) + sizeof(__be32);
+
+ if (pkt->tprot != IPPROTO_TCP)
+ return true;
+
+ return priv->offset > doff || priv->offset + priv->len <= doff;
+}
+
static void nft_payload_set_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
@@ -1105,6 +1116,8 @@ static void nft_payload_set_eval(const struct nft_expr *expr,
case NFT_PAYLOAD_TRANSPORT_HEADER:
if (!(pkt->flags & NFT_PKTINFO_L4PROTO) || pkt->fragoff)
goto err;
+ if (!nft_th_write_ok(pkt, priv))
+ goto err;
offset = nft_thoff(pkt);
break;
case NFT_PAYLOAD_INNER_HEADER:
--
2.47.3
next prev parent reply other threads:[~2026-08-17 23:30 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-17 23:29 [PATCH net-next 0/8] Netfilter/IPVS fixes for net-next Pablo Neira Ayuso
2026-08-17 23:29 ` Pablo Neira Ayuso [this message]
2026-08-17 23:29 ` [PATCH net-next 2/8] netfilter: ipset: remove need to allocate memory on delete operations Pablo Neira Ayuso
2026-08-17 23:29 ` [PATCH net-next 3/8] netfilter: nf_tables: don't queue packet path object notifications Pablo Neira Ayuso
2026-08-17 23:29 ` [PATCH net-next 4/8] netfilter: nf_conntrack_expect: consolidate check for insertion of dead expectation Pablo Neira Ayuso
2026-08-17 23:29 ` [PATCH net-next 5/8] netfilter: ctnetlink: do not expose expectation DEAD flag Pablo Neira Ayuso
2026-08-17 23:29 ` [PATCH net-next 6/8] netfilter: nf_tables: move set_update_list to nftables per-netns Pablo Neira Ayuso
2026-08-17 23:29 ` [PATCH net-next 7/8] netfilter: nf_tables: call set ops .commit when building new ruleset blob Pablo Neira Ayuso
2026-08-17 23:29 ` [PATCH net-next 8/8] ipvs: fix integer overflow in ftp helper port/address parsing Pablo Neira Ayuso
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260817232957.1281637-2-pablo@netfilter.org \
--to=pablo@netfilter.org \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=fw@strlen.de \
--cc=horms@kernel.org \
--cc=ja@ssi.bg \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=netfilter-devel@vger.kernel.org \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox