Skip to content

Unclosed interpreter causes segfault after fork() in child process. #139759

Open

Description

@aurelwildfellner

Bug report

Bug description:

With concurrent.interpreters now available in 3.14 it's possible to trigger a probably already known issue with cleaning up interpreters after a fork.

This minimal example creates an interpreter (on the same thread), doesn't close it and then forks. The child process segfaults.

#!/usr/bin/python3.14

import os
from concurrent import interpreters

GLOBAL_INTERP = None

def create_subinter(do_close: bool) -> None:
    global GLOBAL_INTERP
    interp = interpreters.create()
    GLOBAL_INTERP = interp
    interp.exec("""
x = 42 * 23
""")
    if do_close:
        interp.close()


def main():
    global GLOBAL_INTERP

    create_subinter(do_close=False)
    print("Before fork")
    child_pid = os.fork()

    if child_pid == 0:
        print("in child")
    else:
        print("in parent")

if __name__ == "__main__":
    main()

There seems to be a more general issue with the implementation of PyOS_AfterFork_Child() or PyInterpreterState_Clear() specifically:

cpython/Python/pystate.c

Lines 992 to 994 in a15aeec

// XXX Won't this fail since PyInterpreterState_Clear() requires
// the "current" tstate to be set?
PyInterpreterState_Clear(interp); // XXX must activate?

As the comment points out, the PyInterpreterState_Clear() won't be able to clear any interpreters, as the tstate is set to NULL before calling the function. Which finally results in a Py_DECREF on NULL.

I guess the expected behavior is that manually closing interpreters before a fork is not required and a working clear in the fork. While the combination of subinterpreters and fork() seems a bit obscure, we already ran into this issue in a CI setup.

It also seems that there is a lack of test coverage with the mentioned implementation in pystate.c. If appreciated, I could also follow up with a pull request adding more tests.

Full backtrace:

#0  _Py_DECREF_DecRefTotal () at Objects/object.c:287
#1  0x0000555555816177 in Py_DECREF (filename=filename@entry=0x555555908674 "Python/import.c", lineno=lineno@entry=4099, op=0x7ffff6ea0230) at ./Include/refcount.h:400
#2  0x000055555581d0af in _PyImport_ClearCore (interp=interp@entry=0x7ffff7a59020) at Python/import.c:4099
#3  0x000055555584a008 in PyInterpreterState_Clear (interp=interp@entry=0x7ffff7a59020) at Python/pystate.c:986
#4  0x000055555584a7a3 in _PyInterpreterState_DeleteExceptMain (runtime=runtime@entry=0x555555b809c0 <_PyRuntime>) at Python/pystate.c:1085
#5  0x000055555588f202 in PyOS_AfterFork_Child () at ./Modules/posixmodule.c:741
#6  0x000055555588f49a in os_fork_impl (module=<optimized out>) at ./Modules/posixmodule.c:8159
#7  0x000055555588f516 in os_fork (module=<optimized out>, _unused_ignored=_unused_ignored@entry=0x0) at ./Modules/clinic/posixmodule.c.h:4394
#8  0x00005555556ce81a in cfunction_vectorcall_NOARGS (func=0x7ffff711d3d0, args=<optimized out>, nargsf=<optimized out>, kwnames=<optimized out>) at Objects/methodobject.c:508
#9  0x0000555555676df0 in _PyObject_VectorcallTstate (tstate=0x555555bd1938 <_PyRuntime+331640>, callable=callable@entry=0x7ffff711d3d0, args=args@entry=0x7fffffffd628,
    nargsf=9223372036854775808, kwnames=kwnames@entry=0x0) at ./Include/internal/pycore_call.h:169
#10 0x0000555555676ecc in PyObject_Vectorcall (callable=callable@entry=0x7ffff711d3d0, args=args@entry=0x7fffffffd628, nargsf=<optimized out>, kwnames=kwnames@entry=0x0)
    at Objects/call.c:327
#11 0x00005555557b4ca6 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7ffff7fb2080, throwflag=0) at Python/generated_cases.c.h:1619
#12 0x00005555557d3d57 in _PyEval_EvalFrame (tstate=tstate@entry=0x555555bd1938 <_PyRuntime+331640>, frame=frame@entry=0x7ffff7fb2020, throwflag=throwflag@entry=0)
    at ./Include/internal/pycore_ceval.h:119
#13 0x00005555557d3f28 in _PyEval_Vector (tstate=tstate@entry=0x555555bd1938 <_PyRuntime+331640>, func=func@entry=0x7ffff716ab10, locals=locals@entry=0x7ffff71782f0,
    args=args@entry=0x0, argcount=argcount@entry=0, kwnames=kwnames@entry=0x0) at Python/ceval.c:1965
#14 0x00005555557d4024 in PyEval_EvalCode (co=co@entry=0x7ffff7bd23b0, globals=globals@entry=0x7ffff71782f0, locals=locals@entry=0x7ffff71782f0) at Python/ceval.c:857
#15 0x000055555584b13f in run_eval_code_obj (tstate=tstate@entry=0x555555bd1938 <_PyRuntime+331640>, co=co@entry=0x7ffff7bd23b0, globals=globals@entry=0x7ffff71782f0,
    locals=locals@entry=0x7ffff71782f0) at Python/pythonrun.c:1365
#16 0x000055555584b2dc in run_mod (mod=mod@entry=0x555555d7f060, filename=filename@entry=0x7ffff7198400, globals=globals@entry=0x7ffff71782f0, locals=locals@entry=0x7ffff71782f0,
    flags=flags@entry=0x7fffffffda28, arena=arena@entry=0x7ffff71e4a00, interactive_src=0x0, generate_new_source=0) at Python/pythonrun.c:1459
--Type <RET> for more, q to quit, c to continue without paging--                          
#17 0x000055555584bb5c in pyrun_file (fp=fp@entry=0x555555c49a60, filename=filename@entry=0x7ffff7198400, start=start@entry=257, globals=globals@entry=0x7ffff71782f0,
    locals=locals@entry=0x7ffff71782f0, closeit=closeit@entry=1, flags=0x7fffffffda28) at Python/pythonrun.c:1293
#18 0x000055555584d3f0 in _PyRun_SimpleFileObject (fp=fp@entry=0x555555c49a60, filename=filename@entry=0x7ffff7198400, closeit=closeit@entry=1, flags=flags@entry=0x7fffffffda28)
    at Python/pythonrun.c:521
#19 0x000055555584d5e6 in _PyRun_AnyFileObject (fp=fp@entry=0x555555c49a60, filename=filename@entry=0x7ffff7198400, closeit=closeit@entry=1, flags=flags@entry=0x7fffffffda28)
    at Python/pythonrun.c:81
#20 0x0000555555876b67 in pymain_run_file_obj (program_name=program_name@entry=0x7ffff71b2640, filename=filename@entry=0x7ffff7198400, skip_source_first_line=0) at Modules/main.c:410
#21 0x0000555555876c8f in pymain_run_file (config=config@entry=0x555555b9c9f0 <_PyRuntime+114736>) at Modules/main.c:429
#22 0x00005555558777ca in pymain_run_python (exitcode=exitcode@entry=0x7fffffffdba4) at Modules/main.c:694
#23 0x0000555555877a7f in Py_RunMain () at Modules/main.c:775
#24 0x0000555555877af6 in pymain_main (args=args@entry=0x7fffffffdc00) at Modules/main.c:805
#25 0x0000555555877bc4 in Py_BytesMain (argc=<optimized out>, argv=<optimized out>) at Modules/main.c:829
#26 0x00005555555de166 in main (argc=<optimized out>, argv=<optimized out>) at ./Programs/python.c:15

CPython versions tested on:

3.14

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    3.14bugs and security fixes
    3.15bugs and security fixes
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Oct 8, 2025
  2. efimov-mikhail commented on Oct 8, 2025

    @efimov-mikhail
    Member

    Thanks for the report.
    Unfortunately, I can't get a crash with this code snippet.

    -> % ./python subinterpreteres_crash_139759.py
    Before fork
    in parent
    <sys>:0: RuntimeWarning: remaining subinterpreters; destroy them with _interpreters.destroy()

    There is only RuntimeWarning present.

    Could you please provide more information about your OS and environment?

  3. aurelwildfellner commented on Oct 8, 2025

    @aurelwildfellner
    Author

    Unfortunately, I can't get a crash with this code snippet.

    This may be due to SIGSEV signal not propagating with the fork. You can see from the output that "in child" is not printed which indicates that the process has crashed.

    You can also try to run it in gdb with the right follow-fork-mode:

    gdb -ex "set follow-fork-mode child" --args ./python subinterpreteres_crash_139759.py

    You should see a SIGSEV and get an according backtrace (running with a debug build).

    Could you please provide more information about your OS and environment?

    Tested with a debug build of the 3.14 branch.

    gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

    Ubuntu 24.04.2 LTS (all dependencies installed from system packages).

  4. efimov-mikhail commented on Oct 8, 2025

    @efimov-mikhail
    Member

    gdb -ex "set follow-fork-mode child" --args ./python subinterpreteres_crash_139759.py

    Confirmed on main branch, gcc version 12.2.0 (Debian 12.2.0-14+deb12u1). Thanks!

  5. ZeroIntensity commented on Oct 9, 2025

    @ZeroIntensity
    Member

    Would you like to submit a PR?

  6. efimov-mikhail commented on Oct 9, 2025

    @efimov-mikhail
    Member

    Would you like to submit a PR?

    You're asking me or @aurelwildfellner ?
    I can try to dig into this.

  7. ZeroIntensity commented on Oct 9, 2025

    @ZeroIntensity
    Member

    I was asking either of you.

  8. efimov-mikhail commented on Oct 12, 2025

    @efimov-mikhail
    Member

    Would you like to submit a PR?

    I've made some progress in this investigation and want to finish this work and submit a PR.

  9. aurelwildfellner commented on Mar 5, 2026

    @aurelwildfellner
    Author

    I have created a unittest covering this on a separate PR. Maybe it is useful when working on a fix or can be merged to main after we have a fix. - gh-145545

  10. ZeroIntensity commented on Mar 5, 2026

    @ZeroIntensity
    Member

    Oh, the issue is that we delete all the interpreters after forking:

    status = _PyInterpreterState_DeleteExceptMain(runtime);

    It's not immediately clear to me as to why. @ericsnowcurrently is the person to ask about that.

  11. removed their assignment
    on Apr 8, 2026
  12. davidnich commented on Oct 22, 2026

    @davidnich

    I can reproduce this without any third-party code: os.fork() in the main interpreter while a single subinterpreter exists. The child either crashes or hangs in PyOS_AfterFork_Child(), depending on the version.

    import os, sys
    try:
        import _interpreters as interpreters
    except ImportError:
        import _xxsubinterpreters as interpreters
    if hasattr(interpreters, "new_config"):
        iid = interpreters.create(interpreters.new_config("legacy"))
    else:
        iid = interpreters.create(isolated=False) if "isolated" in interpreters.create.__doc__ else interpreters.create()
    interpreters.run_string(iid, "x = 1") if hasattr(interpreters, "run_string") else interpreters.exec(iid, "x = 1")
    pid = os.fork()
    if pid == 0:
        os._exit(0)
    _, status = os.waitpid(pid, 0)
    print(sys.version.split()[0], "child status:", status)
    Python Platform Child
    3.14.6 (python.org framework) macOS 26.6, arm64 SIGSEGV
    3.14.4 Ubuntu 26.04, x86_64 SIGSEGV
    3.13.15 (MacPorts) macOS 26.6, arm64 SIGABRT
    3.12.13 (MacPorts) macOS 26.6, arm64 SIGSEGV
    3.12.14 Alpine 3.23 (musl), x86_64 hangs
    3.11.15 (MacPorts) macOS 26.6, arm64 hangs
    3.10.20 (MacPorts) macOS 26.6, arm64 hangs
    • 3.14: the result is the same with new_config("isolated"), and when no code was run in the subinterpreter. The crash with faulthandler on (3.14.6, macOS) is:

      _Py_Dealloc
      _PyImport_ClearCore
      PyInterpreterState_Clear
      _PyInterpreterState_DeleteExceptMain
      PyOS_AfterFork_Child
      os_fork_impl
      
    • 3.11: the child waits forever in PyOS_AfterFork_Child() -> PyThread_acquire_lock_timed().

    We hit this in an embedding application. Its subinterpreters are created with allow_fork = 0, and it forks from the main interpreter of a Python process. For now it skips PyOS_BeforeFork() / PyOS_AfterFork_*() for its own fork() calls while subinterpreters exist.

  13. aurelwildfellner commented on Oct 23, 2026

    @aurelwildfellner
    Author

    I also rebased and updated the related pull request with the test. The test still fails as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15bugs and security fixes3.16new features, bugs and security fixesextension-modulesC modules in the Modules dirsprinttopic-subinterpreterstype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions