layers/geneve: seven zero bytes panic the decoder — two off-by-one length guards #8

Open
opened 2026-08-26 09:57:00 +00:00 by claude · 0 comments
Collaborator

Severity: high · layers/geneve.go:96 and layers/geneve.go:55

Geneve.DecodeFromBytes and its option helper both validate a length one byte short of what they then index. Two distinct off-by-ones, both reachable from a single UDP datagram to port 6081.

Defect 1 — the outer guard is short by one

offset, length := uint8(8), int32(gn.OptionsLength)
if len(data) < int(length+7) {          // <-- options begin at byte 8, not 7
        df.SetTruncated()
        return errors.New("geneve packet too short")
}

for length > 0 { ... }

gn.BaseLayer = BaseLayer{data[:offset], data[offset:]}   // offset is 8

With OptionsLength == 0 the guard reduces to len(data) < 7, so a 7-byte payload is accepted. The loop does not run, and data[:offset] slices a 7-byte buffer at 8.

Defect 2 — the option guard is short by one

func decodeGeneveOption(data []byte, gn *Geneve, df gopacket.DecodeFeedback) (*GeneveOption, uint8, error) {
        if len(data) < 3 {              // <-- data[3] is read four lines down
                df.SetTruncated()
                return nil, 0, errors.New("geneve option too small")
        }
        opt := &GeneveOption{}
        opt.Class = binary.BigEndian.Uint16(data[0:2])
        opt.Type = data[2]
        opt.Flags = data[3] >> 4        // needs len(data) >= 4

An option header is 4 bytes. The guard admits 3.

Reproduction

A. 7-byte Geneve, no options: data[:8]      ( 7 B, 00000000000000) -> PANIC: runtime error: slice bounds out of range [:8] with capacity 7
B. 11-byte Geneve, one option: data[3]      (11 B, 0100000000000000000000) -> PANIC: runtime error: index out of range [3] with length 3
C. 8-byte Geneve, no options (well-formed)  ( 8 B, 0000000000000000) -> ok (Contents=8 Payload=0)

Seven zero bytes. On the wire that is a 49-byte frame: Ethernet + IPv4 + UDP + 7 bytes of payload to port 6081. B reaches the second defect: data[0]&0x3f == 1 declares one 4-byte option, the outer guard demands len >= 11, and the loop then hands decodeGeneveOption a 3-byte slice.

Through a full frame:

=== A: 53-byte frame (eth+ip+udp+11) ===
  gopacket.NewPacket (default, recovery ON):  recovered -> runtime error: index out of range [3] with length 3
  NewPacket with SkipDecodeRecovery:          PANIC: runtime error: index out of range [3] with length 3

Reachability

Geneve is registered on UDP 6081 (layers/iana_ports.go / the UDP port map), so any packet a rival sends to that port is decoded. No handshake, no host has to be listening — the capturer parses it regardless.

Honest scoping: layers.Geneve does not implement gopacket.DecodingLayer (no CanDecode), so it cannot be reached through DecodingLayerParser. Under gopacket.NewPacket with default options the panic is recovered and surfaces as an error layer. It is a hard crash for any caller that sets DecodeOptions{SkipDecodeRecovery: true} — a common choice in throughput-sensitive analysers, and the option the shipped layers/fuzz_layer.go harness itself exercises.

Even when recovered, the cost is not nothing: recover() plus stack unwinding per packet is orders of magnitude more expensive than a decode, so a stream of 49-byte frames is a cheap throughput attack, and the packet is dropped from analysis entirely.

Fix

// DecodeFromBytes
if len(data) < int(length)+8 {
        df.SetTruncated()
        return errors.New("geneve packet too short")
}

// decodeGeneveOption
if len(data) < 4 {
        df.SetTruncated()
        return nil, 0, errors.New("geneve option too small")
}

Both belong under a table test of short inputs — for n := 0; n < 16; n++ over truncations of a valid Geneve packet, asserting an error rather than a panic. See also #9, a third defect in the same function.


Verified against b7d9dbd on Go 1.24.4. PoCs: geneve, geneve2.

**Severity: high** · `layers/geneve.go:96` and `layers/geneve.go:55` `Geneve.DecodeFromBytes` and its option helper both validate a length one byte short of what they then index. Two distinct off-by-ones, both reachable from a single UDP datagram to port 6081. ## Defect 1 — the outer guard is short by one ```go offset, length := uint8(8), int32(gn.OptionsLength) if len(data) < int(length+7) { // <-- options begin at byte 8, not 7 df.SetTruncated() return errors.New("geneve packet too short") } for length > 0 { ... } gn.BaseLayer = BaseLayer{data[:offset], data[offset:]} // offset is 8 ``` With `OptionsLength == 0` the guard reduces to `len(data) < 7`, so a 7-byte payload is accepted. The loop does not run, and `data[:offset]` slices a 7-byte buffer at 8. ## Defect 2 — the option guard is short by one ```go func decodeGeneveOption(data []byte, gn *Geneve, df gopacket.DecodeFeedback) (*GeneveOption, uint8, error) { if len(data) < 3 { // <-- data[3] is read four lines down df.SetTruncated() return nil, 0, errors.New("geneve option too small") } opt := &GeneveOption{} opt.Class = binary.BigEndian.Uint16(data[0:2]) opt.Type = data[2] opt.Flags = data[3] >> 4 // needs len(data) >= 4 ``` An option header is 4 bytes. The guard admits 3. ## Reproduction ``` A. 7-byte Geneve, no options: data[:8] ( 7 B, 00000000000000) -> PANIC: runtime error: slice bounds out of range [:8] with capacity 7 B. 11-byte Geneve, one option: data[3] (11 B, 0100000000000000000000) -> PANIC: runtime error: index out of range [3] with length 3 C. 8-byte Geneve, no options (well-formed) ( 8 B, 0000000000000000) -> ok (Contents=8 Payload=0) ``` **Seven zero bytes.** On the wire that is a 49-byte frame: Ethernet + IPv4 + UDP + 7 bytes of payload to port 6081. `B` reaches the second defect: `data[0]&0x3f == 1` declares one 4-byte option, the outer guard demands `len >= 11`, and the loop then hands `decodeGeneveOption` a 3-byte slice. Through a full frame: ``` === A: 53-byte frame (eth+ip+udp+11) === gopacket.NewPacket (default, recovery ON): recovered -> runtime error: index out of range [3] with length 3 NewPacket with SkipDecodeRecovery: PANIC: runtime error: index out of range [3] with length 3 ``` ## Reachability `Geneve` is registered on `UDP 6081` (`layers/iana_ports.go` / the UDP port map), so any packet a rival sends to that port is decoded. No handshake, no host has to be listening — the capturer parses it regardless. Honest scoping: `layers.Geneve` does not implement `gopacket.DecodingLayer` (no `CanDecode`), so it cannot be reached through `DecodingLayerParser`. Under `gopacket.NewPacket` with default options the panic **is** recovered and surfaces as an error layer. It is a hard crash for any caller that sets `DecodeOptions{SkipDecodeRecovery: true}` — a common choice in throughput-sensitive analysers, and the option the shipped `layers/fuzz_layer.go` harness itself exercises. Even when recovered, the cost is not nothing: `recover()` plus stack unwinding per packet is orders of magnitude more expensive than a decode, so a stream of 49-byte frames is a cheap throughput attack, and the packet is dropped from analysis entirely. ## Fix ```go // DecodeFromBytes if len(data) < int(length)+8 { df.SetTruncated() return errors.New("geneve packet too short") } // decodeGeneveOption if len(data) < 4 { df.SetTruncated() return nil, 0, errors.New("geneve option too small") } ``` Both belong under a table test of short inputs — `for n := 0; n < 16; n++` over truncations of a valid Geneve packet, asserting an error rather than a panic. See also #9, a third defect in the same function. --- *Verified against `b7d9dbd` on Go 1.24.4. PoCs: `geneve`, `geneve2`.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
noi/gopacket#8
No description provided.