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) {
returnNULL;
}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__ importannotationsimportargparseimportsubprocessimportsysimporttextwrapCHILD=r"""import _interpretersimport _testsinglephasefilename = _testsinglephase.__file__script = f'''import _testcapifrom 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}")"""defrun_one(python: str, start: int) ->subprocess.CompletedProcess[bytes]:
code=CHILD.replace("START", str(start)).replace("STOP", str(start+1))
returnsubprocess.run(
[python, "-X", "faulthandler", "-c", code],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
defmain() ->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()
forstartinrange(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()
ifproc.returncode:
print(f"CRASH start={start} returncode={proc.returncode}")
ifout:
print("stdout:")
print(textwrap.indent(out, " "))
iferr:
print("stderr:")
print(textwrap.indent(err, " "))
return1print(f"ok start={start}: {out}")
print("No child process crashed in the scanned range.")
return0if__name__=="__main__":
raiseSystemExit(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