ip_frag: reassembled IPv4 total length wraps if only first fragment has options
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: [email protected]
Reporter: [email protected]
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 exampletotal_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-codel3_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 readtotal_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.
Signature · 2 lines
You are receiving this mail because: You are the assignee for the bug.