Segfault on macOS when `setproctitle()` is called after `os.fork()` in worker processes
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
- On macOS arm64 with Python 3.12, install
mutmutandsetproctitle. - Run
mutmut runon any project. - Observe that worker processes exit with
-11. - Comment out or guard the line
and re-run โ the crash disappears.setproctitle(f"mutmut: {mutant_name}")
I can submit a PR implementing a platform-safe guard around setproctitle(...) (e.g. skip on macOS).
Let me know if I can help!
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 ).
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.
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.
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.
Ah yes, with the mutants it could be unstable if it modifies some global state, fork is more resilient in this regard.
Hello @boxed, This means you were not able to reproduce this issue. Right?
Mutmut was developed on macOS fully, so yea, this didn't happen to me.
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!
Also getting this issue. macOS 26 arm64.
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 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?
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.getcall did not segfault - running mutmut on a bigger project segfaulted when doing the
request.getcall inside of the fork - running mutmut on this bigger project with the
request.getremoved 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.