From: bugzilla@dpdk.org
To: dev@dpdk.org
Subject: [DPDK/other Bug 2078] ip_frag: reassembled IPv4 total length wraps if only first fragment has options
Date: Tue, 27 Oct 2026 16:03:48 +0000 [thread overview]
Message-ID: <bug-2078-3@https.bugs.dpdk.org/> (raw)
https://bugs.dpdk.org/show_bug.cgi?id=2078
Bug ID: 2078
Summary: ip_frag: reassembled IPv4 total length wraps if only
first fragment has options
Product: DPDK
Version: 26.11
Hardware: All
OS: All
Status: UNCONFIRMED
Severity: normal
Priority: Normal
Component: other
Assignee: dev@dpdk.org
Reporter: stephen@networkplumber.org
Target Milestone: ---
Created attachment 426
--> https://bugs.dpdk.org/attachment.cgi?id=426&action=edit
AI proposed patch
============================================================================
ANT-2026-DWBZBYAG [NOT SECURITY: ordinary bug]
ip_frag: reassembled IPv4 total length wraps if only first fragment has options
Location: lib/ip_frag/rte_ipv4_reassembly.c:73 in ipv4_frag_reassemble()
Commit: 9a45341f18a1ab47af7123e63d942b90c1b79f38
============================================================================
`rte_ipv4_frag_reassemble_packet()` can return a reassembled IPv4 datagram of
up to 65575 bytes whose `total_length` has wrapped to a value between 0 and 39,
instead of dropping it as oversized. This happens when the first fragment
carries IP options that the later fragments do not repeat, which is how
`rte_ipv4_fragment_packet()` itself fragments a datagram with non-copied
options, and the datagram is within 40 bytes of the 65535-byte maximum. The
application must also set `l3_len` from the IHL field, as the API documentation
asks. The in-tree callers (`examples/ip_reassembly`, `ipsec-secgw`, `lib/node`)
hard-code `l3_len = 20` and do not hit it.
The size check added by commit 23fd984a5a6b ("ip_frag: reject oversized
reassembled datagrams") at `lib/ip_frag/rte_ipv4_reassembly.c:145` tests
`offset + length + l3_len` using the header length of the fragment that is
arriving. `ipv4_frag_reassemble()` then writes `total_length` as
`(uint16_t)(total_size + l3_len)` using the first fragment's header length,
which can be up to 40 bytes larger, so the sum wraps. There is no out-of-bounds
access and an ASan/UBSan build reports nothing. The effect is a header that
contradicts the mbuf, and any receiver will drop the packet if it is forwarded.
The attached reproducer sends three 8-fragment datagrams through `net_pcap` on
the default build at 9a45341f18a1. The two control datagrams behave correctly.
The third, with options only in the first fragment, is forwarded as 65575 bytes
of IP with `total_length` 39. A patch that repeats the check in
`ipv4_frag_reassemble()` against the first fragment's header length, plus a
unit test, is attached.
## Details
This is an ordinary bug, not a security issue: the only effect is one wrong
header field in the reassembled
packet, with no out-of-bounds access, crash, hang or leak, and the malformed
result gives the sender nothing a
bad packet sent directly would not.
`rte_ipv4_frag_reassemble_packet()` limits the reassembled datagram to 65535
bytes using the header length of
the fragment that is arriving, and `ipv4_frag_reassemble()` then writes
`total_length` using the header length
of the first fragment. When the first fragment has IP options and the later
ones do not, the limit passes and
the 16-bit `total_length` wraps.
Tested at commit 9a45341f18a1 (26.11-rc1) with the default build in
`/src/build`. Eight fragments of one
datagram, received through `net_pcap` by an application that takes
`l2_len`/`l3_len` from `rte_net_get_ptype()`:
| fragment | IHL | offset | payload | sum tested at `rte_ipv4_reassembly.c:145`
|
|---|---|---|---|---|
| first | 15 (40 bytes of NOP options) | 0 | 8192 | 0 + 8192 + 60 |
| 2 to 7 | 5 | 8192 … 49152 | 8192 | at most 49152 + 8192 + 20 |
| last | 5 | 57344 | 8171 | 57344 + 8171 + 20 = 65535, accepted |
The library returns one mbuf chain holding 65575 bytes of IP (60 bytes of
header, 65515 of payload) whose
header says `total_length` 39. The application transmits it as it is. The same
payload without options comes
out as 65535 / 65535, and with the options in all eight fragments the last
fragment is dropped and nothing is
forwarded, so the limit works as long as every fragment has the same header
length.
The check on arrival, `lib/ip_frag/rte_ipv4_reassembly.c:126` and `:145`:
```c
ip_len = rte_be_to_cpu_16(ip_hdr->total_length) - mb->l3_len;
…
if (ip_len <= 0 || ip_ofs + ip_len + mb->l3_len > UINT16_MAX) {
IP_FRAG_MBUF2DR(dr, mb);
return NULL;
}
```
`mb` is the fragment at hand. For the last fragment `ip_ofs + ip_len` is what
`ip_frag_process()` stores as the
size of the whole payload (`lib/ip_frag/ip_frag_internal.c:141`,
`fp->total_size = ofs + len`), so the test
amounts to `total_size + l3_len of the last fragment`.
Once all fragments are there, `lib/ip_frag/rte_ipv4_reassembly.c:67`:
```c
m = fp->frags[IP_FIRST_FRAG_IDX].mb;
…
ip_hdr->total_length = rte_cpu_to_be_16((uint16_t)(fp->total_size +
m->l3_len));
```
`m` is now the first fragment, whose header is the one the reassembled packet
keeps. In the example
`total_size` is 65515 and `m->l3_len` is 60; the sum is 65575 and the cast
leaves 39. Nothing between the two
places compares `total_size` with the first fragment's header length.
The two header lengths differ in ordinary traffic. Only options of the "copied"
class are repeated in later
fragments, and `rte_ipv4_fragment_packet()` builds its fragments that way
(`__create_ipopt_frag_hdr()`,
`lib/ip_frag/rte_ipv4_fragmentation.c:44`, skips NOP and every option for which
`RTE_IPV4_HDR_OPT_COPIED()` is
false). The first fragment can therefore have up to 40 bytes more header than
the fragment the check sees.
The check at `:145` was added by commit 23fd984a5a6b ("ip_frag: reject
oversized reassembled datagrams") to
prevent this outcome; its comment says "The total_length field is 16 bits, so
otherwise it is silently truncated
while the mbuf still holds the full length". This is the case that change does
not cover.
The application has to set `l3_len` from the IHL field, which is what the API
asks for ("Incoming mbufs should
have its l2_len/l3_len fields setup correctly",
`lib/ip_frag/rte_ip_frag.h:216`). The in-tree callers
(`examples/ip_reassembly/main.c:340`,
`examples/ipsec-secgw/ipsec-secgw.c:2299`, `lib/node/ip4_reassembly.c:84`)
set `l3_len` to `sizeof(struct rte_ipv4_hdr)` for every fragment and do not
reach the wrap. IPv6 is not
affected: `rte_ipv6_frag_reassemble_packet()` tests `ip_ofs + ip_len` alone and
`ipv6_frag_reassemble()` writes
the same quantity.
## Impact
An application that reassembles IPv4 with `l3_len` taken from the packet's IHL
(`rte_net_get_ptype()`,
`rte_ipv4_hdr_len()`), as the API asks, receives from
`rte_ipv4_frag_reassemble_packet()` a packet of up to
65575 bytes whose `total_length`, written by DPDK, is between 0 and 39 and
always smaller than the packet's own
header length, instead of having the oversized datagram dropped. It happens
only for a datagram within 40 bytes
of the 65535-byte maximum whose first fragment carries IP options the later
fragments do not repeat, so it is
rare in practice; the in-tree callers (`examples/ip_reassembly`, `ipsec-secgw`,
`lib/node`) hard-code
`l3_len = 20` and never see it. The default build is affected, no offload is
involved. To carry the full 65515
bytes in the 8 fragments the library keeps per datagram
(`RTE_LIBRTE_IP_FRAG_MAX_FRAG`), the ingress port must
accept frames of about 8.3 KB.
There is no out-of-bounds access inside `lib/ip_frag`; the ASan/UBSan build
(`/src/build-asan`) forwards the
same frame and reports nothing. The result is a header that contradicts the
mbuf. DPDK helpers that read
`total_length` afterwards mostly cope: `rte_ipv4_phdr_cksum()`
(`lib/net/rte_ip4.h:238`) still yields the true
payload length because the 16-bit subtraction undoes the wrap, and
`rte_ipv4_udptcp_cksum_mbuf()` returns the
same checksum as for the valid datagram with the same payload; only
`__rte_ipv4_udptcp_cksum()` gives up on such
a header (`lib/net/rte_ip4.h:256`). A forwarded copy claims a length below its
own header length and is dropped
by a receiver. The same wrapped header can also be produced with two short
frames, but that form relies on a
separate defect (fragments shorter than their `total_length` says are
accepted), not on this one.
--
You are receiving this mail because:
You are the assignee for the bug.
reply other threads:[~2026-10-27 16:03 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
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=bug-2078-3@https.bugs.dpdk.org/ \
--to=bugzilla@dpdk.org \
--cc=dev@dpdk.org \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.