layers: 26 decoders index past their own length guard on short input — a 15-byte frame reaches most of them #11
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#11
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 ·
layers/— 26 distinct decoders, see tableNot a single bug: a class of bug, found by a deterministic sweep rather than by fuzzing. Every one of these decoders validates a minimum length and then indexes past it, or derives an offset with arithmetic that wraps.
Method
For every registered
gopacket.LayerType, feed buffers of every length 0–80 filled with each of six byte patterns (00,ff,inc,80,0f,3f), withSkipDecodeRecovery: trueso panics surface, then callLayers(),LayerContents()andLayerPayload()on the result.The whole harness is about 60 lines and runs in under a minute. It is attached below as a suggested CI test.
Results
ffslice bounds out of range [8:7]80slice bounds out of range [:136] with capacity 8index out of range [0] with length 000slice bounds out of range [:4] with capacity 2slice bounds out of range [:4] with capacity 0ff[:4] with capacity 0/[6:5]index out of range [0]/[:2] with capacity 100slice bounds out of range [12:8]slice bounds out of range [:4] with capacity 0slice bounds out of range [:2] with capacity 000slice bounds out of range [:4] with capacity 1slice bounds out of range [:6] with capacity 000index out of range [1] with length 100slice bounds out of range [:8] with capacity 7(see #8)index out of range [0]/[:2] with capacity 1slice bounds out of range [:2] with capacity 0slice bounds out of range [:4] with capacity 0ffslice bounds out of range [6:5]incslice bounds out of range [:1035] with capacity 16incindex out of range [8] with length 8incslice bounds out of range [770:14]slice bounds out of range [4:0]ffslice bounds out of range [:4] with capacity 000index out of range [60] with length 6000index out of range [1] with length 1index out of range [0]/[:4] with capacity 2slice bounds out of range [:4] with capacity 0slice bounds out of range [:7] with capacity 000/ 4 Bffindex out of range [1]/[:35] with capacity 400/ 4 Bffindex out of range [1]/[:35] with capacity 4ARP is representative of the arithmetic ones:
HwAddressSize = ProtAddressSize = 0xffgivesarpLength = 260 mod 256 = 4, the guard passes on an 8-byte buffer, and the next line slicesdata[8:263 mod 256] = data[8:7].Reachability
Every one is reachable from a single ordinary Ethernet frame a rival can put on the wire — no handshake, no listener needed, the capturer parses it regardless:
Linux SLLmatters specifically for us:tcpdump -i anyproducesLINKTYPE_LINUX_SLL, so if the capturer ever runs onanyrather than a named interface, a 16-byte frame reaches it.RadioTap,Prism,PFLog,USBandFDDIare link-type dependent and not reachable in our deployment.Severity — what the recovery actually does
Both entry points recover by default, and I want to be precise about this because it is the difference between medium and critical:
gopacket.NewPacketrecovers unlessDecodeOptions{SkipDecodeRecovery: true}.DecodingLayerParser.DecodeLayersrecovers too —parser.go:304installsdefer panicToError(&err)unlessDecodingLayerParserOptions{IgnorePanic: true}.So a default caller gets an error, not a crash:
That leaves three real consequences:
IgnorePanic's doc says handling panics "does add latency to the process of decoding layers". Any consumer that took that advice has a remote crash from a 22-byte frame.Fix
Individually these are one-line guards. The more useful fix is structural — none of this class would have survived a test that tries short inputs, and there isn't one. Suggested addition to
layers/:It runs in well under a minute, is fully deterministic, and would have caught all 39.
Two general rules the individual fixes should follow:
int, not in the header field's own type. ARP, Geneve (#9) and Linux SLL all wrap because the arithmetic stays inuint8/uint16.Verified against
b7d9dbdon Go 1.24.4. PoCs:sweep,reach,dlp. Geneve is filed separately as #8/#9 because it has an additional evasion primitive.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.
Fixed on
fix/issue-11-layer-short-input, all 26 decoders, verified against the full 972,000-probe sweep (zero panics, down from 26 layers / 39 crash shapes) plus the existing test suite.Two things surfaced while writing the fix that go beyond what the original short-input sweep found, both real and both fixed alongside:
RadioTap had no bounds checking at all, not just an off-by-one. Every field is read at an offset driven entirely by which wire-controlled
Presentbits are set, with zero length checks anywhere inDecodeFromBytes. Rewrote it field-by-field with aneed(n)guard before every read, bounded the extended-presence-bitmap loop (it used to readdata[offset:offset+4]unconditionally while chasing extension words), and checked theit_len-derived payload slice and the Datapad realignment against the actual buffer.SCTP's SACK gap-ACK/dup-TSN loop had a real unbounded read, independent of the short-input class (it needs a valid header, not a truncated one, so the sweep didn't catch it): the code pre-computed a clamped capacity for
make()with a comment explaining it was deliberately capped "so we're not allocating tons of memory," but the loop bound used the raw wire-controlledNumGapACKs/NumDuplicateTSNsfields directly, not the clamped value. A SACK chunk with a large declared count and a short actual buffer reads straight past the end. Fixed by clamping the loop bound itself, not just the initial capacity.Also worth a note for anyone extending
USB: fixed a live unsigned-underflow in theData-flagged payload slice (len(data) - UrbDataLengthwraps when the declared length exceeds the buffer) that the uniform-byte sweep can't reach — it needsdata[14]!=0 && data[15]==0, which no single repeated byte value produces — but is real and one crafted packet away.One behavioral note for review:
decodeStringinsflow.go(used bydecodePortnameCounters) gained an error return, since it previously had no way to reject a string length that exceeded what remained in the buffer. Its one caller is updated; no other change to its return semantics (it still returns the padded length as the first value, matching the original — I initially got that wrong and a existing test caught it).PoC/verification:
sweepandreachon branchpentest/2026-08-poc, re-run against the fix — 0/972000 panics.