Skip to content

gh-159418: Fix TypeError in forkserver children when SIGINT has a handler set outside Python - #159484

Open
Om-singhaI wants to merge 1 commit into
python:mainfrom
Om-singhaI:gh-159418-forkserver-sigint
Open

Om-singhaI wants to merge 1 commit into
python:mainfrom
Om-singhaI:gh-159418-forkserver-sigint

Conversation

@Om-singhaI

@Om-singhaI Om-singhaI commented Oct 15, 2026 •

Copy link
Copy Markdown

When a native library installs a SIGINT handler before Python starts (AMD's rocprofv3 does this), signal.signal() in the forkserver returns None for SIGINT, because Python reports handlers it did not install as None. forkserver.main() saved that None in old_handlers, and _serve_one() passed it back to signal.signal() in every child. That raises TypeError, so each worker exits with code 1 before its target runs. Since forkserver is the default start method on Linux from 3.14, code that used to work now fails.

This change stores signal.SIG_DFL instead of None, since a handler installed from C cannot be restored from Python. I used SIG_DFL rather than signal.default_int_handler because the parent never had the Python handler either, and the same loop also covers SIGCHLD.

One alternative: when SIGINT already has a handler from C, the forkserver could leave it in place instead of switching to SIG_IGN, so children inherit the original handler the way they do with fork and spawn. That keeps the library's handler in workers, but the server would no longer ignore Ctrl+C. I went with the smaller change and can switch if that is preferred.

Testing, on macOS with a debug build of main (938144e):

  • The issue's reproducer, with the C library injected through DYLD_INSERT_LIBRARIES: the worker exited with code 1 and the TypeError before, 0 after.
  • The new test preloads a module that makes signal.signal() report None for SIGINT. It fails without the fix (exit code 1) and passes with it, and it also checks that the child's SIGINT handler is SIG_DFL.
  • test_multiprocessing_forkserver filtered to the forkserver and preload tests: 31 run, 4 skipped, 0 failures. -R 3:3 on the new test reports no leaks.

I put the test in _test_multiprocessing.py instead of test_preload.py because 3.14 has no test_preload.py, so the backport can apply cleanly.

… a handler set outside Python

signal.signal() returns None when the handler was already installed
outside Python before the interpreter started, for example by a library
loaded with LD_PRELOAD. The forkserver stored that None and later passed
it back to signal.signal() in every child, which raised TypeError before
the target ran. Store SIG_DFL instead so children start normally.
@Om-singhaI
Om-singhaI requested a review from gpshead as a code owner October 15, 2026 20:14
@python-cla-bot

Copy link
Copy Markdown

The following commit authors need to sign the Contributor License Agreement:

CLA not signed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant