ip4defrag: the "fragment will overrun" security check is vacuous — uint16 wraparound in securityChecks #7
Labels
No labels
core
cpu-dos
critical
dos
evasion
has-poc
high
integer-overflow
ip4defrag
layers
low
medium
memory-exhaustion
other
panic
pcapgo
pentest-2026-08
rce
tcpassembly
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
noi/gopacket#7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: medium ·
ip4defrag/defrag.go:191-195ip.FragOffsetandip.Lengthare bothuint16, sofragOffsetand the sum are computed inuint16.IPv4MaximumSizeis 65535 — the maximum value auint16can hold. The comparisonx > 65535wherexis auint16is therefore false for every possible value ofx, and the sum wraps before it is ever tested.The check is dead code for exactly the inputs it exists to reject.
Reproduction
The maximum offset
securityCheckspermits isIPv4MaximumFragmentOffset = 8183, i.e. byte offset 65464. A fragment there carrying an ordinary 1480-byte payload ends at byte 66944, well past the 65535 limit:The same wrap then propagates into
insert()'s accounting, which is alsouint16:so a fragment whose true end is byte 66944 contributes
66944 mod 65536 = 1408tof.Highest.f.Highestis what becomes the reassembled datagram'sLength(see #6).f.Currentis a running sum over up toIPv4MaximumFragmentListLen = 8192fragments and can wrap many times over on its own — thef.Highest == f.Currenttest that gatesbuild()is comparing two independently-wrapped 16-bit counters.Impact
I did not get this to a panic on its own — the
insert()list-ordering defects in #2 tend to reject the fragment sets that would balance the wrapped accounting beforebuild()is reached. So this is reported as a latent issue rather than a demonstrated crash: the guard that is supposed to keep the defragmenter's arithmetic inside its type does not run, and every downstream length computation in the package is a 16-bit value derived from unvalidated wire fields.It is worth fixing on its own terms, and it is worth fixing before #2, because repairing the list-ordering defects removes the accident that is currently masking this.
Fix
Do the arithmetic in a type that cannot wrap:
and widen
fragmentList.Highest/fragmentList.Currenttouint32so the accounting cannot wrap either. A datagram is at most 65535 bytes, souint32is ample and no other logic needs to change.Worth adding a vet/lint rule or a test for the general shape — a
uint16compared against auint16-max constant is always-false by construction and there may be more of them.Verified against
b7d9dbdon Go 1.24.4. PoC:ovf.Independent verification — the guard is vacuous in
gopacket/gopacket v1.7.0tooConfirmed.
IPv4MaximumSize = 65535is an untyped constant that takes theuint16type of the expression it is compared against, sox > 65535wherexisuint16is false for every possible value:Same numbers as the issue reports (66964 / 1428). The comparison is dead code for exactly the inputs it exists to reject.
Concurring on the "latent" classification
I also could not drive this to a standalone crash, and for the reason this issue gives: the
insert()list-ordering defects in #2 reject the fragment sets that would balance the wrapped accounting beforebuild()is reached. Recording that as an independent second opinion rather than a new result.The sequencing advice here is right and worth repeating for whoever picks these up: fix this before #2, because repairing the list ordering removes the accident currently masking it. If #2 is fixed first, a set that previously died in
insert()starts reachingbuild()with two independently-wrapped 16-bit counters, and the failure mode changes from "silently discarded" to something less predictable.One addition to the proposed fix
The fix as written widens
securityChecks. Worth widening thedontDefrag/insertpath in the same change, sincefragmentList.Highestbecomes the reassembled datagram'sLength(see #6) and is currently auint16accumulating values that this guard was supposed to have bounded. Fixing the guard without widening the counters leaves the arithmetic correct at the door and still wrappable one call later.A vet-style check for the general shape — an unsigned value compared against its own type's maximum — would be worth running across the tree. This is unlikely to be the only instance.
Verified on Go 1.24.4 against
gopacket/gopacket v1.7.0.