Skip to content

First API request forks a subprocess via platform.platform() — child segfaults on macOS in long-running processes #1762

Description

@ratpH1nk

Summary

The first API request a process makes triggers a fork()+exec() of uname -p,
via the lru-cached platform-header computation. In long-running macOS processes
that have initialized Apple networking state (any process doing HTTPS), the forked
child can segfault in Network.framework's atfork handler, producing a macOS
crash report (Python-*.ips) on every process lifetime's first request. The SDK
call itself succeeds — the failure is silent apart from the crash report — but
the side effect (an unexpected subprocess from a serving process) is surprising
and, in fork-hostile environments, harmful.

Chain

  1. platform_headers() is @lru_cache(maxsize=None) (anthropic/_base_client.py)
    — computed once per process, on the first request.
  2. get_platform() calls platform.platform() (_base_client.py, the
    X-Stainless-OS detection).
  3. CPython's platform.platform() unpacks uname() including the lazy
    processor field
    (platform.py:1212 in 3.11).
  4. uname_result.processor resolves via _Processor.get(); there is no
    get_darwin, so macOS falls through to _Processor.from_subprocess()
    subprocess.check_output(['uname', '-p']) (platform.py:753).
  5. subprocess uses fork()+exec() here. If the parent has initialized
    Network.framework (normal after any HTTPS traffic), the forked child can die
    in _pthread_atfork_child_handlersnw_settings_child_has_forked()
    SIGSEGV, before exec runs.
  6. The parent sees a CalledProcessError, which the stdlib swallows
    (platform.py:768), so the request proceeds with processor='' — nothing
    in the SDK observes the failure.

Crash stack (child process, from macOS crash report)

0   libsystem_trace.dylib          _os_log_preferences_refresh + 56
1   libsystem_trace.dylib          os_log_type_enabled + 772
2   libnetworkextension.dylib      NEFlowDirectorDestroy + 64
3   Network                        nw_path_release_globals + 164
4   Network                        nw_settings_child_has_forked() + 296
5   libsystem_pthread.dylib        _pthread_atfork_child_handlers + 76
6   libsystem_c.dylib              fork + 112
7   _posixsubprocess.cpython-311-darwin.so  do_fork_exec + 68
8   _posixsubprocess.cpython-311-darwin.so  subprocess_fork_exec + 928
    ... (Python eval frames: subprocess.check_output ← _Processor.from_subprocess
         ← platform.uname ← platform.platform ← get_platform ← platform_headers
         ← first client request)

Reproduction conditions

  • macOS (observed on macOS 26.5 arm64), CPython 3.11.15 (Homebrew), anthropic 0.116.0.
  • A long-running process (observed: a Streamlit dashboard alive ~9.5 h) that has
    performed unrelated HTTPS traffic, then makes its first anthropic API call.
  • The crash is state-dependent: freshly started processes fork cleanly; aged ones
    produce the child segfault. We captured identical crash reports on two
    consecutive days, each at the process's first API call. Subsequent calls never
    fork (lru_cache), so it is once per process lifetime.

Why the child segfaults is Apple's bug — the ask here is narrower

Fork-without-exec in a Network.framework process is Apple's fragility, and the
uname -p subprocess is CPython's (platform.py has no get_darwin). But the
SDK is the only reason a typical serving process forks at all, and it does so
implicitly, at an unpredictable time (first request), from whatever thread makes
that request.

Suggested fixes

  • Compute the OS string without triggering uname().processor: build from
    platform.system() / platform.release() / platform.machine() +
    platform.mac_ver(), all of which are fork-free. X-Stainless-Arch already
    uses platform.machine().
  • Alternatively, resolve platform headers eagerly at import/client-construction
    time rather than first-request time, so any fork happens while the process is
    young and fork-safe.

Workaround for affected users

Call platform.processor() once at process startup (before significant network
activity). The stdlib caches uname_result, so the SDK's later call never forks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions