Skip to content

pickle protocol 5: writable in-band PickleBuffer with empty tobytes() can memoize b'' via BYTEARRAY8, so later bytes references unpickle as bytearray #148914

Closed

Description

@madkrouger

Bug report

Bug description:

The problem

  • for an in-band, writable PickleBuffer, the pickler does buf = m.tobytes(). That value is always bytes
  • if the buffer is empty, buf is always the b'' singleton, so id(buf) is the same for every such call
  • the pickler then does in_memo = id(buf) in self.memo
  • so once b'' has been memoized anywhere earlier in the same dump, every later empty in-band tobytes() in that dump sees in_memo is true
  • so any later part of the same pickle that is supposed to store b'' as bytes may instead reuse that memo. The unpickler then returns the bytearray, not bytes.

Code that requires bytes (strict isinstance etc.) can fail during unpickling when it receives a bytearray.

Excerpt from pickler.py, but the same applies to the C implementation:

                        if in_memo:
                            pb_branch = "_save_bytearray_no_memo"
                            self._save_bytearray_no_memo(buf)
                        else:
                            pb_branch = "save_bytearray"
                            self.save_bytearray(buf)

One minimal repro example is:

import dill
from pickle import PickleBuffer

# a bit artificial example to trigger wrong flow
def repro_minimal() -> None:
    pb = PickleBuffer(memoryview(bytearray()))

    def f():
        pass

    blob = dill.dumps((pb, f), protocol=5)
    # it fails when trying to pass a bytearray instead of bytes to dill's _create_code
    dill.loads(blob)  
    print("dill.loads ok")

Less synthetic if you have Pandas and PyArrow

import pandas as pd
def repro_arrow_empty_dataframe() -> None:
    col = "EMPTY_STRING_COLUMN"
    df = pd.DataFrame({col: pd.Series([""], dtype="string[pyarrow]")})

    def g():
        pass

    blob = dill.dumps((df, g), protocol=5)
    dill.loads(blob)
    print("dill.loads ok")

Both result in TypeError: code() argument 16 must be bytes, not bytearray

However, that code() case is only one example; the underlying issue is bytes vs bytearray memo reuse, and similar failures can appear anywhere bytes are required.

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Apr 23, 2026
  2. serhiy-storchaka commented on Apr 26, 2026

    @serhiy-storchaka
    Member

    Sorry, I don't understand what the problem is. If m.readonly is false and in_memo is true, _save_bytearray_no_memo(buf) is called, which writes the BYTEARRAY8 opcode without writing MEMOIZE and memoizing buf. What is wrong with this?

    Do you have an example that does not use third-party libraries?

  3. madkrouger commented on Apr 27, 2026

    @madkrouger
    Author

    Hey,

    I think the bug is that bytes b'' can unpickle as a bytearray if the memo for id(b'') was first created as a bytearray:

    import pickle
    from pickle import PickleBuffer
    
    pb = PickleBuffer(memoryview(bytearray()))
    bts = b''
    
    obj = (pb, bts)
    
    blob = pickle.dumps(obj, protocol=5)
    out_pb, out_bts = pickle.loads(blob)
    
    # it fails, because an empty out_bts is of type bytearray
    assert isinstance(out_bts, bytes)

    I believe treating b'' as bytes would be safer and more compatible with callers that expect empty bytes

  4. serhiy-storchaka commented on Apr 27, 2026

    @serhiy-storchaka
    Member

    Thank you for your example @madkrouger. I see that the problem actually is opposite to your initial description. If in_memo is false, the code for empty bytearray is written, and it is memoized as b''. Now the following references to b'' (without PickleBuffer) will be unpickled as the same empty bytearray.

    There was also other bug here, for all PickleBuffers. PickleBuffers were incorrectly memoized, so identical PickleBuffers did not preserve identity after unpickling. This was only in the Python implementation.

    #149052 fixes this, now the Python implementation produces the same data as the C implementation.

  5. added a commit that references this issue on Apr 27, 2026
  6. madkrouger commented on Apr 28, 2026

    @madkrouger
    Author

    Great, thanks for taking care of this!

  7. added 2 commits that reference this issue on May 2, 2026
  8. added a commit that references this issue on May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions