diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index c44c6366..899f0a68 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -157,6 +157,7 @@ cdef class Packer: cdef const char* rawval cdef Py_ssize_t L cdef Py_buffer view + cdef object odata cdef bint strict = self.strict_types if o is None: @@ -213,8 +214,12 @@ cdef class Packer: self._pack(v, nest_limit) elif type(o) is ExtType if strict else isinstance(o, ExtType): # This should be before Tuple because ExtType is namedtuple. - rawval = o.data - L = len(o.data) + # Size the body from the bytes object itself. len() can be + # overridden on a bytes subclass so it disagrees with rawval, + # which would make msgpack_pack_raw_body read past the buffer. + odata = o.data + rawval = odata + L = Py_SIZE(odata) if L > ITEM_LIMIT: raise ValueError("EXT data is too large") msgpack_pack_ext(&self.pk, o.code, L) @@ -287,11 +292,18 @@ cdef class Packer: @cython.critical_section def pack_ext_type(self, typecode, data): + cdef const char *rawval + cdef Py_ssize_t L self._check_exports() - if len(data) > ITEM_LIMIT: + # Take the body length from the bytes object, not len(): a bytes + # subclass can override __len__ so it no longer matches rawval and + # msgpack_pack_raw_body reads past the buffer. + rawval = data + L = Py_SIZE(data) + if L > ITEM_LIMIT: raise ValueError("ext data too large") - msgpack_pack_ext(&self.pk, typecode, len(data)) - msgpack_pack_raw_body(&self.pk, data, len(data)) + msgpack_pack_ext(&self.pk, typecode, L) + msgpack_pack_raw_body(&self.pk, rawval, L) if self.autoreset: buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) self.pk.length = 0 diff --git a/test/test_extension.py b/test/test_extension.py index b9d8aa68..e68ddba6 100644 --- a/test/test_extension.py +++ b/test/test_extension.py @@ -57,6 +57,29 @@ def test_pack_ext_type_autoreset(): assert packer.bytes() == b"" +@pytest.mark.skipif( + msgpack.Packer.__module__ == "msgpack.fallback", + reason="out-of-bounds read is specific to the C extension", +) +def test_pack_ext_type_len_override(): + # A bytes subclass whose __len__ over-reports its size must not make the + # packer copy more than the real buffer. + class BadLen(bytes): + def __len__(self): + return 4096 + + data = BadLen(b"AB") # real length 2 + assert bytes.__len__(data) == 2 + + packed = msgpack.packb(ExtType(0x42, data)) + assert packed == b"\xd5\x42AB" # fixext 2, sized by the real buffer + assert msgpack.unpackb(packed) == ExtType(0x42, b"AB") + + packer = msgpack.Packer(autoreset=False) + packer.pack_ext_type(0x42, data) + assert packer.bytes() == b"\xd5\x42AB" + + def test_unpack_ext_type(): def check(b, expected): assert msgpack.unpackb(b) == expected