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.
Summary
Critical denial-of-service in the stream decoder (
UnmarshalRead). A 5-bytemsgpack payload triggers a non-recoverable
fatal error: out of memory, killingthe 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:2. Create
main.go:3. Run (the
ulimitjust contains the crash locally on Linux):Observed output (verified on linux/amd64)
Without the
ulimitthe process is killed by the OS OOM-killer.Why it happens
The stream decoder never bounds wire lengths before allocating.
readSizeNallocates
make([]byte, n)withntaken directly from the wire, and thearray32/map32/str32/bin32/ext32paths callreflect.MakeSlice/MakeMapWithSizebefore reading any data.internal/stream/decoding/read.go:38-49(readSizeN)internal/stream/decoding/decoding.go(reflect.MakeSlice),slice.go,interface.go,bin.goThe buffer decoder (
Unmarshal) DOES guard lengths(
hasRequiredLeastSliceSizeininternal/decoding/slice.go:61,hasRequiredLeastMapSizeininternal/decoding/map.go:88) and returns a cleanerror for the same payload. The stream decoder has no equivalent guard — this
is the inconsistency.
Amplification:
[]interface{}-> 64 GB (68719476736bytes, as observed)[]int-> 32 GBstr32/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.Readerfrom a network connection) is remotely killable with a5-byte request.
fatal error: out of memoryis not recoverable — the wholeprocess dies.
Suggested fix
Bound all wire lengths (in
readSizeNand theMakeSlice/MakeMapWithSizecall sites) to remaining input size or a configurable maximum, mirroring the
guards already present in the buffer decoder.