From 2d32c8148532ef4759ab14c8e975e6900088fabc Mon Sep 17 00:00:00 2001 From: sudolulo Date: Mon, 15 Jun 2026 02:25:45 +0000 Subject: [PATCH] Fix infinite recursion in import hook and stdout contamination in apply.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sitecustomize.py: exec_module was calling importlib.util.find_spec(fullname) after Python had already added the module to sys.modules. find_spec short-circuits to sys.modules[name].__spec__, which is our own wrapper spec, so real_spec.loader.exec_module(module) immediately called exec_module again — infinite recursion. Fix: resolve the real file spec in find_spec (before sys.modules is populated) and store it on the Loader. exec_module now uses the stored spec directly with no find_spec call. Also move _mark_done into the finally block so a module that fails to load doesn't trigger infinite retry loops on subsequent import attempts. apply.sh: warn() writes to stdout. Inside $(find_mw_python), stdout is captured by the command substitution, so any warn() call folded its text into $PYTHON — causing every subsequent "$PYTHON" invocation to fail. Fix: emit the fallback warning to stderr (>&2) so it goes to the log without being captured. --- patch/apply.sh | 3 ++- patch/sitecustomize.py | 46 +++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/patch/apply.sh b/patch/apply.sh index 3a674b1..7028123 100755 --- a/patch/apply.sh +++ b/patch/apply.sh @@ -53,8 +53,9 @@ find_mw_python() { fi # Verify the chosen interpreter can actually import middlewared. + # Use >&2 so this message goes to stderr, not captured by $(...) substitution. if ! "$py" -c "import middlewared" 2>/dev/null; then - warn "Detected Python '$py' cannot import middlewared; falling back to python3" + echo "WARNING: '$py' cannot import middlewared; falling back to python3" >&2 py="python3" fi diff --git a/patch/sitecustomize.py b/patch/sitecustomize.py index 98c97cd..a958cf0 100644 --- a/patch/sitecustomize.py +++ b/patch/sitecustomize.py @@ -42,12 +42,32 @@ class _Finder: def find_spec(self, fullname, path, target=None): # noqa: ARG002 import importlib.machinery + import importlib.util + if ( fullname in self._targets and fullname not in self._done and fullname not in self._loading ): - return importlib.machinery.ModuleSpec(fullname, _Loader(self, fullname)) + # Find the real file spec HERE, before Python adds the module to + # sys.modules. If we deferred this to exec_module, find_spec would + # short-circuit via sys.modules[fullname].__spec__ (our own spec) and + # exec_module would call itself recursively forever. + self._loading.add(fullname) + try: + real_spec = importlib.util.find_spec(fullname) + finally: + self._loading.discard(fullname) + + if real_spec is None: + return None # module doesn't exist; don't intercept + + return importlib.machinery.ModuleSpec( + fullname, + _Loader(self, fullname, real_spec), + origin=real_spec.origin, + is_package=real_spec.submodule_search_locations is not None, + ) return None def _mark_done(self, fullname): @@ -60,37 +80,31 @@ class _Finder: class _Loader: - def __init__(self, finder, fullname): + def __init__(self, finder, fullname, real_spec): self._finder = finder self._fullname = fullname + self._real_spec = real_spec def create_module(self, spec): # noqa: ARG002 return None # use Python's default module creation def exec_module(self, module): - import importlib.util - fullname = self._fullname - self._finder._loading.add(fullname) + real_spec = self._real_spec + try: - # find_spec for the real file — our finder returns None while - # fullname is in _loading, so the normal finders handle this. - real_spec = importlib.util.find_spec(fullname) - if real_spec is None: - raise ImportError(f"No module named {fullname!r}") - real_spec.loader.exec_module(module) - # Fix module metadata so it looks like a normal import. module.__spec__ = real_spec module.__loader__ = real_spec.loader - if getattr(real_spec, "origin", None): + if real_spec.origin: module.__file__ = real_spec.origin finally: - self._finder._loading.discard(fullname) - - self._finder._mark_done(fullname) + # Mark done even on failure so a broken module doesn't cause + # infinite retry loops on subsequent import attempts. + self._finder._mark_done(fullname) + # exec_module succeeded — apply our patch. try: _PATCHES[fullname](module) except Exception as exc: