Skip to content

Commit 6f4c560

Browse files
authored
FIX check the type of the entries in sys.modules (#326)
FIX type-check entries of sys.modules in _whichmodule Some modules such as coverage can inject some non-modules objects inside sys.modules, creating unpredictable bugs while trying to infer the module of a function.
1 parent 8f851f7 commit 6f4c560

File tree

3 files changed

+93
-24
lines changed

3 files changed

+93
-24
lines changed

Diff for: CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
1.2.3
22
=====
33

4+
- Fix a bug affecting cloudpickle when non-modules objects are added into
5+
sys.modules
6+
([PR #326](https://github.com/cloudpipe/cloudpickle/pull/326)).
7+
48
- Fix a regression in cloudpickle and python3.8 causing an error when trying to
59
pickle property objects.
610
([PR #329](https://github.com/cloudpipe/cloudpickle/pull/329)).

Diff for: cloudpickle/cloudpickle.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,13 @@ def _whichmodule(obj, name):
155155
# modules that trigger imports of other modules upon calls to getattr or
156156
# other threads importing at the same time.
157157
for module_name, module in sys.modules.copy().items():
158-
if module_name == '__main__' or module is None:
158+
# Some modules such as coverage can inject non-module objects inside
159+
# sys.modules
160+
if (
161+
module_name == '__main__' or
162+
module is None or
163+
not isinstance(module, types.ModuleType)
164+
):
159165
continue
160166
try:
161167
if _getattribute(module, name)[0] is obj:

Diff for: tests/cloudpickle_test.py

+82-23
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import cloudpickle
4343
from cloudpickle.cloudpickle import _is_dynamic
4444
from cloudpickle.cloudpickle import _make_empty_cell, cell_set
45-
from cloudpickle.cloudpickle import _extract_class_dict
45+
from cloudpickle.cloudpickle import _extract_class_dict, _whichmodule
4646

4747
from .testutils import subprocess_pickle_echo
4848
from .testutils import assert_run_python_script
@@ -1048,35 +1048,94 @@ def __init__(self, x):
10481048

10491049
self.assertEqual(set(weakset), {depickled1, depickled2})
10501050

1051-
def test_faulty_module(self):
1052-
for module_name in ['_missing_module', None]:
1053-
class FaultyModule(object):
1054-
def __getattr__(self, name):
1055-
# This throws an exception while looking up within
1056-
# pickle.whichmodule or getattr(module, name, None)
1057-
raise Exception()
1051+
def test_non_module_object_passing_whichmodule_test(self):
1052+
# https://github.com/cloudpipe/cloudpickle/pull/326: cloudpickle should
1053+
# not try to instrospect non-modules object when trying to discover the
1054+
# module of a function/class. This happenened because codecov injects
1055+
# tuples (and not modules) into sys.modules, but type-checks were not
1056+
# carried out on the entries of sys.modules, causing cloupdickle to
1057+
# then error in unexpected ways
1058+
def func(x):
1059+
return x ** 2
1060+
1061+
# Trigger a loop during the execution of whichmodule(func) by
1062+
# explicitly setting the function's module to None
1063+
func.__module__ = None
1064+
1065+
class NonModuleObject(object):
1066+
def __getattr__(self, name):
1067+
# We whitelist func so that a _whichmodule(func, None) call returns
1068+
# the NonModuleObject instance if a type check on the entries
1069+
# of sys.modules is not carried out, but manipulating this
1070+
# instance thinking it really is a module later on in the
1071+
# pickling process of func errors out
1072+
if name == 'func':
1073+
return func
1074+
else:
1075+
raise AttributeError
1076+
1077+
non_module_object = NonModuleObject()
1078+
1079+
assert func(2) == 4
1080+
assert func is non_module_object.func
1081+
1082+
# Any manipulation of non_module_object relying on attribute access
1083+
# will raise an Exception
1084+
with pytest.raises(AttributeError):
1085+
_is_dynamic(non_module_object)
1086+
1087+
try:
1088+
sys.modules['NonModuleObject'] = non_module_object
10581089

1059-
class Foo(object):
1060-
__module__ = module_name
1090+
func_module_name = _whichmodule(func, None)
1091+
assert func_module_name != 'NonModuleObject'
1092+
assert func_module_name is None
10611093

1062-
def foo(self):
1094+
depickled_func = pickle_depickle(func, protocol=self.protocol)
1095+
assert depickled_func(2) == 4
1096+
1097+
finally:
1098+
sys.modules.pop('NonModuleObject')
1099+
1100+
def test_unrelated_faulty_module(self):
1101+
# Check that pickling a dynamically defined function or class does not
1102+
# fail when introspecting the currently loaded modules in sys.modules
1103+
# as long as those faulty modules are unrelated to the class or
1104+
# function we are currently pickling.
1105+
for base_class in (object, types.ModuleType):
1106+
for module_name in ['_missing_module', None]:
1107+
class FaultyModule(base_class):
1108+
def __getattr__(self, name):
1109+
# This throws an exception while looking up within
1110+
# pickle.whichmodule or getattr(module, name, None)
1111+
raise Exception()
1112+
1113+
class Foo(object):
1114+
__module__ = module_name
1115+
1116+
def foo(self):
1117+
return "it works!"
1118+
1119+
def foo():
10631120
return "it works!"
10641121

1065-
def foo():
1066-
return "it works!"
1122+
foo.__module__ = module_name
10671123

1068-
foo.__module__ = module_name
1124+
if base_class is types.ModuleType: # noqa
1125+
faulty_module = FaultyModule('_faulty_module')
1126+
else:
1127+
faulty_module = FaultyModule()
1128+
sys.modules["_faulty_module"] = faulty_module
10691129

1070-
sys.modules["_faulty_module"] = FaultyModule()
1071-
try:
1072-
# Test whichmodule in save_global.
1073-
self.assertEqual(pickle_depickle(Foo()).foo(), "it works!")
1130+
try:
1131+
# Test whichmodule in save_global.
1132+
self.assertEqual(pickle_depickle(Foo()).foo(), "it works!")
10741133

1075-
# Test whichmodule in save_function.
1076-
cloned = pickle_depickle(foo, protocol=self.protocol)
1077-
self.assertEqual(cloned(), "it works!")
1078-
finally:
1079-
sys.modules.pop("_faulty_module", None)
1134+
# Test whichmodule in save_function.
1135+
cloned = pickle_depickle(foo, protocol=self.protocol)
1136+
self.assertEqual(cloned(), "it works!")
1137+
finally:
1138+
sys.modules.pop("_faulty_module", None)
10801139

10811140
def test_dynamic_pytest_module(self):
10821141
# Test case for pull request https://github.com/cloudpipe/cloudpickle/pull/116

0 commit comments

Comments
 (0)