Skip to content

Commit 4371c0a

Browse files
tjb900pitrou
authored andcommitted
bpo-34572: change _pickle unpickling to use import rather than retrieving from sys.modules (GH-9047)
Fix C implementation of pickle.loads to use importlib's locking mechanisms, and thereby avoid using partially-loaded modules.
1 parent 4a7f44a commit 4371c0a

File tree

3 files changed

+75
-7
lines changed

3 files changed

+75
-7
lines changed

Lib/test/pickletester.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
import dbm
44
import io
55
import functools
6+
import os
67
import pickle
78
import pickletools
9+
import shutil
810
import struct
911
import sys
12+
import threading
1013
import unittest
1114
import weakref
15+
from textwrap import dedent
1216
from http.cookies import SimpleCookie
1317

1418
from test import support
1519
from test.support import (
1620
TestFailed, TESTFN, run_with_locale, no_tracing,
17-
_2G, _4G, bigmemtest,
21+
_2G, _4G, bigmemtest, reap_threads, forget,
1822
)
1923

2024
from pickle import bytes_types
@@ -1174,6 +1178,67 @@ def test_truncated_data(self):
11741178
for p in badpickles:
11751179
self.check_unpickling_error(self.truncated_errors, p)
11761180

1181+
@reap_threads
1182+
def test_unpickle_module_race(self):
1183+
# https://bugs.python.org/issue34572
1184+
locker_module = dedent("""
1185+
import threading
1186+
barrier = threading.Barrier(2)
1187+
""")
1188+
locking_import_module = dedent("""
1189+
import locker
1190+
locker.barrier.wait()
1191+
class ToBeUnpickled(object):
1192+
pass
1193+
""")
1194+
1195+
os.mkdir(TESTFN)
1196+
self.addCleanup(shutil.rmtree, TESTFN)
1197+
sys.path.insert(0, TESTFN)
1198+
self.addCleanup(sys.path.remove, TESTFN)
1199+
with open(os.path.join(TESTFN, "locker.py"), "wb") as f:
1200+
f.write(locker_module.encode('utf-8'))
1201+
with open(os.path.join(TESTFN, "locking_import.py"), "wb") as f:
1202+
f.write(locking_import_module.encode('utf-8'))
1203+
self.addCleanup(forget, "locker")
1204+
self.addCleanup(forget, "locking_import")
1205+
1206+
import locker
1207+
1208+
pickle_bytes = (
1209+
b'\x80\x03clocking_import\nToBeUnpickled\nq\x00)\x81q\x01.')
1210+
1211+
# Then try to unpickle two of these simultaneously
1212+
# One of them will cause the module import, and we want it to block
1213+
# until the other one either:
1214+
# - fails (before the patch for this issue)
1215+
# - blocks on the import lock for the module, as it should
1216+
results = []
1217+
barrier = threading.Barrier(3)
1218+
def t():
1219+
# This ensures the threads have all started
1220+
# presumably barrier release is faster than thread startup
1221+
barrier.wait()
1222+
results.append(pickle.loads(pickle_bytes))
1223+
1224+
t1 = threading.Thread(target=t)
1225+
t2 = threading.Thread(target=t)
1226+
t1.start()
1227+
t2.start()
1228+
1229+
barrier.wait()
1230+
# could have delay here
1231+
locker.barrier.wait()
1232+
1233+
t1.join()
1234+
t2.join()
1235+
1236+
from locking_import import ToBeUnpickled
1237+
self.assertEqual(
1238+
[type(x) for x in results],
1239+
[ToBeUnpickled] * 2)
1240+
1241+
11771242

11781243
class AbstractPickleTests(unittest.TestCase):
11791244
# Subclass must define self.dumps, self.loads.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix C implementation of pickle.loads to use importlib's locking
2+
mechanisms, and thereby avoid using partially-loaded modules.
3+
Patch by Tim Burgess.

Modules/_pickle.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6636,13 +6636,13 @@ _pickle_Unpickler_find_class_impl(UnpicklerObject *self,
66366636
}
66376637
}
66386638

6639-
module = PyImport_GetModule(module_name);
6639+
/*
6640+
* we don't use PyImport_GetModule here, because it can return partially-
6641+
* initialised modules, which then cause the getattribute to fail.
6642+
*/
6643+
module = PyImport_Import(module_name);
66406644
if (module == NULL) {
6641-
if (PyErr_Occurred())
6642-
return NULL;
6643-
module = PyImport_Import(module_name);
6644-
if (module == NULL)
6645-
return NULL;
6645+
return NULL;
66466646
}
66476647
global = getattribute(module, global_name, self->proto >= 4);
66486648
Py_DECREF(module);

0 commit comments

Comments
 (0)