From 108a245ee81950e92b152aa88f35cd7a6647d1b7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 7 Oct 2026 14:28:17 +0100 Subject: [PATCH] [mypyc] Fix segfault when importing a package with module-level __getattr__ Fixes mypyc/mypyc#1198. Importing a compiled package that defines a module-level `__getattr__` could segfault: ```python # pkg/__init__.py def __getattr__(name: str) -> object: raise AttributeError(name) ``` When initializing a module, mypyc checks whether `__file__`, `__path__` and other module attributes are already set using `getattr`, and this calls the module `__getattr__` if the attribute is not found. Since module-level functions are available before the module body has been executed, this calls compiled code before the module has been fully initialized. Now these attributes are looked up directly from the module dict, so that `__getattr__` is not called. --- mypyc/lib-rt/misc_ops.c | 26 +++++++++++++++++++------- mypyc/test-data/run-multimodule.test | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index 595e6538d7c9..e5f51b4e3c44 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -1224,6 +1224,18 @@ static int CPyImport_InitSpecClasses(void) { return 0; } +// Look up an attribute directly from the module dict. Unlike +// PyObject_GetOptionalAttrString, this doesn't call a module-level __getattr__, +// which isn't safe to call before the module has been initialized. +static int CPyImport_GetModuleDictItem(PyObject *modobj, const char *name, PyObject **result) { + PyObject *dict = PyModule_GetDict(modobj); // Borrowed + if (dict == NULL) { + *result = NULL; + return -1; + } + return PyDict_GetItemStringRef(dict, name, result); +} + // Set __package__ before executing the module body so it is available // during module initialization. For a package, __package__ is the module // name itself. For a non-package submodule "a.b.c", it is "a.b". For a @@ -1231,7 +1243,7 @@ static int CPyImport_InitSpecClasses(void) { static int CPyImport_SetModulePackage(PyObject *modobj, PyObject *module_name, Py_ssize_t is_package) { PyObject *pkg = NULL; - int rc = PyObject_GetOptionalAttrString(modobj, "__package__", &pkg); + int rc = CPyImport_GetModuleDictItem(modobj, "__package__", &pkg); if (rc < 0) { return -1; } @@ -1271,7 +1283,7 @@ static int CPyImport_SetModuleFile(PyObject *modobj, PyObject *module_name, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package) { PyObject *file = NULL; - int rc = PyObject_GetOptionalAttrString(modobj, "__file__", &file); + int rc = CPyImport_GetModuleDictItem(modobj, "__file__", &file); if (rc < 0) { return -1; } @@ -1396,7 +1408,7 @@ static int CPyImport_SetModuleFile(PyObject *modobj, PyObject *module_name, // Returns 0 on success, -1 on error. static int CPyImport_SetModulePath(PyObject *modobj) { PyObject *existing_path = NULL; - int rc = PyObject_GetOptionalAttrString(modobj, "__path__", &existing_path); + int rc = CPyImport_GetModuleDictItem(modobj, "__path__", &existing_path); if (rc < 0) { return -1; } @@ -1405,7 +1417,7 @@ static int CPyImport_SetModulePath(PyObject *modobj) { return 0; } PyObject *file = NULL; - rc = PyObject_GetOptionalAttrString(modobj, "__file__", &file); + rc = CPyImport_GetModuleDictItem(modobj, "__file__", &file); if (rc <= 0) { return rc; } @@ -1435,7 +1447,7 @@ static int CPyImport_SetModulePath(PyObject *modobj) { static int CPyImport_SetModuleSpec(PyObject *modobj, PyObject *module_name, Py_ssize_t is_package) { PyObject *spec = NULL; - int rc = PyObject_GetOptionalAttrString(modobj, "__spec__", &spec); + int rc = CPyImport_GetModuleDictItem(modobj, "__spec__", &spec); if (rc < 0) { return -1; } @@ -1449,7 +1461,7 @@ static int CPyImport_SetModuleSpec(PyObject *modobj, PyObject *module_name, return -1; } PyObject *file = NULL; - if (PyObject_GetOptionalAttrString(modobj, "__file__", &file) < 0) { + if (CPyImport_GetModuleDictItem(modobj, "__file__", &file) < 0) { return -1; } if (file == NULL) { @@ -1479,7 +1491,7 @@ static int CPyImport_SetModuleSpec(PyObject *modobj, PyObject *module_name, if (is_package) { // Set submodule_search_locations from __path__ PyObject *path = NULL; - if (PyObject_GetOptionalAttrString(modobj, "__path__", &path) < 0) { + if (CPyImport_GetModuleDictItem(modobj, "__path__", &path) < 0) { Py_DECREF(spec); Py_DECREF(loader); return -1; diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index 74fcf2e8d94b..73d8258599eb 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -4198,3 +4198,23 @@ assert o.ITEMS == [1, 2] b: Any = Base() assert b.COMPUTED == 7 assert b.ITEMS is Base.ITEMS + +[case testImportPackageWithModuleGetattr] +[file other_pkg/__init__.py] +def __getattr__(name: str) -> str: + if name.startswith("__"): + raise AttributeError(name) + return name +[file other_pkg/other_sub.py] +x = 1 +[file driver.py] +import importlib.machinery +import other_pkg.other_sub + +assert other_pkg.other_sub.x == 1 +ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES +assert any(other_pkg.__file__.endswith(s) for s in ext_suffixes), other_pkg.__file__ +assert other_pkg.__path__ +assert other_pkg.__spec__ is not None +assert other_pkg.__package__ == "other_pkg" +assert other_pkg.missing == "missing"