mutmut icon indicating copy to clipboard operation
mutmut copied to clipboard

Segfault on macOS when `setproctitle()` is called after `os.fork()` in worker processes

Open tobikugel opened this issue 2 months ago โ€ข 12 comments

Hello ๐Ÿ‘‹
I spent several hours trying to get this library running on macOS (arm64) with Python 3.12. Eventually I discovered that mutmut consistently crashes with a SIGSEGV (exit code -11) in its worker processes before any tests actually run.

The crash disappears if I comment out this line:
main.py L1091

setproctitle(f"mutmut: {mutant_name}")

This call is executed in the child process immediately after os.fork().
Removing or guarding it makes mutation testing work normally.

What happens

Running mutmut run causes every worker to exit with code -11 unless I comment out the setproctitle(...) call in the child branch.

When pytest is invoked inside the worker, Python reports a fatal segmentation fault:

    worker exit code -11

Running a single mutant with pytest output enabled also shows โ€œSegmentation faultโ€ before any test code executes.
Once I disable setproctitle(...), workers proceed and mutation testing completes successfully.

Expected behavior

Workers should start and run tests normally.
setproctitle(...) should not cause a crash in the child process.

Actual behavior

Calling setproctitle(...) in the child immediately after a raw os.fork() reliably segfaults on macOS arm64.

Analysis

This is what my LLM said: This appears to be a classic โ€œfork-without-execโ€ issue with native extensions on macOS.
After fork(), only async-signal-safe operations are guaranteed to be safe in a multi-threaded process.
Calling into a C extension (such as setproctitle) right after fork is known to be fragile on macOS; similar crashes are seen in other projects that call non-fork-safe C code immediately after forking.

setproctitle(...) is the first native call made by the child.
Removing it completely avoids the crash and mutation testing proceeds normally.

Environment

  • OS: macOS 14 + (Darwin arm64) โ€” platform: macOS-26.0.1-arm64-arm-64bit
  • Python: 3.12.11 (venv)
  • mutmut: 3.3.1
  • pytest: 8.4.2
  • setproctitle: 1.3.7

Reproduction steps

  1. On macOS arm64 with Python 3.12, install mutmut and setproctitle.
  2. Run mutmut run on any project.
  3. Observe that worker processes exit with -11.
  4. Comment out or guard the line
    setproctitle(f"mutmut: {mutant_name}")
    
    and re-run โ€” the crash disappears.

I can submit a PR implementing a platform-safe guard around setproctitle(...) (e.g. skip on macOS).
Let me know if I can help!

tobikugel avatar Oct 14 '25 21:10 tobikugel

Hi, thanks for raising this issue. Here's the relevant issue at setproctitle with a lot of further pointers: https://github.com/dvarrazzo/py-setproctitle/issues/113

A PR would be good. We should also not import the library on MacOS, so the import statement should be guarded as well.

In the future, we could use spawn instead of fork for MacOS, which would allow to reenable this (equal to the windows support at #397 ).

Otto-AA avatar Oct 15 '25 06:10 Otto-AA

I don't have this problem on macOS personally, so one has to wonder if there's some additional issue at play.

The reason mutmut3 uses fork instead of spawn is due to the quite massive speedup I saw early on. It would be quite sad to lose it.

boxed avatar Oct 15 '25 12:10 boxed

The reason mutmut3 uses fork instead of spawn is due to the quite massive speedup I saw early on. It would be quite sad to lose it.

The implementation in #404 would allow to choose the process creation method (fork or spawn). But with the process pool, even spawn is reasonably fast (in my test it was 70s instead of 50s), because we only need to spawn a fixed number of processes. Still slower though.

Otto-AA avatar Oct 15 '25 15:10 Otto-AA

Process pools have the issue of worker processes potentially getting polluted by some side effect of a mutant and then never recovering. Fork guarantees that doesn't happen.

boxed avatar Oct 15 '25 15:10 boxed

Ah yes, with the mutants it could be unstable if it modifies some global state, fork is more resilient in this regard.

Otto-AA avatar Oct 15 '25 18:10 Otto-AA

Hello @boxed, This means you were not able to reproduce this issue. Right?

tobikugel avatar Oct 16 '25 15:10 tobikugel

Mutmut was developed on macOS fully, so yea, this didn't happen to me.

boxed avatar Oct 16 '25 17:10 boxed

I am having the same issue. As a temporary workaround, I made a little script that runs mutmut in a docker container:

#!/bin/bash

set -e

docker_tag="tag-name"

docker build -t $docker_tag .

docker run --rm \
    -v "$(pwd)/mutants:/app/mutants" \
    $docker_tag \
    sh -c "
        # Run mutmut
        mutmut run --max-children=4 || true
        
        # Show summary
        mutmut results | grep survived
    "

Environment

platform: macOS-26.0.1-arm64-arm-64bit
Python: 3.11.6 (venv)
mutmut: 3.3.1
pytest: 8.4.1
setproctitle: 1.3.7

By the way, great job on mutmut 3! Even with this workaround, running mutmut is so much faster!

memery-rbx avatar Oct 21 '25 16:10 memery-rbx

Also getting this issue. macOS 26 arm64.

Va1a avatar Oct 29 '25 19:10 Va1a

Could one of you test if importing after forking fixes the issue for you? I've created a PR here: #450 (clone the branch and use pip install -e <path-to-mutmut-repo> to install this mutmut version).

I don't have macOS so I cannot test it.

From some research, it seems that this is an issue on newer macOS versions, when using system libraries + forking. Here are some relevant quotes from docs and issues:

Changed in version 3.8: On macOS, the spawn start method is now the default. The fork start method should be considered unsafe as it can lead to crashes of the subprocess as macOS system libraries may start threads. See bpo-33725.

https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods

The problem is that all the "damage" is already done by import setproctitle. It eagerly initializes things and touches Apple libraries which, in turn, initializes them. Nothing is done after that and there is nothing to detect. Then after a fork a different library (urllib) also uses Apple's libraries. The Apple's code detects running under a different PID after being initialized and proactively crashes. This is new in macOS 13.2 it seems. Earlier versions were fine with such usage. Now if you go earlier in this issue to about Aug 2022 we made the initialization run on import in order to avoid a different manifestation of the same problem. So it appears that the only safe (now and in any future version of macOS) way of using the library together with plain fork would be either: require users to import the library after the fork. Note that for processes that fork dynamically in response to input this would mean never to use it in the parent. [...or change in library + calling after fork...]

https://github.com/dvarrazzo/py-setproctitle/issues/113#issuecomment-1409542685

This suggests, that prior to macOS 13.2 (released 01.2023) it could have worked and since then it always crashes.

Otto-AA avatar Oct 30 '25 20:10 Otto-AA

@Otto-AA Appreciate the effort, but still no luck:

โ ธ 2462/2471  ๐ŸŽ‰ 0 ๐Ÿซฅ 165  โฐ 0  ๐Ÿค” 2297  ๐Ÿ™ 0  ๐Ÿ”‡ 0objc[7142]: +[NSNumber initialize] may have been in progress in another thread when fork() was called.
objc[7142]: +[NSNumber initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
โ ผ 2463/2471  ๐ŸŽ‰ 0 ๐Ÿซฅ 165  โฐ 0  ๐Ÿค” 2298  ๐Ÿ™ 0  ๐Ÿ”‡ 0objc[7143]: +[NSNumber initialize] may have been in progress in another thread when fork() was called.
objc[7143]: +[NSNumber initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
objc[7144]: +[NSNumber initialize] may have been in progress in another thread when fork() was called.
objc[7144]: +[NSNumber initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.

Once again segfaults, although this time with a perhaps more descriptive error message?

Va1a avatar Oct 30 '25 21:10 Va1a

I got to test it on a macOS 15 and could not reproduce it (or only partially):

  • running mutmut on a minimal project did not segfault (would be good if someone with this issue could verify this: https://github.com/Otto-AA/mini-repro-repo)
  • running mutmut on a minimal project that makes a request.get call did not segfault
  • running mutmut on a bigger project segfaulted when doing the request.get call inside of the fork
  • running mutmut on this bigger project with the request.get removed does not segfault

So... I don't really know what's the issue. In my example, setproctitle did not matter. Only the request.get inside my project mattered for the segfault. And this request.get also did not cause it in the minimal project setup, so potentially there's another interaction I am missing.

If removing setproctitle does fix it for your setup, we could add a config option to disable its usage. Though I am curious what exactly such a setup is.

Otto-AA avatar Oct 31 '25 16:10 Otto-AA