ip4defrag: overlapping fragment sets never reassemble — broken currentOffset arithmetic plus a silent fragment drop #2
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
noi/gopacket#2
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: high ·
ip4defrag/defrag.go:298(build),ip4defrag/defrag.go:220-248(insert)ip4defragadvertises BSD-Right overlap handling (insert's doc comment: "we are inserting fragment based on their offset, latest first. This is sometimes called BSD-Right"). It does not implement it. Two separate defects mean an overlapping fragment set is always discarded, and the fragment list is left poisoned so the datagram can never complete.For a monitoring tool this is an availability bug with an offensive use: one extra small fragment injected into a fragmented flow makes the analyser lose the entire datagram, silently, with a benign-looking
errorrather than a signal.Defect 1 — the overlap branch advances
currentOffsetby the wrong quantityCompare the non-overlapping branch four lines above, which is correct:
After splicing an overlapping fragment,
currentOffsetmust become that fragment's end, i.e.frag.FragOffset*8 + (frag.Length - 20). Instead it becomescurrentOffset + frag.FragOffset*8— the running offset plus the fragment's start, which is not a meaningful quantity in any coordinate system. The two coincide only whencurrentOffset == frag.Length-20, by accident.Every subsequent fragment is then compared against a bogus
currentOffsetand reported as a hole.Defect 2 —
insert()silently drops a fragment while still counting itIf
fragOffset < f.Highestbut the fragment's offset is larger than every offset already in the list, the loop runs off the end without inserting anything — and thenCurrentandHighestare updated as though it had been. The fragment's bytes are counted but do not exist.Currentis now permanently inflated relative to what the list can tile, sof.Highest == f.Currentcan only ever be satisfied by a set that does not actually tile, whichbuild()then rejects as a hole. The list is never freed except by an explicitDiscardOlderThan.This also makes fragment handling order-dependent in a way nothing documents: the same three fragments reassemble or do not depending purely on arrival order.
Reproduction
An ordinary overlapping set —
A=[0,24),B=[8,40),C=[56,64), all fully formed, no truncation, allIHL=5:Trace:
AsetscurrentOffset = 24.Bat byte 8 takes the overlap branch and setscurrentOffset = 24 + 8 = 32instead of8 + 32 = 40.Cat byte 56 is then> 32→ "hole found".The silent drop (
A=[0,16),X=[8,16),C=[16,24), sent in that order):Impact
(SrcIP, DstIP, Id, Protocol)are all readable off the wire, someone else's — and the whole datagram vanishes from the analyser's view. It costs one packet and needs no host to accept it.DiscardOlderThan; see #4.build()can be reached.Fix
and in
insert(), make the fall-through case explicit rather than silent:Both belong under a test that asserts a known-good overlapping set reassembles to the BSD-Right result the doc comment promises — there is currently no such test.
Verified against
b7d9dbdon Go 1.24.4. PoC:defrag_more.Can this be exploited? Is there a way to reproduce this issue over the wire? Like someone would send the malicious packets to a service that's being dumped, then Daisy would parse it with gopacket. If not, close the issue.
Correction to the reproduction in the issue body, and a third defect it missed.
While writing the fix I found my example was badly chosen. The set I used —
A=[0,24),B=[8,40),C=[56,64)— has a genuine hole at[40,56), so "hole found" was the correct answer for it. It did not demonstrate the defect. The accounting happened to balance by coincidence, which is what let it reach the overlap branch at all.A set that genuinely tiles tells a much worse story.
A=[0,24),B=[8,40),C=[40,48)covers[0,48)contiguously with an overlap at[8,24), and every real IP stack reassembles it:No arrival order works, and there is no error — the caller gets
nil, nilforever and the fragment list is retained until an explicitDiscardOlderThan.The root cause is upstream of both defects in the issue body
f.Currentis a sum of fragment lengths. An overlapping set counts the shared bytes once per fragment that carries them, soCurrentovershootsHighestpermanently and the equality can never hold. For the set above:Current = 24+32+8 = 64,Highest = 48.So
build()is never even called, which means thecurrentOffsetarithmetic defect described in the issue body is real but normally unreachable — it only executes when the sum balances by accident, as in my original example. Same for the silent-drop defect: it makes things worse, but it is not what blocks the common case.Ranking the three, most to least important:
currentOffsetadvance in the overlap branch is wrong — reached once (1) is fixed.insert()silently drops a fragment that belongs after every stored element while still counting it.Fixed in
fix/issue-2-ip4defrag-overlapf.Current >= f.Highestand letbuild()decide whether the fragments tile; a hole becomes "not complete yet" (nil, nil) rather than a permanent error, since more fragments may still arrive.currentOffset = frag.FragOffset*8 + frag.Length - uint16(frag.IHL)*4— advance to the fragment's end.After the fix, all six orders produce the same 48 bytes with first-fragment-wins resolution:
Covered by
TestDefragOverlapping(all six orders) andTestDefragFragmentPastListTail. Both fail onmain.One consequence worth flagging for review: fixing this means overlapping sets now do reassemble, so the first-wins resolution policy becomes security-relevant. Linux ≥ 4.19 drops overlapping IPv4 fragments outright rather than resolving them. If matching Linux is the goal, rejecting the datagram may be the better behaviour than reassembling it first-wins — that is a policy call, not a bug fix, so I did not make it here. Worth deciding before merging.
PoC:
ovl2on branchpentest/2026-08-poc.Independent verification — both defects present in
gopacket/gopacket v1.7.0Confirmed by reading the fork's source directly. Both defects are verbatim:
The send-order dependency this issue describes is confirmed from the other direction too: reproducing #1's root cause B requires the higher-offset fragment C to arrive before the crafted overlapping fragment B, exactly as predicted here. Send them in offset order and B hits the silent-drop path and
build()is never reached. That interaction is worth keeping cross-referenced — it is the difference between #1 reproducing and not.A third path to "never reassembles", distinct from both defects here
While verifying #1 I found a way to make a fragment set permanently un-reassemblable that involves neither the overlap branch nor the silent drop, and needs no overlapping fragment at all.
insert()computesfragLength := in.Length - 20whilesecurityChecks()usesip.Length - uint16(ip.IHL)*4. Put IP options on any non-last fragment andCurrentandHighestare computed on different bases, so they can never converge —build()is simply never called:Two ordinary contiguous fragments, no overlap, every field RFC-valid, 4 bytes of Router Alert. A real stack reassembles it without complaint.
This matters for the fix proposed here: correcting the
currentOffsetarithmetic and making the fall-through explicit does not close it, because the set never reachesbuild()in the first place. It needs theinsert()length fix from #1:Suggest the test this issue asks for ("a known-good overlapping set reassembles to the BSD-Right result") be joined by one asserting that a contiguous set with
IHL > 5on a non-last fragment reassembles too. That case has no test today and is the cheapest of the three to exploit.On the impact framing
Confirmed in principle, with one practical caveat from live testing: on a path where a middlebox reassembles (a Linux bridge with
bridge-nf-call-iptables=1, or any conntracking hop), an overlapping set is normalised or dropped before it reaches a downstream capture, so the injected fragment never arrives. The IP-options variant above does not have that limitation — Router Alert's copy-on-fragment bit is set, so the option survives a reassemble/re-fragment cycle and the evasion works through the middlebox. Worth noting when assessing reachability.Verified on Go 1.24.4 against
gopacket/gopacket v1.7.0.