Skip to content

Commit 0c3ea30

Browse files
gh-123431: Harmonize extension code checks in pickle (GH-123434)
This checks are redundant in normal circumstances and can only work if the extension registry was intentionally broken. * The Python implementation now raises exception for the extension code with false boolean value. * Simplify the C code. RuntimeError is now raised in explicit checks. * Add many tests.
1 parent c9930f5 commit 0c3ea30

File tree

3 files changed

+73
-28
lines changed

3 files changed

+73
-28
lines changed

Lib/pickle.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,11 +1086,16 @@ def save_global(self, obj, name=None):
10861086

10871087
module_name = whichmodule(obj, name)
10881088
if self.proto >= 2:
1089-
code = _extension_registry.get((module_name, name))
1090-
if code:
1091-
assert code > 0
1089+
code = _extension_registry.get((module_name, name), _NoValue)
1090+
if code is not _NoValue:
10921091
if code <= 0xff:
1093-
write(EXT1 + pack("<B", code))
1092+
data = pack("<B", code)
1093+
if data == b'\0':
1094+
# Should never happen in normal circumstances,
1095+
# since the type and the value of the code are
1096+
# checked in copyreg.add_extension().
1097+
raise RuntimeError("extension code 0 is out of range")
1098+
write(EXT1 + data)
10941099
elif code <= 0xffff:
10951100
write(EXT2 + pack("<H", code))
10961101
else:
@@ -1581,9 +1586,8 @@ def load_ext4(self):
15811586
dispatch[EXT4[0]] = load_ext4
15821587

15831588
def get_extension(self, code):
1584-
nil = []
1585-
obj = _extension_cache.get(code, nil)
1586-
if obj is not nil:
1589+
obj = _extension_cache.get(code, _NoValue)
1590+
if obj is not _NoValue:
15871591
self.append(obj)
15881592
return
15891593
key = _inverted_registry.get(code)

Lib/test/pickletester.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,35 @@ def find_class(module_name, global_name):
13111311
self.assertEqual(loads(b'cmath\nlog\n.'), ('math', 'log'))
13121312
self.assertEqual(loads(b'\x8c\x04math\x8c\x03log\x93.'), ('math', 'log'))
13131313

1314+
def test_bad_ext_code(self):
1315+
# unregistered extension code
1316+
self.check_unpickling_error(ValueError, b'\x82\x01.')
1317+
self.check_unpickling_error(ValueError, b'\x82\xff.')
1318+
self.check_unpickling_error(ValueError, b'\x83\x01\x00.')
1319+
self.check_unpickling_error(ValueError, b'\x83\xff\xff.')
1320+
self.check_unpickling_error(ValueError, b'\x84\x01\x00\x00\x00.')
1321+
self.check_unpickling_error(ValueError, b'\x84\xff\xff\xff\x7f.')
1322+
# EXT specifies code <= 0
1323+
self.check_unpickling_error(pickle.UnpicklingError, b'\x82\x00.')
1324+
self.check_unpickling_error(pickle.UnpicklingError, b'\x83\x00\x00.')
1325+
self.check_unpickling_error(pickle.UnpicklingError, b'\x84\x00\x00\x00\x00.')
1326+
self.check_unpickling_error(pickle.UnpicklingError, b'\x84\x00\x00\x00\x80.')
1327+
self.check_unpickling_error(pickle.UnpicklingError, b'\x84\xff\xff\xff\xff.')
1328+
1329+
@support.cpython_only
1330+
def test_bad_ext_inverted_registry(self):
1331+
code = 1
1332+
def check(key, exc):
1333+
with support.swap_item(copyreg._inverted_registry, code, key):
1334+
with self.assertRaises(exc):
1335+
self.loads(b'\x82\x01.')
1336+
check(None, ValueError)
1337+
check((), ValueError)
1338+
check((__name__,), (TypeError, ValueError))
1339+
check((__name__, "MyList", "x"), (TypeError, ValueError))
1340+
check((__name__, None), (TypeError, ValueError))
1341+
check((None, "MyList"), (TypeError, ValueError))
1342+
13141343
def test_bad_reduce(self):
13151344
self.assertEqual(self.loads(b'cbuiltins\nint\n)R.'), 0)
13161345
self.check_unpickling_error(TypeError, b'N)R.')
@@ -2163,6 +2192,28 @@ def persistent_id(self, obj):
21632192
check({Clearer(): 1, Clearer(): 2})
21642193
check({1: Clearer(), 2: Clearer()})
21652194

2195+
@support.cpython_only
2196+
def test_bad_ext_code(self):
2197+
# This should never happen in normal circumstances, because the type
2198+
# and the value of the extesion code is checked in copyreg.add_extension().
2199+
key = (__name__, 'MyList')
2200+
def check(code, exc):
2201+
assert key not in copyreg._extension_registry
2202+
assert code not in copyreg._inverted_registry
2203+
with (support.swap_item(copyreg._extension_registry, key, code),
2204+
support.swap_item(copyreg._inverted_registry, code, key)):
2205+
for proto in protocols[2:]:
2206+
with self.assertRaises(exc):
2207+
self.dumps(MyList, proto)
2208+
2209+
check(object(), TypeError)
2210+
check(None, TypeError)
2211+
check(-1, (RuntimeError, struct.error))
2212+
check(0, RuntimeError)
2213+
check(2**31, (RuntimeError, OverflowError, struct.error))
2214+
check(2**1000, (OverflowError, struct.error))
2215+
check(-2**1000, (OverflowError, struct.error))
2216+
21662217

21672218
class AbstractPickleTests:
21682219
# Subclass must define self.dumps, self.loads.

Modules/_pickle.c

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3650,34 +3650,24 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
36503650
if (extension_key == NULL) {
36513651
goto error;
36523652
}
3653-
code_obj = PyDict_GetItemWithError(st->extension_registry,
3654-
extension_key);
3653+
if (PyDict_GetItemRef(st->extension_registry, extension_key, &code_obj) < 0) {
3654+
Py_DECREF(extension_key);
3655+
goto error;
3656+
}
36553657
Py_DECREF(extension_key);
3656-
/* The object is not registered in the extension registry.
3657-
This is the most likely code path. */
36583658
if (code_obj == NULL) {
3659-
if (PyErr_Occurred()) {
3660-
goto error;
3661-
}
3659+
/* The object is not registered in the extension registry.
3660+
This is the most likely code path. */
36623661
goto gen_global;
36633662
}
36643663

3665-
/* XXX: pickle.py doesn't check neither the type, nor the range
3666-
of the value returned by the extension_registry. It should for
3667-
consistency. */
3668-
3669-
/* Verify code_obj has the right type and value. */
3670-
if (!PyLong_Check(code_obj)) {
3671-
PyErr_Format(st->PicklingError,
3672-
"Can't pickle %R: extension code %R isn't an integer",
3673-
obj, code_obj);
3674-
goto error;
3675-
}
3676-
code = PyLong_AS_LONG(code_obj);
3664+
code = PyLong_AsLong(code_obj);
3665+
Py_DECREF(code_obj);
36773666
if (code <= 0 || code > 0x7fffffffL) {
3667+
/* Should never happen in normal circumstances, since the type and
3668+
the value of the code are checked in copyreg.add_extension(). */
36783669
if (!PyErr_Occurred())
3679-
PyErr_Format(st->PicklingError, "Can't pickle %R: extension "
3680-
"code %ld is out of range", obj, code);
3670+
PyErr_Format(PyExc_RuntimeError, "extension code %ld is out of range", code);
36813671
goto error;
36823672
}
36833673

0 commit comments

Comments
 (0)