* [PATCH net] netfilter: nf_reject: validate TCP header length before sending resets
@ 2026-11-02 15:56 zhangmh25
2026-11-02 22:28 ` Pablo Neira Ayuso
2026-11-03 15:57 ` netdev-bot+sashiko
0 siblings, 2 replies; 3+ messages in thread
From: zhangmh25 @ 2026-11-02 15:56 UTC (permalink / raw)
To: netfilter-devel; +Cc: netdev, pablo, coreteam, stable, zhangmh25, jianjun
From: Minghao Zhang <zhangmh25@mails.tsinghua.edu.cn>
The TCP header validation in both nf_reject_ip_tcphdr_get() and
nf_reject_ip6_tcphdr_get() checks that the TCP segment is at least
sizeof(struct tcphdr) long, but does not verify that the header
length claimed via the data offset is actually present in the packet.
A peer can send a TCP packet claiming a 60-byte header although only
the fixed 20-byte header is present.
The reset generation then computes the ACK number using the claimed
header length, and the subtraction of the data offset from the TCP
segment length underflows, producing a wrapped ACK value in the
generated RST. For a 20-byte segment with doff = 15, ACK bit clear,
sequence number 0, SYN and FIN clear, the RST carries ack_seq
0xffffffd8 instead of a meaningful value.
Reject TCP headers whose data offset is smaller than the fixed header
or exceeds the available TCP segment length. This prevents generating
resets with an ACK number derived from an invalid TCP header length.
Found during a systematic audit of TCP header validation in the
netfilter reject paths. Tested in network namespaces with nftables
"reject with tcp reset" rules: on both IPv4 and IPv6, a probe with
doff = 15 produced an RST with a wrapped ack_seq; with the fix no
RST is generated for such packets, while probes with well-formed
headers still get a correct RST.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Fixes: 764d8a9f2407 ("[NETFILTER]: Add IPv6 REJECT target")
Reported-by: Minghao Zhang <zhangmh25@mails.tsinghua.edu.cn>
Reported-by: Jianjun Chen <jianjun@tsinghua.edu.cn>
Cc: stable@vger.kernel.org
Signed-off-by: Minghao Zhang <zhangmh25@mails.tsinghua.edu.cn>
---
net/ipv4/netfilter/nf_reject_ipv4.c | 5 +++++
net/ipv6/netfilter/nf_reject_ipv6.c | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/net/ipv4/netfilter/nf_reject_ipv4.c b/net/ipv4/netfilter/nf_reject_ipv4.c
index 59ec465a9..0e8823a7d 100644
--- a/net/ipv4/netfilter/nf_reject_ipv4.c
+++ b/net/ipv4/netfilter/nf_reject_ipv4.c
@@ -189,6 +189,11 @@ nf_reject_ip_tcphdr_get(struct sk_buff *oldskb,
if (oth == NULL)
return NULL;
+ /* Reject invalid or truncated TCP headers. */
+ if (oth->doff < sizeof(struct tcphdr) / 4 ||
+ oldskb->len - ip_hdrlen(oldskb) < (oth->doff << 2))
+ return NULL;
+
/* No RST for RST. */
if (oth->rst)
return NULL;
diff --git a/net/ipv6/netfilter/nf_reject_ipv6.c b/net/ipv6/netfilter/nf_reject_ipv6.c
index 07cdaa10d..9dc112e50 100644
--- a/net/ipv6/netfilter/nf_reject_ipv6.c
+++ b/net/ipv6/netfilter/nf_reject_ipv6.c
@@ -223,6 +223,11 @@ nf_reject_ip6_tcphdr_get(struct sk_buff *oldskb,
if (otcph == NULL)
return NULL;
+ /* Reject invalid or truncated TCP headers. */
+ if (otcph->doff < sizeof(struct tcphdr) / 4 ||
+ *otcplen < (otcph->doff << 2))
+ return NULL;
+
/* No RST for RST. */
if (otcph->rst) {
pr_debug("RST is set\n");
--
2.34.1
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net] netfilter: nf_reject: validate TCP header length before sending resets
2026-11-02 15:56 [PATCH net] netfilter: nf_reject: validate TCP header length before sending resets zhangmh25
@ 2026-11-02 22:28 ` Pablo Neira Ayuso
2026-11-03 15:57 ` netdev-bot+sashiko
1 sibling, 0 replies; 3+ messages in thread
From: Pablo Neira Ayuso @ 2026-11-02 22:28 UTC (permalink / raw)
To: zhangmh25; +Cc: netfilter-devel, netdev, coreteam, stable, jianjun
On Mon, Nov 02, 2026 at 11:56:22PM +0800, zhangmh25@mails.tsinghua.edu.cn wrote:
> From: Minghao Zhang <zhangmh25@mails.tsinghua.edu.cn>
>
> The TCP header validation in both nf_reject_ip_tcphdr_get() and
> nf_reject_ip6_tcphdr_get() checks that the TCP segment is at least
> sizeof(struct tcphdr) long, but does not verify that the header
> length claimed via the data offset is actually present in the packet.
> A peer can send a TCP packet claiming a 60-byte header although only
> the fixed 20-byte header is present.
>
> The reset generation then computes the ACK number using the claimed
> header length, and the subtraction of the data offset from the TCP
> segment length underflows, producing a wrapped ACK value in the
> generated RST. For a 20-byte segment with doff = 15, ACK bit clear,
> sequence number 0, SYN and FIN clear, the RST carries ack_seq
> 0xffffffd8 instead of a meaningful value.
So a malformed TCP packet brings another malformed TCP packet...
> Reject TCP headers whose data offset is smaller than the fixed header
> or exceeds the available TCP segment length. This prevents generating
> resets with an ACK number derived from an invalid TCP header length.
Maybe, yes. I think TCP conntrack is marking this as invalid?
In that case, only stateless ruleset behind in this way.
> Found during a systematic audit of TCP header validation in the
> netfilter reject paths. Tested in network namespaces with nftables
> "reject with tcp reset" rules: on both IPv4 and IPv6, a probe with
> doff = 15 produced an RST with a wrapped ack_seq; with the fix no
> RST is generated for such packets, while probes with well-formed
> headers still get a correct RST.
Why did your LLM pick the net.git for this?
Then, another option is nf.git, but I don't think this is nf.git
material, maybe nf-next.git is a better target tree for this?
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Fixes: 764d8a9f2407 ("[NETFILTER]: Add IPv6 REJECT target")
> Reported-by: Minghao Zhang <zhangmh25@mails.tsinghua.edu.cn>
> Reported-by: Jianjun Chen <jianjun@tsinghua.edu.cn>
> Cc: stable@vger.kernel.org
> Signed-off-by: Minghao Zhang <zhangmh25@mails.tsinghua.edu.cn>
> ---
> net/ipv4/netfilter/nf_reject_ipv4.c | 5 +++++
> net/ipv6/netfilter/nf_reject_ipv6.c | 5 +++++
> 2 files changed, 10 insertions(+)
>
> diff --git a/net/ipv4/netfilter/nf_reject_ipv4.c b/net/ipv4/netfilter/nf_reject_ipv4.c
> index 59ec465a9..0e8823a7d 100644
> --- a/net/ipv4/netfilter/nf_reject_ipv4.c
> +++ b/net/ipv4/netfilter/nf_reject_ipv4.c
> @@ -189,6 +189,11 @@ nf_reject_ip_tcphdr_get(struct sk_buff *oldskb,
> if (oth == NULL)
> return NULL;
>
> + /* Reject invalid or truncated TCP headers. */
> + if (oth->doff < sizeof(struct tcphdr) / 4 ||
> + oldskb->len - ip_hdrlen(oldskb) < (oth->doff << 2))
> + return NULL;
> +
> /* No RST for RST. */
> if (oth->rst)
> return NULL;
> diff --git a/net/ipv6/netfilter/nf_reject_ipv6.c b/net/ipv6/netfilter/nf_reject_ipv6.c
> index 07cdaa10d..9dc112e50 100644
> --- a/net/ipv6/netfilter/nf_reject_ipv6.c
> +++ b/net/ipv6/netfilter/nf_reject_ipv6.c
> @@ -223,6 +223,11 @@ nf_reject_ip6_tcphdr_get(struct sk_buff *oldskb,
> if (otcph == NULL)
> return NULL;
>
> + /* Reject invalid or truncated TCP headers. */
> + if (otcph->doff < sizeof(struct tcphdr) / 4 ||
> + *otcplen < (otcph->doff << 2))
> + return NULL;
> +
> /* No RST for RST. */
> if (otcph->rst) {
> pr_debug("RST is set\n");
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH net] netfilter: nf_reject: validate TCP header length before sending resets
2026-11-02 15:56 [PATCH net] netfilter: nf_reject: validate TCP header length before sending resets zhangmh25
2026-11-02 22:28 ` Pablo Neira Ayuso
@ 2026-11-03 15:57 ` netdev-bot+sashiko
1 sibling, 0 replies; 3+ messages in thread
From: netdev-bot+sashiko @ 2026-11-03 15:57 UTC (permalink / raw)
To: zhangmh25; +Cc: netfilter-devel, netdev, pablo, coreteam, stable, jianjun, kuba
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 0 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] The patch does not fully close the case it says it closes.
--
netfilter: nf_reject: validate TCP header length before sending resets
This adds a check to nf_reject_ip_tcphdr_get() and
nf_reject_ip6_tcphdr_get(). It rejects TCP headers whose doff is smaller
than the fixed header or larger than the available TCP segment. The goal
is to stop sending an RST whose ack_seq comes from an invalid header
length.
> diff --git a/net/ipv4/netfilter/nf_reject_ipv4.c b/net/ipv4/netfilter/nf_reject_ipv4.c
> index 7cb33af671e26..5ed97772bbaaf 100644
> --- a/net/ipv4/netfilter/nf_reject_ipv4.c
> +++ b/net/ipv4/netfilter/nf_reject_ipv4.c
> @@ -189,6 +189,11 @@ nf_reject_ip_tcphdr_get(struct sk_buff *oldskb,
> if (oth == NULL)
> return NULL;
>
> + /* Reject invalid or truncated TCP headers. */
> + if (oth->doff < sizeof(struct tcphdr) / 4 ||
> + oldskb->len - ip_hdrlen(oldskb) < (oth->doff << 2))
> + return NULL;
[Severity: High]
Should this check use the length from the IP header instead of
oldskb->len?
nf_reject_iphdr_validate() only rejects skb->len < tot_len. Also,
nf_reject_skb_v4_tcp_reset() never trims oldskb:
net/ipv4/netfilter/nf_reject_ipv4.c:nf_reject_iphdr_validate() {
...
len = ntohs(iph->tot_len);
if (skb->len < len)
return 0;
...
}
The unreach builder in the same file trims before it uses the length:
net/ipv4/netfilter/nf_reject_ipv4.c:nf_reject_skb_v4_unreach() {
...
if (pskb_trim_rcsum(oldskb, ntohs(ip_hdr(oldskb)->tot_len)))
return NULL;
...
}
Callers inside the IP stack get an skb that ip_rcv_core() has already
trimmed. The netdev ingress and bridge reject expressions don't:
nft_do_chain_netdev()
nft_reject_netdev_eval()
nft_reject_netdev_send_v4_tcp_reset()
nf_reject_skb_v4_tcp_reset()
nf_reject_ip_tcphdr_get()
nft_reject_br_send_v4_tcp_reset() works the same way. On these paths
skb->len still counts Ethernet padding and any trailing bytes the sender
adds.
For those skbs, can a doff that runs past the end of the IP-declared
segment, but fits inside the trailer, still pass this check?
Take tot_len = 40 (a 20-byte TCP segment), doff = 15 and 40 trailing
bytes. The check sees 60 >= 60 and passes. nf_ip_checksum() also sums
over skb->len - dataoff, so it doesn't catch this either. Then
nf_reject_ip_tcphdr_put() computes:
tcph->ack_seq = htonl(ntohl(oth->seq) + oth->syn + oth->fin +
oldskb->len - ip_hdrlen(oldskb) -
(oth->doff << 2));
and an RST still goes out. That seems to contradict this part of the
commit message:
on both IPv4 and IPv6, a probe with doff = 15 produced an RST with a
wrapped ack_seq; with the fix no RST is generated for such packets
A minimum-size frame shows the same thing. With tot_len = 40, 6 bytes of
padding and doff = 6, the check passes (26 >= 24) and the RST carries
ack = seq + 2.
The wraparound itself is fixed, because the check and the ack_seq
calculation now use the same length.
IPv6 looks the same. In nf_reject_ip6_tcphdr_get(), *otcplen is
oldskb->len - tcphoff. nf_reject_ip6hdr_validate() only rejects
payload_len + 40 > skb->len, and nf_reject_skb_v6_tcp_reset() never
trims. The IPv6 unreach path does trim to payload_len, in
nf_reject_v6_csum_ok(). nft_reject_netdev_send_v6_tcp_reset() and
nft_reject_br_send_v6_tcp_reset() pass untrimmed skbs into this code.
Would it be better to use ntohs(tot_len) - ip_hdrlen() (and the
payload_len based length for IPv6) for both this check and the ack_seq
calculation? Trimming oldskb first, as the unreach paths do, would also
work.
Either change would also fix an older problem: for well-formed doff = 5
packets on these paths, ack_seq is off by the padding length.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261102155622.2658789-1-zhangmh25%40mails.tsinghua.edu.cn
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-03 15:57 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-02 15:56 [PATCH net] netfilter: nf_reject: validate TCP header length before sending resets zhangmh25
2026-11-02 22:28 ` Pablo Neira Ayuso
2026-11-03 15:57 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox