Skip to content

Commit

Permalink
pythongh-93627: Align Python implementation of pickle with C implemen…
Browse files Browse the repository at this point in the history
…tation of pickle (pythonGH-103035)

If a method like __reduce_ex_ or __reduce__ is set to None, a TypeError is raised.
  • Loading branch information
eendebakpt committed Sep 10, 2023
1 parent 9257891 commit 85a5d3d
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 10 deletions.
22 changes: 12 additions & 10 deletions Lib/pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ def decode_long(data):
return int.from_bytes(data, byteorder='little', signed=True)


_NoValue = object()

# Pickling machinery

class _Pickler:
Expand Down Expand Up @@ -542,8 +544,8 @@ def save(self, obj, save_persistent_id=True):
return

rv = NotImplemented
reduce = getattr(self, "reducer_override", None)
if reduce is not None:
reduce = getattr(self, "reducer_override", _NoValue)
if reduce is not _NoValue:
rv = reduce(obj)

if rv is NotImplemented:
Expand All @@ -556,8 +558,8 @@ def save(self, obj, save_persistent_id=True):

# Check private dispatch table if any, or else
# copyreg.dispatch_table
reduce = getattr(self, 'dispatch_table', dispatch_table).get(t)
if reduce is not None:
reduce = getattr(self, 'dispatch_table', dispatch_table).get(t, _NoValue)
if reduce is not _NoValue:
rv = reduce(obj)
else:
# Check for a class with a custom metaclass; treat as regular
Expand All @@ -567,12 +569,12 @@ def save(self, obj, save_persistent_id=True):
return

# Check for a __reduce_ex__ method, fall back to __reduce__
reduce = getattr(obj, "__reduce_ex__", None)
if reduce is not None:
reduce = getattr(obj, "__reduce_ex__", _NoValue)
if reduce is not _NoValue:
rv = reduce(self.proto)
else:
reduce = getattr(obj, "__reduce__", None)
if reduce is not None:
reduce = getattr(obj, "__reduce__", _NoValue)
if reduce is not _NoValue:
rv = reduce()
else:
raise PicklingError("Can't pickle %r object: %r" %
Expand Down Expand Up @@ -1705,8 +1707,8 @@ def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None)
if setstate is not None:
setstate = getattr(inst, "__setstate__", _NoValue)
if setstate is not _NoValue:
setstate(state)
return
slotstate = None
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2408,6 +2408,22 @@ def test_reduce_calls_base(self):
y = self.loads(s)
self.assertEqual(y._reduce_called, 1)

def test_reduce_ex_None(self):
c = REX_None()
with self.assertRaises(TypeError):
self.dumps(c)

def test_reduce_None(self):
c = R_None()
with self.assertRaises(TypeError):
self.dumps(c)

def test_pickle_setstate_None(self):
c = C_None_setstate()
p = self.dumps(c)
with self.assertRaises(TypeError):
self.loads(p)

@no_tracing
def test_bad_getattr(self):
# Issue #3514: crash when there is an infinite loop in __getattr__
Expand Down Expand Up @@ -3349,6 +3365,21 @@ def __setstate__(self, state):
def __reduce__(self):
return type(self), (), self.state

class REX_None:
""" Setting __reduce_ex__ to None should fail """
__reduce_ex__ = None

class R_None:
""" Setting __reduce__ to None should fail """
__reduce__ = None

class C_None_setstate:
""" Setting __setstate__ to None should fail """
def __getstate__(self):
return 1

__setstate__ = None


# Test classes for newobj

Expand Down Expand Up @@ -3752,6 +3783,25 @@ def test_unpickling_buffering_readline(self):
unpickler = self.unpickler_class(f)
self.assertEqual(unpickler.load(), data)

def test_pickle_invalid_reducer_override(self):
# gh-103035
obj = object()

f = io.BytesIO()
class MyPickler(self.pickler_class):
pass
pickler = MyPickler(f)
pickler.dump(obj)

pickler.clear_memo()
pickler.reducer_override = None
with self.assertRaises(TypeError):
pickler.dump(obj)

pickler.clear_memo()
pickler.reducer_override = 10
with self.assertRaises(TypeError):
pickler.dump(obj)

# Tests for dispatch_table attribute

Expand Down Expand Up @@ -3914,6 +3964,15 @@ def dumps(obj, protocol=None):

self._test_dispatch_table(dumps, dt)

def test_dispatch_table_None_item(self):
# gh-93627
obj = object()
f = io.BytesIO()
pickler = self.pickler_class(f)
pickler.dispatch_table = {type(obj): None}
with self.assertRaises(TypeError):
pickler.dump(obj)

def _test_dispatch_table(self, dumps, dispatch_table):
def custom_load_dump(obj):
return pickle.loads(dumps(obj, 0))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update the Python pickle module implementation to match the C implementation of the pickle module. For objects setting reduction methods like :meth:`~object.__reduce_ex__` or :meth:`~object.__reduce__` to ``None``, pickling will result in a :exc:`TypeError`.

0 comments on commit 85a5d3d

Please sign in to comment.