From 98c73749bf37d212c23672aa7bb19243aba7a9f2 Mon Sep 17 00:00:00 2001 From: Ignacio Van Droogenbroeck Date: Fri, 12 Jun 2026 15:25:22 -0600 Subject: [PATCH 1/2] fix: clamp DecodeUntypedMap allocation at maxMapSize DecodeUntypedMap allocated map[interface{}]interface{} with the attacker-declared capacity, unlike every sibling map decode path (decodeMapValue, decodeMapStringInterfaceN, decodeTypedMapN), which clamp the size hint at maxMapSize unless DisableAllocLimit is set. A map32 header declaring ~4G entries forced a multi-GB upfront map allocation before any payload was read. Found by internal security review of #63. --- decode_map.go | 7 ++++++- msgpack_test.go | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/decode_map.go b/decode_map.go index 0900c16d..51dd6793 100644 --- a/decode_map.go +++ b/decode_map.go @@ -250,7 +250,12 @@ func (d *Decoder) DecodeUntypedMap() (map[interface{}]interface{}, error) { return nil, nil } - m := make(map[interface{}]interface{}, n) + ln := n + if d.flags&disableAllocLimitFlag == 0 { + ln = min(ln, maxMapSize) + } + + m := make(map[interface{}]interface{}, ln) for i := 0; i < n; i++ { mk, err := d.decodeInterfaceCond() diff --git a/msgpack_test.go b/msgpack_test.go index 296adf8c..db3329ce 100644 --- a/msgpack_test.go +++ b/msgpack_test.go @@ -75,6 +75,26 @@ func (t *MsgpackTest) TestLargeString() { t.Equal(dst, src) } +func (t *MsgpackTest) TestDecodeUntypedMapHugeDeclaredLen() { + // A map32 header declaring ~4G entries with no payload: the map size + // hint must be clamped at maxMapSize before allocation (with the old + // code this allocated a multi-GB map upfront), then fail decoding the + // first key. + data := []byte{0xdf, 0xff, 0xff, 0xff, 0xff} + dec := msgpack.NewDecoder(bytes.NewReader(data)) + _, err := dec.DecodeUntypedMap() + t.NotNil(err) +} + +func (t *MsgpackTest) TestDecodeUntypedMap() { + in := map[interface{}]interface{}{int8(1): "one", "two": int8(2)} + t.Nil(t.enc.Encode(in)) + + out, err := t.dec.DecodeUntypedMap() + t.Nil(err) + t.Equal(in, out) +} + func (t *MsgpackTest) TestSliceOfStructs() { in := []*nameStruct{{"hello"}} var out []*nameStruct From 11be10ba8e884f0afe3ac57aa2c970fa87e6e11b Mon Sep 17 00:00:00 2001 From: Ignacio Van Droogenbroeck Date: Fri, 21 Aug 2026 16:47:13 -0600 Subject: [PATCH 2/2] test: assert the allocation bound in the untyped-map clamp test Two fixes from the review on #76. The header was 0xffffffff, which #79 now rejects at the int-overflow check before it ever reaches the clamp -- the test would have passed for the wrong reason. Changed to 0x7fffffff, which fits in an int on every platform and exercises the clamp itself. The test also only asserted that an error came back, which it did with or without the clamp: unclamped, the decode still failed on the missing payload, just after allocating the declared ~2G entries. It now measures TotalAlloc across the decode and fails above 256 MiB. Verified: without the clamp it reports 164000 MiB and fails in 33s; with it, well under the bound in 0.3s. Co-Authored-By: Claude Opus 5 (1M context) --- msgpack_test.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/msgpack_test.go b/msgpack_test.go index db3329ce..d99abb17 100644 --- a/msgpack_test.go +++ b/msgpack_test.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "reflect" + "runtime" "testing" "time" @@ -76,14 +77,33 @@ func (t *MsgpackTest) TestLargeString() { } func (t *MsgpackTest) TestDecodeUntypedMapHugeDeclaredLen() { - // A map32 header declaring ~4G entries with no payload: the map size + // A map32 header declaring ~2G entries with no payload: the map size // hint must be clamped at maxMapSize before allocation (with the old // code this allocated a multi-GB map upfront), then fail decoding the // first key. - data := []byte{0xdf, 0xff, 0xff, 0xff, 0xff} + // + // 0x7fffffff rather than 0xffffffff: the latter is now rejected by the + // int-overflow check on 32-bit builds before reaching the clamp, so it + // would pass for the wrong reason. This value fits in an int on every + // platform and exercises the clamp itself. + data := []byte{0xdf, 0x7f, 0xff, 0xff, 0xff} + + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + dec := msgpack.NewDecoder(bytes.NewReader(data)) _, err := dec.DecodeUntypedMap() t.NotNil(err) + + runtime.ReadMemStats(&after) + // The clamp caps the hint at maxMapSize (1M entries). Without it the + // declared ~2G entries are allocated upfront -- hundreds of MB and tens + // of seconds. Allow generous headroom while still failing loudly if the + // clamp is removed. + if alloc := after.TotalAlloc - before.TotalAlloc; alloc > 256<<20 { + t.Failf("allocation not clamped", "decode allocated %d MiB, want < 256 MiB", alloc>>20) + } } func (t *MsgpackTest) TestDecodeUntypedMap() {