layers/dns: name-decompression bomb — one 65 KB packet retains 377 MB, ~21 packets OOM an 8 GB box #13
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#13
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 ·
layers/dns.go:536-610(decodeName),layers/dns.go:637,711,920-980(call sites)decodeNamebounds pointer-chase depth atmaxRecursionLevel = 255. Nothing bounds how much each level appends, and nothing bounds how many names one packet asks it to decode. The product is the bomb.Mechanism
Each level may append up to a 63-byte label plus a separator before following its pointer, so one name expands to
depth × 64bytes. Withdepth = 248that is 15,872 bytes per name. The depth cap is per name, not per packet — every record name starts again atlevel = 1.bufferis the packet's shared decode buffer and it is retained on the decodedDNSlayer, so this is live memory, not churn.Construction
Compression pointers are 14 bits, so every chain block must live below offset 16383. The sequential record parser would otherwise walk straight into the chain, so it is buried inside one record's
RDATA—RDLENGTHmakes the parser skip it while pointers still reach in:Every additional answer record costs 12 bytes and buys another 15,872 bytes of expansion.
Measurements
err=<nil>throughout. This is not a malformed packet that trips an error path — it is a completely successful parse. NoSetTruncated, no error layer, nothing for a caller to check.Cost to the attacker
Delivery: a 65 KB UDP datagram is IP-fragmented into ~45 frames on a 1500-byte MTU. That is fine here — the capturer records the fragments and an analyser that defragments (as ours does) reassembles and parses them. DNS over TCP/53 carries 64 KB messages natively via stream reassembly, with no fragmentation needed at all.
Scaling down honestly: confined to a single unfragmented 1472-byte datagram the same construction only reaches roughly 30×, because the chain itself needs ~16 KB before the cheap referring records start paying off. The headline numbers need a large datagram, which both delivery paths above provide.
Fix
The depth cap is the wrong dimension. Bound the output:
Two further hardenings worth having alongside it:
offsetp < indexmakes chains finite by construction and costs nothing on valid traffic — it is the standard defence and is what most other DNS parsers do.(2) alone reduces the maximum chain to the number of distinct decreasing offsets, and combined with (1) makes the whole class unreachable.
Worth adding the generator above as a test asserting a bounded allocation, next to the existing
FuzzDecodeFromBytesinlayers/dns_test.go.Verified against
b7d9dbdon Go 1.24.4. PoC:dnsbomb.Reachability note — not exploitable against the consumer audited in this round
No dispute with the library finding; I did not re-measure the 377 MB figure. Adding a reachability data point that may be useful for prioritising this against the other issues in the
pentest-2026-08set.The consumer audited alongside these issues is not reachable by this bug, for a reason worth writing down because it is easy to lose:
DecodingLayerParserwith an explicit decoder set — Ethernet, Dot1Q, IPv4, IPv6, IPv6ExtensionSkipper, TCP, UDP, Payload.LayerTypeDNSis not registered, andIgnoreUnsupportedis set, so a UDP/53 datagram terminates the chain atPayload.gopacket.NewPacketwithDecodeOptions{Lazy: true}and only ever callsNetworkLayer()andTransportLayer(). Lazy decoding stops as soon as the requested layer is available, solayers.UDP.NextLayerType()returningLayerTypeDNSnever causes the DNS decoder to run.packet.Layers()orpacket.ApplicationLayer(), either of which would force the full chain and re-open this.So the exposure is gated on calling the DNS decoder at all, which for a flow-level analyser is optional. That is a meaningfully narrower blast radius than #1 or #3, both of which fire on the decode path every caller uses.
Two things this suggests:
LayerTypeDNSor forced application-layer decode. That distinction changes who needs to act urgently.packet.Layers()— a debug dump, a "store all decoded layer types" feature, a protocol-detection heuristic — silently re-opens this with no local signal. Worth a note for callers that a lazy-decode strategy is load-bearing security, not just a performance choice.Same reasoning applies to #8 and #9 (no Geneve decoder registered) and to #14 (the
reassemblypackage is not used, though that finding does block any future migration offtcpassembly).Reachability assessed against
gopacket/gopacket v1.7.0.