Skip to content

Commit ae438a4

Browse files
committed
Check for script shadowing stdlib
1 parent afe03da commit ae438a4

File tree

2 files changed

+150
-31
lines changed

2 files changed

+150
-31
lines changed

Lib/test/test_import/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,48 @@ def test_issue105979(self):
790790
self.assertIn("Frozen object named 'x' is invalid",
791791
str(cm.exception))
792792

793+
def test_cwd_script_shadowing_stdlib(self):
794+
with CleanImport('collections'):
795+
import collections
796+
collections.__spec__ = types.SimpleNamespace()
797+
collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py')
798+
with self.assertRaisesRegex(
799+
AttributeError,
800+
r"module 'collections' has no attribute 'does_not_exist' \(most "
801+
r"likely due to '.*collections.py' shadowing the standard "
802+
r"library module named 'collections'\)"
803+
):
804+
collections.does_not_exist
805+
806+
def test_shadowing_stdlib_edge_cases(self):
807+
with CleanImport('collections'):
808+
import collections
809+
collections.__spec__ = types.SimpleNamespace()
810+
collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py')
811+
with CleanImport('sys'):
812+
import sys
813+
sys.stdlib_module_names = None
814+
with self.assertRaisesRegex(
815+
AttributeError,
816+
r"module 'collections' has no attribute 'does_not_exist'"
817+
):
818+
collections.does_not_exist
819+
820+
del sys.stdlib_module_names
821+
with self.assertRaisesRegex(
822+
AttributeError,
823+
r"module 'collections' has no attribute 'does_not_exist'"
824+
):
825+
collections.does_not_exist
826+
with CleanImport("os"), CleanImport('os.path'):
827+
import os as clean_os
828+
del clean_os.path.dirname
829+
with self.assertRaisesRegex(
830+
AttributeError,
831+
r"module '.*path' has no attribute 'dirname'"
832+
):
833+
collections.does_not_exist
834+
793835

794836
@skip_if_dont_write_bytecode
795837
class FilePermissionTests(unittest.TestCase):

Objects/moduleobject.c

Lines changed: 108 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ PyObject*
788788
_Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
789789
{
790790
// When suppress=1, this function suppresses AttributeError.
791-
PyObject *attr, *mod_name, *getattr, *origin;
791+
PyObject *attr, *mod_name, *getattr;
792792
attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress);
793793
if (attr) {
794794
return attr;
@@ -837,48 +837,125 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
837837
Py_DECREF(mod_name);
838838
return NULL;
839839
}
840-
int rc = _PyModuleSpec_IsInitializing(spec);
841-
if (rc > 0) {
842-
int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin);
843-
if (valid_spec == -1) {
844-
Py_XDECREF(spec);
840+
PyObject *origin = NULL;
841+
if (spec) {
842+
int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin);
843+
if (rc == -1) {
844+
Py_DECREF(spec);
845845
Py_DECREF(mod_name);
846846
return NULL;
847847
}
848-
if (valid_spec == 1 && !PyUnicode_Check(origin)) {
849-
valid_spec = 0;
850-
Py_DECREF(origin);
851-
}
852-
if (valid_spec == 1) {
853-
PyErr_Format(PyExc_AttributeError,
854-
"partially initialized "
855-
"module '%U' from '%U' has no attribute '%U' "
856-
"(most likely due to a circular import)",
857-
mod_name, origin, name);
848+
if (rc == 1 && !PyUnicode_Check(origin)) {
858849
Py_DECREF(origin);
850+
origin = NULL;
859851
}
860-
else {
861-
PyErr_Format(PyExc_AttributeError,
862-
"partially initialized "
863-
"module '%U' has no attribute '%U' "
864-
"(most likely due to a circular import)",
865-
mod_name, name);
852+
}
853+
854+
int is_script_shadowing_stdlib = 0;
855+
// Check mod.__name__ in sys.stdlib_module_names
856+
// and os.path.dirname(mod.__spec__.origin) == os.getcwd()
857+
PyObject *stdlib = NULL;
858+
if (origin) {
859+
// Checks against mod_name are to avoid bad recursion
860+
if (
861+
PyUnicode_CompareWithASCIIString(mod_name, "sys") != 0
862+
&& PyUnicode_CompareWithASCIIString(mod_name, "builtins") != 0
863+
) {
864+
stdlib = _PyImport_GetModuleAttrString("sys", "stdlib_module_names");
865+
if (!stdlib) {
866+
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
867+
PyErr_Clear();
868+
} else {
869+
goto done;
870+
}
871+
}
872+
if (stdlib && PyFrozenSet_Check(stdlib) && PySet_Contains(stdlib, mod_name)) {
873+
if (
874+
PyUnicode_CompareWithASCIIString(mod_name, "os") != 0
875+
&& PyUnicode_CompareWithASCIIString(mod_name, "posixpath") != 0
876+
&& PyUnicode_CompareWithASCIIString(mod_name, "ntpath") != 0
877+
) {
878+
PyObject *os_path = _PyImport_GetModuleAttrString("os", "path");
879+
if (!os_path) {
880+
goto done;
881+
}
882+
PyObject *dirname = PyObject_GetAttrString(os_path, "dirname");
883+
Py_DECREF(os_path);
884+
if (!dirname) {
885+
goto done;
886+
}
887+
PyObject *origin_dir = _PyObject_CallOneArg(dirname, origin);
888+
Py_DECREF(dirname);
889+
if (!origin_dir) {
890+
goto done;
891+
}
892+
893+
PyObject *getcwd = _PyImport_GetModuleAttrString("os", "getcwd");
894+
if (!getcwd) {
895+
Py_DECREF(origin_dir);
896+
goto done;
897+
}
898+
PyObject *cwd = _PyObject_CallNoArgs(getcwd);
899+
Py_DECREF(getcwd);
900+
if (!cwd) {
901+
Py_DECREF(origin_dir);
902+
goto done;
903+
}
904+
905+
is_script_shadowing_stdlib = PyObject_RichCompareBool(origin_dir, cwd, Py_EQ);
906+
Py_DECREF(origin_dir);
907+
Py_DECREF(cwd);
908+
if (is_script_shadowing_stdlib < 0) {
909+
goto done;
910+
}
911+
}
912+
}
866913
}
867914
}
868-
else if (rc == 0) {
869-
rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name);
915+
916+
if (is_script_shadowing_stdlib == 1) {
917+
PyErr_Format(PyExc_AttributeError,
918+
"module '%U' has no attribute '%U' "
919+
"(most likely due to '%U' shadowing the standard library "
920+
"module named '%U')",
921+
mod_name, name, origin, mod_name);
922+
} else {
923+
int rc = _PyModuleSpec_IsInitializing(spec);
870924
if (rc > 0) {
871-
PyErr_Format(PyExc_AttributeError,
872-
"cannot access submodule '%U' of module '%U' "
873-
"(most likely due to a circular import)",
874-
name, mod_name);
925+
if (origin) {
926+
PyErr_Format(PyExc_AttributeError,
927+
"partially initialized "
928+
"module '%U' from '%U' has no attribute '%U' "
929+
"(most likely due to a circular import)",
930+
mod_name, origin, name);
931+
}
932+
else {
933+
PyErr_Format(PyExc_AttributeError,
934+
"partially initialized "
935+
"module '%U' has no attribute '%U' "
936+
"(most likely due to a circular import)",
937+
mod_name, name);
938+
}
875939
}
876940
else if (rc == 0) {
877-
PyErr_Format(PyExc_AttributeError,
878-
"module '%U' has no attribute '%U'",
879-
mod_name, name);
941+
rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name);
942+
if (rc > 0) {
943+
PyErr_Format(PyExc_AttributeError,
944+
"cannot access submodule '%U' of module '%U' "
945+
"(most likely due to a circular import)",
946+
name, mod_name);
947+
}
948+
else if (rc == 0) {
949+
PyErr_Format(PyExc_AttributeError,
950+
"module '%U' has no attribute '%U'",
951+
mod_name, name);
952+
}
880953
}
881954
}
955+
956+
done:
957+
Py_XDECREF(stdlib);
958+
Py_XDECREF(origin);
882959
Py_XDECREF(spec);
883960
Py_DECREF(mod_name);
884961
return NULL;

0 commit comments

Comments
 (0)