Skip to content

fix: skip Launch Services calls in forked children on macOS#1

Open
gthomas-strike wants to merge 1 commit intomasterfrom
fix-macos-tahoe-fork-sigsegv
Open

fix: skip Launch Services calls in forked children on macOS#1
gthomas-strike wants to merge 1 commit intomasterfrom
fix-macos-tahoe-fork-sigsegv

Conversation

@gthomas-strike
Copy link
Copy Markdown
Owner

SIGSEGV in forked child processes on macOS Tahoe (26.x)

Summary

setproctitle() crashes with SIGSEGV when called in a forked child process on macOS Tahoe (26.x). The crash occurs in the Darwin-specific Launch Services code path (darwin_set_process_name.c), where CoreFoundation and Launch Services IPC calls are made after fork(). This breaks any application using a pre-fork server model (gunicorn, uWSGI, etc.).

Affected versions

  • setproctitle: 1.3.5 and 1.3.7 (binary wheels and source builds)
  • macOS: Tahoe 26.1 (Apple Silicon). May affect earlier versions (Sequoia 15.x) as well.
  • Python: 3.11 (likely affects all supported versions)

Background

The existing checked_in guard in launch_services_set_process_title() (added in commit f5013d1 to fix dvarrazzo#111) protects the _LSApplicationCheckIn call from being invoked in a forked child. However, it does not protect:

  1. launch_services_init() itself, which calls dlopen(), CFBundleGetBundleWithIdentifier(), and CFBundleGetFunctionPointerForName() - all CoreFoundation calls
  2. LSGetCurrentApplicationASN() (line 124) - Launch Services IPC
  3. LSSetApplicationInformationItem() (line 130-134) - Launch Services IPC

CoreFoundation is not fork-safe. On older macOS versions these calls happened to survive fork(), but macOS Tahoe has tightened enforcement and they now SIGSEGV.

Crash trace

Captured via faulthandler:

Fatal Python error: Segmentation fault

Current thread 0x00000001fa86a080 (most recent call first):
  File ".venv/lib/python3.11/site-packages/gunicorn/util.py", line 53 in _setproctitle
  File ".venv/lib/python3.11/site-packages/gunicorn/arbiter.py", line 605 in spawn_worker
  File ".venv/lib/python3.11/site-packages/gunicorn/arbiter.py", line 641 in spawn_workers
  File ".venv/lib/python3.11/site-packages/gunicorn/arbiter.py", line 570 in manage_workers
  File ".venv/lib/python3.11/site-packages/gunicorn/arbiter.py", line 201 in run
  File ".venv/lib/python3.11/site-packages/gunicorn/app/base.py", line 71 in run

Extension modules: setproctitle._setproctitle, ...

The crash is at gunicorn/arbiter.py:605, which is the _setproctitle() call in the child process immediately after fork():

# gunicorn/arbiter.py, spawn_worker()
pid = os.fork()
if pid != 0:
    ...
    return pid

# Child process
worker.pid = os.getpid()
util._setproctitle("worker [%s]" % self.proc_name)  # <-- SIGSEGV here

Reproduction

import os
import socket
from setproctitle import setproctitle

# Parent calls setproctitle (triggers lazy init of Launch Services state)
setproctitle("master")

# Parent sets up listening sockets (as gunicorn does before forking)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 0))
s.listen(1)

pid = os.fork()
if pid == 0:
    setproctitle("worker")  # SIGSEGV on macOS Tahoe
    os._exit(0)
else:
    _, status = os.waitpid(pid, 0)
    sig = status & 0xff
    if sig == 11:
        print("SIGSEGV!")
    else:
        print(f"OK (signal={sig})")

Proposed fix

In darwin_set_process_title(), track the PID on first call. In forked children (where getpid() != initial_pid), skip all Launch Services and CoreFoundation work. Only call pthread_setname_np(), which is fork-safe.

bool darwin_set_process_title(const char * title) {
    ...
    static pid_t initial_pid = 0;

    if (initial_pid == 0) {
        initial_pid = getpid();
    }
    if (getpid() != initial_pid) {
        /* In a forked child: only set the thread name, skip Launch Services */
        (void)darwin_pthread_setname_np(title);
        return true;
    }

    /* Existing Launch Services code path (unchanged) */
    ...
}

This is placed in darwin_set_process_title() rather than launch_services_set_process_title() to guard launch_services_init() as well, which also makes CoreFoundation calls.

What still works in forked children

  • argv clobbering (PS_USE_CLOBBER_ARGV in spt_status.c): Unaffected. The ps_buffer and save_argv pointers are valid in the child, so ps output still updates correctly.
  • pthread_setname_np(): Fork-safe. Thread names are set correctly in child processes.

What is skipped in forked children

  • Activity Monitor display name: The LSSetApplicationInformationItem call that updates the name in Activity Monitor. This is irrelevant for worker processes.

Why PID-based detection over pthread_atfork

  • Zero overhead: a single getpid() syscall (cached by the kernel on macOS)
  • No global registration needed
  • No interaction with other libraries' atfork handlers
  • Simple, auditable, and contained to one function

  CoreFoundation and Launch Services IPC calls are not fork-safe.
  On macOS Tahoe (26.x), calling these in a forked child process
  causes a SIGSEGV. This affects any application using gunicorn or
  similar pre-fork servers (e.g. Apache Airflow).

  Track the PID on first call to darwin_set_process_title(). In
  forked children, skip all Launch Services / CoreFoundation work
  and only call pthread_setname_np(), which is fork-safe. The argv
  clobbering path (handled by the caller) still functions correctly,
  so ps output continues to update in child processes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Segfault on macOS 12.5 / setproctitle 1.3.0

1 participant