Skip to content

import_run_extension() can return on the wrong interpreter after _PyUnicode_Copy() failure #150454

@lpyu001

Description

@lpyu001

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-importlibtype-bugAn unexpected behavior, bug, or error
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions