diff mbox series

[net] netfilter: nf_conntrack_reasm: avoid truncating header offset

Message ID 20260929090027.200041-1-tjdqudcks0424@naver.com
State New
Headers show
Series [net] netfilter: nf_conntrack_reasm: avoid truncating header offset | expand

Commit Message

sung byeongchan Sept. 29, 2026, 9 a.m. UTC
From: 성병찬 <tjdqudcks0424@naver.com>

find_prev_fhdr() stores the offset of the previous Next Header field in
an 8-bit variable. A valid IPv6 extension header chain can place that
field at offset 256, causing the value to wrap to zero.

The truncated value is later stored in frag_queue.nhoffset and used by
nf_ct_frag6_reasm() as the index at which the Fragment Header's next
header value is written. With an offset of 256, this overwrites byte
zero of the IPv6 header instead of the preceding extension header's
Next Header field. The reassembled packet is then rejected because its
IPv6 version field has been corrupted.

Use int for prev_nhoff, matching the type of start and the prevhoff
output argument.

This was reproduced on Linux v7.2.8 with KASAN enabled. Before the
change, a control packet with the preceding Next Header field at offset
248 was delivered, while the equivalent packet at offset 256 was
dropped and Ip6InHdrErrors increased by one. After the change, both
packets were delivered and Ip6InHdrErrors did not increase. The
before/after result was reproduced twice.

Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
Cc: stable@vger.kernel.org
Signed-off-by: 성병찬 <tjdqudcks0424@naver.com>
---
 net/ipv6/netfilter/nf_conntrack_reasm.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


base-commit: 72d3fcf802c45d00b300f25b848a93c3a2bd7c7e

Comments

netdev-bot+sashiko@kernel.org Oct. 2, 2026, 12:02 a.m. UTC | #1
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 widens prev_nhoff to int, but header offsets in this
  parse-and-reassemble path are still truncated to 16 bits with no range…

--

netfilter: nf_conntrack_reasm: avoid truncating header offset

This patch changes prev_nhoff in find_prev_fhdr() from u8 to int, so a
Next Header offset of 256 or more is no longer truncated. Before the
change, nf_ct_frag6_reasm() used the truncated value from
frag_queue.nhoffset and overwrote the IPv6 header instead of the
preceding extension header's Next Header field.

> diff --git a/net/ipv6/netfilter/nf_conntrack_reasm.c b/net/ipv6/netfilter/nf_conntrack_reasm.c
> index 599c49bf0a0af..be72c4346f8b7 100644
> --- a/net/ipv6/netfilter/nf_conntrack_reasm.c
> +++ b/net/ipv6/netfilter/nf_conntrack_reasm.c
> @@ -398,7 +398,7 @@ find_prev_fhdr(struct sk_buff *skb, u8 *prevhdrp, int *prevhoff, int *fhoff)
>  {
>  	u8 nexthdr = ipv6_hdr(skb)->nexthdr;
>  	const int netoff = skb_network_offset(skb);
> -	u8 prev_nhoff = netoff + offsetof(struct ipv6hdr, nexthdr);
> +	int prev_nhoff = netoff + offsetof(struct ipv6hdr, nexthdr);

[Severity: High]
This fixes the 8-bit truncation. Are the same offsets still truncated to
16 bits later in this path?

find_prev_fhdr() bounds prev_nhoff and fhoff only by skb->len. Both values
then go into 16-bit storage with no range check:

include/net/ipv6_frag.h:struct frag_queue {
    ...
	__u16			nhoffset;
    ...
}

net/ipv6/netfilter/nf_conntrack_reasm.c:nf_ct_frag6_queue() {
    ...
	if (offset == 0) {
		fq->nhoffset = nhoff;
    ...
}

net/ipv6/netfilter/nf_conntrack_reasm.c:nf_ct_frag6_gather() {
    ...
	skb_set_transport_header(skb, fhoff);
	hdr = ipv6_hdr(skb);
	fhdr = (struct frag_hdr *)skb_transport_header(skb);
    ...
}

skb_set_transport_header() adds fhoff to the u16 skb->transport_header.
If headroom + fhoff is above 65535, the value wraps and fhdr no longer
points at the real Fragment Header.

At PRE_ROUTING this seems harmless. ip6_rcv_core() trims skb->len to
payload_len + 40, and nf_ct_frag6_queue() then appears to drop the wrapped
cases because end exceeds IPV6_MAXPLEN.

The LOCAL_OUT raw path looks different. rawv6_send_hdrinc() limits the
length only by dev->mtu, which is 65536 on lo by default. It does not
rewrite payload_len and calls NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_OUT, ...)
directly.

lo has 16 bytes of headroom. So a 65536-byte IPV6_HDRINCL packet can put
fhoff at 65528 behind a chain of Destination Options headers.
transport_header then wraps to 8, and fhdr points into the headroom at
skb->head + 8.

The real Fragment Header can carry a nonzero offset, so
ipv6frag_thdr_truncated() still passes. Suppose the stale headroom bytes
decode as offset 0 with MF clear, for example zeroed memory under
init_on_alloc. Would the packet then be reassembled as a single fragment?

If so, could it reach this sequence?

nf_ct_frag6_gather()
  nf_ct_frag6_queue()
  nf_ct_frag6_reasm()
    skb_network_header(skb)[fq->nhoffset] = skb_transport_header(skb)[0];
    ...
    skb->network_header += sizeof(struct frag_hdr);
    ...
    inet_frag_reasm_finish()
      skb_push(head, head->data - skb_network_header(head));

The first line copies a byte of uninitialized headroom into the packet
data.

The network_header increment moves it to head + 24, past skb->data at
head + 16. The skb_push() length is then -8, which becomes 0xFFFFFFF8 as
an unsigned int. Would that trigger skb_under_panic()?

This path needs CAP_NET_RAW, which an unprivileged user may get inside a
user and net namespace. It also needs conntrack defrag active in that
netns, for example through an nft ct rule.

Also, if lo's MTU is raised above 65536, can prev_nhoff reach 65536 or
more? The store into the __u16 nhoffset would then wrap to 0, 8 and so
on, which is the same misdirected Next Header write this commit fixes for
the u8 case.

Should find_prev_fhdr() or nf_ct_frag6_gather() reject nhoff and fhoff
values that the 16-bit consumers cannot hold? For fhoff, that means
including the headroom and sizeof(struct frag_hdr).

>  	int start = netoff + sizeof(struct ipv6hdr);
>  	int len = skb->len - start;
>  	u8 prevhdr = NEXTHDR_IPV6;
sung byeongchan Oct. 2, 2026, 7:42 a.m. UTC | #2
From: Sung Byeongchan <tjdqudcks0424@naver.com>

Hi,

Resending in plain text because my previous webmail reply was rejected by
the mailing lists for containing an HTML part.

I independently tested the 16-bit header-offset concern raised in this
review. The concern is valid and can cause a local kernel panic.

This is separate from the existing prev_nhoff u8 truncation issue. To
isolate the two issues, I tested a current-mainline-based kernel at:

  ce1e0223d8ad4211275c82a17ed6d43ab81e13d9

with only the already-public u8-to-int prev_nhoff fix applied.

The reproducer sends a 65536-byte IPV6_HDRINCL packet to ::1 with the
real Fragment Header at fhoff=65528. The loopback raw-output path
reserves 16 bytes of headroom, so:

  headroom + fhoff = 16 + 65528 = 65544

This value is stored in the u16 skb->transport_header field and wraps
to 8.

With init_on_alloc=1, the wrapped pointer reads the zeroed headroom as
a Fragment Header with offset 0 and MF clear. Reassembly completes,
advances network_header from 16 to 24 while skb->data remains at 16,
and inet_frag_reasm_finish() passes -8 as the unsigned length to
skb_push().

The resulting diagnostic is:

  skbuff: skb_under_panic: ... len:65520 put:-8 ...
  kernel BUG at net/core/skbuff.c
  skb_push
  inet_frag_reasm_finish
  nf_ct_frag6_gather
  ipv6_defrag
  rawv6_sendmsg
  Kernel panic - not syncing: Fatal exception in interrupt

I reproduced the panic in three isolated QEMU runs:

  1. Linux v7.2.8 as root
  2. The mainline-based u8-fixed baseline as root
  3. The same mainline-based baseline from outer UID 65534 after
     entering a new user and network namespace

In the third case, the process had CAP_NET_RAW only inside its new
user and network namespace. No loopback MTU change was required.

The crash runs used nf_conntrack.enable_hooks=1 to activate IPv6
conntrack defragmentation. I confirmed from the source that an nftables
ct expression can also acquire the IPv6 defrag hook in the caller's
network namespace, but I did not perform an additional crash run using
only that activation method.

I tested the following minimal guard on top of the public u8 fix:

  if (nhoff != (u16)nhoff ||
      !skb_set_transport_header_careful(skb, fhoff))
          return -EINVAL;

With an otherwise identical KASAN configuration:

  - the exact crash packet was safely rejected in 3/3 runs;
  - short fragmented packets passed in 3/3 runs;
  - the previous offset-248 and offset-256 cases passed in 3/3 runs;
  - ordinary IPv6 UDP and TCP passed in 3/3 runs;
  - no KASAN, WARNING, BUG, Oops, or panic was observed.

I searched current mainline history, lore, Patchwork, and
linux-cve-announce. I found the existing prev_nhoff u8 fix, but did not
find an existing patch or commit that checks the u16 nhoff storage or
uses skb_set_transport_header_careful() in this path.

The demonstrated impact is a local kernel-wide denial of service when
IPv6 conntrack defragmentation is active. The panic occurs before
packet delivery, so I found no evidence of information disclosure,
privilege escalation, arbitrary memory corruption, or remote
reachability.

Would you prefer this check to be folded into the existing prev_nhoff
patch, or should I submit it as a separate follow-up patch on top of
that change? I have the reproducer, serial logs, A/B results, and an
applyable follow-up patch ready.

Regards,
Sung Byeongchan
Pablo Neira Ayuso Oct. 7, 2026, 10:57 p.m. UTC | #3
Hi,

On Fri, Oct 02, 2026 at 04:42:10PM +0900, tjdqudcks0424@naver.com wrote:
[...]
> Would you prefer this check to be folded into the existing prev_nhoff
> patch, or should I submit it as a separate follow-up patch on top of
> that change? I have the reproducer, serial logs, A/B results, and an
> applyable follow-up patch ready.

Please, send a single patch to address these truncation issues.

Thanks.
diff mbox series

Patch

diff --git a/net/ipv6/netfilter/nf_conntrack_reasm.c b/net/ipv6/netfilter/nf_conntrack_reasm.c
index 599c49bf0a0a..be72c4346f8b 100644
--- a/net/ipv6/netfilter/nf_conntrack_reasm.c
+++ b/net/ipv6/netfilter/nf_conntrack_reasm.c
@@ -398,7 +398,7 @@  find_prev_fhdr(struct sk_buff *skb, u8 *prevhdrp, int *prevhoff, int *fhoff)
 {
 	u8 nexthdr = ipv6_hdr(skb)->nexthdr;
 	const int netoff = skb_network_offset(skb);
-	u8 prev_nhoff = netoff + offsetof(struct ipv6hdr, nexthdr);
+	int prev_nhoff = netoff + offsetof(struct ipv6hdr, nexthdr);
 	int start = netoff + sizeof(struct ipv6hdr);
 	int len = skb->len - start;
 	u8 prevhdr = NEXTHDR_IPV6;