Bug report
Bug description:
Summary
import_run_extension() can return NULL while the current thread is still switched to the main interpreter. This happens in the subinterpreter extension import path when copying info->filename with _PyUnicode_Copy() fails.
The function switches from the importing subinterpreter to the main interpreter before calling the extension module init function. For single-phase init modules, it then copies the original filename because the original object may have been allocated by the subinterpreter's obmalloc:
filename = _PyUnicode_Copy(info->filename);
if (filename == NULL) {
return NULL;
}
That direct return NULL bypasses the main_finally cleanup path, including switch_back_from_main_interpreter(). The caller resumes with the wrong interpreter current for the thread, which can lead to a crash or interpreter state corruption.
This is a sub-issue of #146102 with gist details
Affected area
Python/import.c, in import_run_extension(), around the single-phase extension import handling after switching to the main interpreter.
Reproducer
The following script runs each attempt in a child process so that a vulnerable build can crash without taking down the driver process. It requires a CPython build tree with _interpreters, _testcapi, and _testsinglephase available.
from __future__ import annotations
import argparse
import subprocess
import sys
import textwrap
CHILD = r"""
import _interpreters
import _testsinglephase
filename = _testsinglephase.__file__
script = f'''
import _testcapi
from test.test_import import import_extension_from_file
_testcapi.set_nomemory(START, STOP)
try:
import_extension_from_file('_testsinglephase_basic_copy', {filename!r})
finally:
_testcapi.remove_mem_hooks()
'''
interp = _interpreters.create()
try:
exc = _interpreters.run_string(interp, script)
finally:
_interpreters.destroy(interp)
if exc is None:
print("import unexpectedly succeeded")
else:
print(f"{exc.type.__name__}: {exc.msg}")
"""
def run_one(python: str, start: int) -> subprocess.CompletedProcess[bytes]:
code = CHILD.replace("START", str(start)).replace("STOP", str(start + 1))
return subprocess.run(
[python, "-X", "faulthandler", "-c", code],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--python", default=sys.executable)
parser.add_argument("--start", type=int, default=1)
parser.add_argument("--stop", type=int, default=50)
args = parser.parse_args()
for start in range(args.start, args.stop + 1):
proc = run_one(args.python, start)
out = proc.stdout.decode("utf-8", "replace").strip()
err = proc.stderr.decode("utf-8", "replace").strip()
if proc.returncode:
print(f"CRASH start={start} returncode={proc.returncode}")
if out:
print("stdout:")
print(textwrap.indent(out, " "))
if err:
print("stderr:")
print(textwrap.indent(err, " "))
return 1
print(f"ok start={start}: {out}")
print("No child process crashed in the scanned range.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Example command on Windows:
.\PCbuild\amd64\python.exe repro_import_run_extension_copy_failure.py --start 1 --stop 50
On a vulnerable build, one of the child processes can terminate with an access violation or another abnormal return code.
On a fixed build, allocation failures in this area should be converted into a controlled subinterpreter-side ImportError, for example:
ImportError: failed to import from subinterpreter due to exception
Expected behavior
Any error after switching to the main interpreter must go through the cleanup path that switches back to the original interpreter before returning to the caller.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Windows
Linked PRs
Bug report
Bug description:
Summary
import_run_extension()can returnNULLwhile the current thread is still switched to the main interpreter. This happens in the subinterpreter extension import path when copyinginfo->filenamewith_PyUnicode_Copy()fails.The function switches from the importing subinterpreter to the main interpreter before calling the extension module init function. For single-phase init modules, it then copies the original filename because the original object may have been allocated by the subinterpreter's obmalloc:
That direct
return NULLbypasses themain_finallycleanup path, includingswitch_back_from_main_interpreter(). The caller resumes with the wrong interpreter current for the thread, which can lead to a crash or interpreter state corruption.This is a sub-issue of #146102 with gist details
Affected area
Python/import.c, inimport_run_extension(), around the single-phase extension import handling after switching to the main interpreter.Reproducer
The following script runs each attempt in a child process so that a vulnerable build can crash without taking down the driver process. It requires a CPython build tree with
_interpreters,_testcapi, and_testsinglephaseavailable.Example command on Windows:
On a vulnerable build, one of the child processes can terminate with an access violation or another abnormal return code.
On a fixed build, allocation failures in this area should be converted into a controlled subinterpreter-side
ImportError, for example:Expected behavior
Any error after switching to the main interpreter must go through the cleanup path that switches back to the original interpreter before returning to the caller.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Windows
Linked PRs